chore: one changeset for the release, and no new em-dashes

Consolidates the four contributor changesets, and rewords the em-dashes this release
added in comments and docs. The five in test/settings-save-failure-ui.test.ts stay:
they assert existing toast text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-04 17:31:48 +01:00
1 parent eb3b08d58b
commit 4960bee779
36 files changed
+117 -106

No files matched your search

+1 -1
View File
@@ -84,7 +84,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
⚠️ **`sendEnterKey()` MUST go through `terminal._core.coreService.triggerDataEvent('\r', true)`** — not `sendInput()`, and never a raw POST to `/api/sessions/:id/input`. `localEchoEnabled` defaults to `MobileDetection.isTouchDevice()`, so on every phone the characters you type are buffered in the `LocalEchoOverlay` and have **never reached the PTY**; the `onData` Enter branch in terminal-ui.js is what flushes `pendingText` first and only then sends `\r` (after an 80ms delay so text lands first). Sending a bare `\r` submits an empty line and strands the typed text on screen, so the button looks dead. Replaying the keypress reuses the overlay flush, the flushed-offset cleanup and the ordering instead of reimplementing them. `KeyboardAccessory.sendKey()` is for escape sequences (arrows/Esc) and is the WRONG template to copy for input.
⚠️ **Skin overrides outrank plain class rules.** `styles.css` nests its skin block inside `html:not([data-skin="og"]) { … }`, so a bare `.btn-toolbar` rule in there resolves to specificity **(0,2,1)** and beats a `.btn-toolbar.btn-x` rule **(0,2,0)** in `mobile.css` regardless of load order. Toolbar-button colors set from mobile.css therefore need `!important` — that is why mobile.css leans on it so heavily. Symptom: only your `!important` properties land and everything else silently renders in generic toolbar grey. Press feedback on the phone bars sidesteps this: the `:active` rules for `.btn-toolbar`, the mobile icon buttons and `.accessory-btn` (mobile.css) use `transform` and `filter` only, which nothing on these buttons sets at a higher specificity, so one rule reads the same on every skin; a background `:active` lost to the skin block on all but og, and an inset `box-shadow` lost to the skin hover rules, which is why neither is used. The light skins darken instead of brightening, and `prefers-reduced-motion` drops the scale.
⚠️ **Skin overrides outrank plain class rules.** `styles.css` nests its skin block inside `html:not([data-skin="og"]) { … }`, so a bare `.btn-toolbar` rule in there resolves to specificity **(0,2,1)** and beats a `.btn-toolbar.btn-x` rule **(0,2,0)** in `mobile.css` regardless of load order. Toolbar-button colors set from mobile.css therefore need `!important`: that is why mobile.css leans on it so heavily. Symptom: only your `!important` properties land and everything else silently renders in generic toolbar grey. Press feedback on the phone bars sidesteps this: the `:active` rules for `.btn-toolbar`, the mobile icon buttons and `.accessory-btn` (mobile.css) use `transform` and `filter` only, which nothing on these buttons sets at a higher specificity, so one rule reads the same on every skin; a background `:active` lost to the skin block on all but og, and an inset `box-shadow` lost to the skin hover rules, which is why neither is used. The light skins darken instead of brightening, and `prefers-reduced-motion` drops the scale.
**Connection-loss UI** (`computeConnectionLossUi()` in constants.js, writer `_updateConnectionLossUi()` in app.js): the service worker serves the cached app shell, so an unreachable server (phone off the tailnet, VPN down, server stopped) used to render a normal-looking empty dashboard whose only tell was the 8px header dot, which reads as "no sessions", not "no connection". Two surfaces now: a full-screen **overlay** while no server state has loaded this page load (nothing behind it is worth preserving), and a non-blocking **banner** once it has (the terminal scrollback stays readable). ⚠️ A **2.5s grace** is load-bearing: a COM deploy restarts the server and SSE is back in ~200ms, and a banner on every deploy trains the user to ignore it. `navigator.onLine === false` skips the grace, since that is never a blip. Retry re-arms SSE **and** the terminal WS (`planWsReconnect` can 'give-up', and the SSE backoff caps at 30s).
+1 -1
View File
@@ -119,7 +119,7 @@ function breathe(): Promise<void> {
* Bash candidates whose probed file is a regular file with an mtime inside the call's window
* (files claude sent to the user skip the window: they exist and were shown on purpose).
* A candidate whose file version is already recorded is skipped: `known(path)` is the newest
* entry time for that path, and an entry at or after `mtime − slack` saw this version — the
* entry time for that path, and an entry at or after `mtime − slack` saw this version: the
* `ls *.png` right after the render, or a Write followed by a listing. Candidates are taken
* in call order so a batch dedupes itself the same way.
*/
+3 -3
View File
@@ -1013,7 +1013,7 @@ Object.assign(CodemanApp.prototype, {
* Resolves the raw route payload rather than a model id so the caller can tell the
* three answers apart: llama-swap naming a model; `isLlamaSwap: false`, which is what
* the route answers for EVERY failure it catches (a server without a `/running`
* route, a refused or unresolvable endpoint, a 401 from a wrong key) — safe to
* route, a refused or unresolvable endpoint, a 401 from a wrong key): safe to
* explain in the UI as long as the wording also fits an endpoint that is down; and
* `null`, only when Codeman's own request to the route failed or the client-side
* bound below won the race, which stays silent, since the endpoint may merely have
@@ -1175,7 +1175,7 @@ Object.assign(CodemanApp.prototype, {
})
.join('');
modal.classList.add('active');
// A fresh open always starts unfiltered with the caret in the search box — the
// A fresh open always starts unfiltered with the caret in the search box: the
// list was just rebuilt, so clearing the input is the whole reset; focus is the
// user's next keystroke away. Inside the generation guard above, so a
// superseded probe can neither focus nor clear a modal that now shows another
@@ -1203,7 +1203,7 @@ Object.assign(CodemanApp.prototype, {
* query restores the list byte for byte. ⚠️ `.run-mode-option` carries its own
* `display: flex` (styles.css, restated in mobile.css), which outranks the UA
* `[hidden]` rule, so styles.css re-asserts `display: none !important` for
* these rows — without it the filter would silently do nothing.
* these rows: without it the filter would silently do nothing.
*/
filterCustomModelPick(value) {
const list = document.getElementById('customModelPickList');
+2 -2
View File
@@ -7815,7 +7815,7 @@ html[data-skin="og"] .welcome-primary:hover {
}
/* The picker's search box reuses .session-sidebar-filter-input's theme-token
styling (same reasoning as the .set-scope pill above) — only its own spacing
styling (same reasoning as the .set-scope pill above): only its own spacing
and the no-matches line are added here. 0.35rem is the standard gap the
modal's neighbouring form fields already use (`.form-hint`'s own margin-top):
the global `* { margin: 0 }` reset left the box sitting flush under the CLI
@@ -22681,7 +22681,7 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
/* ── Edit history: header button + panel (edit-history-ui.js) ───────────────── */
/* "Edit history" header button — opt-in (App Settings → Header & Panels), hidden by default.
/* "Edit history" header button: opt-in (App Settings → Header & Panels), hidden by default.
Same marker pattern as the Attachments button: a base inline-flex !important so an inline
style can't override it, and a more-specific marker rule to hide. Phones hide it regardless
(mobile.css). */
+4 -4
View File
@@ -364,7 +364,7 @@ async function discoverModels(
// itself calls loaded. Listed figures above are unaffected: reading them triggers nothing.
const hasStatusField = entries.some((m) => m && typeof m === 'object' && 'status' in m);
const slotCounts: Record<string, number> = {};
// Ids whose figure THIS round's loaded-model probe answered — the listed figure remains
// Ids whose figure THIS round's loaded-model probe answered: the listed figure remains
// the source only for the ids NOT in this set (see the per-slot correction below).
const ctxFromProbe = new Set<string>();
if (hasStatusField) {
@@ -412,13 +412,13 @@ async function discoverModels(
}
}
// llama-swap documents the LISTED `context_length` as the PER-SLOT window, so whenever
// the listed figure is the one taken this round — the model was not loaded, or the
// probe answered nothing — its slot count is 1 by definition: the floor check must NOT
// the listed figure is the one taken this round (the model was not loaded, or the
// probe answered nothing), its slot count is 1 by definition: the floor check must NOT
// divide a per-slot figure a second time. Without recording 1 here, the count a loaded
// round stored survives the merge (applyDiscoveredModels keeps what a later round does
// not overwrite): 262144 @ 4 slots stored, model unloaded, listed 65536 → 65536 with 4
// slots → a false 16384-per-slot warning. Even a count this round's launch command
// parsed must yield — pairing a per-slot listed figure with the cmd's --parallel is the
// parsed must yield: pairing a per-slot listed figure with the cmd's --parallel is the
// same double division. Same rule the /props branch above applies. Ids whose figure the
// probe DID answer keep their probe-derived count untouched.
for (const id of Object.keys(contextLengths)) {
+2 -2
View File
@@ -1535,7 +1535,7 @@ export class WebServer extends EventEmitter {
// already fired once) and the session-env real-home lookup: the injected VALUES are
// never persisted, so after a real restart the session can no longer carry its
// allowlisted `DSH_HOME` either (`getEnvOverridesForPersist()` keeps only
// `CLAUDE_CODE_*`/`CLAUDE_CONFIG_DIR`) — the lookup could only ever hit on the first
// `CLAUDE_CODE_*`/`CLAUDE_CONFIG_DIR`): the lookup could only ever hit on the first
// apply in-process. Real-home resolution therefore falls back to the SERVER's
// `DSH_HOME` or the recipe's `realHomeFallback` (~/.dsh).
return applyCustomModelInjection(
@@ -1556,7 +1556,7 @@ export class WebServer extends EventEmitter {
* bookkeeping must come from the CURRENT recipe, not from the saved record: this is the
* first feature where a shipped recipe changed KIND (deepseek: env redirect -> per-session
* patch home), so a selection saved by an old build restores with e.g. no `configDir`
* while the new recipe writes one — and clearing it later could then never delete the
* while the new recipe writes one, and clearing it later could then never delete the
* generated dir. `envKeys` is the UNION of saved and applied keys: the tmux session may
* still carry `setenv`s from both, and a clear has to unset every one.
*/