46e3899945
- 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 …".
134 lines
5.2 KiB
JavaScript
134 lines
5.2 KiB
JavaScript
/**
|
|
* @file native-dialog.test.js
|
|
* @description Unit tests for the server-side native folder/file picker: the
|
|
* argv each platform gets, the fallback from zenity to kdialog, Cancel versus a
|
|
* chosen path, and the "no dialog program" answer the client falls back on.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
const { describe, it, afterEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const os = require("node:os");
|
|
const nd = require("../lib/native-dialog");
|
|
const desktop = require("../lib/desktop-env");
|
|
|
|
const linux = process.platform === "linux";
|
|
|
|
describe("native-dialog", () => {
|
|
afterEach(() => nd.__setExecImpl(null));
|
|
|
|
it("starts in the given folder, a file's folder, or home", () => {
|
|
assert.equal(nd.startDir(os.tmpdir()), os.tmpdir());
|
|
assert.equal(nd.startDir("/definitely/not/here/x"), os.homedir());
|
|
assert.equal(nd.startDir("~"), os.homedir());
|
|
assert.equal(nd.startDir(undefined), os.homedir());
|
|
});
|
|
|
|
it("builds zenity then kdialog on Linux, the request's folder one argv element", () => {
|
|
const [[z, zargs], [k, kargs]] = nd.dialogCommands("dir", "/tmp/a b", "linux");
|
|
assert.equal(z, "zenity");
|
|
assert.deepEqual(zargs, [
|
|
"--file-selection",
|
|
"--title=Select a folder",
|
|
"--filename=/tmp/a b/",
|
|
"--directory",
|
|
]);
|
|
assert.equal(k, "kdialog");
|
|
assert.deepEqual(kargs.slice(0, 2), ["--getexistingdirectory", "/tmp/a b"]);
|
|
assert.ok(!nd.dialogCommands("file", "/tmp", "linux")[0][1].includes("--directory"));
|
|
});
|
|
|
|
it("passes the folder to osascript as an argument, never inside the script", () => {
|
|
const [[cmd, args]] = nd.dialogCommands("file", '/x"; do shell script "id', "darwin");
|
|
assert.equal(cmd, "osascript");
|
|
assert.equal(args.at(-1), '/x"; do shell script "id');
|
|
assert.ok(!args.slice(0, -2).join(" ").includes("shell script"));
|
|
});
|
|
|
|
it("brings osascript forward before the macOS dialog, so it does not open behind the browser", () => {
|
|
const [[, args]] = nd.dialogCommands("dir", "/x", "darwin");
|
|
const script = args.filter((_, i) => args[i - 1] === "-e");
|
|
assert.ok(script.indexOf("activate") > -1);
|
|
assert.ok(script.indexOf("activate") < script.findIndex((s) => s.includes("choose folder")));
|
|
});
|
|
|
|
it("gives the Windows dialog a foreground TopMost owner, so it does not open behind the browser", () => {
|
|
for (const kind of ["dir", "file"]) {
|
|
const [[cmd, args]] = nd.dialogCommands(kind, "C:\\x", "win32");
|
|
assert.equal(cmd, "powershell.exe");
|
|
assert.equal(args.at(-2), "-EncodedCommand");
|
|
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
|
assert.match(
|
|
script,
|
|
/\$o\.TopMost = \$true;[\s\S]*ShowWindow\(\$o\.Handle, 1\)[\s\S]*\$o\.Show\(\)[\s\S]*Front\(\$o\.Handle\)[\s\S]*\$d\.ShowDialog\(\$o\)/
|
|
);
|
|
assert.match(script, /AttachThreadInput/);
|
|
assert.ok(!script.includes("C:\\x"), "the folder travels in env, never the script");
|
|
}
|
|
});
|
|
|
|
it(
|
|
"answers the chosen path, null on Cancel, and falls through a missing program",
|
|
{
|
|
skip: !linux,
|
|
},
|
|
async () => {
|
|
desktop.__setExecImpl(() => "DISPLAY=:0\n");
|
|
const tried = [];
|
|
nd.__setExecImpl(async (cmd) => {
|
|
tried.push(cmd);
|
|
return cmd === "zenity" ? { missing: true } : { code: 0, stdout: "/home/u/proj\n" };
|
|
});
|
|
assert.equal(await nd.pickPath("dir"), "/home/u/proj");
|
|
assert.deepEqual(tried, ["zenity", "kdialog"]);
|
|
|
|
nd.__setExecImpl(async () => ({ code: 1, stdout: "" }));
|
|
assert.equal(await nd.pickPath("file"), null);
|
|
|
|
nd.__setExecImpl(async () => ({ missing: true }));
|
|
await assert.rejects(nd.pickPath("dir"), { code: "ENODIALOG" });
|
|
}
|
|
);
|
|
|
|
it(
|
|
"answers ENODIALOG, not a Cancel, when the program could not reach the desktop",
|
|
{ skip: !linux },
|
|
async () => {
|
|
desktop.__setExecImpl(() => "DISPLAY=:0\n");
|
|
nd.__setExecImpl(async () => ({
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: "(zenity:1): Gtk-WARNING **: cannot open display: :0\n",
|
|
}));
|
|
await assert.rejects(nd.pickPath("dir"), { code: "ENODIALOG" });
|
|
}
|
|
);
|
|
|
|
it("passes the abort signal through to the dialog program", { skip: !linux }, async () => {
|
|
desktop.__setExecImpl(() => "DISPLAY=:0\n");
|
|
const ac = new AbortController();
|
|
let seen;
|
|
nd.__setExecImpl(async (cmd, args, env, signal) => {
|
|
seen = signal;
|
|
return { code: 1, stdout: "", stderr: "" };
|
|
});
|
|
assert.equal(await nd.pickPath("dir", undefined, ac.signal), null);
|
|
assert.equal(seen, ac.signal);
|
|
});
|
|
|
|
it("tells a Cancel from a failure per program", () => {
|
|
assert.equal(nd.isCancel("zenity", ""), true);
|
|
assert.equal(
|
|
nd.isCancel("zenity", "Gtk-Message: GtkDialog mapped without a transient parent"),
|
|
true
|
|
);
|
|
assert.equal(nd.isCancel("kdialog", "qt.qpa.xcb: could not connect to display :1"), false);
|
|
assert.equal(nd.isCancel("osascript", "execution error: User canceled. (-128)"), true);
|
|
assert.equal(
|
|
nd.isCancel("osascript", "execution error: No user interaction allowed. (-1713)"),
|
|
false
|
|
);
|
|
assert.equal(nd.isCancel("powershell.exe", ""), true);
|
|
assert.equal(nd.isCancel("powershell.exe", "Add-Type : Cannot add type"), false);
|
|
});
|
|
});
|