@rind-ai/cli 0.4.1 → 0.6.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.
Files changed (49) hide show
  1. package/bin/rind.js +5 -5
  2. package/lib/assistant-renderer.js +179 -265
  3. package/lib/choice-menu-state.js +46 -46
  4. package/lib/cli-input-actions.js +548 -0
  5. package/lib/cli-output-controller.js +460 -0
  6. package/lib/cli-runtime-controller.js +350 -0
  7. package/lib/cli-state-store.js +32 -0
  8. package/lib/cli-state.js +41 -0
  9. package/lib/command-controller.js +159 -126
  10. package/lib/compact-context-state.js +22 -22
  11. package/lib/components/assistant-message.js +169 -0
  12. package/lib/components/composer-area.js +25 -0
  13. package/lib/components/dynamic-block.js +20 -0
  14. package/lib/components/monitor-stack.js +35 -0
  15. package/lib/components/text-block.js +47 -0
  16. package/lib/components/tool-block.js +122 -0
  17. package/lib/composer-terminal.js +224 -203
  18. package/lib/event-controller.js +243 -242
  19. package/lib/frontend-cli-implementation.js +656 -1111
  20. package/lib/input-controller.js +75 -94
  21. package/lib/input-errors.js +3 -3
  22. package/lib/interrupt-state.js +9 -9
  23. package/lib/line-editor.js +541 -541
  24. package/lib/local-slash-commands.js +217 -0
  25. package/lib/markdown-lines.js +103 -0
  26. package/lib/model-menu-state.js +50 -50
  27. package/lib/one-shot-progress.js +145 -0
  28. package/lib/one-shot.js +228 -0
  29. package/lib/question-menu-state.js +61 -0
  30. package/lib/rendering.js +1309 -1060
  31. package/lib/runtime-client.js +241 -193
  32. package/lib/runtime-env.js +21 -21
  33. package/lib/runtime-protocol.js +122 -15
  34. package/lib/slash-command-mode.js +0 -11
  35. package/lib/slash-menu-state.js +59 -59
  36. package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
  37. package/lib/terminal-key.js +97 -97
  38. package/lib/text-width.js +335 -151
  39. package/lib/theme-menu-state.js +31 -0
  40. package/lib/theme.js +134 -0
  41. package/lib/tool-display.js +675 -0
  42. package/lib/tui/component.js +55 -0
  43. package/lib/tui/cursor.js +29 -0
  44. package/lib/tui/input-buffer.js +172 -0
  45. package/lib/tui/tui.js +591 -0
  46. package/lib/turn-controller.js +68 -78
  47. package/package.json +28 -28
  48. package/lib/assistant-stream-buffer.js +0 -25
  49. package/lib/terminal-ui.js +0 -581
