From 62128913f187c0902ea483f25cdb5391a7701aa1 Mon Sep 17 00:00:00 2001 From: Lexus2016 Date: Sun, 23 Aug 2026 15:06:22 +0200 Subject: [PATCH 1/2] feat: global new-chat defaults with per-project overrides (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every new chat opened on literals: newTab() assigned curMode='auto' / curAgent='single', the model carried over from whichever tab was open last, and #maxTurns shipped value="50" in the markup. A user who always works on Opus / Task retyped that on every chat. chat-defaults.js resolves a two-link chain instead: project override > global default (config.json) > BUILTIN The project link is deliberately SPARSE — a project stores only the dials it pinned. A full five-key snapshot would freeze each project at whatever the global happened to be the day it was created, which is the opposite of what the issue asked for; it is also why the API test asserts the object written to data/projects.json and not just the resolved answer. BUILTIN is the pre-#58 behaviour verbatim, so an install that never opens the settings form behaves exactly as before. Global half: five rows in the settings catalog (section 'defaults'), so the Settings UI gets its section, source badges and per-row Reset for free. coerceValue() gained optional min/max so the form cannot store a turns value its own refuses to display. Project half: PUT/DELETE /api/projects/:id/defaults plus the star/reset pair in the toolbar, following the existing "set as default" idiom. A PUT touches only the keys it names; an invalid value is a 400 that names the key rather than a silently trimmed save. Pinned dials are marked with an accent dot on the toolbar group label. Only a chat with no session of its own is seeded — an existing chat carries its own mode/model in SQLite and loadSess() keeps winning. Tests: chat-defaults.test.js (46 assertions, pure resolver) and chat-defaults-api.test.js (42 assertions, real server in a throwaway APP_DIR). The falsyFallsThrough guard in config-resolve.test.js was split rather than weakened: flat keys still have their || / ?? operator read out of loadMergedConfig(), nested ones are checked behaviourally. --- CLAUDE.md | 41 ++++++- chat-defaults.js | 113 ++++++++++++++++++ config-resolve.js | 29 ++++- package.json | 2 +- public/index.html | 137 ++++++++++++++++++++-- server.js | 53 +++++++++ test/chat-defaults-api.test.js | 203 +++++++++++++++++++++++++++++++++ test/chat-defaults.test.js | 128 +++++++++++++++++++++ test/config-resolve.test.js | 20 +++- 9 files changed, 710 insertions(+), 16 deletions(-) create mode 100644 chat-defaults.js create mode 100644 test/chat-defaults-api.test.js create mode 100644 test/chat-defaults.test.js diff --git a/CLAUDE.md b/CLAUDE.md index b15dae7b..2f514c64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ docker compose up -d docker compose logs -f claude-chat ``` -No linting and no build step configured. `npm test` chains 61 test files under `test/`: 18 DOM-less render/UI-logic tests (`test/render/*.test.mjs`, run through `node --test`) plus 43 plain-`node` suites in `test/` covering the overload detector, env load order, multi-agent results, terminals, bots, telegram, updates, kanban scheduling, i18n completeness, the config precedence resolver plus its secret masking, usage-limit detection, the filesystem path guard (including the SVG sandbox header and the symlink rule on the `@`-mention search endpoints) plus the tunnel-blocks-terminal rule, WS session re-subscription, the SSH remote CLI-session import, the live engine pane / interactive-prompt watchdog, the cross-project global workspace aggregation, the rule that an SSH credential never leaves the server process, the Windows command-quoting oracle, the auth token lifecycle, the multi-agent dependency scheduler (waves, plan sanitising, and the rule that a failure warning must survive dep-context truncation), the SSH stream parser's three guards, the remote non-interactive shell environment (`remote-env.test.js`, which runs the generated prelude through real `bash -lc`: it must parse, print nothing on stdout, and never end on a false test — the caller chains `&& claude …` behind it), the remote CLI-list framing parser, the bot inbox's SQL seam (`bot-inbox.test.js` pins that `from_bot AS "from"` keeps the exact key `planInboxDelivery` reads — rename one without the other and every letter is silently retired as malformed), the one-time config/.env migration onto CCS_CONFIG_PATH and the mid-task clarification delivery contract on the subscription engine (`interrupt-delivery.test.js` — pins that the tmux injection block sits BEFORE the poll loop's completion `break`, that draining does not imply delivery, that a failed paste is re-queued and warns non-terminally, and that the task runner passes the same callbacks the chat path does), the CLAUDE.md / AGENTS.md discovery rules (`agents-md.test.js`, which also pins that AGENTS.md reaches the subprocess as `--append-system-prompt` and never as `--system-prompt`), and the remote file browser's three guard layers (`remote-files.test.js` runs the generated POSIX script through a real `/bin/sh` against a temp tree that contains symlinks OUT of the project; `remote-files-api.test.js` boots a server against a fake remote via `CCS_REMOTE_EXEC_HOOK` and drives `/api/files` the way the SPA does). On the render side, `tables.test.mjs` also pins the ReDoS bound in renderMd step 3.4, `xss.test.mjs` runs 24 adversarial payloads end-to-end, and `forged-tokens.test.mjs` covers the case where user text contains the renderer's own placeholder control bytes, and `pane-font.test.mjs` pins the clamp DIRECTION of `_fitEnginePaneFont` (a wide engine pane may only shrink; a narrow split pane must be allowed to grow). `script-scope.test.mjs` pins which `