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
+49 -1
View File
@@ -12,7 +12,8 @@
* non-admin owns no host-folder workspace and a pane's cwd is its
* workspace's account's; a pane token works only from its pane's uid; an
* analyst's GET path stays in their own folder; a drop goes to the pane's
* account. A fake passwd (every member = this test's uid) and server.conf
* account; an image a session read is served only from a member session's
* own home (a host session's from anywhere). A fake passwd (every member = this test's uid) and server.conf
* make the members provisioned; fake sudo runs their commands as this
* account — and a fake `sudo` on PATH that never returns stands in for a
* stalled member, in a child process.
@@ -674,7 +675,54 @@ describe("a dropped file", () => {
const host = await drop("boss", hostLane);
assert.equal(host.status, 200, JSON.stringify(host.body));
assert.equal(path.dirname(host.body.path), path.join(DATA, "drops"));
// …and the admin's chat shows it back, though the GET names no workspace.
fs.mkdirSync(path.join(USERS, "boss", ".agenthub", "handoff"), { recursive: true });
const shown = await fetch(`${BASE}/api/run/drop/${path.basename(host.body.path)}`, {
headers: { cookie: cookies.boss },
});
assert.equal(shown.status, 200);
assert.equal(await shown.text(), "hello");
// Another member's workspace is not one to name.
assert.equal((await drop("bob", miaLane)).status, 404);
});
});
describe("an image a session read (the Chat pane's Files read preview)", () => {
const readBy = (sid, cwd, file) => {
db.prepare("INSERT OR IGNORE INTO sessions (id, cwd) VALUES (?, ?)").run(sid, cwd);
db.prepare(
"INSERT INTO events (session_id, event_type, tool_name, data) VALUES (?, 'PostToolUse', 'Read', ?)"
).run(sid, JSON.stringify({ tool_input: { file_path: file } }));
};
const get = (sid, file) =>
fetch(`${BASE}/api/sessions/${sid}/image?path=${encodeURIComponent(file)}`, {
headers: { cookie: cookies.boss },
});
it("is served for the host's own session and a member's file in that member's home", async () => {
const hostPng = path.join(TMP, "hostdir", "shot.png");
const miaPng = path.join(MIA, "proj", "shot.png");
fs.mkdirSync(path.dirname(hostPng), { recursive: true });
fs.writeFileSync(hostPng, "HOSTPNG");
fs.writeFileSync(miaPng, "MIAPNG");
readBy("img-host", path.join(TMP, "hostdir"), hostPng);
readBy("img-mia", path.join(MIA, "proj"), miaPng);
const host = await get("img-host", hostPng);
assert.equal(host.status, 200);
assert.equal(await host.text(), "HOSTPNG");
const mia = await get("img-mia", miaPng);
assert.equal(mia.status, 200);
assert.equal(await mia.text(), "MIAPNG");
});
it("is refused when a member's session names a file outside that member's home", async () => {
readBy("img-mia-out", path.join(MIA, "proj"), path.join(TMP, "hostdir", "shot.png"));
readBy("img-mia-bob", path.join(MIA, "proj"), path.join(BOB, "b.png"));
fs.writeFileSync(path.join(BOB, "b.png"), "BOBPNG");
for (const f of [path.join(TMP, "hostdir", "shot.png"), path.join(BOB, "b.png")]) {
const r = await get(f.includes("bob") ? "img-mia-bob" : "img-mia-out", f);
assert.equal(r.status, 404, f);
assert.ok(!/PNG/.test(await r.text()));
}
});
});