fix(cli-registry): merge-time fixes for the run-menu consolidation (#458)

- test/opencode-resize.test.ts: retarget the launcher guard at the real code (this.selectSession(firstSessionId), any this.activeSessionId assignment) with an anti-vacuity check; the old strings existed nowhere, so it could never fail
- session-ui.js: restore as comments the two invariants the merged bodies lost (deepseek leaves statusReporting unset, i.e. ON; no effort field for external CLIs, it is Claude-specific)
- docs/cli-registry.md: move the frontend-guard paragraph below the two backend-guard paragraphs so they keep their antecedent, and note the widened comparison shape
- test/frontend-cli-no-id-branching.test.ts: the comparison shape accepts any left-hand identifier (const m = this._runMode; m === 'codex' was invisible), normalized to `mode`; the two `m !== 'shell'` display filters are allowlisted and the remaining blind spots documented
- test/run-mode-dispatch.test.ts: table-driven pin of run() dispatch (claude to runClaude, each RUN_MODE_LAUNCH id to _runCliMode(id), shell to runShell, unknown to runClaude, lock held and released)
- CLAUDE.md: name the second CI-gated guard next to the backend one
- server.ts: every </head> injection passes a replacer function; a clis.json label containing $' re-injected the rest of the document past escapeScriptJson (two render tests pin it, proven failing on the string form)
- _isAltCliMode(): no reference anywhere in the tree, nothing to fix

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 1ea363ff808a62861559bc141e724b163cc1c56e)
This commit is contained in:
Codeman maintainer
2026-09-21 04:37:45 +02:00
parent d3f2ec0220
commit 0f955327b2
8 changed files with 280 additions and 23 deletions
+9
View File
@@ -105,6 +105,11 @@ const RUN_MODE_LAUNCH = {
// sibling Run button sends its bypass switch. The harness has no bypass
// FLAG, so this rides the `DSH_PERMISSION_MODE` export instead, and the
// multi-user clamp forces it back down to `workspace-write` server-side.
//
// `statusReporting` is deliberately LEFT UNSET, i.e. ON: it is what upgrades
// this mode from output-stabilization guessing to definitive idle/blocked
// hook events (the harness reports to Codeman as its supervisor, see
// deepseek-status-shim.ts). Never send `statusReporting: false` from here.
buildConfig: () => ({ deepSeekConfig: { permissionMode: 'danger-full-access' } }),
},
};
@@ -2157,6 +2162,10 @@ Object.assign(CodemanApp.prototype, {
const globalSettings = this.loadAppSettingsFromStorage();
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
// No `effort` field for ANY entry in RUN_MODE_LAUNCH: effort is
// Claude-specific (runClaude() alone sends it, and the backend turns it
// into `claude --settings`); none of these CLIs has an /effort. Each of
// the eight bodies this launcher replaced carried that rule as a comment.
const firstSessionId = await this._launchQuickStartInstances(
caseName,
tabCount,
+16 -5
View File
@@ -1581,9 +1581,17 @@ export class WebServer extends EventEmitter {
// the /session/:id URL path; this global is a belt-and-suspenders fallback.
// The id is gated to JSON + <-escaped so it can't break out of the inline
// <script> (ids are UUIDs in practice, but defense-in-depth is cheap).
//
// Every `</head>` injection below passes a replacer FUNCTION, never a
// replacement STRING: `String.replace` interprets `$&`, `$'`, `` $` `` and
// `$<n>` inside a string replacement, so a payload carrying `$'` would splice
// the rest of the document (the whole <body>) into the inline script, past
// any escaping applied to the payload itself. The custom-model list below
// carries a user-settable `label` (clis.json), which is the site that made
// this real; the others follow the same rule so the class of bug stays out.
if (soloSessionId) {
const safeId = JSON.stringify(soloSessionId).replace(/</g, '\\u003c');
html = html.replace('</head>', `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
html = html.replace('</head>', () => `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
}
// Gesture-control overlay (Phase 5): dashboard only (not solo popups, which
// have no tab strip). `CODEMAN_GESTURE=1` makes the feature *available* on
@@ -1655,7 +1663,7 @@ export class WebServer extends EventEmitter {
};
html = html.replace(
'</head>',
`<script>window.__codemanCliAvailable=${JSON.stringify(available)};</script>\n</head>`
() => `<script>window.__codemanCliAvailable=${JSON.stringify(available)};</script>\n</head>`
);
// Which run modes the Run-menu picker (docs/custom-model-endpoints-plan.md) may
// generate an entry for: read generically off the registry's `capabilities`
@@ -1672,16 +1680,19 @@ export class WebServer extends EventEmitter {
const customModelClisJson = escapeScriptJson(JSON.stringify(customModelClis));
html = html.replace(
'</head>',
`<script>window.__codemanCustomModelClis=${customModelClisJson};</script>\n</head>`
() => `<script>window.__codemanCustomModelClis=${customModelClisJson};</script>\n</head>`
);
}
if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
html = html.replace('</head>', `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
html = html.replace('</head>', () => `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
if (settings.gestureControlEnabled === true) {
const v = this.gestureBundleVersion();
// Relative src so the injected `<base href>` resolves it under the mount
// prefix (a root-absolute `/gesture/...` would escape a sub-path mount).
html = html.replace('</head>', `<script type="module" src="gesture/gesture-codeman.js${v}"></script>\n</head>`);
html = html.replace(
'</head>',
() => `<script type="module" src="gesture/gesture-codeman.js${v}"></script>\n</head>`
);
}
}
return html;