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
+16 -4
View File
@@ -480,8 +480,10 @@ router.get("/:id/outline", (req, res) => {
// pane's Files read preview when it lies outside the workspace (a screenshot
// under ~/.cache, /tmp). The path is authorized only by that event, so: raster
// types only, under a sandbox CSP with nosniff; never for a pushed session
// (another machine's paths), and not in server mode, where a member's hook
// could name a host-only file that this process would then read as the host.
// (another machine's paths). In server mode only with S3, where a member's hook
// reports sessions in its own home only (hook-auth.js): a member's session gets
// only a file inside that home and owned by them; a host session (a trusted
// peer's) anything. Without S3 a member's hook could name a host-only file.
const READ_IMAGE_TYPES = {
png: "image/png",
jpg: "image/jpeg",
@@ -500,12 +502,18 @@ router.get("/:id/image", async (req, res) => {
const fail = (status, code, message) => res.status(status).json({ error: { code, message } });
const session = stmts.getSession.get(req.params.id);
if (!session) return fail(404, "NOT_FOUND", "Session not found");
if (isServerMode() || String(session.source || "").startsWith("push:")) {
const s3 = require("../lib/linux-accounts").s3Enabled();
if ((isServerMode() && !s3) || String(session.source || "").startsWith("push:")) {
return fail(403, "EUNSUPPORTED", "not available for this session");
}
const file = typeof req.query.path === "string" ? req.query.path : "";
const type = READ_IMAGE_TYPES[path.extname(file).slice(1).toLowerCase()];
if (!path.isAbsolute(file) || !type) return fail(400, "ENOTIMAGE", "not an image path");
const runAs = require("../lib/run-as");
const member = s3 ? runAs.ownerForRead(session.cwd) : null;
if (member && !require("../lib/hook-auth").inside(member.home, file)) {
return fail(404, "ENOTFOUND", "the session never read that image");
}
if (!sessionReadFile.get(session.id, file)) {
return fail(404, "ENOTFOUND", "the session never read that image");
}
@@ -514,7 +522,11 @@ router.get("/:id/image", async (req, res) => {
// NONBLOCK: a FIFO named like an image must not hang the event loop.
fh = await fs.promises.open(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0));
const st = await fh.stat();
if (!st.isFile()) return fail(404, "ENOTFOUND", "no such image");
// A file in a member's home is read only when that member owns it (openSafe's rule).
const fileOwner = s3 ? runAs.ownerForRead(file) : null;
if (!st.isFile() || (fileOwner && st.uid !== fileOwner.uid)) {
return fail(404, "ENOTFOUND", "no such image");
}
if (st.size > READ_IMAGE_MAX) return fail(400, "ETOOBIG", "image is larger than 20 MB");
const buf = await fh.readFile();
res.set({