@visorcraft/idlehands 1.1.6 → 1.1.8

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 (122) hide show
  1. package/README.md +32 -0
  2. package/dist/agent/formatting.js +251 -0
  3. package/dist/agent/formatting.js.map +1 -0
  4. package/dist/agent/review-artifact.js +147 -0
  5. package/dist/agent/review-artifact.js.map +1 -0
  6. package/dist/agent/tool-calls.js +226 -0
  7. package/dist/agent/tool-calls.js.map +1 -0
  8. package/dist/agent.js +314 -695
  9. package/dist/agent.js.map +1 -1
  10. package/dist/anton/controller.js +1 -1
  11. package/dist/anton/controller.js.map +1 -1
  12. package/dist/anton/lock.js +0 -3
  13. package/dist/anton/lock.js.map +1 -1
  14. package/dist/anton/parser.js +0 -1
  15. package/dist/anton/parser.js.map +1 -1
  16. package/dist/anton/reporter.js +1 -1
  17. package/dist/anton/reporter.js.map +1 -1
  18. package/dist/bot/commands.js +3 -2
  19. package/dist/bot/commands.js.map +1 -1
  20. package/dist/bot/confirm-telegram.js +2 -1
  21. package/dist/bot/confirm-telegram.js.map +1 -1
  22. package/dist/bot/discord-routing.js +179 -0
  23. package/dist/bot/discord-routing.js.map +1 -0
  24. package/dist/bot/discord-streaming.js +171 -0
  25. package/dist/bot/discord-streaming.js.map +1 -0
  26. package/dist/bot/discord.js +25 -221
  27. package/dist/bot/discord.js.map +1 -1
  28. package/dist/bot/format.js +2 -25
  29. package/dist/bot/format.js.map +1 -1
  30. package/dist/bot/telegram.js +56 -12
  31. package/dist/bot/telegram.js.map +1 -1
  32. package/dist/cli/args.js +4 -1
  33. package/dist/cli/args.js.map +1 -1
  34. package/dist/cli/build-repl-context.js.map +1 -1
  35. package/dist/cli/command-registry.js +2 -1
  36. package/dist/cli/command-registry.js.map +1 -1
  37. package/dist/cli/command-utils.js +27 -0
  38. package/dist/cli/command-utils.js.map +1 -0
  39. package/dist/cli/commands/anton.js +3 -2
  40. package/dist/cli/commands/anton.js.map +1 -1
  41. package/dist/cli/commands/model.js +8 -7
  42. package/dist/cli/commands/model.js.map +1 -1
  43. package/dist/cli/commands/project.js +5 -4
  44. package/dist/cli/commands/project.js.map +1 -1
  45. package/dist/cli/commands/session.js +118 -8
  46. package/dist/cli/commands/session.js.map +1 -1
  47. package/dist/cli/commands/tools.js +4 -3
  48. package/dist/cli/commands/tools.js.map +1 -1
  49. package/dist/cli/input.js +2 -1
  50. package/dist/cli/input.js.map +1 -1
  51. package/dist/cli/repl-dispatch.js +85 -0
  52. package/dist/cli/repl-dispatch.js.map +1 -0
  53. package/dist/cli/runtime-cmds.js +7 -7
  54. package/dist/cli/runtime-cmds.js.map +1 -1
  55. package/dist/cli/service.js +0 -14
  56. package/dist/cli/service.js.map +1 -1
  57. package/dist/cli/setup.js +25 -5
  58. package/dist/cli/setup.js.map +1 -1
  59. package/dist/cli/watch.js +2 -1
  60. package/dist/cli/watch.js.map +1 -1
  61. package/dist/client.js +51 -4
  62. package/dist/client.js.map +1 -1
  63. package/dist/config.js +79 -0
  64. package/dist/config.js.map +1 -1
  65. package/dist/context.js +101 -10
  66. package/dist/context.js.map +1 -1
  67. package/dist/harnesses.js +1 -1
  68. package/dist/harnesses.js.map +1 -1
  69. package/dist/hooks/index.js +5 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/loader.js +58 -0
  72. package/dist/hooks/loader.js.map +1 -0
  73. package/dist/hooks/manager.js +180 -0
  74. package/dist/hooks/manager.js.map +1 -0
  75. package/dist/hooks/plugins/example-console.js +24 -0
  76. package/dist/hooks/plugins/example-console.js.map +1 -0
  77. package/dist/hooks/scaffold.js +53 -0
  78. package/dist/hooks/scaffold.js.map +1 -0
  79. package/dist/hooks/types.js +8 -0
  80. package/dist/hooks/types.js.map +1 -0
  81. package/dist/index.js +16 -64
  82. package/dist/index.js.map +1 -1
  83. package/dist/progress/agent-hooks.js +37 -0
  84. package/dist/progress/agent-hooks.js.map +1 -0
  85. package/dist/progress/ir.js +7 -0
  86. package/dist/progress/ir.js.map +1 -0
  87. package/dist/progress/progress-message-renderer.js +63 -0
  88. package/dist/progress/progress-message-renderer.js.map +1 -0
  89. package/dist/progress/serialize-discord.js +60 -0
  90. package/dist/progress/serialize-discord.js.map +1 -0
  91. package/dist/progress/serialize-telegram.js +55 -0
  92. package/dist/progress/serialize-telegram.js.map +1 -0
  93. package/dist/progress/serialize-tui.js +39 -0
  94. package/dist/progress/serialize-tui.js.map +1 -0
  95. package/dist/progress/tool-summary.js +58 -0
  96. package/dist/progress/tool-summary.js.map +1 -0
  97. package/dist/progress/tool-tail.js +48 -0
  98. package/dist/progress/tool-tail.js.map +1 -0
  99. package/dist/progress/turn-progress.js +215 -0
  100. package/dist/progress/turn-progress.js.map +1 -0
  101. package/dist/replay.js +2 -5
  102. package/dist/replay.js.map +1 -1
  103. package/dist/safety.js +0 -1
  104. package/dist/safety.js.map +1 -1
  105. package/dist/spinner.js +8 -0
  106. package/dist/spinner.js.map +1 -1
  107. package/dist/tools.js +422 -29
  108. package/dist/tools.js.map +1 -1
  109. package/dist/tui/branch-picker.js.map +1 -1
  110. package/dist/tui/command-handler.js.map +1 -1
  111. package/dist/tui/controller.js +417 -33
  112. package/dist/tui/controller.js.map +1 -1
  113. package/dist/tui/keymap.js +15 -0
  114. package/dist/tui/keymap.js.map +1 -1
  115. package/dist/tui/render.js +115 -3
  116. package/dist/tui/render.js.map +1 -1
  117. package/dist/tui/state.js +82 -1
  118. package/dist/tui/state.js.map +1 -1
  119. package/dist/upgrade.js.map +1 -1
  120. package/dist/utils.js +17 -0
  121. package/dist/utils.js.map +1 -1
  122. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { createSession } from "../agent.js";