@@ -1,307 +1,429 @@
1
- import { backgroundMonitorText } from "./rendering.js";
2
-
3
- export function createBackgroundController({
4
- request,
5
- state,
6
- redraw = () => {},
7
- log = () => {},
8
- terminalUi = false,
9
- }) {
10
- const tasks = new Map();
11
- const pendingCommands = new Map();
12
- let refreshTimer = null;
13
- let listInFlight = null;
14
- let monitorTimer = null;
1
+ import { backgroundMonitorText, delegateMonitorText, taskMonitorTabs } from "./rendering.js";
2
+ import { runtimeMethods } from "./runtime-protocol.js";
3
+
4
+ const PAGES = ["background", "delegates"];
5
+
6
+ export function createTaskMonitorController({
7
+ request,
8
+ state,
9
+ redraw = () => {},
10
+ log = () => {},
11
+ terminalUi = false,
12
+ }) {
13
+ const tasks = new Map();
14
+ const pendingCommands = new Map();
15
+ const delegates = new Map();
16
+ let refreshTimer = null;
17
+ let listInFlight = null;
18
+ let monitorTimer = null;
15
19
  let monitorPollInFlight = false;
16
20
  let monitor = null;
17
21
  let monitorInputWasActive = false;
18
-
19
- function refresh() {
20
- if (!terminalUi || state.runtimeClosing) {
21
- return Promise.resolve();
22
- }
23
- if (listInFlight) {
24
- return listInFlight;
25
- }
26
- listInFlight = request("background.list")
22
+ let generation = 0;
23
+ let pollToken = 0;
24
+
25
+ function refresh() {
26
+ if (!terminalUi || state.runtimeClosing) {
27
+ return Promise.resolve();
28
+ }
29
+ if (listInFlight) {
30
+ return listInFlight;
31
+ }
32
+ const requestGeneration = generation;
33
+ const promise = request(runtimeMethods.backgroundList)
27
34
  .then((result) => {
28
- const listed = Array.isArray(result?.tasks) ? result.tasks : [];
29
- const ids = new Set();
30
- for (const task of listed) {
31
- const bgId = String(task?.bg_id || "").trim();
32
- if (!bgId) {
33
- continue;
34
- }
35
- ids.add(bgId);
36
- tasks.set(bgId, { ...(tasks.get(bgId) || {}), ...task, bg_id: bgId });
37
- }
38
- for (const bgId of tasks.keys()) {
39
- if (!ids.has(bgId) && tasks.get(bgId)?.status === "running") {
40
- tasks.delete(bgId);
41
- }
35
+ if (requestGeneration !== generation) {
36
+ return;
42
37
  }
38
+ const listed = Array.isArray(result?.tasks) ? result.tasks : [];
39
+ const ids = new Set();
40
+ for (const task of listed) {
41
+ const bgId = String(task?.bg_id || "").trim();
42
+ if (!bgId) {
43
+ continue;
44
+ }
45
+ ids.add(bgId);
46
+ tasks.set(bgId, { ...(tasks.get(bgId) || {}), ...task, bg_id: bgId });
47
+ }
48
+ for (const bgId of tasks.keys()) {
49
+ if (!ids.has(bgId) && tasks.get(bgId)?.status === "running") {
50
+ tasks.delete(bgId);
51
+ }
52
+ }
43
53
  updateCount();
44
54
  if (monitor) {
45
55
  monitor.selectedIndex = clampIndex(monitor.selectedIndex);
46
- redraw(true);
56
+ redraw();
47
57
  }
48
58
  })
49
59
  .finally(() => {
50
- listInFlight = null;
60
+ if (listInFlight === promise) {
61
+ listInFlight = null;
62
+ }
51
63
  });
52
- return listInFlight;
53
- }
54
-
55
- function updateCount() {
56
- const count = [...tasks.values()].filter((task) => task.status === "running").length;
57
- if (Number(state.sessionInfo?.background_count) !== count) {
58
- state.sessionInfo = { ...state.sessionInfo, background_count: count };
59
- redraw();
60
- }
61
- if (count > 0) {
62
- startRefresh();
63
- } else {
64
- stopRefresh();
65
- }
66
- }
67
-
68
- function startRefresh() {
69
- if (refreshTimer || state.runtimeClosing) {
70
- return;
71
- }
72
- refreshTimer = setInterval(() => {
73
- void refresh().catch(() => {});
74
- }, 1000);
75
- refreshTimer.unref?.();
76
- }
77
-
78
- function stopRefresh() {
79
- if (!refreshTimer) {
80
- return;
81
- }
82
- clearInterval(refreshTimer);
83
- refreshTimer = null;
84
- }
85
-
64
+ listInFlight = promise;
65
+ return promise;
66
+ }
67
+
68
+ function updateCount() {
69
+ const backgroundCount = [...tasks.values()].filter((task) => task.status === "running").length;
70
+ const delegateCount = [...delegates.values()].filter((delegate) => delegate.status === "running").length;
71
+ if (
72
+ Number(state.sessionInfo?.background_count) !== backgroundCount
73
+ || Number(state.sessionInfo?.delegate_count) !== delegateCount
74
+ ) {
75
+ state.sessionInfo = {
76
+ ...state.sessionInfo,
77
+ background_count: backgroundCount,
78
+ delegate_count: delegateCount,
79
+ };
80
+ redraw();
81
+ }
82
+ if (backgroundCount > 0) {
83
+ startRefresh();
84
+ } else {
85
+ stopRefresh();
86
+ }
87
+ }
88
+
89
+ function startRefresh() {
90
+ if (refreshTimer || state.runtimeClosing) {
91
+ return;
92
+ }
93
+ refreshTimer = setInterval(() => {
94
+ void refresh().catch(() => {});
95
+ }, 1000);
96
+ refreshTimer.unref?.();
97
+ }
98
+
99
+ function stopRefresh() {
100
+ if (!refreshTimer) {
101
+ return;
102
+ }
103
+ clearInterval(refreshTimer);
104
+ refreshTimer = null;
105
+ }
106
+
86
107
  function clear() {
108
+ generation += 1;
109
+ listInFlight = null;
110
+ pollToken += 1;
111
+ monitorPollInFlight = false;
87
112
  tasks.clear();
88
- pendingCommands.clear();
89
- stopRefresh();
90
- updateCount();
91
- }
92
-
93
- function recordCommand(event) {
94
- if (event?.tool_name !== "bash" || !event.tool_call_id) {
95
- return;
96
- }
97
- const args = parseObject(event.args_preview);
98
- if (args.command) {
99
- pendingCommands.set(event.tool_call_id, String(args.command));
100
- }
101
- }
102
-
103
- function recordResult(event) {
104
- const parsed = parseObject(event?.result);
105
- const data = parsed.data && typeof parsed.data === "object" ? parsed.data : parsed;
106
- const bgId = String(data?.bg_id || "").trim();
107
- if (!bgId) {
108
- return;
109
- }
110
- const previous = tasks.get(bgId) || {};
111
- const command = pendingCommands.get(event.tool_call_id) || previous.command || "";
112
- tasks.set(bgId, { ...previous, ...data, bg_id: bgId, command });
113
- if (event.tool_call_id) {
114
- pendingCommands.delete(event.tool_call_id);
115
- }
116
- updateCount();
117
- void pollMonitor();
118
- }
119
-
120
- function enterMonitor() {
121
- if (monitor || state.runtimeClosing) {
122
- return;
123
- }
124
- monitorInputWasActive = state.inputActive;
125
- monitor = { selectedIndex: 0 };
126
- state.inputActive = true;
127
- redraw(true);
128
- void refresh()
129
- .then(() => {
130
- if (!monitor) {
131
- return;
132
- }
133
- if (!tasks.size) {
134
- exitMonitor();
135
- log("No background tasks.");
136
- return;
137
- }
138
- monitor.selectedIndex = clampIndex(monitor.selectedIndex);
139
- startMonitorPolling();
140
- void pollMonitor();
141
- redraw(true);
142
- })
143
- .catch((error) => {
144
- if (!monitor || state.runtimeClosing) {
145
- return;
146
- }
147
- exitMonitor();
148
- log(`Background monitor failed: ${error instanceof Error ? error.message : String(error)}`);
149
- });
150
- }
151
-
152
- function exitMonitor() {
153
- stopMonitorPolling();
154
- monitor = null;
155
- state.inputActive = monitorInputWasActive;
156
- redraw(true);
157
- }
158
-
159
- function startMonitorPolling() {
160
- if (monitorTimer) {
161
- return;
162
- }
163
- monitorTimer = setInterval(() => {
164
- void pollMonitor();
165
- }, 500);
166
- monitorTimer.unref?.();
167
- }
168
-
169
- function stopMonitorPolling() {
170
- if (!monitorTimer) {
171
- return;
172
- }
173
- clearInterval(monitorTimer);
174
- monitorTimer = null;
175
- }
176
-
177
- async function pollMonitor() {
178
- if (!monitor || monitorPollInFlight || state.runtimeClosing) {
179
- return;
180
- }
181
- const available = [...tasks.values()];
182
- if (!available.length) {
183
- return;
184
- }
185
- monitor.selectedIndex = clampIndex(monitor.selectedIndex);
186
- const selected = available[monitor.selectedIndex];
187
- if (!selected?.bg_id) {
188
- return;
189
- }
113
+ pendingCommands.clear();
114
+ delegates.clear();
115
+ stopRefresh();
116
+ updateCount();
117
+ }
118
+
119
+ function clearDelegates() {
120
+ delegates.clear();
121
+ updateCount();
122
+ if (monitor?.page === "delegates") {
123
+ monitor.selectedIndex = 0;
124
+ redraw();
125
+ }
126
+ }
127
+
128
+ function recordCommand(event) {
129
+ if (event?.tool_name !== "bash" || !event.tool_call_id) {
130
+ return;
131
+ }
132
+ const args = parseObject(event.args_preview);
133
+ if (args.command) {
134
+ pendingCommands.set(event.tool_call_id, String(args.command));
135
+ }
136
+ }
137
+
138
+ function recordResult(event) {
139
+ const parsed = parseObject(event?.result);
140
+ const data = parsed.data && typeof parsed.data === "object" ? parsed.data : parsed;
141
+ const bgId = String(data?.bg_id || "").trim();
142
+ if (!bgId) {
143
+ return;
144
+ }
145
+ const previous = tasks.get(bgId) || {};
146
+ const command = pendingCommands.get(event.tool_call_id) || previous.command || "";
147
+ tasks.set(bgId, { ...previous, ...data, bg_id: bgId, command });
148
+ if (event.tool_call_id) {
149
+ pendingCommands.delete(event.tool_call_id);
150
+ }
151
+ updateCount();
152
+ void pollMonitor();
153
+ }
154
+
155
+ function recordDelegateRequest(event) {
156
+ if (event?.tool_name !== "delegate" || !event.tool_call_id) {
157
+ return;
158
+ }
159
+ const args = parseObject(event.args_preview);
160
+ const agentId = String(args.agent_id || "").trim();
161
+ if (!agentId) {
162
+ return;
163
+ }
164
+ delegates.set(event.tool_call_id, {
165
+ agent_id: agentId,
166
+ task: String(args.task || "").trim(),
167
+ status: "running",
168
+ summary: "",
169
+ });
170
+ updateCount();
171
+ if (monitor?.page === "delegates") {
172
+ monitor.selectedIndex = clampIndex(monitor.selectedIndex);
173
+ redraw();
174
+ }
175
+ }
176
+
177
+ function recordDelegateResult(event) {
178
+ if (event?.tool_name !== "delegate" || !event.tool_call_id) {
179
+ return;
180
+ }
181
+ const previous = delegates.get(event.tool_call_id);
182
+ if (!previous) {
183
+ return;
184
+ }
185
+ const parsed = parseObject(event.result);
186
+ const data = parsed.data && typeof parsed.data === "object" ? parsed.data : parsed;
187
+ const status = String(data.status || event.status || "completed").trim();
188
+ const summary = String(data.summary || "").trim();
189
+ delegates.set(event.tool_call_id, { ...previous, status, summary });
190
+ updateCount();
191
+ if (monitor?.page === "delegates") {
192
+ redraw();
193
+ }
194
+ }
195
+
196
+ function enterMonitor() {
197
+ if (monitor || state.runtimeClosing) {
198
+ return;
199
+ }
200
+ monitorInputWasActive = state.inputActive;
201
+ monitor = { page: initialPage(), selectedIndex: 0, pageChanged: false };
202
+ state.inputActive = true;
203
+ redraw(true);
204
+ void refresh()
205
+ .then(() => {
206
+ if (!monitor) {
207
+ return;
208
+ }
209
+ if (!tasks.size && !delegates.size) {
210
+ exitMonitor();
211
+ log("No background tasks or delegates.");
212
+ return;
213
+ }
214
+ if (!monitor.pageChanged) {
215
+ monitor.page = initialPage();
216
+ }
217
+ monitor.selectedIndex = clampIndex(monitor.selectedIndex);
218
+ startMonitorPolling();
219
+ void pollMonitor();
220
+ redraw();
221
+ })
222
+ .catch((error) => {
223
+ if (!monitor || state.runtimeClosing) {
224
+ return;
225
+ }
226
+ exitMonitor();
227
+ log(`Task monitor failed: ${error instanceof Error ? error.message : String(error)}`);
228
+ });
229
+ }
230
+
231
+ function exitMonitor() {
232
+ stopMonitorPolling();
233
+ monitor = null;
234
+ state.inputActive = monitorInputWasActive;
235
+ redraw(true);
236
+ }
237
+
238
+ function startMonitorPolling() {
239
+ if (monitorTimer) {
240
+ return;
241
+ }
242
+ monitorTimer = setInterval(() => {
243
+ void pollMonitor();
244
+ }, 500);
245
+ monitorTimer.unref?.();
246
+ }
247
+
248
+ function stopMonitorPolling() {
249
+ if (!monitorTimer) {
250
+ return;
251
+ }
252
+ clearInterval(monitorTimer);
253
+ monitorTimer = null;
254
+ }
255
+
256
+ async function pollMonitor() {
257
+ if (!monitor || monitor.page !== "background" || monitorPollInFlight || state.runtimeClosing) {
258
+ return;
259
+ }
260
+ const available = pageItems("background");
261
+ if (!available.length) {
262
+ return;
263
+ }
264
+ monitor.selectedIndex = clampIndex(monitor.selectedIndex);
265
+ const selected = available[monitor.selectedIndex];
266
+ if (!selected?.bg_id) {
267
+ return;
268
+ }
269
+ const requestGeneration = generation;
270
+ const requestToken = ++pollToken;
190
271
  monitorPollInFlight = true;
191
- try {
192
- const result = await request("background.output", {
193
- bg_id: selected.bg_id,
194
- max_output_chars: 20000,
195
- });
196
- if (result?.task && typeof result.task === "object") {
197
- tasks.set(selected.bg_id, {
198
- ...selected,
199
- ...result.task,
200
- command: selected.command || "",
201
- });
202
- updateCount();
203
- redraw();
204
- }
205
- } catch {
206
- // Periodic list refresh reconciles expired tasks without interrupting input.
272
+ try {
273
+ const result = await request(runtimeMethods.backgroundOutput, {
274
+ bg_id: selected.bg_id,
275
+ max_output_chars: 20000,
276
+ });
277
+ if (requestGeneration === generation && result?.task && typeof result.task === "object") {
278
+ tasks.set(selected.bg_id, {
279
+ ...selected,
280
+ ...result.task,
281
+ command: selected.command || "",
282
+ });
283
+ updateCount();
284
+ redraw();
285
+ }
286
+ } catch {
287
+ // Periodic list refresh reconciles expired tasks without interrupting input.
207
288
  } finally {
208
- monitorPollInFlight = false;
209
- }
210
- }
211
-
212
- function moveSelection(delta) {
213
- const count = tasks.size;
214
- if (!monitor || !count) {
215
- return;
216
- }
217
- const current = clampIndex(monitor.selectedIndex);
218
- monitor.selectedIndex = (current + delta + count) % count;
219
- void pollMonitor();
220
- redraw();
221
- }
222
-
223
- function handleInput(key) {
224
- const modified = key.ctrl || key.alt || key.shift;
225
- if (!modified && key.name === "escape") {
226
- exitMonitor();
227
- return true;
228
- }
229
- if (key.ctrl && key.name === "b") {
230
- exitMonitor();
231
- return true;
232
- }
233
- if (modified) {
234
- return true;
235
- }
236
- if (key.name === "up" || key.text === "k") {
237
- moveSelection(-1);
238
- return true;
239
- }
240
- if (key.name === "down" || key.text === "j") {
241
- moveSelection(1);
242
- return true;
243
- }
244
- return true;
245
- }
246
-
247
- function frame(width) {
248
- const list = [...tasks.values()];
249
- const selectedIndex = clampIndex(monitor?.selectedIndex);
250
- const text = backgroundMonitorText(
251
- list,
252
- selectedIndex,
253
- list[selectedIndex],
254
- Math.max(20, Number(width) - 4),
255
- );
256
- const lines = text.split("\n");
257
- const selectedRow = list.length ? 2 + selectedIndex : Math.max(0, lines.length - 1);
258
- return {
259
- lines,
260
- cursorRow: Math.min(selectedRow, Math.max(0, lines.length - 1)),
261
- cursorColumn: 0,
262
- focusRow: Math.min(selectedRow, Math.max(0, lines.length - 1)),
263
- };
264
- }
265
-
289
+ if (requestToken === pollToken) {
290
+ monitorPollInFlight = false;
291
+ }
292
+ }
293
+ }
294
+
295
+ function moveSelection(delta) {
296
+ if (!monitor) {
297
+ return;
298
+ }
299
+ const count = pageItems(monitor.page).length;
300
+ if (!count) {
301
+ return;
302
+ }
303
+ const current = clampIndex(monitor.selectedIndex);
304
+ monitor.selectedIndex = (current + delta + count) % count;
305
+ void pollMonitor();
306
+ redraw();
307
+ }
308
+
309
+ function switchPage(delta) {
310
+ if (!monitor) {
311
+ return;
312
+ }
313
+ const current = PAGES.indexOf(monitor.page);
314
+ monitor.page = PAGES[(current + delta + PAGES.length) % PAGES.length];
315
+ monitor.pageChanged = true;
316
+ monitor.selectedIndex = clampIndex(monitor.selectedIndex);
317
+ void pollMonitor();
318
+ redraw();
319
+ }
320
+
321
+ function handleInput(key) {
322
+ const modified = key.ctrl || key.alt || key.shift;
323
+ if (!modified && key.name === "escape") {
324
+ exitMonitor();
325
+ return true;
326
+ }
327
+ if (key.ctrl && key.name === "b") {
328
+ exitMonitor();
329
+ return true;
330
+ }
331
+ if (!modified && key.name === "left") {
332
+ switchPage(-1);
333
+ return true;
334
+ }
335
+ if (!modified && key.name === "right") {
336
+ switchPage(1);
337
+ return true;
338
+ }
339
+ if (modified) {
340
+ return true;
341
+ }
342
+ if (key.name === "up" || key.text === "k") {
343
+ moveSelection(-1);
344
+ return true;
345
+ }
346
+ if (key.name === "down" || key.text === "j") {
347
+ moveSelection(1);
348
+ return true;
349
+ }
350
+ return true;
351
+ }
352
+
353
+ function frame(width) {
354
+ const page = monitor?.page || initialPage();
355
+ const list = pageItems(page);
356
+ const selectedIndex = clampIndex(monitor?.selectedIndex, page);
357
+ const contentWidth = Math.max(20, Number(width) - 4);
358
+ const tabs = taskMonitorTabs(page, tasks.size, delegates.size, contentWidth).split("\n");
359
+ const text = page === "delegates"
360
+ ? delegateMonitorText(list, selectedIndex, list[selectedIndex], contentWidth)
361
+ : backgroundMonitorText(list, selectedIndex, list[selectedIndex], contentWidth);
362
+ const lines = [...tabs, ...text.split("\n")];
363
+ const selectedRow = list.length
364
+ ? tabs.length + 1 + selectedIndex
365
+ : Math.max(0, lines.length - 1);
366
+ return {
367
+ lines,
368
+ focusRow: Math.min(selectedRow, Math.max(0, lines.length - 1)),
369
+ };
370
+ }
371
+
266
372
  function stop() {
373
+ generation += 1;
374
+ listInFlight = null;
375
+ pollToken += 1;
376
+ monitorPollInFlight = false;
267
377
  stopRefresh();
268
- stopMonitorPolling();
269
- monitor = null;
270
- pendingCommands.clear();
271
- tasks.clear();
272
- }
273
-
274
- return {
275
- refresh,
276
- recordCommand,
277
- recordResult,
278
- enterMonitor,
279
- exitMonitor,
280
- handleInput,
281
- clear,
282
- stop,
283
- frame,
284
- isMonitoring: () => Boolean(monitor),
285
- moveSelection,
286
- };
287
-
288
- function clampIndex(index) {
289
- const count = tasks.size;
290
- if (!count) {
291
- return 0;
292
- }
293
- return Math.min(count - 1, Math.max(0, Number(index) || 0));
294
- }
295
- }
296
-
297
- function parseObject(value) {
298
- if (value && typeof value === "object") {
299
- return value;
300
- }
301
- try {
302
- const parsed = JSON.parse(String(value || ""));
303
- return parsed && typeof parsed === "object" ? parsed : {};
304
- } catch {
305
- return {};
306
- }
307
- }
378
+ stopMonitorPolling();
379
+ monitor = null;
380
+ pendingCommands.clear();
381
+ tasks.clear();
382
+ delegates.clear();
383
+ }
384
+
385
+ function pageItems(page) {
386
+ return page === "delegates" ? [...delegates.values()] : [...tasks.values()];
387
+ }
388
+
389
+ function initialPage() {
390
+ return tasks.size ? "background" : "delegates";
391
+ }
392
+
393
+ function clampIndex(index, page = monitor?.page || "background") {
394
+ const count = pageItems(page).length;
395
+ if (!count) {
396
+ return 0;
397
+ }
398
+ return Math.min(count - 1, Math.max(0, Number(index) || 0));
399
+ }
400
+
401
+ return {
402
+ refresh,
403
+ recordCommand,
404
+ recordResult,
405
+ recordDelegateRequest,
406
+ recordDelegateResult,
407
+ clearDelegates,
408
+ enterMonitor,
409
+ exitMonitor,
410
+ handleInput,
411
+ clear,
412
+ stop,
413
+ frame,
414
+ isMonitoring: () => Boolean(monitor),
415
+ moveSelection,
416
+ };
417
+ }
418
+
419
+ function parseObject(value) {
420
+ if (value && typeof value === "object") {
421
+ return value;
422
+ }
423
+ try {
424
+ const parsed = JSON.parse(String(value || ""));
425
+ return parsed && typeof parsed === "object" ? parsed : {};
426
+ } catch {
427
+ return {};
428
+ }
429
+ }