fix: v2.7.4 — Changes matches JetBrains (#72), the chat says when Claude waits on a trust prompt (#73), Vietnamese Telex in Safari

- Changes (#72): a file git reports `MM` whose working copy equals HEAD again
  (a staged change since undone on disk) is no longer listed — JetBrains'
  GitChangesCollector drops it the same way. Mode-only changes stay listed.
  statusCounts (the dirty-tree safety checks) is unchanged.
- Chat (#73): Claude Code runs no hook before a folder is trusted, so a new
  pane's session has no row; the chat now says the session has not started,
  offers Open terminal, and asks before typing a message or /model //effort
  into the pane (an Enter would pick the trust prompt's "No, exit").
- Terminal: Safari + macOS's own Vietnamese Telex replaces the whole word on
  each key (insertReplacementText), which xterm.js ignores — only each word's
  first letter reached the pane. The edit is now sent as DELs + the new tail.
- Also: Ctrl+V pastes a screenshot into the terminal as a file path; the
  folder browser fallback lists files for a file field (`files=1`); the macOS
  native picker opens in front of the browser; a hidden tab is marked while a
  chat waits or once a turn ended; chat live line shows running calls, turn
  time and tokens, and "Done in …".
This commit is contained in:
2026-10-12 15:40:47 +07:00
parent 5d8fc9bb98
commit 46e3899945
44 changed files with 1329 additions and 297 deletions
+10 -4
View File
@@ -2970,14 +2970,20 @@ export const api = {
* the Add Lane modal's folder browser. Browsers cannot expose absolute
* filesystem paths from a native picker, so this drives a server-backed one.
* @param path Absolute path to list; defaults server-side to the home dir.
* @param files List files too (`file: true`) — a file field's picker.
* @returns `{ path, parent, entries }` — `parent` is null at the root.
*/
browse: (path?: string) =>
request<{
browse: (path?: string, files?: boolean) => {
const q = new URLSearchParams();
if (path) q.set("path", path);
if (files) q.set("files", "1");
const qs = q.toString();
return request<{
path: string;
parent: string | null;
entries: { name: string; path: string; isGitRepo: boolean }[];
}>(`/lanes/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`),
entries: { name: string; path: string; isGitRepo: boolean; file?: boolean }[];
}>(`/lanes/browse${qs ? `?${qs}` : ""}`);
},
/**
* POST /api/lanes/pick-path — open the OS's own folder/file picker on the
* server's desktop and wait for the user. Throws `.code` `EREMOTE` (not a
+49
View File
@@ -0,0 +1,49 @@
/**
* @file tabBadge.ts — the browser tab's title mark for Chat panes while the
* page is hidden: "● Waiting" while a pane's CLI asks the user something (a
* permission prompt), else "✓ Done" once a turn ended. Shown again, the
* page drops the "Done" marks; a "Waiting" one lasts until its question is
* answered. Only the mark this module put in front of the title is ever
* removed, so a page that sets its own title meanwhile keeps it.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import i18n from "../i18n";
export type TabMark = "waiting" | "done";
const marks = new Map<string, TabMark>();
let applied = "";
function apply() {
const title = document.title;
const base = applied && title.startsWith(applied) ? title.slice(applied.length) : title;
const all = [...marks.values()];
const mark =
document.visibilityState !== "hidden"
? null
: all.includes("waiting")
? "waiting"
: all.includes("done")
? "done"
: null;
applied = mark ? `${mark === "waiting" ? "●" : "✓"} ${i18n.t(`run:chat.tab.${mark}`)} · ` : "";
document.title = applied + base;
}
/** Sets (or, with null, drops) the mark of one pane (`key`). */
export function setTabBadge(key: string, mark: TabMark | null): void {
// A turn ending in front of the user needs no mark.
if (mark === "done" && document.visibilityState !== "hidden") mark = null;
if (mark) marks.set(key, mark);
else marks.delete(key);
apply();
}
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "hidden") {
for (const [key, mark] of marks) if (mark === "done") marks.delete(key);
}
apply();
});
}