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
@@ -0,0 +1,42 @@
/**
* @file Tests for the Changes panel's folder tree: grouping by folder,
* compacting single-child chains, counts, ordering and the per-folder file list.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect } from "vitest";
import { changeTree, treeFiles } from "../changeTree";
const rows = [
"app2/Services/TwitterService.php",
"app2/Services/Redis/SlowAlertRedisService.php",
"app2/Enums/Redis/RedisSlowAlertKey.php",
"config/monitoring.php",
"config/app.php",
"README.md",
].map((path) => ({ path }));
describe("changeTree", () => {
const root = changeTree(rows);
it("puts root files at the top level and counts every file", () => {
expect(root.count).toBe(6);
expect(root.files.map((f) => f.name)).toEqual(["README.md"]);
expect(root.dirs.map((d) => d.name)).toEqual(["app2", "config"]);
});
it("compacts a folder chain with nothing else in it", () => {
const app2 = root.dirs[0]!;
expect(app2.count).toBe(3);
expect(app2.dirs.map((d) => [d.name, d.path])).toEqual([
["Enums/Redis", "app2/Enums/Redis"],
["Services", "app2/Services"],
]);
// Services holds a file, so it is not folded into Redis.
expect(app2.dirs[1]!.dirs[0]!.name).toBe("Redis");
});
it("sorts files by name and lists a folder's files", () => {
expect(root.dirs[1]!.files.map((f) => f.name)).toEqual(["app.php", "monitoring.php"]);
expect(treeFiles(root.dirs[0]!).map((f) => f.path)).toHaveLength(3);
});
});