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:
2026-10-06 12:36:06 +07:00
parent 6c605849d8
commit f0500dc592
41 changed files with 1201 additions and 249 deletions
@@ -82,6 +82,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
cwds: vi.fn(async () => ({ items: [] })),
kill: vi.fn(async () => ({ ok: true })),
history: vi.fn(async () => ({ items: [] })),
forgetHistory: vi.fn(async () => ({ removed: "" })),
},
aiTools: {
list: vi.fn(async () => ({ tools: [], default: "claude" })),
@@ -417,6 +418,30 @@ describe("WorkspaceTree", () => {
expect(screen.getByTestId("where").textContent).toBe("/workspace?lane=1&run=agenthub-lane-1-3");
});
it("removes a closed terminal from its hover X and its right-click menu", async () => {
const shell = (id: string) => ({
id,
tool: "shell",
status: "exited",
started_at: "2026-10-06T08:00:00Z",
ended_at: "2026-10-06T08:05:00Z",
isLive: false,
});
vi.mocked(api.run.history).mockImplementation(
async () => ({ items: [shell("t-a"), shell("t-b")] }) as never
);
const user = userEvent.setup();
renderTree();
await user.click(await screen.findByTestId("workspace-toggle-1"));
await user.click(await screen.findByTestId("workspace-terminal-forget-t-a"));
expect(api.run.forgetHistory).toHaveBeenCalledWith("t-a");
await waitFor(() => expect(screen.queryByTestId("workspace-terminal-closed-t-a")).toBeNull());
fireEvent.contextMenu(screen.getByTestId("workspace-terminal-closed-t-b"));
await user.click(screen.getByText("Remove from list"));
expect(api.run.forgetHistory).toHaveBeenCalledWith("t-b");
await waitFor(() => expect(screen.queryByTestId("workspace-terminal-closed-t-b")).toBeNull());
});
it("selects by URL, and a click selects and expands", async () => {
const user = userEvent.setup();
renderTree("/workspace?lane=2");