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
+20 -2
View File
@@ -8,7 +8,8 @@
* theirs, not a choice made here:
*
* - **Claude Code** gets `--settings=<file>`: a settings file holding the
* profile's eight `env` keys, `<data dir>/router-settings/<name>.json`
* profile's eight `env` keys (plus `modelPicker` rows when the profile
* sets `behavesAs`), `<data dir>/router-settings/<name>.json`
* (0600). The command-line layer outranks the project-local and user
* settings (verified on 2.1.286), so two sessions of one workspace can run
* on two routers, and the key stays out of argv — only the path is there.
@@ -73,12 +74,29 @@ function writeSecret(file, body) {
fs.chmodSync(file, 0o600); // an existing file keeps its mode on write
}
/**
* `modelPicker` rows telling Claude Code which known model each of the router's
* own names behaves as — without one, a name like 9Router's `agent` draws an
* "isn't described by this version's model catalog" warning and a 200k
* compact window (issue #56). Only when the profile sets `behavesAs`: this
* layer's `modelPicker` replaces the user's own for the session. A bare
* `claude-*` id is skipped, since Claude Code already knows it.
*/
function modelPicker(profile) {
if (!profile.behavesAs) return {};
const names = new Set([profile.model, ...Object.values(profile.models || {})]);
const options = [...names]
.filter((m) => m && !m.startsWith("claude-"))
.map((model) => ({ model, behavesAs: profile.behavesAs }));
return options.length ? { modelPicker: { options } } : {};
}
function writeClaudeSettings(profile) {
const file = claudeSettingsPath(profile.name);
// Empty values are left out: a blank key in the command-line layer would
// blank the user's own setting, not leave it alone.
const env = Object.fromEntries(Object.entries(envForProfile(profile)).filter(([, v]) => v));
writeSecret(file, `${JSON.stringify({ env }, null, 2)}\n`);
writeSecret(file, `${JSON.stringify({ env, ...modelPicker(profile) }, null, 2)}\n`);
return file;
}