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
+21 -1
View File
@@ -189,6 +189,21 @@ function normalizeModels(model, models) {
return out;
}
/**
* A known Claude model id that Claude Code should treat the router's own model
* names as (its `modelPicker` `behavesAs`, issue #56). Optional; `""` clears it.
* The user picks it, never AgentHub: it also sets the context window Claude
* Code compacts at, and a router may route a name to a non-Claude model.
*/
function normalizeBehavesAs(value) {
const clean = typeof value === "string" ? value.trim() : null;
if (clean === "") return "";
if (!clean || !/^claude-[a-z0-9][a-z0-9.-]{0,99}$/.test(clean)) {
throw err("EBADMODEL", "behavesAs must be a Claude model id, e.g. claude-opus-5-5");
}
return clean;
}
function assertNotBuiltin(name) {
if (name === BUILTIN_NAME) {
throw err("EBUILTIN", `'${BUILTIN_NAME}' is the built-in profile and cannot be changed`);
@@ -250,7 +265,7 @@ function getRouter(name) {
/**
* @param {{label:string, baseUrl:string, apiKey?:string, model:string,
* models?:Record<string,string>}} input
* models?:Record<string,string>, behavesAs?:string}} input
* @returns the browser-safe view of what was saved
*/
function createRouter(input = {}) {
@@ -265,6 +280,7 @@ function createRouter(input = {}) {
apiKey: typeof input.apiKey === "string" ? input.apiKey.trim() : "",
model,
models: normalizeModels(model, input.models),
behavesAs: input.behavesAs === undefined ? "" : normalizeBehavesAs(input.behavesAs),
savedAt: new Date().toISOString(),
};
@@ -297,6 +313,10 @@ function updateRouter(name, changes = {}) {
changes.model === undefined && changes.models === undefined
? stored.models
: normalizeModels(model, changes.models ?? stored.models),
behavesAs:
changes.behavesAs === undefined
? (stored.behavesAs ?? "")
: normalizeBehavesAs(changes.behavesAs),
savedAt: new Date().toISOString(),
};