2
2
  import { decodeRawInput, resolveAction } from "./keymap.js";
3
3
  import { createInitialTuiState, reduceTuiState } from "./state.js";
4
4
  import { renderTui, setRenderTheme } from "./render.js";
5
+ import { calculateLayout } from "./layout.js";
5
6
  import { enterFullScreen, leaveFullScreen } from "./screen.js";
6
7
  import { saveSessionFile, lastSessionPath, projectSessionPath } from "../cli/session-state.js";
7
8
  import { loadBranches, executeBranchSelect } from "./branch-picker.js";
@@ -9,8 +10,15 @@ import { ensureCommandsRegistered, allCommandNames, runShellCommand, runSlashCom
9
10
  import { projectDir } from "../utils.js";
10
11
  import { formatWatchdogCancelMessage, resolveWatchdogSettings } from "../watchdog.js";
11
12
  import { TuiConfirmProvider } from "./confirm.js";
12
- /** Commands that need special TUI handling instead of the registry adapter. */
13
- const TUI_OVERRIDES = new Set(['/quit', '/exit', '/clear', '/help', '/branches']);
13
+ import { splitTokens } from '../cli/command-utils.js';
14
+ import { TurnProgressController } from '../progress/turn-progress.js';
15
+ import { chainAgentHooks } from '../progress/agent-hooks.js';
16
+ import { formatToolCallSummary } from '../progress/tool-summary.js';
17
+ import { ToolTailBuffer } from '../progress/tool-tail.js';
18
+ import { ProgressMessageRenderer } from '../progress/progress-message-renderer.js';
19
+ import { renderTuiLines } from '../progress/serialize-tui.js';
20
+ const THEME_OPTIONS = ['default', 'dark', 'light', 'minimal', 'hacker'];
21
+ const APPROVAL_OPTIONS = ['plan', 'default', 'auto-edit', 'yolo'];
14
22
  export class TuiController {
15
23
  config;
16
24
  state = createInitialTuiState();
@@ -54,7 +62,7 @@ export class TuiController {
54
62
  this.tabIndex = -1;
55
63
  const names = allCommandNames();
56
64
  // Add TUI-specific commands that aren't in the registry
57
- const extra = ['/quit', '/exit', '/clear', '/help'];
65
+ const extra = ['/quit', '/exit', '/clear', '/help', '/branches', '/steps', '/settings', '/hooks'];
58
66
  const all = [...new Set([...names, ...extra])];
59
67
  this.tabCandidates = all.filter(n => n.toLowerCase().startsWith(prefix)).sort();
60
68
  }
@@ -99,6 +107,214 @@ export class TuiController {
99
107
  this.dispatch({ type: 'ALERT_PUSH', id: `br_${Date.now()}`, level: result.level ?? 'error', text: result.message });
100
108
  }
101
109
  }
110
+ transcriptLineStarts() {
111
+ const starts = [];
112
+ let line = 0;
113
+ for (const item of this.state.transcript) {
114
+ starts.push(line);
115
+ const chunks = String(item.text ?? '').split('\n');
116
+ line += Math.max(1, chunks.length);
117
+ }
118
+ return starts;
119
+ }
120
+ buildStepNavigatorItems(query) {
121
+ const q = (query ?? '').trim().toLowerCase();
122
+ const starts = this.transcriptLineStarts();
123
+ const items = this.state.transcript.map((item, idx) => {
124
+ const preview = String(item.text ?? '').split(/\r?\n/)[0]?.trim() ?? '';
125
+ return {
126
+ id: item.id,
127
+ ts: item.ts,
128
+ role: item.role,
129
+ preview,
130
+ lineStart: starts[idx] ?? 0,
131
+ };
132
+ });
133
+ if (!q)
134
+ return items;
135
+ return items.filter((it) => (`${it.role} ${it.preview}`).toLowerCase().includes(q));
136
+ }
137
+ openStepNavigator(query = '') {
138
+ const items = this.buildStepNavigatorItems(query);
139
+ this.dispatch({ type: 'STEP_NAV_OPEN', items, query });
140
+ }
141
+ stepNavigatorQueryAppend(text) {
142
+ const current = this.state.stepNavigator?.query ?? '';
143
+ const next = `${current}${text}`;
144
+ this.openStepNavigator(next);
145
+ }
146
+ stepNavigatorQueryBackspace() {
147
+ const current = this.state.stepNavigator?.query ?? '';
148
+ const next = current.slice(0, -1);
149
+ this.openStepNavigator(next);
150
+ }
151
+ jumpToStepSelection() {
152
+ const nav = this.state.stepNavigator;
153
+ if (!nav?.items.length) {
154
+ this.dispatch({ type: 'STEP_NAV_CLOSE' });
155
+ return;
156
+ }
157
+ const selected = nav.items[nav.selectedIndex];
158
+ if (!selected) {
159
+ this.dispatch({ type: 'STEP_NAV_CLOSE' });
160
+ return;
161
+ }
162
+ const layout = calculateLayout(process.stdout.rows ?? 30, process.stdout.columns ?? 120);
163
+ const starts = this.transcriptLineStarts();
164
+ const totalLines = starts.length
165
+ ? (starts[starts.length - 1] ?? 0) +
166
+ Math.max(1, String(this.state.transcript[this.state.transcript.length - 1]?.text ?? '').split('\n').length)
167
+ : 0;
168
+ const desiredStart = Math.max(0, Math.min(selected.lineStart, Math.max(0, totalLines - layout.transcriptRows)));
169
+ const scrollBack = Math.max(0, totalLines - (desiredStart + layout.transcriptRows));
170
+ this.dispatch({ type: 'SCROLL_SET', panel: 'transcript', value: scrollBack });
171
+ this.dispatch({ type: 'STEP_NAV_CLOSE' });
172
+ this.dispatch({ type: 'ALERT_PUSH', id: `step_${Date.now()}`, level: 'info', text: `Jumped to ${selected.role}: ${selected.preview || '(no preview)'}` });
173
+ }
174
+ buildSettingsItems() {
175
+ const watchdog = resolveWatchdogSettings(undefined, this.config);
176
+ return [
177
+ {
178
+ key: 'theme',
179
+ label: 'Theme',
180
+ value: this.config.theme || 'default',
181
+ hint: 'Cycle TUI themes instantly.',
182
+ },
183
+ {
184
+ key: 'approval',
185
+ label: 'Approval mode',
186
+ value: this.config.approval_mode || 'default',
187
+ hint: 'Plan/default/auto-edit/yolo for next turns.',
188
+ },
189
+ {
190
+ key: 'watchdog_timeout',
191
+ label: 'Watchdog timeout',
192
+ value: `${watchdog.timeoutMs} ms`,
193
+ hint: 'Longer timeout helps with slower models.',
194
+ },
195
+ {
196
+ key: 'watchdog_compactions',
197
+ label: 'Max compactions',
198
+ value: String(watchdog.maxCompactions),
199
+ hint: 'Retries before watchdog cancellation.',
200
+ },
201
+ {
202
+ key: 'watchdog_grace',
203
+ label: 'Grace windows',
204
+ value: String(watchdog.idleGraceTimeouts),
205
+ hint: 'Extra idle windows before first compaction.',
206
+ },
207
+ {
208
+ key: 'debug_abort',
209
+ label: 'Debug abort reason',
210
+ value: watchdog.debugAbortReason ? 'on' : 'off',
211
+ hint: 'Show raw abort reason in cancellation messages.',
212
+ },
213
+ ];
214
+ }
215
+ openSettingsMenu() {
216
+ this.dispatch({ type: 'SETTINGS_OPEN', items: this.buildSettingsItems() });
217
+ }
218
+ refreshSettingsMenu(selectedIndex) {
219
+ if (!this.state.settingsMenu)
220
+ return;
221
+ this.dispatch({ type: 'SETTINGS_UPDATE', items: this.buildSettingsItems(), selectedIndex });
222
+ }
223
+ adjustSelectedSetting(delta) {
224
+ const menu = this.state.settingsMenu;
225
+ if (!menu?.items.length)
226
+ return;
227
+ const selected = menu.items[menu.selectedIndex];
228
+ if (!selected)
229
+ return;
230
+ switch (selected.key) {
231
+ case 'theme': {
232
+ const current = (this.config.theme || 'default');
233
+ const idx = Math.max(0, THEME_OPTIONS.indexOf(current));
234
+ const next = THEME_OPTIONS[(idx + (delta >= 0 ? 1 : -1) + THEME_OPTIONS.length) % THEME_OPTIONS.length];
235
+ this.config.theme = next;
236
+ setRenderTheme(next);
237
+ break;
238
+ }
239
+ case 'approval': {
240
+ const current = (this.config.approval_mode || 'default');
241
+ const idx = Math.max(0, APPROVAL_OPTIONS.indexOf(current));
242
+ const next = APPROVAL_OPTIONS[(idx + (delta >= 0 ? 1 : -1) + APPROVAL_OPTIONS.length) % APPROVAL_OPTIONS.length];
243
+ this.config.approval_mode = next;
244
+ break;
245
+ }
246
+ case 'watchdog_timeout': {
247
+ const cur = this.config.watchdog_timeout_ms ?? 120_000;
248
+ const step = cur >= 180_000 ? 60_000 : 30_000;
249
+ this.config.watchdog_timeout_ms = Math.max(30_000, cur + (delta >= 0 ? step : -step));
250
+ break;
251
+ }
252
+ case 'watchdog_compactions': {
253
+ const cur = this.config.watchdog_max_compactions ?? 3;
254
+ this.config.watchdog_max_compactions = Math.max(0, cur + (delta >= 0 ? 1 : -1));
255
+ break;
256
+ }
257
+ case 'watchdog_grace': {
258
+ const cur = this.config.watchdog_idle_grace_timeouts ?? 1;
259
+ this.config.watchdog_idle_grace_timeouts = Math.max(0, cur + (delta >= 0 ? 1 : -1));
260
+ break;
261
+ }
262
+ case 'debug_abort': {
263
+ this.config.debug_abort_reason = !(this.config.debug_abort_reason === true);
264
+ break;
265
+ }
266
+ default:
267
+ return;
268
+ }
269
+ this.refreshSettingsMenu(menu.selectedIndex);
270
+ }
271
+ buildHookInspectorLines(mode) {
272
+ const manager = this.session?.hookManager;
273
+ if (!manager || typeof manager.getSnapshot !== 'function') {
274
+ return ['Hooks are unavailable for this session.'];
275
+ }
276
+ const snap = manager.getSnapshot();
277
+ const totalEvents = Object.values(snap.eventCounts || {}).reduce((sum, n) => sum + Number(n || 0), 0);
278
+ if (mode === 'errors') {
279
+ return snap.recentErrors?.length
280
+ ? snap.recentErrors.map((x) => `• ${x}`)
281
+ : ['No recent hook errors.'];
282
+ }
283
+ if (mode === 'slow') {
284
+ return snap.recentSlowHandlers?.length
285
+ ? snap.recentSlowHandlers.map((x) => `• ${x}`)
286
+ : ['No recent slow hook handlers.'];
287
+ }
288
+ if (mode === 'plugins') {
289
+ if (!snap.plugins?.length)
290
+ return ['No hook plugins loaded.'];
291
+ const lines = [];
292
+ for (const p of snap.plugins) {
293
+ lines.push(`• ${p.name} (${p.source})`);
294
+ lines.push(` granted: ${p.grantedCapabilities.join(', ') || 'none'}`);
295
+ if (p.deniedCapabilities?.length)
296
+ lines.push(` denied: ${p.deniedCapabilities.join(', ')}`);
297
+ }
298
+ return lines;
299
+ }
300
+ return [
301
+ `Enabled: ${snap.enabled ? 'yes' : 'no'}`,
302
+ `Strict mode: ${snap.strict ? 'yes' : 'no'}`,
303
+ `Allowed capabilities: ${(snap.allowedCapabilities || []).join(', ')}`,
304
+ `Plugins: ${snap.plugins?.length ?? 0}`,
305
+ `Handlers: ${snap.handlers?.length ?? 0}`,
306
+ `Events observed: ${totalEvents}`,
307
+ `Recent errors: ${snap.recentErrors?.length ?? 0}`,
308
+ `Recent slow handlers: ${snap.recentSlowHandlers?.length ?? 0}`,
309
+ ];
310
+ }
311
+ openHooksInspector(mode = 'status') {
312
+ this.dispatch({
313
+ type: 'HOOKS_INSPECTOR_OPEN',
314
+ mode,
315
+ lines: this.buildHookInspectorLines(mode),
316
+ });
317
+ }
102
318
  /** Push a system-role transcript item and re-render. */
103
319
  pushSystemMessage(text) {
104
320
  const item = { id: `sys_${Date.now()}`, role: "system", text, ts: Date.now() };
@@ -124,7 +340,8 @@ export class TuiController {
124
340
  }
125
341
  /** Handle a slash command. Returns true if handled. */
126
342
  async handleSlashCommand(line) {
127
- const head = (line.trim().split(/\s+/)[0] || "").toLowerCase();
343
+ const parts = splitTokens(line);
344
+ const head = (parts[0] || "").toLowerCase();
128
345
  if (!head.startsWith("/"))
129
346
  return false;
130
347
  ensureCommandsRegistered();
@@ -141,16 +358,37 @@ export class TuiController {
141
358
  }
142
359
  if (head === "/help") {
143
360
  const cmds = allCommandNames().join(" ");
144
- this.pushSystemMessage(`Commands: ${cmds}\nShell: !<cmd> to run, !! to inject output\nTUI: /branches to browse conversation branches`);
361
+ this.pushSystemMessage(`Commands: ${cmds}\n` +
362
+ `Shell: !<cmd> to run, !! to inject output\n` +
363
+ `TUI: /branches [browse|checkout|merge], /steps, /settings, /hooks [status|errors|slow|plugins]\n` +
364
+ `Hotkeys: Ctrl+G step navigator, Ctrl+O quick settings`);
145
365
  return true;
146
366
  }
147
367
  if (head === "/branches") {
148
- const parts = line.trim().split(/\s+/);
149
368
  const sub = (parts[1] || '').toLowerCase();
150
369
  const action = sub === 'checkout' ? 'checkout' : sub === 'merge' ? 'merge' : 'browse';
151
370
  await this.openBranchPicker(action);
152
371
  return true;
153
372
  }
373
+ if (head === '/steps') {
374
+ const query = line.replace(/^\/steps\s*/i, '').trim();
375
+ this.openStepNavigator(query);
376
+ return true;
377
+ }
378
+ if (head === '/settings') {
379
+ this.openSettingsMenu();
380
+ return true;
381
+ }
382
+ if (head === '/hooks') {
383
+ const modeRaw = line.replace(/^\/hooks\s*/i, '').trim().toLowerCase();
384
+ const mode = (modeRaw || 'status');
385
+ if (!['status', 'errors', 'slow', 'plugins'].includes(mode)) {
386
+ this.dispatch({ type: 'ALERT_PUSH', id: `hooks_${Date.now()}`, level: 'warn', text: 'Usage: /hooks [status|errors|slow|plugins]' });
387
+ return true;
388
+ }
389
+ this.openHooksInspector(mode);
390
+ return true;
391
+ }
154
392
  const result = await runSlashCommand(line, this.session, this.config, this.cleanupFn, () => this.saveTuiSessionSnapshot());
155
393
  if (!result.found) {
156
394
  this.dispatch({ type: "ALERT_PUSH", id: `cmd_${Date.now()}`, level: "warn", text: `Unknown command: ${head}` });
@@ -258,6 +496,35 @@ export class TuiController {
258
496
  }
259
497
  }
260
498
  }, 5_000);
499
+ const progressRenderer = new ProgressMessageRenderer({
500
+ maxToolLines: 6,
501
+ maxTailLines: 4,
502
+ maxAssistantChars: 800,
503
+ });
504
+ const tails = new ToolTailBuffer({ maxChars: 4096, maxLines: 4 });
505
+ let activeToolId = null;
506
+ let streamedSoFar = '';
507
+ const flushStatus = (snap) => {
508
+ const tail = activeToolId ? tails.get(activeToolId) : null;
509
+ const doc = progressRenderer.render({
510
+ statusLine: snap.statusLine,
511
+ toolLines: snap.toolLines,
512
+ toolTail: tail ? { name: tail.name, stream: tail.stream, lines: tail.lines } : null,
513
+ assistantMarkdown: streamedSoFar,
514
+ });
515
+ const lines = renderTuiLines(doc, { maxLines: 8 });
516
+ const status = lines.find((l) => l.trim().length > 0) ?? '';
517
+ this.dispatch({ type: 'STATUS_SET', text: status });
518
+ };
519
+ const progress = new TurnProgressController((snap) => {
520
+ flushStatus(snap);
521
+ }, {
522
+ heartbeatMs: 1000,
523
+ bucketMs: 5000,
524
+ maxToolLines: 6,
525
+ toolCallSummary: (c) => formatToolCallSummary({ name: c.name, args: c.args }),
526
+ });
527
+ progress.start();
261
528
  try {
262
529
  let askComplete = false;
263
530
  let isRetryAfterCompaction = false;
@@ -267,36 +534,59 @@ export class TuiController {
267
534
  const askText = isRetryAfterCompaction
268
535
  ? 'Continue working on the task from where you left off. Context was compacted to free memory — do NOT restart from the beginning.'
269
536
  : trimmed;
537
+ const uiHooks = {
538
+ signal: attemptController.signal,
539
+ onToken: (t) => {
540
+ this.lastProgressAt = Date.now();
541
+ watchdogGraceUsed = 0;
542
+ streamedSoFar += t;
543
+ this.dispatch({ type: 'AGENT_STREAM_TOKEN', id, token: t });
544
+ },
545
+ onToolCall: (c) => {
546
+ this.lastProgressAt = Date.now();
547
+ watchdogGraceUsed = 0;
548
+ activeToolId = c.id;
549
+ tails.reset(c.id, c.name);
550
+ this.dispatch({
551
+ type: 'TOOL_START',
552
+ id: c.id,
553
+ name: c.name,
554
+ detail: formatToolCallSummary({ name: c.name, args: c.args }),
555
+ });
556
+ },
557
+ onToolStream: (ev) => {
558
+ if (!activeToolId || ev.id !== activeToolId)
559
+ return;
560
+ const tailSnap = tails.push(ev);
561
+ const last = tailSnap?.lines?.[tailSnap.lines.length - 1];
562
+ if (last)
563
+ this.dispatch({ type: 'TOOL_TAIL', id: ev.id, tail: last });
564
+ flushStatus(progress.snapshot('manual'));
565
+ },
566
+ onToolResult: (r) => {
567
+ this.lastProgressAt = Date.now();
568
+ watchdogGraceUsed = 0;
569
+ if (activeToolId === r.id) {
570
+ activeToolId = null;
571
+ tails.clear(r.id);
572
+ }
573
+ this.dispatch({ type: r.success ? 'TOOL_END' : 'TOOL_ERROR', id: r.id, name: r.name, detail: r.summary });
574
+ },
575
+ onTurnEnd: () => {
576
+ this.lastProgressAt = Date.now();
577
+ watchdogGraceUsed = 0;
578
+ },
579
+ };
580
+ const hooks = chainAgentHooks(uiHooks, progress.hooks);
270
581
  try {
271
- await this.session.ask(askText, {
272
- signal: attemptController.signal,
273
- onToken: (t) => {
274
- this.lastProgressAt = Date.now();
275
- watchdogGraceUsed = 0;
276
- this.dispatch({ type: "AGENT_STREAM_TOKEN", id, token: t });
277
- },
278
- onToolCall: (c) => {
279
- this.lastProgressAt = Date.now();
280
- watchdogGraceUsed = 0;
281
- this.dispatch({ type: "TOOL_START", id: `${c.name}-${Date.now()}`, name: c.name, detail: JSON.stringify(c.args).slice(0, 120) });
282
- },
283
- onToolResult: async (r) => {
284
- this.lastProgressAt = Date.now();
285
- watchdogGraceUsed = 0;
286
- this.dispatch({ type: r.success ? "TOOL_END" : "TOOL_ERROR", id: `${r.name}-${Date.now()}`, name: r.name, detail: r.summary });
287
- },
288
- onTurnEnd: () => {
289
- this.lastProgressAt = Date.now();
290
- watchdogGraceUsed = 0;
291
- },
292
- });
582
+ await this.session.ask(askText, hooks);
293
583
  askComplete = true;
294
584
  }
295
585
  catch (e) {
296
586
  const msg = e?.message ?? String(e);
297
587
  const isAbort = msg.includes('AbortError') || msg.toLowerCase().includes('aborted');
298
588
  if (isAbort && watchdogCompactPending) {
299
- this.dispatch({ type: "ALERT_PUSH", id: `compact_${Date.now()}`, level: "info", text: `Context too large — compacting and retrying (attempt ${this.watchdogCompactAttempts}/${maxWatchdogCompacts})...` });
589
+ this.dispatch({ type: 'ALERT_PUSH', id: `compact_${Date.now()}`, level: 'info', text: `Context too large — compacting and retrying (attempt ${this.watchdogCompactAttempts}/${maxWatchdogCompacts})...` });
300
590
  while (watchdogCompactPending) {
301
591
  await new Promise((r) => setTimeout(r, 500));
302
592
  }
@@ -311,17 +601,19 @@ export class TuiController {
311
601
  debugAbortReason,
312
602
  abortReason: msg,
313
603
  });
314
- this.dispatch({ type: "ALERT_PUSH", id: `err_${Date.now()}`, level: "error", text });
604
+ this.dispatch({ type: 'ALERT_PUSH', id: `err_${Date.now()}`, level: 'error', text });
315
605
  }
316
606
  else {
317
- this.dispatch({ type: "ALERT_PUSH", id: `err_${Date.now()}`, level: "error", text: msg });
607
+ this.dispatch({ type: 'ALERT_PUSH', id: `err_${Date.now()}`, level: 'error', text: msg });
318
608
  }
319
609
  }
320
610
  }
321
611
  }
