@agentdeck/bridge 0.2.3 → 1.0.1

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.
Files changed (188) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/apme/antigravity-transcript.d.ts.map +1 -1
  3. package/dist/apme/antigravity-transcript.js +16 -3
  4. package/dist/apme/antigravity-transcript.js.map +1 -1
  5. package/dist/apme/collector.d.ts +4 -0
  6. package/dist/apme/collector.d.ts.map +1 -1
  7. package/dist/apme/collector.js +1 -0
  8. package/dist/apme/collector.js.map +1 -1
  9. package/dist/apme/dashboard-html.d.ts.map +1 -1
  10. package/dist/apme/dashboard-html.js +30 -0
  11. package/dist/apme/dashboard-html.js.map +1 -1
  12. package/dist/apme/http.d.ts.map +1 -1
  13. package/dist/apme/http.js +9 -0
  14. package/dist/apme/http.js.map +1 -1
  15. package/dist/apme/index.d.ts.map +1 -1
  16. package/dist/apme/index.js +14 -3
  17. package/dist/apme/index.js.map +1 -1
  18. package/dist/apme/judge-detect.d.ts +26 -0
  19. package/dist/apme/judge-detect.d.ts.map +1 -0
  20. package/dist/apme/judge-detect.js +69 -0
  21. package/dist/apme/judge-detect.js.map +1 -0
  22. package/dist/apme/runner.d.ts +7 -0
  23. package/dist/apme/runner.d.ts.map +1 -1
  24. package/dist/apme/runner.js +188 -33
  25. package/dist/apme/runner.js.map +1 -1
  26. package/dist/apme/settings.d.ts +10 -8
  27. package/dist/apme/settings.d.ts.map +1 -1
  28. package/dist/apme/settings.js +11 -16
  29. package/dist/apme/settings.js.map +1 -1
  30. package/dist/awaiting-overlay.d.ts +17 -4
  31. package/dist/awaiting-overlay.d.ts.map +1 -1
  32. package/dist/awaiting-overlay.js +39 -8
  33. package/dist/awaiting-overlay.js.map +1 -1
  34. package/dist/bridge-core.d.ts +10 -0
  35. package/dist/bridge-core.d.ts.map +1 -1
  36. package/dist/bridge-core.js +50 -0
  37. package/dist/bridge-core.js.map +1 -1
  38. package/dist/claude-permission-rules.d.ts +55 -0
  39. package/dist/claude-permission-rules.d.ts.map +1 -0
  40. package/dist/claude-permission-rules.js +197 -0
  41. package/dist/claude-permission-rules.js.map +1 -0
  42. package/dist/cli.d.ts.map +1 -1
  43. package/dist/cli.js +10 -1
  44. package/dist/cli.js.map +1 -1
  45. package/dist/d200h/hid-protocol.d.ts +62 -0
  46. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  47. package/dist/d200h/hid-protocol.js +124 -0
  48. package/dist/d200h/hid-protocol.js.map +1 -0
  49. package/dist/d200h/image-renderer.d.ts +24 -0
  50. package/dist/d200h/image-renderer.d.ts.map +1 -0
  51. package/dist/d200h/image-renderer.js +321 -0
  52. package/dist/d200h/image-renderer.js.map +1 -0
  53. package/dist/daemon-server.d.ts.map +1 -1
  54. package/dist/daemon-server.js +374 -26
  55. package/dist/daemon-server.js.map +1 -1
  56. package/dist/daemon.js +1 -1
  57. package/dist/esp32-serial.d.ts +40 -0
  58. package/dist/esp32-serial.d.ts.map +1 -1
  59. package/dist/esp32-serial.js +203 -11
  60. package/dist/esp32-serial.js.map +1 -1
  61. package/dist/fallback-task-timeline.d.ts.map +1 -1
  62. package/dist/fallback-task-timeline.js +9 -2
  63. package/dist/fallback-task-timeline.js.map +1 -1
  64. package/dist/hook-server.d.ts.map +1 -1
  65. package/dist/hook-server.js +6 -5
  66. package/dist/hook-server.js.map +1 -1
  67. package/dist/idotmatrix/idotmatrix-daemon-sync.js +2 -2
  68. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/logger.d.ts +3 -1
  73. package/dist/logger.d.ts.map +1 -1
  74. package/dist/logger.js +6 -2
  75. package/dist/logger.js.map +1 -1
  76. package/dist/model-catalog.d.ts +1 -1
  77. package/dist/model-catalog.d.ts.map +1 -1
  78. package/dist/model-catalog.js +2 -4
  79. package/dist/model-catalog.js.map +1 -1
  80. package/dist/modules/d200h-module.d.ts +65 -0
  81. package/dist/modules/d200h-module.d.ts.map +1 -0
  82. package/dist/modules/d200h-module.js +495 -0
  83. package/dist/modules/d200h-module.js.map +1 -0
  84. package/dist/modules/d200h-renderer.d.ts +7 -0
  85. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  86. package/dist/modules/d200h-renderer.js +321 -0
  87. package/dist/modules/d200h-renderer.js.map +1 -0
  88. package/dist/modules/trmnl-module.d.ts +38 -0
  89. package/dist/modules/trmnl-module.d.ts.map +1 -0
  90. package/dist/modules/trmnl-module.js +111 -0
  91. package/dist/modules/trmnl-module.js.map +1 -0
  92. package/dist/observed-steering.d.ts +93 -0
  93. package/dist/observed-steering.d.ts.map +1 -0
  94. package/dist/observed-steering.js +247 -0
  95. package/dist/observed-steering.js.map +1 -0
  96. package/dist/opencode-steering.d.ts +36 -0
  97. package/dist/opencode-steering.d.ts.map +1 -0
  98. package/dist/opencode-steering.js +84 -0
  99. package/dist/opencode-steering.js.map +1 -0
  100. package/dist/output-parser.d.ts.map +1 -1
  101. package/dist/output-parser.js +48 -10
  102. package/dist/output-parser.js.map +1 -1
  103. package/dist/passive-observer.d.ts +6 -0
  104. package/dist/passive-observer.d.ts.map +1 -1
  105. package/dist/passive-observer.js +31 -1
  106. package/dist/passive-observer.js.map +1 -1
  107. package/dist/permission-resolver.d.ts +13 -5
  108. package/dist/permission-resolver.d.ts.map +1 -1
  109. package/dist/permission-resolver.js +18 -9
  110. package/dist/permission-resolver.js.map +1 -1
  111. package/dist/pixoo/micro-glyphs.d.ts +10 -41
  112. package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
  113. package/dist/pixoo/micro-glyphs.js +130 -263
  114. package/dist/pixoo/micro-glyphs.js.map +1 -1
  115. package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
  116. package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
  117. package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
  118. package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
  119. package/dist/pixoo/pixoo-bridge.d.ts +14 -6
  120. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  121. package/dist/pixoo/pixoo-bridge.js +63 -39
  122. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  123. package/dist/pixoo/pixoo-renderer.d.ts +21 -2
  124. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  125. package/dist/pixoo/pixoo-renderer.js +401 -132
  126. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  127. package/dist/pixoo/pixoo-sprites.d.ts +24 -0
  128. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  129. package/dist/pixoo/pixoo-sprites.js +108 -0
  130. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  131. package/dist/review-runner.d.ts +113 -0
  132. package/dist/review-runner.d.ts.map +1 -0
  133. package/dist/review-runner.js +428 -0
  134. package/dist/review-runner.js.map +1 -0
  135. package/dist/session-aggregator.d.ts +8 -0
  136. package/dist/session-aggregator.d.ts.map +1 -1
  137. package/dist/session-aggregator.js.map +1 -1
  138. package/dist/session-focus-relay.d.ts.map +1 -1
  139. package/dist/session-focus-relay.js +7 -1
  140. package/dist/session-focus-relay.js.map +1 -1
  141. package/dist/session-registry.d.ts +3 -0
  142. package/dist/session-registry.d.ts.map +1 -1
  143. package/dist/session-registry.js +3 -0
  144. package/dist/session-registry.js.map +1 -1
  145. package/dist/timeline-store.d.ts +49 -0
  146. package/dist/timeline-store.d.ts.map +1 -1
  147. package/dist/timeline-store.js +175 -7
  148. package/dist/timeline-store.js.map +1 -1
  149. package/dist/trmnl/byos-server.d.ts +39 -0
  150. package/dist/trmnl/byos-server.d.ts.map +1 -0
  151. package/dist/trmnl/byos-server.js +301 -0
  152. package/dist/trmnl/byos-server.js.map +1 -0
  153. package/dist/trmnl/frame-cache.d.ts +37 -0
  154. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  155. package/dist/trmnl/frame-cache.js +157 -0
  156. package/dist/trmnl/frame-cache.js.map +1 -0
  157. package/dist/trmnl/image-renderer.d.ts +27 -0
  158. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  159. package/dist/trmnl/image-renderer.js +235 -0
  160. package/dist/trmnl/image-renderer.js.map +1 -0
  161. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  162. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  163. package/dist/trmnl/trmnl-settings.js +177 -0
  164. package/dist/trmnl/trmnl-settings.js.map +1 -0
  165. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  166. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  167. package/dist/trmnl/trmnl-telemetry.js +56 -0
  168. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  169. package/dist/tui/dashboard.d.ts.map +1 -1
  170. package/dist/tui/dashboard.js +14 -4
  171. package/dist/tui/dashboard.js.map +1 -1
  172. package/dist/tui/renderer.d.ts.map +1 -1
  173. package/dist/tui/renderer.js +63 -13
  174. package/dist/tui/renderer.js.map +1 -1
  175. package/dist/tui/terrarium.d.ts.map +1 -1
  176. package/dist/tui/terrarium.js +7 -2
  177. package/dist/tui/terrarium.js.map +1 -1
  178. package/dist/usage-event.d.ts.map +1 -1
  179. package/dist/usage-event.js +43 -2
  180. package/dist/usage-event.js.map +1 -1
  181. package/dist/version-check.d.ts.map +1 -1
  182. package/dist/version-check.js +1 -17
  183. package/dist/version-check.js.map +1 -1
  184. package/dist/ws-server.d.ts +0 -7
  185. package/dist/ws-server.d.ts.map +1 -1
  186. package/dist/ws-server.js +5 -20
  187. package/dist/ws-server.js.map +1 -1
  188. 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;AA+HtC,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;IA8BnB,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;IAoIpB,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"}
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"}
@@ -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
- this.buffer += clean;
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(-2000));
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(-2000));
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(-2000));
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(-2000));
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
- // Indented text line likely option description, tolerate within limit
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 text or no options found yet — hard block boundary
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 for downstream consumers
1089
- const contiguous = bestRun.map((opt, i) => ({ ...opt, index: i }));
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
  }