feat: AI commit messages, change tree, router behavesAs, sidebar times (v2.2.0)
- Changes panel: Files and Changes merged into one panel; the overview's groups drawn as a folder tree (compacted chains, file counts, fold, stage/unstage a whole folder) - AI-suggested commit message for what is staged (POST /api/lanes/:id/git/commit-message, claude -p --safe-mode, haiku) - AI Router: optional "Behaves as" Claude model id, written as modelPicker options into the router's --settings file (issue #56) - Sidebar: session rows show last activity, live rows their start time; a closed Terminal row can be removed (DELETE /api/run/history/:id) - Every pane's CLI starts only once a client is attached; claude panes no longer forced inline; tmux scrollbar one column wide - Update notifier: "Check for updates" reopens a closed notice
This commit is contained in:
@@ -75,11 +75,9 @@ describe("pty-run", () => {
|
||||
const { spawn } = require("node:child_process");
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), "fake-tmux-"));
|
||||
const flag = path.join(bin, "attached");
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "tmux"),
|
||||
`#!/bin/sh\n[ -e '${flag}' ] && echo 1 || echo 0\n`,
|
||||
{ mode: 0o755 }
|
||||
);
|
||||
fs.writeFileSync(path.join(bin, "tmux"), `#!/bin/sh\n[ -e '${flag}' ] && echo 1 || echo 0\n`, {
|
||||
mode: 0o755,
|
||||
});
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
const child = spawn("sh", ["-c", argv[2]], {
|
||||
@@ -492,11 +490,43 @@ describe("pty-run", () => {
|
||||
return calls.find((c) => c[0] === "new-session");
|
||||
}
|
||||
|
||||
/** The pane's own command line — everything tmux passes after the `--`. */
|
||||
/** The CLI's own command line: what tmux passes after the `--`, less the
|
||||
* `sh -c '<wait for a client>; exec "$@"' sh` wrapper spawnRun puts first. */
|
||||
function paneArgv(newSessionCall) {
|
||||
return newSessionCall.slice(newSessionCall.indexOf("--") + 1);
|
||||
const argv = newSessionCall.slice(newSessionCall.indexOf("--") + 1);
|
||||
assert.deepEqual([argv[0], argv[1], argv[3]], ["sh", "-c", "sh"]);
|
||||
assert.match(argv[2], /session_attached.*exec "\$@"$/);
|
||||
return argv.slice(4);
|
||||
}
|
||||
|
||||
it("spawnRun starts the CLI only once the pane has its real size, argv untouched", () => {
|
||||
// A claude drawn at tmux's birth size duplicated its banner on the first
|
||||
// resize; the argv rides as positional parameters, never script text.
|
||||
const call = captureNewSession(() =>
|
||||
pty.spawnRun({ laneId: 14, cwd: "/tmp/repo", initialPrompt: "a'; rm -rf / #" })
|
||||
);
|
||||
const argv = paneArgv(call);
|
||||
assert.ok(argv.includes("a'; rm -rf / #"));
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const out = execFileSync(
|
||||
"sh",
|
||||
[
|
||||
...call.slice(call.indexOf("--") + 2, call.indexOf("--") + 4),
|
||||
"sh",
|
||||
"printf",
|
||||
"%s|",
|
||||
"x y",
|
||||
"$HOME",
|
||||
],
|
||||
{
|
||||
env: { ...process.env, PATH: "/nonexistent-tmux-dir:" + process.env.PATH, TMUX_PANE: "" },
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
assert.equal(out, "x y|$HOME|");
|
||||
});
|
||||
|
||||
it("spawnRun builds a codex argv for a codex lane", () => {
|
||||
const call = captureNewSession(() =>
|
||||
pty.spawnRun({
|
||||
@@ -583,15 +613,11 @@ describe("pty-run", () => {
|
||||
assert.ok(!envFlags(claude).some((a) => a.startsWith("CODEX_HOME=")));
|
||||
});
|
||||
|
||||
it("keeps a claude pane off the alternate screen, so tmux scrolls (and draws a scrollbar for) it", () => {
|
||||
it("leaves a claude pane's screen mode to the user's own tui setting", () => {
|
||||
// Forcing it inline duplicated the transcript on every pane narrowing.
|
||||
const envFlags = (call) => call.filter((a, n) => call[n - 1] === "-e");
|
||||
const claude = captureNewSession(() => pty.spawnRun({ laneId: 42, cwd: "/tmp/repo" }));
|
||||
assert.ok(envFlags(claude).includes("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1"));
|
||||
tmux.__reset();
|
||||
const codex = captureNewSession(() =>
|
||||
pty.spawnRun({ laneId: 43, cwd: "/tmp/repo", tool: "codex" })
|
||||
);
|
||||
assert.ok(!envFlags(codex).some((a) => a.startsWith("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=")));
|
||||
assert.ok(!envFlags(claude).some((a) => a.startsWith("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=")));
|
||||
});
|
||||
|
||||
describe("catalog tools", () => {
|
||||
@@ -719,7 +745,11 @@ describe("pty-run", () => {
|
||||
assert.equal(envOf(ns, "GOOSE_MODE"), "auto");
|
||||
assert.ok(!calls.some((c) => c[0] === "set-buffer"));
|
||||
live = true;
|
||||
t.mock.timers.tick(3000);
|
||||
// delayMs (3000) counts from when the CLI starts: after up to 2 s of
|
||||
// waiting for a client to attach.
|
||||
t.mock.timers.tick(4999);
|
||||
assert.ok(!calls.some((c) => c[0] === "set-buffer"));
|
||||
t.mock.timers.tick(1);
|
||||
assert.deepEqual(
|
||||
calls.find((c) => c[0] === "set-buffer"),
|
||||
["set-buffer", "-b", "agenthub-paste-agenthub-lane-16", "--", "fix it"]
|
||||
|
||||
Reference in New Issue
Block a user