@agentdeck/bridge 0.2.3 → 1.0.0
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/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/apme/collector.d.ts +4 -0
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +1 -0
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +30 -0
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +9 -0
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/index.d.ts.map +1 -1
- package/dist/apme/index.js +14 -3
- package/dist/apme/index.js.map +1 -1
- package/dist/apme/judge-detect.d.ts +26 -0
- package/dist/apme/judge-detect.d.ts.map +1 -0
- package/dist/apme/judge-detect.js +69 -0
- package/dist/apme/judge-detect.js.map +1 -0
- package/dist/apme/runner.d.ts +7 -0
- package/dist/apme/runner.d.ts.map +1 -1
- package/dist/apme/runner.js +188 -33
- package/dist/apme/runner.js.map +1 -1
- package/dist/apme/settings.d.ts +10 -8
- package/dist/apme/settings.d.ts.map +1 -1
- package/dist/apme/settings.js +11 -16
- package/dist/apme/settings.js.map +1 -1
- package/dist/awaiting-overlay.d.ts +17 -4
- package/dist/awaiting-overlay.d.ts.map +1 -1
- package/dist/awaiting-overlay.js +39 -8
- package/dist/awaiting-overlay.js.map +1 -1
- package/dist/bridge-core.d.ts +10 -0
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +50 -0
- package/dist/bridge-core.js.map +1 -1
- package/dist/claude-permission-rules.d.ts +55 -0
- package/dist/claude-permission-rules.d.ts.map +1 -0
- package/dist/claude-permission-rules.js +197 -0
- package/dist/claude-permission-rules.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +24 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +321 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +374 -26
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/esp32-serial.d.ts +30 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +148 -11
- package/dist/esp32-serial.js.map +1 -1
- package/dist/fallback-task-timeline.d.ts.map +1 -1
- package/dist/fallback-task-timeline.js +9 -2
- package/dist/fallback-task-timeline.js.map +1 -1
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +6 -5
- package/dist/hook-server.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +2 -2
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +3 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +6 -2
- package/dist/logger.js.map +1 -1
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +2 -4
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/d200h-module.d.ts +65 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +495 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/observed-steering.d.ts +93 -0
- package/dist/observed-steering.d.ts.map +1 -0
- package/dist/observed-steering.js +247 -0
- package/dist/observed-steering.js.map +1 -0
- package/dist/opencode-steering.d.ts +36 -0
- package/dist/opencode-steering.d.ts.map +1 -0
- package/dist/opencode-steering.js +84 -0
- package/dist/opencode-steering.js.map +1 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +48 -10
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +6 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +31 -1
- package/dist/passive-observer.js.map +1 -1
- package/dist/permission-resolver.d.ts +13 -5
- package/dist/permission-resolver.d.ts.map +1 -1
- package/dist/permission-resolver.js +18 -9
- package/dist/permission-resolver.js.map +1 -1
- package/dist/pixoo/micro-glyphs.d.ts +10 -41
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
- package/dist/pixoo/micro-glyphs.js +130 -263
- package/dist/pixoo/micro-glyphs.js.map +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
- package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
- package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
- package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +14 -6
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +63 -39
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +21 -2
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +401 -132
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts +24 -0
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +108 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/review-runner.d.ts +113 -0
- package/dist/review-runner.d.ts.map +1 -0
- package/dist/review-runner.js +428 -0
- package/dist/review-runner.js.map +1 -0
- package/dist/session-aggregator.d.ts +8 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-focus-relay.d.ts.map +1 -1
- package/dist/session-focus-relay.js +7 -1
- package/dist/session-focus-relay.js.map +1 -1
- package/dist/session-registry.d.ts +3 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +3 -0
- package/dist/session-registry.js.map +1 -1
- package/dist/timeline-store.d.ts +49 -0
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +175 -7
- package/dist/timeline-store.js.map +1 -1
- package/dist/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +14 -4
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +63 -13
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +7 -2
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +25 -2
- package/dist/usage-event.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -17
- package/dist/version-check.js.map +1 -1
- package/dist/ws-server.d.ts +0 -7
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +5 -20
- package/dist/ws-server.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steering state for observed (direct `claude`, no PTY) sessions.
|
|
3
|
+
*
|
|
4
|
+
* The daemon cannot type into an observed session's terminal, but Claude Code
|
|
5
|
+
* hooks form a synchronous RPC channel that supports three steering
|
|
6
|
+
* primitives, all resolved here:
|
|
7
|
+
*
|
|
8
|
+
* 1. Device approval — a held PreToolUse response (permission-resolver.ts)
|
|
9
|
+
* answers a genuine permission prompt with allow/deny. Gate eligibility
|
|
10
|
+
* is decided by `shouldHoldPreToolUse` below (precision-first: every
|
|
11
|
+
* uncertainty resolves to "don't hold" — see claude-permission-rules.ts).
|
|
12
|
+
* 2. Soft STOP — a stop flag consumed by the next PreToolUse, which returns
|
|
13
|
+
* `deny` with an instruction to halt. Not an instant Ctrl+C (pure text
|
|
14
|
+
* generation runs to the next tool call), but a real stop at the next
|
|
15
|
+
* tool boundary.
|
|
16
|
+
* 3. Turn-end directives — prompts queued while the session is processing,
|
|
17
|
+
* delivered by the Stop hook as `{decision:'block', reason}` so Claude
|
|
18
|
+
* continues with the queued instruction instead of ending the turn.
|
|
19
|
+
* Bounded by design: each Stop drains at most one directive, the queue
|
|
20
|
+
* is hard-capped, and an empty queue always lets the turn end (no
|
|
21
|
+
* stop_hook_active loop).
|
|
22
|
+
*
|
|
23
|
+
* Keyed by the Claude session UUID (same key as awaiting-overlay.ts). Lives
|
|
24
|
+
* outside the state machine for the same reason: per-session attribution.
|
|
25
|
+
*/
|
|
26
|
+
import { randomUUID } from 'crypto';
|
|
27
|
+
import { evaluatePermissionRules, isNeverPromptTool, isPromptProneTool, } from './claude-permission-rules.js';
|
|
28
|
+
import { shouldGatePreToolUse } from './awaiting-overlay.js';
|
|
29
|
+
import { debug } from './logger.js';
|
|
30
|
+
const STOP_FLAG_TTL_MS = 10 * 60_000; // stale STOP must not deny a tool an hour later
|
|
31
|
+
const DIRECTIVE_TTL_MS = 60 * 60_000;
|
|
32
|
+
const DIRECTIVE_QUEUE_CAP = 3;
|
|
33
|
+
/** After a hold releases undecided, how long we correlate PostToolUse-without-
|
|
34
|
+
* Notification to learn "this signature was auto-approved" (session
|
|
35
|
+
* "always allow" answers live only in Claude's memory — this is the only way
|
|
36
|
+
* to see them). */
|
|
37
|
+
const ASK_RELEASE_LEARN_WINDOW_MS = 8_000;
|
|
38
|
+
const sessions = new Map();
|
|
39
|
+
function ses(sid) {
|
|
40
|
+
let s = sessions.get(sid);
|
|
41
|
+
if (!s) {
|
|
42
|
+
s = { directives: [], suppressed: new Set(), recentAskReleases: [] };
|
|
43
|
+
sessions.set(sid, s);
|
|
44
|
+
}
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
/** Bash signature = first two command tokens (the granularity of Claude's own
|
|
48
|
+
* "always allow `git push`"-style session approvals); other tools = tool name. */
|
|
49
|
+
export function gateSignature(tool, toolInput) {
|
|
50
|
+
if (tool === 'Bash' && typeof toolInput?.command === 'string') {
|
|
51
|
+
const head = toolInput.command.trim().split(/\s+/).slice(0, 2).join(' ');
|
|
52
|
+
return `Bash|${head}`;
|
|
53
|
+
}
|
|
54
|
+
return tool;
|
|
55
|
+
}
|
|
56
|
+
/** Human-readable question for the gate's awaiting overlay — device-native
|
|
57
|
+
* semantics ("Allow Bash: git push …?"), never a fabricated mirror of the
|
|
58
|
+
* TUI prompt's option labels. Overlay caps length at 120 chars. */
|
|
59
|
+
export function buildGateQuestion(tool, toolInput) {
|
|
60
|
+
const preview = tool === 'Bash' && typeof toolInput?.command === 'string' ? toolInput.command
|
|
61
|
+
: typeof toolInput?.file_path === 'string' ? toolInput.file_path
|
|
62
|
+
: typeof toolInput?.url === 'string' ? toolInput.url : '';
|
|
63
|
+
return preview ? `Allow ${tool}: ${preview}` : `Allow ${tool}?`;
|
|
64
|
+
}
|
|
65
|
+
// ─── Soft STOP ───
|
|
66
|
+
export function requestStop(sid) {
|
|
67
|
+
ses(sid).stopRequestedAt = Date.now();
|
|
68
|
+
debug('steering', `stop requested for ${sid}`);
|
|
69
|
+
}
|
|
70
|
+
export function clearStop(sid) {
|
|
71
|
+
const s = sessions.get(sid);
|
|
72
|
+
if (!s || s.stopRequestedAt === undefined)
|
|
73
|
+
return false;
|
|
74
|
+
s.stopRequestedAt = undefined;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
export function isStopRequested(sid) {
|
|
78
|
+
const s = sessions.get(sid);
|
|
79
|
+
if (!s || s.stopRequestedAt === undefined)
|
|
80
|
+
return false;
|
|
81
|
+
if (Date.now() - s.stopRequestedAt > STOP_FLAG_TTL_MS) {
|
|
82
|
+
s.stopRequestedAt = undefined;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
/** One-shot consume by the PreToolUse deny path. */
|
|
88
|
+
export function consumeStop(sid) {
|
|
89
|
+
if (!isStopRequested(sid))
|
|
90
|
+
return false;
|
|
91
|
+
ses(sid).stopRequestedAt = undefined;
|
|
92
|
+
debug('steering', `stop consumed by PreToolUse deny for ${sid}`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
export const STOP_DENY_REASON = 'AgentDeck: the user pressed STOP on their AgentDeck controller. '
|
|
96
|
+
+ 'Halt the current work now, briefly summarize where you left off, and wait '
|
|
97
|
+
+ 'for the user\'s next instruction. Do not start new tool calls.';
|
|
98
|
+
// ─── Turn-end directive queue ───
|
|
99
|
+
export function queueDirective(sid, text) {
|
|
100
|
+
const trimmed = text.trim();
|
|
101
|
+
if (!trimmed)
|
|
102
|
+
return false;
|
|
103
|
+
const s = ses(sid);
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
s.directives = s.directives.filter((d) => now - d.ts < DIRECTIVE_TTL_MS);
|
|
106
|
+
if (s.directives.length >= DIRECTIVE_QUEUE_CAP)
|
|
107
|
+
return false;
|
|
108
|
+
s.directives.push({ text: trimmed, ts: now });
|
|
109
|
+
debug('steering', `directive queued for ${sid}: "${trimmed.slice(0, 60)}" (${s.directives.length} queued)`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/** Pop exactly one directive (Stop hook drains one per turn end). A pending
|
|
113
|
+
* STOP outranks directives: stopping wins, the queue is discarded. */
|
|
114
|
+
export function takeDirective(sid) {
|
|
115
|
+
const s = sessions.get(sid);
|
|
116
|
+
if (!s)
|
|
117
|
+
return undefined;
|
|
118
|
+
if (isStopRequested(sid)) {
|
|
119
|
+
s.directives = [];
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
s.directives = s.directives.filter((d) => now - d.ts < DIRECTIVE_TTL_MS);
|
|
124
|
+
return s.directives.shift()?.text;
|
|
125
|
+
}
|
|
126
|
+
export function queuedDirectiveCount(sid) {
|
|
127
|
+
const s = sessions.get(sid);
|
|
128
|
+
if (!s)
|
|
129
|
+
return 0;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
s.directives = s.directives.filter((d) => now - d.ts < DIRECTIVE_TTL_MS);
|
|
132
|
+
return s.directives.length;
|
|
133
|
+
}
|
|
134
|
+
/** User re-engaged in the terminal — their own prompt supersedes anything the
|
|
135
|
+
* deck queued, and a pending STOP is moot. */
|
|
136
|
+
export function clearOnUserPrompt(sid) {
|
|
137
|
+
const s = sessions.get(sid);
|
|
138
|
+
if (!s)
|
|
139
|
+
return false;
|
|
140
|
+
const had = s.directives.length > 0 || s.stopRequestedAt !== undefined;
|
|
141
|
+
s.directives = [];
|
|
142
|
+
s.stopRequestedAt = undefined;
|
|
143
|
+
return had;
|
|
144
|
+
}
|
|
145
|
+
export function clearSession(sid) {
|
|
146
|
+
sessions.delete(sid);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Should this PreToolUse be held for device approval? Precision-first: hold
|
|
150
|
+
* ONLY when every check says Claude would genuinely prompt the user. Any
|
|
151
|
+
* uncertainty → pass through untouched (Claude's normal flow, zero latency).
|
|
152
|
+
*/
|
|
153
|
+
export function shouldHoldPreToolUse(ctx) {
|
|
154
|
+
if (!ctx.enabled)
|
|
155
|
+
return { hold: false, reason: 'disabled' };
|
|
156
|
+
if (ctx.clientCount < 1)
|
|
157
|
+
return { hold: false, reason: 'no clients' };
|
|
158
|
+
if (!ctx.tool)
|
|
159
|
+
return { hold: false, reason: 'no tool name' };
|
|
160
|
+
if (ctx.tool.startsWith('mcp__'))
|
|
161
|
+
return { hold: false, reason: 'mcp tool (trust state unknown)' };
|
|
162
|
+
if (isNeverPromptTool(ctx.tool))
|
|
163
|
+
return { hold: false, reason: 'never-prompt tool' };
|
|
164
|
+
if (!isPromptProneTool(ctx.tool))
|
|
165
|
+
return { hold: false, reason: 'not prompt-prone' };
|
|
166
|
+
if (!shouldGatePreToolUse(ctx.permissionMode, ctx.tool)) {
|
|
167
|
+
return { hold: false, reason: `permission_mode=${ctx.permissionMode ?? 'default'} auto-approves` };
|
|
168
|
+
}
|
|
169
|
+
const s = ses(ctx.sessionId);
|
|
170
|
+
const signature = gateSignature(ctx.tool, ctx.toolInput);
|
|
171
|
+
if (s.suppressed.has(signature))
|
|
172
|
+
return { hold: false, reason: 'signature learned auto-approved' };
|
|
173
|
+
if (s.heldRequestId)
|
|
174
|
+
return { hold: false, reason: 'another gate already held' };
|
|
175
|
+
const verdict = evaluatePermissionRules(ctx.tool, ctx.toolInput, ctx.cwd);
|
|
176
|
+
if (verdict === 'unknown')
|
|
177
|
+
return { hold: false, reason: 'settings unreadable' };
|
|
178
|
+
if (verdict === 'deny')
|
|
179
|
+
return { hold: false, reason: 'deny rule may match' };
|
|
180
|
+
if (verdict === 'allow')
|
|
181
|
+
return { hold: false, reason: 'allow rule may match' };
|
|
182
|
+
const requestId = randomUUID();
|
|
183
|
+
s.heldRequestId = requestId;
|
|
184
|
+
return { hold: true, requestId, reason: verdict === 'ask' ? 'ask rule matches' : 'prompt-prone, no rule match' };
|
|
185
|
+
}
|
|
186
|
+
/** The held gate resolved (device decision, timeout, sweep). `undecided` marks
|
|
187
|
+
* a pass-through release, which arms the auto-approval learner below. */
|
|
188
|
+
export function gateReleased(sid, requestId, opts) {
|
|
189
|
+
const s = sessions.get(sid);
|
|
190
|
+
if (!s)
|
|
191
|
+
return;
|
|
192
|
+
if (s.heldRequestId === requestId)
|
|
193
|
+
s.heldRequestId = undefined;
|
|
194
|
+
if (opts.undecided) {
|
|
195
|
+
s.recentAskReleases.push({
|
|
196
|
+
tool: opts.tool,
|
|
197
|
+
signature: gateSignature(opts.tool, opts.toolInput),
|
|
198
|
+
ts: Date.now(),
|
|
199
|
+
});
|
|
200
|
+
if (s.recentAskReleases.length > 8)
|
|
201
|
+
s.recentAskReleases.shift();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** A permission_prompt Notification arrived — every recent undecided release
|
|
205
|
+
* was a GENUINE prompt, so nothing should be learned as auto-approved. */
|
|
206
|
+
export function notePermissionPromptShown(sid) {
|
|
207
|
+
const s = sessions.get(sid);
|
|
208
|
+
if (s)
|
|
209
|
+
s.recentAskReleases = [];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* PostToolUse arrived. If a recent undecided release matches this tool and no
|
|
213
|
+
* permission_prompt Notification came in between, Claude auto-approved it
|
|
214
|
+
* (session-scoped "always allow" we cannot read) — suppress the signature so
|
|
215
|
+
* it is never held again this session.
|
|
216
|
+
*/
|
|
217
|
+
export function noteToolEnd(sid, tool) {
|
|
218
|
+
const s = sessions.get(sid);
|
|
219
|
+
if (!s || !tool || s.recentAskReleases.length === 0)
|
|
220
|
+
return;
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const kept = [];
|
|
223
|
+
for (const r of s.recentAskReleases) {
|
|
224
|
+
if (now - r.ts > ASK_RELEASE_LEARN_WINDOW_MS)
|
|
225
|
+
continue; // expired
|
|
226
|
+
if (r.tool === tool) {
|
|
227
|
+
s.suppressed.add(r.signature);
|
|
228
|
+
debug('steering', `learned auto-approved signature for ${sid}: ${r.signature}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
kept.push(r);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
s.recentAskReleases = kept;
|
|
235
|
+
}
|
|
236
|
+
/** Enrichment snapshot for sessions_list (devices render STOPPING / queue badges). */
|
|
237
|
+
export function steeringSnapshot(sid) {
|
|
238
|
+
return {
|
|
239
|
+
stopRequested: isStopRequested(sid),
|
|
240
|
+
queuedDirectives: queuedDirectiveCount(sid),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Test helper. */
|
|
244
|
+
export function _resetSteering() {
|
|
245
|
+
sessions.clear();
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=observed-steering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observed-steering.js","sourceRoot":"","sources":["../src/observed-steering.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command queue for observed (standalone TUI) OpenCode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Unlike Claude Code, OpenCode gives its observer plugin a full in-process
|
|
5
|
+
* server client — so steering is DIRECT, not hook-mediated: the AgentDeck
|
|
6
|
+
* plugin (hooks/src/opencode-install.ts) long-polls
|
|
7
|
+
* `GET /opencode/commands?sid=<sessionID>` and executes what it receives via
|
|
8
|
+
* the SDK (`session.abort`, `session.prompt`). That makes interrupt immediate
|
|
9
|
+
* and prompt injection possible even while the session is idle — near-managed
|
|
10
|
+
* steering with zero PTY.
|
|
11
|
+
*
|
|
12
|
+
* The daemon side is deliberately dumb: a bounded per-session FIFO plus
|
|
13
|
+
* long-poll waiters. Commands are enqueued from device session_command
|
|
14
|
+
* routing; the plugin drains them within one poll round-trip.
|
|
15
|
+
*/
|
|
16
|
+
export interface OpenCodeCommand {
|
|
17
|
+
type: 'interrupt' | 'send_prompt' | 'permission_respond';
|
|
18
|
+
text?: string;
|
|
19
|
+
/** permission_respond: the OpenCode permission id (from permission.asked). */
|
|
20
|
+
permissionId?: string;
|
|
21
|
+
/** permission_respond: device decision; the plugin maps allow→"once", deny→"reject". */
|
|
22
|
+
response?: 'allow' | 'deny';
|
|
23
|
+
}
|
|
24
|
+
/** Queue a command; wakes a pending long-poll immediately. Returns false when
|
|
25
|
+
* the queue is full (device keeps its button semantics honest via this). */
|
|
26
|
+
export declare function enqueueOpenCodeCommand(sid: string, cmd: OpenCodeCommand): boolean;
|
|
27
|
+
/** Long-poll: resolve immediately when commands are queued, otherwise hold up
|
|
28
|
+
* to waitMs and resolve with an empty batch. */
|
|
29
|
+
export declare function pollOpenCodeCommands(sid: string, waitMs: number): Promise<OpenCodeCommand[]>;
|
|
30
|
+
/** Is a plugin actively polling this session (steerable right now)? */
|
|
31
|
+
export declare function isOpenCodeSteerable(sid: string, now?: number): boolean;
|
|
32
|
+
/** Release all waiters (daemon shutdown). */
|
|
33
|
+
export declare function drainOpenCodeSteering(): void;
|
|
34
|
+
/** Test helper. */
|
|
35
|
+
export declare function _resetOpenCodeSteering(): void;
|
|
36
|
+
//# sourceMappingURL=opencode-steering.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-steering.d.ts","sourceRoot":"","sources":["../src/opencode-steering.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,GAAG,aAAa,GAAG,oBAAoB,CAAC;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC7B;AA0BD;6EAC6E;AAC7E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAWjF;AAED;iDACiD;AACjD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5F;AAED,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAKlF;AAED,6CAA6C;AAC7C,wBAAgB,qBAAqB,IAAI,IAAI,CAQ5C;AAED,mBAAmB;AACnB,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command queue for observed (standalone TUI) OpenCode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Unlike Claude Code, OpenCode gives its observer plugin a full in-process
|
|
5
|
+
* server client — so steering is DIRECT, not hook-mediated: the AgentDeck
|
|
6
|
+
* plugin (hooks/src/opencode-install.ts) long-polls
|
|
7
|
+
* `GET /opencode/commands?sid=<sessionID>` and executes what it receives via
|
|
8
|
+
* the SDK (`session.abort`, `session.prompt`). That makes interrupt immediate
|
|
9
|
+
* and prompt injection possible even while the session is idle — near-managed
|
|
10
|
+
* steering with zero PTY.
|
|
11
|
+
*
|
|
12
|
+
* The daemon side is deliberately dumb: a bounded per-session FIFO plus
|
|
13
|
+
* long-poll waiters. Commands are enqueued from device session_command
|
|
14
|
+
* routing; the plugin drains them within one poll round-trip.
|
|
15
|
+
*/
|
|
16
|
+
const QUEUE_CAP = 8;
|
|
17
|
+
const queues = new Map();
|
|
18
|
+
function entry(sid) {
|
|
19
|
+
let e = queues.get(sid);
|
|
20
|
+
if (!e) {
|
|
21
|
+
e = { commands: [], waiters: [], lastPollAt: 0 };
|
|
22
|
+
queues.set(sid, e);
|
|
23
|
+
}
|
|
24
|
+
return e;
|
|
25
|
+
}
|
|
26
|
+
/** Queue a command; wakes a pending long-poll immediately. Returns false when
|
|
27
|
+
* the queue is full (device keeps its button semantics honest via this). */
|
|
28
|
+
export function enqueueOpenCodeCommand(sid, cmd) {
|
|
29
|
+
const e = entry(sid);
|
|
30
|
+
if (e.commands.length >= QUEUE_CAP)
|
|
31
|
+
return false;
|
|
32
|
+
e.commands.push(cmd);
|
|
33
|
+
const waiter = e.waiters.shift();
|
|
34
|
+
if (waiter) {
|
|
35
|
+
clearTimeout(waiter.timer);
|
|
36
|
+
const cmds = e.commands.splice(0, e.commands.length);
|
|
37
|
+
waiter.resolve(cmds);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Long-poll: resolve immediately when commands are queued, otherwise hold up
|
|
42
|
+
* to waitMs and resolve with an empty batch. */
|
|
43
|
+
export function pollOpenCodeCommands(sid, waitMs) {
|
|
44
|
+
const e = entry(sid);
|
|
45
|
+
e.lastPollAt = Date.now();
|
|
46
|
+
if (e.commands.length > 0) {
|
|
47
|
+
return Promise.resolve(e.commands.splice(0, e.commands.length));
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const waiter = {
|
|
51
|
+
resolve,
|
|
52
|
+
timer: setTimeout(() => {
|
|
53
|
+
const idx = e.waiters.indexOf(waiter);
|
|
54
|
+
if (idx >= 0)
|
|
55
|
+
e.waiters.splice(idx, 1);
|
|
56
|
+
resolve([]);
|
|
57
|
+
}, Math.max(1_000, Math.min(50_000, waitMs))),
|
|
58
|
+
};
|
|
59
|
+
e.waiters.push(waiter);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Is a plugin actively polling this session (steerable right now)? */
|
|
63
|
+
export function isOpenCodeSteerable(sid, now = Date.now()) {
|
|
64
|
+
const e = queues.get(sid);
|
|
65
|
+
if (!e)
|
|
66
|
+
return false;
|
|
67
|
+
// A live long-poll re-arms at least every wait window + margin.
|
|
68
|
+
return e.waiters.length > 0 || now - e.lastPollAt < 40_000;
|
|
69
|
+
}
|
|
70
|
+
/** Release all waiters (daemon shutdown). */
|
|
71
|
+
export function drainOpenCodeSteering() {
|
|
72
|
+
for (const e of queues.values()) {
|
|
73
|
+
for (const w of e.waiters.splice(0)) {
|
|
74
|
+
clearTimeout(w.timer);
|
|
75
|
+
w.resolve([]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
queues.clear();
|
|
79
|
+
}
|
|
80
|
+
/** Test helper. */
|
|
81
|
+
export function _resetOpenCodeSteering() {
|
|
82
|
+
drainOpenCodeSteering();
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=opencode-steering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-steering.js","sourceRoot":"","sources":["../src/opencode-steering.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AA8ItC,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,WAAW,CAA8C;IACjE,OAAO,CAAC,WAAW,CAAuB;IAC1C,2EAA2E;IAC3E,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAG1C,OAAO,CAAC,aAAa,CAAS;IAE9B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAA8C;IAErE,OAAO,CAAC,oBAAoB,CAA8C;IAC1E,OAAO,CAAC,mBAAmB,CAAuB;IAElD,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,WAAW,CAAM;IAGzB,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,SAAS,CAAuB;IAExC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAqB3B,OAAO,CAAC,WAAW;IAmCnB,0FAA0F;IAC1F,OAAO,CAAC,eAAe;IAqEvB,wDAAwD;IACxD,OAAO,CAAC,kBAAkB;IAgE1B,6CAA6C;IAC7C,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;IAoTtB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IA+BtB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,eAAe;IA6BvB,OAAO,CAAC,cAAc;IAuCtB,OAAO,CAAC,mBAAmB;IAmB3B,iFAAiF;IACjF,oBAAoB,IAAI,IAAI;IAa5B,sEAAsE;IACtE,OAAO,CAAC,sBAAsB;IAuB9B;;;sFAGkF;IAClF,OAAO,CAAC,0BAA0B;IAYlC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,mBAAmB;IAsC3B,8DAA8D;IAC9D,OAAO,CAAC,gBAAgB;IAoBxB,qDAAqD;IACrD,OAAO,CAAC,aAAa;IAYrB,+FAA+F;IAC/F,OAAO,CAAC,mBAAmB;IAK3B,oFAAoF;IACpF,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,YAAY;IAuJpB,OAAO,CAAC,qBAAqB;IAO7B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwDxB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,gBAAgB;IAIxB,kGAAkG;IAClG,wBAAwB,IAAI,IAAI;IAOhC,OAAO,CAAC,wBAAwB;IAIhC;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOnC,cAAc,IAAI,MAAM,GAAG,IAAI;IAC/B,YAAY,IAAI,MAAM,GAAG,IAAI;IAC7B,cAAc,IAAI,MAAM,GAAG,IAAI;IAE/B,KAAK,IAAI,IAAI;CAyBd"}
|
package/dist/output-parser.js
CHANGED
|
@@ -11,6 +11,19 @@ const DIFF_PROMPT = /\(V\)iew diff.*\(A\)pply.*\(D\)eny|\(a\)pply.*\(d\)eny.*\(v
|
|
|
11
11
|
// ANSI stripping can remove spaces (e.g. "❯3.Haiku" instead of "❯ 3. Haiku")
|
|
12
12
|
const OPTION_NUMBERED = /^\s*❯?\s*\d{1,2}[.)]\s*.+/m;
|
|
13
13
|
const OPTION_BULLET = /^\s*[►▸●○]\s+.+/m;
|
|
14
|
+
// Bounded window (chars) the option-block parser scans back over. Must fit a tall
|
|
15
|
+
// AskUserQuestion prompt (question + per-option descriptions + trailing
|
|
16
|
+
// affordances) once oversized rules are collapsed (see BOX_RULE_RUN). The
|
|
17
|
+
// backward block-scan + longest-run filter in parseOptions keep stale numbered
|
|
18
|
+
// lists from earlier in the buffer out, so extra headroom is safe.
|
|
19
|
+
const OPTION_SCAN_WINDOW = 3000;
|
|
20
|
+
// Oversized full-width rule. Claude Code's AskUserQuestion draws a horizontal rule
|
|
21
|
+
// (─────) between options; ANSI cursor positioning strips its newline, so hundreds
|
|
22
|
+
// of box-drawing chars (U+2500–U+257F, optionally interspersed with the spaces the
|
|
23
|
+
// cursor-forward→space step inserts) get glued onto the adjacent option label AND
|
|
24
|
+
// exhaust OPTION_SCAN_WINDOW — dropping the real leading options. Collapse any long
|
|
25
|
+
// run to a short newline-delimited rule.
|
|
26
|
+
const BOX_RULE_RUN = /(?:[─-╿][ \t]*){12,}/g;
|
|
14
27
|
// Claude Code uses ❯ as its prompt char. May have \u00A0 (nbsp) or spaces around it.
|
|
15
28
|
// v2.1.49+: autocomplete suggestions appear on the same line (e.g. "❯ Try "refactor..."")
|
|
16
29
|
// so we can't require end-of-line. Just check for ❯ at start of line.
|
|
@@ -167,7 +180,12 @@ export class OutputParser extends EventEmitter {
|
|
|
167
180
|
.replace(/\x1b\[\d*(?:;\d*)?[Hf]/g, '\n') // CUP/HVP → newline
|
|
168
181
|
.replace(/\x1b\[\d*[ABEF]/g, '\n'); // CUU/CUD/CNL/CPL → newline
|
|
169
182
|
const clean = stripAnsi(spaced);
|
|
170
|
-
|
|
183
|
+
// Collapse oversized box-drawing rules (AskUserQuestion full-width separators)
|
|
184
|
+
// before buffering. A newline-stripped rule otherwise glues hundreds of box
|
|
185
|
+
// chars onto the adjacent option label AND exhausts the bounded option-scan
|
|
186
|
+
// window, dropping the real leading options. Raw `clean` is still used for the
|
|
187
|
+
// chunk-level pattern triggers below.
|
|
188
|
+
this.buffer += clean.replace(BOX_RULE_RUN, '\n──\n');
|
|
171
189
|
if (this.buffer.length > 8192) {
|
|
172
190
|
this.buffer = this.buffer.slice(-4096);
|
|
173
191
|
}
|
|
@@ -441,7 +459,7 @@ export class OutputParser extends EventEmitter {
|
|
|
441
459
|
// numbered/bulleted lines, box-drawing chrome, cursor overwrite, and
|
|
442
460
|
// recommended/selected, and block-scopes so unrelated numbered response
|
|
443
461
|
// text isn't pulled in.
|
|
444
|
-
const rich = this.parseOptions(this.buffer.slice(-
|
|
462
|
+
const rich = this.parseOptions(this.buffer.slice(-OPTION_SCAN_WINDOW));
|
|
445
463
|
if (rich.options.length >= 2) {
|
|
446
464
|
const options = rich.options.map(opt => ({
|
|
447
465
|
...opt,
|
|
@@ -496,7 +514,7 @@ export class OutputParser extends EventEmitter {
|
|
|
496
514
|
this.resetOptionTimer();
|
|
497
515
|
this.optionTimer = setTimeout(() => {
|
|
498
516
|
this.optionTimer = null;
|
|
499
|
-
const parsed = this.parseOptions(this.buffer.slice(-
|
|
517
|
+
const parsed = this.parseOptions(this.buffer.slice(-OPTION_SCAN_WINDOW));
|
|
500
518
|
if (parsed.options.length > 0) {
|
|
501
519
|
// Check if this looks like a permission prompt (Yes/No style from tool approval)
|
|
502
520
|
if (this.looksLikePermission(parsed.options) && !this.isCursorSelectionUI()) {
|
|
@@ -547,7 +565,7 @@ export class OutputParser extends EventEmitter {
|
|
|
547
565
|
this.resetOptionTimer();
|
|
548
566
|
this.optionTimer = setTimeout(() => {
|
|
549
567
|
this.optionTimer = null;
|
|
550
|
-
const parsed = this.parseOptions(this.buffer.slice(-
|
|
568
|
+
const parsed = this.parseOptions(this.buffer.slice(-OPTION_SCAN_WINDOW));
|
|
551
569
|
if (parsed.navigable) {
|
|
552
570
|
// Options still present — emit cursor_update if index changed
|
|
553
571
|
if (parsed.cursorIndex !== this.lastCursorIndex) {
|
|
@@ -580,7 +598,7 @@ export class OutputParser extends EventEmitter {
|
|
|
580
598
|
this.resetOptionTimer();
|
|
581
599
|
this.optionTimer = setTimeout(() => {
|
|
582
600
|
this.optionTimer = null;
|
|
583
|
-
const parsed = this.parseOptions(this.buffer.slice(-
|
|
601
|
+
const parsed = this.parseOptions(this.buffer.slice(-OPTION_SCAN_WINDOW));
|
|
584
602
|
if (parsed.navigable && parsed.cursorIndex !== this.lastCursorIndex) {
|
|
585
603
|
this.lastCursorIndex = parsed.cursorIndex;
|
|
586
604
|
debug('Parser', `EMIT cursor_update (ANSI reposition): cursorIndex=${parsed.cursorIndex}`);
|
|
@@ -988,15 +1006,20 @@ export class OutputParser extends EventEmitter {
|
|
|
988
1006
|
// Blank or separator line — always tolerate (TUI redraws create variable blank runs)
|
|
989
1007
|
blockStart--;
|
|
990
1008
|
}
|
|
991
|
-
else if (foundOption && /^\s/.test(line)) {
|
|
992
|
-
//
|
|
1009
|
+
else if (foundOption && (/^\s/.test(line) || optLineRe.test(allLines[blockStart - 2] ?? ''))) {
|
|
1010
|
+
// Option description between options. Two shapes: classic INDENTED text,
|
|
1011
|
+
// OR an UNINDENTED (col 0) description sitting directly under its option —
|
|
1012
|
+
// which is how Claude Code's AskUserQuestion renders them. For the
|
|
1013
|
+
// unindented case we require an option on the line directly above, so the
|
|
1014
|
+
// scan can't bridge into a stale numbered list separated from the real
|
|
1015
|
+
// prompt by prose (e.g. "Would you like to proceed?"). Tolerate within limit.
|
|
993
1016
|
descGap++;
|
|
994
1017
|
if (descGap > MAX_DESC_GAP)
|
|
995
1018
|
break;
|
|
996
1019
|
blockStart--;
|
|
997
1020
|
}
|
|
998
1021
|
else {
|
|
999
|
-
// Unindented
|
|
1022
|
+
// Unindented prose (not a description) or no option seen yet — hard boundary.
|
|
1000
1023
|
break;
|
|
1001
1024
|
}
|
|
1002
1025
|
}
|
|
@@ -1005,14 +1028,22 @@ export class OutputParser extends EventEmitter {
|
|
|
1005
1028
|
let cursorIndex = 0;
|
|
1006
1029
|
// Use a Map keyed by index so later (newer) lines overwrite earlier (stale) ones
|
|
1007
1030
|
const byIndex = new Map();
|
|
1031
|
+
// Indices whose option carried the ❯ cursor — authoritative for the ACTIVE
|
|
1032
|
+
// prompt. A ❯-marked option must never be clobbered by a later non-cursor
|
|
1033
|
+
// line at the same index (e.g. a stale markdown "1. …" from chat scrollback
|
|
1034
|
+
// that got scanned into this block).
|
|
1035
|
+
const cursorLocked = new Set();
|
|
1008
1036
|
for (const line of lines) {
|
|
1009
1037
|
const hasCursor = /^\s*❯/.test(line);
|
|
1010
1038
|
const nm = line.match(/^\s*❯?\s*(\d{1,2})[.)]\s*(.+)/);
|
|
1011
1039
|
if (nm) {
|
|
1012
1040
|
const idx = parseInt(nm[1], 10) - 1;
|
|
1041
|
+
if (!hasCursor && cursorLocked.has(idx))
|
|
1042
|
+
continue;
|
|
1013
1043
|
if (hasCursor) {
|
|
1014
1044
|
navigable = true;
|
|
1015
1045
|
cursorIndex = idx;
|
|
1046
|
+
cursorLocked.add(idx);
|
|
1016
1047
|
}
|
|
1017
1048
|
let raw = nm[2].trim();
|
|
1018
1049
|
// Strip TUI footer text concatenated after last option (no newline from cursor positioning)
|
|
@@ -1085,8 +1116,15 @@ export class OutputParser extends EventEmitter {
|
|
|
1085
1116
|
}
|
|
1086
1117
|
if (currentRun.length > bestRun.length)
|
|
1087
1118
|
bestRun = currentRun;
|
|
1088
|
-
// Re-index to 0-based
|
|
1089
|
-
|
|
1119
|
+
// Re-index to 0-based ONLY when the leading options were genuinely truncated
|
|
1120
|
+
// off the top (run doesn't start at option 0 — e.g. a long list scrolled so
|
|
1121
|
+
// "1." was cut off). When option 0 is present we keep the true parsed indices
|
|
1122
|
+
// so `select_option` targets the right cursor row — never silently renumber a
|
|
1123
|
+
// trailing subset as 1..N when the real leading options exist but were dropped.
|
|
1124
|
+
const runStart = bestRun.length > 0 ? bestRun[0].index : 0;
|
|
1125
|
+
const contiguous = runStart > 0
|
|
1126
|
+
? bestRun.map((opt, i) => ({ ...opt, index: i }))
|
|
1127
|
+
: bestRun.map((opt) => ({ ...opt }));
|
|
1090
1128
|
const finalOptions = contiguous.length >= 2 ? contiguous : sorted;
|
|
1091
1129
|
return { options: finalOptions, navigable, cursorIndex };
|
|
1092
1130
|
}
|