322
612
  finally {
613
+ progress.stop();
614
+ this.dispatch({ type: 'STATUS_CLEAR' });
323
615
  clearInterval(watchdog);
324
- this.dispatch({ type: "AGENT_STREAM_DONE", id });
616
+ this.dispatch({ type: 'AGENT_STREAM_DONE', id });
325
617
  this.inFlight = false;
326
618
  this.aborter = null;
327
619
  }
@@ -345,7 +637,7 @@ export class TuiController {
345
637
  type: "ALERT_PUSH",
346
638
  id: `info_${Date.now()}`,
347
639
  level: "info",
348
- text: "Input policy: Enter=send, Ctrl+J/Alt+Enter=newline, Up/Down=history.",
640
+ text: "Input: Enter=send, Ctrl+J/Alt+Enter=newline, Up/Down=history, Ctrl+G=steps, Ctrl+O=settings, /hooks inspector.",
349
641
  });
350
642
  const onSigwinch = () => {
351
643
  renderTui(this.state);
@@ -406,6 +698,90 @@ export class TuiController {
406
698
  }
407
699
  continue; // swallow all other input during picker
408
700
  }
701
+ // Step navigator: type to filter, arrows to select, Enter to jump.
702
+ if (this.state.stepNavigator) {
703
+ const nAction = resolveAction(key);
704
+ if (key.startsWith('text:')) {
705
+ const ch = key.slice(5);
706
+ if (ch === 'q' && !this.state.stepNavigator.query) {
707
+ this.dispatch({ type: 'STEP_NAV_CLOSE' });
708
+ }
709
+ else {
710
+ this.stepNavigatorQueryAppend(ch);
711
+ }
712
+ continue;
713
+ }
714
+ if (nAction === 'backspace') {
715
+ this.stepNavigatorQueryBackspace();
716
+ continue;
717
+ }
718
+ if (nAction === 'history_prev' || nAction === 'cursor_left') {
719
+ this.dispatch({ type: 'STEP_NAV_MOVE', delta: -1 });
720
+ continue;
721
+ }
722
+ if (nAction === 'history_next' || nAction === 'cursor_right') {
723
+ this.dispatch({ type: 'STEP_NAV_MOVE', delta: 1 });
724
+ continue;
725
+ }
726
+ if (nAction === 'scroll_up') {
727
+ this.dispatch({ type: 'STEP_NAV_MOVE', delta: -10 });
728
+ continue;
729
+ }
730
+ if (nAction === 'scroll_down') {
731
+ this.dispatch({ type: 'STEP_NAV_MOVE', delta: 10 });
732
+ continue;
733
+ }
734
+ if (nAction === 'send') {
735
+ this.jumpToStepSelection();
736
+ continue;
737
+ }
738
+ if (nAction === 'cancel' || nAction === 'quit') {
739
+ this.dispatch({ type: 'STEP_NAV_CLOSE' });
740
+ continue;
741
+ }
742
+ continue;
743
+ }
744
+ // Settings menu: arrows/select to adjust config quickly.
745
+ if (this.state.settingsMenu) {
746
+ const sAction = resolveAction(key);
747
+ if (sAction === 'history_prev') {
748
+ this.dispatch({ type: 'SETTINGS_MOVE', delta: -1 });
749
+ continue;
750
+ }
751
+ if (sAction === 'history_next') {
752
+ this.dispatch({ type: 'SETTINGS_MOVE', delta: 1 });
753
+ continue;
754
+ }
755
+ if (sAction === 'cursor_left') {
756
+ this.adjustSelectedSetting(-1);
757
+ continue;
758
+ }
759
+ if (sAction === 'cursor_right' || sAction === 'send') {
760
+ this.adjustSelectedSetting(1);
761
+ continue;
762
+ }
763
+ if (sAction === 'cancel' || sAction === 'quit' || key === 'text:q') {
764
+ this.dispatch({ type: 'SETTINGS_CLOSE' });
765
+ continue;
766
+ }
767
+ continue;
768
+ }
769
+ if (this.state.hooksInspector) {
770
+ const hAction = resolveAction(key);
771
+ if (hAction === 'history_prev' || hAction === 'scroll_up') {
772
+ this.dispatch({ type: 'HOOKS_INSPECTOR_MOVE', delta: -1 });
773
+ continue;
774
+ }
775
+ if (hAction === 'history_next' || hAction === 'scroll_down') {
776
+ this.dispatch({ type: 'HOOKS_INSPECTOR_MOVE', delta: 1 });
777
+ continue;
778
+ }
779
+ if (hAction === 'cancel' || hAction === 'quit' || key === 'text:q' || hAction === 'send') {
780
+ this.dispatch({ type: 'HOOKS_INSPECTOR_CLOSE' });
781
+ continue;
782
+ }
783
+ continue;
784
+ }
409
785
  if (key.startsWith("text:")) {
410
786
  this.resetTab();
411
787
  this.dispatch({ type: "USER_INPUT_INSERT", text: key.slice(5) });
@@ -421,6 +797,14 @@ export class TuiController {
421
797
  }
422
798
  // Any non-tab action resets tab cycling
423
799
  this.resetTab();
800
+ if (action === 'open_step_navigator') {
801
+ this.openStepNavigator();
802
+ continue;
803
+ }
804
+ if (action === 'open_settings') {
805
+ this.openSettingsMenu();
806
+ continue;
807
+ }
424
808
  if (action === "quit") {
425
809
  void cleanup();
426
810
  continue;