@adhdev/daemon-core 0.9.82-rc.288 → 0.9.82-rc.289
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +993 -892
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1005 -904
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/fsm-types.d.ts +22 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/package.json +2 -2
- package/src/commands/router.ts +26 -0
- package/src/providers/spec/fsm-driver.ts +7 -0
- package/src/providers/spec/fsm-loader.ts +10 -0
- package/src/providers/spec/fsm-types.ts +23 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/types/interactive-prompt.ts +45 -3
|
@@ -58,6 +58,20 @@ export interface FsmTransition {
|
|
|
58
58
|
/** Human label for the debugger. */
|
|
59
59
|
label?: string;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Declarative "trust this folder before spawn" config. Some agent CLIs gate the
|
|
63
|
+
* first run in a new folder behind an interactive trust prompt and persist the
|
|
64
|
+
* answer as a string array in a JSON settings file. Declaring this lets the
|
|
65
|
+
* engine add the workspace path to that array before spawn so the prompt never
|
|
66
|
+
* appears — the robust alternative to detecting and auto-clicking the modal.
|
|
67
|
+
* CLIs without such a gate omit this field and the engine does nothing.
|
|
68
|
+
*/
|
|
69
|
+
export interface PreLaunchTrust {
|
|
70
|
+
/** Path to the CLI's JSON settings file. A leading `~` expands to $HOME. */
|
|
71
|
+
settings_path: string;
|
|
72
|
+
/** Key of the string-array of trusted folder paths within that file. */
|
|
73
|
+
key: string;
|
|
74
|
+
}
|
|
61
75
|
export interface CliSpecV4 {
|
|
62
76
|
$schema: 'adhdev:cli/spec@4';
|
|
63
77
|
id: string;
|
|
@@ -81,6 +95,14 @@ export interface CliSpecV4 {
|
|
|
81
95
|
send_on_spawn?: string[];
|
|
82
96
|
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
83
97
|
send_on_spawn_delay_ms?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Optional pre-spawn folder-trust step. When present, the engine adds the
|
|
100
|
+
* launch workspace path to the declared trusted-folders array before
|
|
101
|
+
* spawning, so a CLI that gates first run on a "trust this folder?" prompt
|
|
102
|
+
* (e.g. antigravity's `agy`) starts trusted and never blocks. Omitted for
|
|
103
|
+
* CLIs without such a gate. See pre-launch-trust.ts.
|
|
104
|
+
*/
|
|
105
|
+
pre_launch_trust?: PreLaunchTrust;
|
|
84
106
|
send_message: {
|
|
85
107
|
submit_key: string;
|
|
86
108
|
delay_ms_before_submit?: number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PreLaunchTrust } from './fsm-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Idempotently add `workingDir` (realpath) to the trusted-folders array named
|
|
4
|
+
* by `trust.key` inside the JSON settings file at `trust.settings_path`.
|
|
5
|
+
*
|
|
6
|
+
* - Creates the file (and parent dir) if missing.
|
|
7
|
+
* - Preserves all other settings; only the trust array is touched.
|
|
8
|
+
* - No-ops if the path is already present.
|
|
9
|
+
* - Best-effort: any failure is logged and swallowed. A failed pre-trust must
|
|
10
|
+
* not block the launch — the worst case is the old behavior (the FSM still
|
|
11
|
+
* detects the trust modal as an approval state), not a crash.
|
|
12
|
+
*
|
|
13
|
+
* Returns the path that was added (realpath), or null if nothing changed /
|
|
14
|
+
* an error occurred — purely so callers/tests can assert the effect.
|
|
15
|
+
*/
|
|
16
|
+
export declare function applyPreLaunchTrust(trust: PreLaunchTrust, workingDir: string): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.289",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.289",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -6965,6 +6965,32 @@ export class DaemonCommandRouter {
|
|
|
6965
6965
|
const mesh = meshRecord?.mesh;
|
|
6966
6966
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
6967
6967
|
|
|
6968
|
+
// Guard: refuse to remove the coordinator's OWN local base node
|
|
6969
|
+
// (same machine, NOT a worktree). Removing it breaks live mesh
|
|
6970
|
+
// membership — the coordinator can no longer be reached and has
|
|
6971
|
+
// to be restarted. Worktree clones are always safe to remove;
|
|
6972
|
+
// only the non-worktree node bound to this daemon is protected.
|
|
6973
|
+
// An explicit force:true overrides for intentional mesh teardown.
|
|
6974
|
+
if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
|
|
6975
|
+
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
|
|
6976
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
|
|
6977
|
+
const selfDaemonId = this.deps.statusInstanceId || '';
|
|
6978
|
+
const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
|
|
6979
|
+
const isCoordinatorBaseNode =
|
|
6980
|
+
(!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
|
|
6981
|
+
|| (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
|
|
6982
|
+
if (isCoordinatorBaseNode) {
|
|
6983
|
+
return {
|
|
6984
|
+
success: false,
|
|
6985
|
+
removed: false,
|
|
6986
|
+
code: 'mesh_remove_coordinator_base_node_protected',
|
|
6987
|
+
error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
|
|
6988
|
+
+ `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
|
|
6989
|
+
recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
|
|
6990
|
+
};
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
|
|
6968
6994
|
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
6969
6995
|
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
6970
6996
|
);
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
initialState, stateById, statusForState, outgoingTransitions,
|
|
35
35
|
} from './fsm-types.js';
|
|
36
36
|
import { loadFsmSpec } from './fsm-loader.js';
|
|
37
|
+
import { applyPreLaunchTrust } from './pre-launch-trust.js';
|
|
37
38
|
import type { Control, DelegateTrigger } from './types.js';
|
|
38
39
|
import { LOG } from '../../logging/logger.js';
|
|
39
40
|
|
|
@@ -245,6 +246,12 @@ export class FsmDriver implements ISpecDriver {
|
|
|
245
246
|
this.currentStateId = init.id;
|
|
246
247
|
this.stateEnteredAt = now;
|
|
247
248
|
this.prevStateAt = now;
|
|
249
|
+
// Pre-trust the workspace before spawning so a first-run folder-trust
|
|
250
|
+
// prompt never appears (best-effort; failures fall back to the FSM's
|
|
251
|
+
// trust-modal detection). Only runs for specs that declare it.
|
|
252
|
+
if (this.spec.pre_launch_trust) {
|
|
253
|
+
applyPreLaunchTrust(this.spec.pre_launch_trust, this.opts.workingDir);
|
|
254
|
+
}
|
|
248
255
|
this.adapter.start();
|
|
249
256
|
// Prime focus-gated TUIs (see CliSpecV4.send_on_spawn). Written once,
|
|
250
257
|
// shortly after spawn, so the input stream is awake before the first
|
|
@@ -36,6 +36,16 @@ export function validateFsmSpec(raw: unknown): string[] {
|
|
|
36
36
|
if (!spec.binary) errs.push('binary is required');
|
|
37
37
|
if (!spec.send_message?.submit_key) errs.push('send_message.submit_key is required');
|
|
38
38
|
|
|
39
|
+
if (spec.pre_launch_trust !== undefined) {
|
|
40
|
+
const t = spec.pre_launch_trust as { settings_path?: unknown; key?: unknown };
|
|
41
|
+
if (!t || typeof t !== 'object' || Array.isArray(t)) {
|
|
42
|
+
errs.push('pre_launch_trust must be an object');
|
|
43
|
+
} else {
|
|
44
|
+
if (typeof t.settings_path !== 'string' || !t.settings_path) errs.push('pre_launch_trust.settings_path is required');
|
|
45
|
+
if (typeof t.key !== 'string' || !t.key) errs.push('pre_launch_trust.key is required');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
if (!Array.isArray(spec.states) || spec.states.length === 0) {
|
|
40
50
|
errs.push('states[] must be a non-empty array');
|
|
41
51
|
return errs;
|
|
@@ -118,6 +118,21 @@ export interface FsmTransition {
|
|
|
118
118
|
label?: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Declarative "trust this folder before spawn" config. Some agent CLIs gate the
|
|
123
|
+
* first run in a new folder behind an interactive trust prompt and persist the
|
|
124
|
+
* answer as a string array in a JSON settings file. Declaring this lets the
|
|
125
|
+
* engine add the workspace path to that array before spawn so the prompt never
|
|
126
|
+
* appears — the robust alternative to detecting and auto-clicking the modal.
|
|
127
|
+
* CLIs without such a gate omit this field and the engine does nothing.
|
|
128
|
+
*/
|
|
129
|
+
export interface PreLaunchTrust {
|
|
130
|
+
/** Path to the CLI's JSON settings file. A leading `~` expands to $HOME. */
|
|
131
|
+
settings_path: string;
|
|
132
|
+
/** Key of the string-array of trusted folder paths within that file. */
|
|
133
|
+
key: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
137
|
// CliSpecV4 — the v4 runtime spec
|
|
123
138
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -145,6 +160,14 @@ export interface CliSpecV4 {
|
|
|
145
160
|
send_on_spawn?: string[];
|
|
146
161
|
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
147
162
|
send_on_spawn_delay_ms?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Optional pre-spawn folder-trust step. When present, the engine adds the
|
|
165
|
+
* launch workspace path to the declared trusted-folders array before
|
|
166
|
+
* spawning, so a CLI that gates first run on a "trust this folder?" prompt
|
|
167
|
+
* (e.g. antigravity's `agy`) starts trusted and never blocks. Omitted for
|
|
168
|
+
* CLIs without such a gate. See pre-launch-trust.ts.
|
|
169
|
+
*/
|
|
170
|
+
pre_launch_trust?: PreLaunchTrust;
|
|
148
171
|
send_message: {
|
|
149
172
|
submit_key: string;
|
|
150
173
|
delay_ms_before_submit?: number;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pre_launch_trust — generic, declarative "trust this folder before spawn"
|
|
3
|
+
* step for spec-backed CLI providers.
|
|
4
|
+
*
|
|
5
|
+
* Some agent CLIs (the canonical case is antigravity's `agy`) gate the first
|
|
6
|
+
* run in any new folder behind an interactive "Do you trust the files in this
|
|
7
|
+
* folder?" prompt. Under the v4 FSM spec path the daemon spawns the binary
|
|
8
|
+
* directly in the worktree (`cwd = workingDir`), so every fresh worktree —
|
|
9
|
+
* every delegated mesh task running in its own clone — hits that prompt and
|
|
10
|
+
* stalls until something clicks through it. (The legacy bash-wrapper symlink
|
|
11
|
+
* trick in provider.v1.json's `spawn` block is not used by SpecCliAdapter.)
|
|
12
|
+
*
|
|
13
|
+
* These CLIs persist their trusted folders in a JSON settings file as a string
|
|
14
|
+
* array. If we add the workspace path to that array *before* spawning, the
|
|
15
|
+
* prompt never appears. That is the most robust fix: the agent runs trusted
|
|
16
|
+
* from the first frame instead of relying on the FSM to detect and auto-click
|
|
17
|
+
* a modal whose wording or position could drift.
|
|
18
|
+
*
|
|
19
|
+
* The mechanism is intentionally data-driven and CLI-agnostic. A spec declares
|
|
20
|
+
* the settings file and the array key; the engine does the rest. CLIs that do
|
|
21
|
+
* not have a folder-trust gate simply omit `pre_launch_trust` and this code
|
|
22
|
+
* never runs for them — so other providers (claude/codex/hermes) are untouched.
|
|
23
|
+
*/
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as os from 'node:os';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import type { PreLaunchTrust } from './fsm-types.js';
|
|
30
|
+
import { LOG } from '../../logging/logger.js';
|
|
31
|
+
|
|
32
|
+
/** Expand a leading `~` to the user's home directory. */
|
|
33
|
+
function expandHome(p: string): string {
|
|
34
|
+
if (p === '~') return os.homedir();
|
|
35
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the canonical, real (symlink-followed) absolute form of the
|
|
41
|
+
* workspace path. Trust files store realpaths, and matching has to be exact, so
|
|
42
|
+
* we normalise the same way the CLI does. Falls back to the raw path if the
|
|
43
|
+
* directory can't be stat'd (e.g. it does not exist yet).
|
|
44
|
+
*/
|
|
45
|
+
function realWorkspacePath(workingDir: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return fs.realpathSync(workingDir);
|
|
48
|
+
} catch {
|
|
49
|
+
return path.resolve(workingDir);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Idempotently add `workingDir` (realpath) to the trusted-folders array named
|
|
55
|
+
* by `trust.key` inside the JSON settings file at `trust.settings_path`.
|
|
56
|
+
*
|
|
57
|
+
* - Creates the file (and parent dir) if missing.
|
|
58
|
+
* - Preserves all other settings; only the trust array is touched.
|
|
59
|
+
* - No-ops if the path is already present.
|
|
60
|
+
* - Best-effort: any failure is logged and swallowed. A failed pre-trust must
|
|
61
|
+
* not block the launch — the worst case is the old behavior (the FSM still
|
|
62
|
+
* detects the trust modal as an approval state), not a crash.
|
|
63
|
+
*
|
|
64
|
+
* Returns the path that was added (realpath), or null if nothing changed /
|
|
65
|
+
* an error occurred — purely so callers/tests can assert the effect.
|
|
66
|
+
*/
|
|
67
|
+
export function applyPreLaunchTrust(trust: PreLaunchTrust, workingDir: string): string | null {
|
|
68
|
+
const settingsPath = expandHome(trust.settings_path);
|
|
69
|
+
const key = trust.key;
|
|
70
|
+
const real = realWorkspacePath(workingDir);
|
|
71
|
+
try {
|
|
72
|
+
let parsed: Record<string, unknown> = {};
|
|
73
|
+
if (fs.existsSync(settingsPath)) {
|
|
74
|
+
const text = fs.readFileSync(settingsPath, 'utf8');
|
|
75
|
+
if (text.trim().length > 0) {
|
|
76
|
+
const json = JSON.parse(text);
|
|
77
|
+
if (json && typeof json === 'object' && !Array.isArray(json)) {
|
|
78
|
+
parsed = json as Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const existing = parsed[key];
|
|
84
|
+
const list: string[] = Array.isArray(existing)
|
|
85
|
+
? existing.filter((v): v is string => typeof v === 'string')
|
|
86
|
+
: [];
|
|
87
|
+
|
|
88
|
+
if (list.includes(real)) {
|
|
89
|
+
LOG.debug('pre-launch-trust', `[${trust.settings_path}] ${real} already trusted — no change`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
list.push(real);
|
|
94
|
+
parsed[key] = list;
|
|
95
|
+
|
|
96
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
98
|
+
LOG.info('pre-launch-trust', `pre-trusted workspace in ${trust.settings_path} (key="${key}")`);
|
|
99
|
+
return real;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
LOG.warn('pre-launch-trust', `failed to pre-trust workspace in ${trust.settings_path}: ${(err as Error).message}`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -137,7 +137,14 @@ export interface ClaudeInteractiveTuiPage {
|
|
|
137
137
|
header?: string;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
// Option rows look like "❯ 1. Label". The multi-select picker additionally
|
|
141
|
+
// draws a checkbox marker that can sit before or after the number
|
|
142
|
+
// ("❯ [ ] 1. Label" / "❯ 1. [x] Label" / "☐ 1. Label"); the optional,
|
|
143
|
+
// non-capturing checkbox groups absorb it so the captured label stays clean.
|
|
144
|
+
const CLAUDE_TUI_OPTION_CHECKBOX = '(?:\\[[ xX]\\]|[☐☒◻◼])';
|
|
145
|
+
const CLAUDE_TUI_OPTION_PATTERN = new RegExp(
|
|
146
|
+
`^\\s*(?:[❯›>]\\s*)?(?:${CLAUDE_TUI_OPTION_CHECKBOX}\\s*)?(\\d+)\\.\\s+(?:${CLAUDE_TUI_OPTION_CHECKBOX}\\s*)?(.+?)\\s*$`,
|
|
147
|
+
);
|
|
141
148
|
|
|
142
149
|
function claudeTuiQuestionHeaders(screenText: string): string[] {
|
|
143
150
|
const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
|
|
@@ -155,6 +162,41 @@ function isClaudeTuiSelectFooter(text: string): boolean {
|
|
|
155
162
|
return /Enter to select/i.test(text) && /Esc to cancel/i.test(text);
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Decide whether a captured claude-cli AskUserQuestion TUI page is multi-select.
|
|
167
|
+
*
|
|
168
|
+
* The original heuristic only matched the footer hint `/Space to select|toggle
|
|
169
|
+
* selections/i`. That string drifts between claude-cli versions, so when it
|
|
170
|
+
* changed the dashboard silently fell back to multiSelect:false and rendered
|
|
171
|
+
* single-select (radio) controls even though the on-screen picker showed
|
|
172
|
+
* checkboxes — the user could not check more than one box. (The CLI's own
|
|
173
|
+
* terminal still rendered `[ ]` correctly because it never depends on this
|
|
174
|
+
* parse.)
|
|
175
|
+
*
|
|
176
|
+
* Make detection robust by ALSO recognising the actual checkbox markers the
|
|
177
|
+
* multi-select picker draws on its option rows (`[ ]` / `[x]` / `☐` / `☒` /
|
|
178
|
+
* `◻` / `◼`). Single-select rows are drawn with a `❯`/number cursor only and
|
|
179
|
+
* carry none of these box glyphs, so their presence is a reliable signal. The
|
|
180
|
+
* broadened footer patterns ("Space to", "toggle", "select multiple") are kept
|
|
181
|
+
* as a secondary signal for layouts that render markers differently.
|
|
182
|
+
*/
|
|
183
|
+
function detectClaudeTuiMultiSelect(screenText: string): boolean {
|
|
184
|
+
if (/Space to (?:select|toggle)|toggle selection|select multiple|select all that apply/i.test(screenText)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// A checkbox glyph sitting in front of a NUMBERED option row only appears in
|
|
188
|
+
// the multi-select picker (e.g. "❯ [ ] 1. TypeScript" / "☐ 2. Python"). We
|
|
189
|
+
// require the numbered "N." option marker so we don't false-positive on the
|
|
190
|
+
// `✔ Submit` nav line (per-question answered-state ☐/☒) or on the headerless
|
|
191
|
+
// variant where the QUESTION line itself begins with `☐ ` (single-select).
|
|
192
|
+
const optionCheckbox = /^\s*(?:[❯›>]\s*)?(?:\[[ xX]\]|[☐☒◻◼])\s*\d+\.\s+\S/;
|
|
193
|
+
for (const line of screenText.split(/\r?\n/)) {
|
|
194
|
+
if (line.includes('✔ Submit')) continue; // header/nav line
|
|
195
|
+
if (optionCheckbox.test(line)) return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
158
200
|
function readClaudeHeaderLine(lines: string[], beforeIndex: number): string | undefined {
|
|
159
201
|
for (let i = beforeIndex; i >= 0; i -= 1) {
|
|
160
202
|
const candidate = lines[i].trim();
|
|
@@ -254,7 +296,7 @@ function parseClaudeHeaderlessInteractiveTuiQuestion(page: ClaudeInteractiveTuiP
|
|
|
254
296
|
questionId: `q${index + 1}`,
|
|
255
297
|
question,
|
|
256
298
|
...(header ? { header } : {}),
|
|
257
|
-
multiSelect:
|
|
299
|
+
multiSelect: detectClaudeTuiMultiSelect(page.screenText),
|
|
258
300
|
options,
|
|
259
301
|
...(allowFreeform ? { allowFreeform: true } : {}),
|
|
260
302
|
};
|
|
@@ -313,7 +355,7 @@ function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index
|
|
|
313
355
|
questionId: `q${index + 1}`,
|
|
314
356
|
question,
|
|
315
357
|
...(header ? { header } : {}),
|
|
316
|
-
multiSelect:
|
|
358
|
+
multiSelect: detectClaudeTuiMultiSelect(page.screenText),
|
|
317
359
|
options,
|
|
318
360
|
...(allowFreeform ? { allowFreeform: true } : {}),
|
|
319
361
|
};
|