@gajae-code/coding-agent 0.4.2 → 0.4.4

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 (115) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/types/async/job-manager.d.ts +44 -1
  3. package/dist/types/cli/setup-cli.d.ts +14 -1
  4. package/dist/types/commands/coordinator.d.ts +19 -0
  5. package/dist/types/commands/mcp-serve.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +41 -0
  7. package/dist/types/commit/model-selection.d.ts +1 -1
  8. package/dist/types/config/model-registry.d.ts +3 -1
  9. package/dist/types/config/model-resolver.d.ts +1 -19
  10. package/dist/types/config/models-config-schema.d.ts +12 -0
  11. package/dist/types/config/settings-schema.d.ts +15 -1
  12. package/dist/types/coordinator/contract.d.ts +4 -0
  13. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  14. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  15. package/dist/types/coordinator-mcp/server.d.ts +52 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  17. package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
  18. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  19. package/dist/types/harness-control-plane/types.d.ts +7 -2
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  22. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  23. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
  24. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
  25. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
  26. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
  27. package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
  28. package/dist/types/session/agent-session.d.ts +12 -1
  29. package/dist/types/session/session-manager.d.ts +1 -1
  30. package/dist/types/setup/hermes-setup.d.ts +71 -0
  31. package/dist/types/task/render.d.ts +7 -1
  32. package/dist/types/tools/bash.d.ts +2 -0
  33. package/dist/types/tools/browser/actions.d.ts +54 -0
  34. package/dist/types/tools/browser.d.ts +80 -0
  35. package/dist/types/tools/image-gen.d.ts +1 -0
  36. package/dist/types/tools/index.d.ts +3 -1
  37. package/dist/types/tools/job.d.ts +1 -1
  38. package/dist/types/tools/subagent-render.d.ts +25 -0
  39. package/dist/types/tools/subagent.d.ts +5 -1
  40. package/package.json +7 -7
  41. package/src/async/job-manager.ts +163 -2
  42. package/src/cli/setup-cli.ts +86 -2
  43. package/src/cli.ts +2 -0
  44. package/src/commands/coordinator.ts +70 -0
  45. package/src/commands/mcp-serve.ts +62 -0
  46. package/src/commands/setup.ts +30 -1
  47. package/src/commands/ultragoal.ts +7 -1
  48. package/src/commit/agentic/index.ts +2 -2
  49. package/src/commit/model-selection.ts +7 -22
  50. package/src/commit/pipeline.ts +2 -2
  51. package/src/config/model-registry.ts +17 -9
  52. package/src/config/model-resolver.ts +14 -84
  53. package/src/config/models-config-schema.ts +2 -0
  54. package/src/config/settings-schema.ts +14 -1
  55. package/src/coordinator/contract.ts +20 -0
  56. package/src/coordinator-mcp/policy.ts +160 -0
  57. package/src/coordinator-mcp/safety.ts +80 -0
  58. package/src/coordinator-mcp/server.ts +1316 -0
  59. package/src/extensibility/extensions/types.ts +13 -0
  60. package/src/gjc-runtime/goal-mode-request.ts +21 -1
  61. package/src/gjc-runtime/session-state-sidecar.ts +79 -0
  62. package/src/harness-control-plane/owner.ts +3 -3
  63. package/src/harness-control-plane/rpc-adapter.ts +7 -1
  64. package/src/harness-control-plane/types.ts +8 -11
  65. package/src/internal-urls/docs-index.generated.ts +6 -5
  66. package/src/memories/index.ts +1 -1
  67. package/src/modes/acp/acp-agent.ts +17 -9
  68. package/src/modes/acp/acp-event-mapper.ts +33 -1
  69. package/src/modes/components/custom-editor.ts +19 -3
  70. package/src/modes/components/hook-selector.ts +109 -5
  71. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  72. package/src/modes/controllers/input-controller.ts +27 -7
  73. package/src/modes/controllers/selector-controller.ts +7 -1
  74. package/src/modes/interactive-mode.ts +3 -1
  75. package/src/modes/rpc/rpc-client.ts +16 -3
  76. package/src/modes/rpc/rpc-mode.ts +5 -2
  77. package/src/modes/shared/agent-wire/command-contract.ts +18 -0
  78. package/src/modes/shared/agent-wire/event-contract.ts +147 -0
  79. package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
  80. package/src/modes/shared/agent-wire/event-observation.ts +397 -0
  81. package/src/modes/shared/agent-wire/protocol.ts +24 -81
  82. package/src/modes/utils/context-usage.ts +2 -2
  83. package/src/prompts/agents/architect.md +6 -0
  84. package/src/prompts/agents/critic.md +6 -0
  85. package/src/prompts/agents/explore.md +1 -1
  86. package/src/prompts/agents/plan.md +1 -1
  87. package/src/prompts/agents/planner.md +8 -1
  88. package/src/prompts/agents/reviewer.md +1 -1
  89. package/src/prompts/tools/browser.md +3 -2
  90. package/src/runtime-mcp/manager.ts +15 -2
  91. package/src/sdk.ts +3 -1
  92. package/src/session/agent-session.ts +66 -4
  93. package/src/session/session-manager.ts +1 -1
  94. package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
  95. package/src/setup/hermes-setup.ts +429 -0
  96. package/src/task/agents.ts +1 -1
  97. package/src/task/index.ts +2 -0
  98. package/src/task/render.ts +14 -0
  99. package/src/tools/ask.ts +30 -10
  100. package/src/tools/bash.ts +6 -1
  101. package/src/tools/browser/actions.ts +189 -0
  102. package/src/tools/browser.ts +91 -1
  103. package/src/tools/image-gen.ts +42 -15
  104. package/src/tools/index.ts +7 -1
  105. package/src/tools/inspect-image.ts +10 -8
  106. package/src/tools/job.ts +12 -2
  107. package/src/tools/monitor.ts +98 -17
  108. package/src/tools/renderers.ts +2 -0
  109. package/src/tools/subagent-render.ts +160 -0
  110. package/src/tools/subagent.ts +49 -7
  111. package/src/utils/commit-message-generator.ts +6 -13
  112. package/src/utils/title-generator.ts +1 -1
  113. package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
  114. package/src/harness-control-plane/frame-mapper.ts +0 -286
  115. package/src/priority.json +0 -37
@@ -388,7 +388,7 @@ async function runPhase2(options: {
388
388
  const phase2Model = await resolveMemoryModel({
389
389
  modelRegistry,
390
390
  session,
391
- fallbackRole: "smol",
391
+ fallbackRole: "default",
392
392
  });
393
393
  if (!phase2Model) {
394
394
  markPhase2FailureWithFallback(db, {
@@ -72,10 +72,11 @@ import {
72
72
  } from "../../session/session-manager";
73
73
  import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
74
74
  import { parseThinkingLevel } from "../../thinking";
75
+ import { toAgentWireEventPayload } from "../shared/agent-wire/event-envelope";
75
76
  import { createAcpClientBridge } from "./acp-client-bridge";
76
77
  import {
77
78
  buildToolCallStartUpdate,
78
- mapAgentSessionEventToAcpSessionUpdates,
79
+ mapAgentWireEventPayloadToAcpSessionUpdates,
79
80
  normalizeReplayToolArguments,
80
81
  } from "./acp-event-mapper";
81
82
  import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
@@ -1128,12 +1129,16 @@ export class AcpAgent implements Agent {
1128
1129
  }
1129
1130
 
1130
1131
  this.#prepareLiveAssistantMessage(record, event);
1131
- for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
1132
- getMessageId: message => this.#getLiveMessageId(record, message),
1133
- getMessageProgress: message => this.#getLiveMessageProgress(record, message),
1134
- getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
1135
- cwd: record.session.sessionManager.getCwd(),
1136
- })) {
1132
+ for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(
1133
+ toAgentWireEventPayload(event),
1134
+ record.session.sessionId,
1135
+ {
1136
+ getMessageId: message => this.#getLiveMessageId(record, message),
1137
+ getMessageProgress: message => this.#getLiveMessageProgress(record, message),
1138
+ getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
1139
+ cwd: record.session.sessionManager.getCwd(),
1140
+ },
1141
+ )) {
1137
1142
  await this.#connection.sessionUpdate(notification);
1138
1143
  }
1139
1144
  if (event.type === "tool_execution_end") {
@@ -1887,14 +1892,17 @@ export class AcpAgent implements Agent {
1887
1892
  errorMessage: message.errorMessage,
1888
1893
  },
1889
1894
  };
1890
- const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
1895
+ const notifications = mapAgentWireEventPayloadToAcpSessionUpdates(toAgentWireEventPayload(endEvent), sessionId, {
1891
1896
  cwd,
1892
1897
  getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
1893
1898
  });
1894
1899
  if (options.includeStart === false) {
1895
1900
  return notifications;
1896
1901
  }
1897
- return [...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }), ...notifications];
1902
+ return [
1903
+ ...mapAgentWireEventPayloadToAcpSessionUpdates(toAgentWireEventPayload(startEvent), sessionId, { cwd }),
1904
+ ...notifications,
1905
+ ];
1898
1906
  }
1899
1907
 
1900
1908
  #buildReplayToolArgs(details: unknown): { path?: string } {
@@ -9,6 +9,7 @@ import type {
9
9
  import type { AgentSessionEvent } from "../../session/agent-session";
10
10
  import { resolveToCwd } from "../../tools/path-utils";
11
11
  import type { TodoStatus } from "../../tools/todo-write";
12
+ import type { AgentWireEventPayload } from "../shared/agent-wire/event-contract";
12
13
 
13
14
  interface MessageProgress {
14
15
  textEmitted: boolean;
@@ -145,6 +146,14 @@ export function mapToolKind(toolName: string): ToolKind {
145
146
  }
146
147
  }
147
148
 
149
+ export function mapAgentWireEventPayloadToAcpSessionUpdates(
150
+ payload: AgentWireEventPayload,
151
+ sessionId: string,
152
+ options: AcpEventMapperOptions = {},
153
+ ): SessionNotification[] {
154
+ return mapAgentSessionEventToAcpSessionUpdates(payload.event, sessionId, options);
155
+ }
156
+
148
157
  export function mapAgentSessionEventToAcpSessionUpdates(
149
158
  event: AgentSessionEvent,
150
159
  sessionId: string,
@@ -221,11 +230,34 @@ export function mapAgentSessionEventToAcpSessionUpdates(
221
230
  }
222
231
  case "todo_auto_clear":
223
232
  return [toSessionNotification(sessionId, { sessionUpdate: "plan", entries: [] })];
224
- default:
233
+ // These event types are intentionally not represented as ACP session updates.
234
+ case "agent_start":
235
+ case "agent_end":
236
+ case "turn_start":
237
+ case "turn_end":
238
+ case "message_start":
239
+ case "auto_compaction_start":
240
+ case "auto_compaction_end":
241
+ case "auto_retry_start":
242
+ case "auto_retry_end":
243
+ case "retry_fallback_applied":
244
+ case "retry_fallback_succeeded":
245
+ case "ttsr_triggered":
246
+ case "irc_message":
247
+ case "notice":
248
+ case "thinking_level_changed":
249
+ case "goal_updated":
225
250
  return [];
251
+ default:
252
+ return assertNeverAcp(event);
226
253
  }
227
254
  }
228
255
 
256
+ function assertNeverAcp(event: never): SessionNotification[] {
257
+ void (event as AgentSessionEvent);
258
+ return [];
259
+ }
260
+
229
261
  function mapAssistantMessageUpdate(
230
262
  event: Extract<AgentSessionEvent, { type: "message_update" }>,
231
263
  sessionId: string,
@@ -51,6 +51,13 @@ type PastePendingClearReason = "timeout" | "queue-limit";
51
51
  */
52
52
  export class CustomEditor extends Editor {
53
53
  onEscape?: () => void;
54
+ /**
55
+ * Optional high-priority interrupt consumer. Invoked when the interrupt key
56
+ * is pressed, before `onEscape`. Returning `true` consumes the keystroke.
57
+ * Used so a transient UI (e.g. the btw panel) stays dismissable even while
58
+ * another controller has temporarily installed its own `onEscape` handler.
59
+ */
60
+ onInterruptPriority?: () => boolean;
54
61
  shouldBypassAutocompleteOnEscape?: () => boolean;
55
62
  onClear?: () => void;
56
63
  onExit?: () => void;
@@ -285,10 +292,19 @@ export class CustomEditor extends Editor {
285
292
 
286
293
  // Intercept configured interrupt shortcut.
287
294
  // Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
288
- if (this.#matchesAction(data, "app.interrupt") && this.onEscape) {
295
+ if (this.#matchesAction(data, "app.interrupt")) {
289
296
  if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
290
- this.onEscape();
291
- return;
297
+ // A priority interrupt consumer (e.g. an open btw panel) must win over any
298
+ // transient onEscape handler other controllers install (auto-compaction,
299
+ // auto-retry, manual compaction, etc.) so dismissal stays wired regardless
300
+ // of which handler currently owns onEscape.
301
+ if (this.onInterruptPriority?.()) {
302
+ return;
303
+ }
304
+ if (this.onEscape) {
305
+ this.onEscape();
306
+ return;
307
+ }
292
308
  }
293
309
  }
294
310
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import {
6
6
  Container,
7
+ Editor,
7
8
  Markdown,
8
9
  matchesKey,
9
10
  padding,
@@ -16,8 +17,13 @@ import {
16
17
  visibleWidth,
17
18
  wrapTextWithAnsi,
18
19
  } from "@gajae-code/tui";
19
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
20
- import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
20
+ import { getEditorTheme, getMarkdownTheme, theme } from "../../modes/theme/theme";
21
+ import {
22
+ matchesAppExternalEditor,
23
+ matchesAppInterrupt,
24
+ matchesSelectCancel,
25
+ } from "../../modes/utils/keybinding-matchers";
26
+ import { getEditorCommand, openInEditor } from "../../utils/external-editor";
21
27
  import { CountdownTimer } from "./countdown-timer";
22
28
  import { DynamicBorder } from "./dynamic-border";
23
29
 
@@ -56,6 +62,17 @@ export interface HookSelectorOptions {
56
62
  */
57
63
  wrapFocused?: boolean;
58
64
  scrollTitleRows?: number;
65
+ /**
66
+ * Inline free-text entry for the option with this label (e.g. the ask
67
+ * tool's "Other (type your own)"). Selecting it keeps the title and option
68
+ * list on screen and opens a prompt-style editor below the list instead of
69
+ * replacing the whole selector. Enter submits via `onSubmit`; Escape
70
+ * returns to option selection.
71
+ */
72
+ customInput?: {
73
+ optionLabel: string;
74
+ onSubmit: (text: string) => void;
75
+ };
59
76
  }
60
77
 
61
78
  class OutlinedList extends Container {
@@ -297,6 +314,12 @@ export class HookSelectorComponent extends Container {
297
314
  #wrapFocused: boolean;
298
315
  #outline: boolean;
299
316
  #scrollTitleRows: number | undefined;
317
+ #customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
318
+ #inputArea: Container;
319
+ #inlineEditor: Editor | undefined;
320
+ #helpTextComponent: Text;
321
+ #baseHelpText: string;
322
+ #tui: TUI | undefined;
300
323
  constructor(
301
324
  title: string,
302
325
  options: string[],
@@ -317,6 +340,8 @@ export class HookSelectorComponent extends Container {
317
340
  this.#onExternalEditorCallback = opts?.onExternalEditor;
318
341
  this.#wrapFocused = opts?.wrapFocused === true;
319
342
  this.#outline = opts?.outline === true;
343
+ this.#customInput = opts?.customInput;
344
+ this.#tui = opts?.tui;
320
345
 
321
346
  this.addChild(new DynamicBorder());
322
347
  this.addChild(new Spacer(1));
@@ -364,9 +389,12 @@ export class HookSelectorComponent extends Container {
364
389
  this.#listContainer = new Container();
365
390
  this.addChild(this.#listContainer);
366
391
  }
392
+ this.#inputArea = new Container();
393
+ this.addChild(this.#inputArea);
367
394
  this.addChild(new Spacer(1));
368
- const controlsHint = opts?.helpText ?? "up/down navigate enter select esc cancel";
369
- this.addChild(new Text(theme.fg("dim", controlsHint), 1, 0));
395
+ this.#baseHelpText = opts?.helpText ?? "up/down navigate enter select esc cancel";
396
+ this.#helpTextComponent = new Text(theme.fg("dim", this.#baseHelpText), 1, 0);
397
+ this.addChild(this.#helpTextComponent);
370
398
  this.addChild(new Spacer(1));
371
399
  this.addChild(new DynamicBorder());
372
400
 
@@ -432,6 +460,10 @@ export class HookSelectorComponent extends Container {
432
460
  this.#scrollableTitle?.scrollBy(this.#scrollTitleRows);
433
461
  return;
434
462
  }
463
+ if (this.#inlineEditor) {
464
+ this.#handleInputModeKey(keyData, this.#inlineEditor);
465
+ return;
466
+ }
435
467
  if (matchesKey(keyData, "up") || keyData === "k") {
436
468
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
437
469
  this.#updateList();
@@ -440,7 +472,12 @@ export class HookSelectorComponent extends Container {
440
472
  this.#updateList();
441
473
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
442
474
  const selected = this.#options[this.#selectedIndex];
443
- if (selected) this.#onSelectCallback(selected);
475
+ if (!selected) return;
476
+ if (this.#customInput && selected === this.#customInput.optionLabel) {
477
+ this.#enterInputMode();
478
+ return;
479
+ }
480
+ this.#onSelectCallback(selected);
444
481
  } else if (matchesKey(keyData, "left")) {
445
482
  this.#onLeftCallback?.();
446
483
  } else if (matchesKey(keyData, "right")) {
@@ -452,6 +489,73 @@ export class HookSelectorComponent extends Container {
452
489
  }
453
490
  }
454
491
 
492
+ /** Keys while the inline custom-input editor is open below the option list. */
493
+ #handleInputModeKey(keyData: string, editor: Editor): void {
494
+ // Escape backs out to option selection instead of cancelling the dialog,
495
+ // so a stray Esc never throws away the question context.
496
+ if (matchesKey(keyData, "escape") || matchesAppInterrupt(keyData)) {
497
+ this.#exitInputMode();
498
+ return;
499
+ }
500
+ if (matchesAppExternalEditor(keyData)) {
501
+ void this.#openExternalEditor(editor);
502
+ return;
503
+ }
504
+ if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
505
+ this.#customInput?.onSubmit(editor.getText());
506
+ return;
507
+ }
508
+ editor.handleInput(keyData);
509
+ }
510
+
511
+ #enterInputMode(): void {
512
+ if (this.#inlineEditor) return;
513
+ // Stop the auto-select countdown for good: the user is actively typing,
514
+ // matching the old behavior where the separate editor had no timeout.
515
+ if (this.#countdown) {
516
+ this.#countdown.dispose();
517
+ this.#countdown = undefined;
518
+ this.#titleComponent.setText(this.#baseTitle);
519
+ }
520
+ const editor = new Editor(getEditorTheme());
521
+ editor.setBorderVisible(false);
522
+ editor.setPromptGutter("> ");
523
+ editor.disableSubmit = true;
524
+ this.#inlineEditor = editor;
525
+ this.#inputArea.addChild(new Spacer(1));
526
+ this.#inputArea.addChild(editor);
527
+ const scrollHint = this.#scrollTitleRows === undefined ? "" : " wheel/PgUp/PgDn scroll question";
528
+ this.#helpTextComponent.setText(
529
+ theme.fg("dim", `enter submit esc back to options ctrl+g external editor${scrollHint}`),
530
+ );
531
+ this.invalidate();
532
+ }
533
+
534
+ #exitInputMode(): void {
535
+ if (!this.#inlineEditor) return;
536
+ this.#inlineEditor = undefined;
537
+ this.#inputArea.clear();
538
+ this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
539
+ this.invalidate();
540
+ }
541
+
542
+ async #openExternalEditor(editor: Editor): Promise<void> {
543
+ const editorCmd = getEditorCommand();
544
+ if (!editorCmd || !this.#tui) return;
545
+
546
+ const currentText = editor.getExpandedText();
547
+ try {
548
+ this.#tui.stop();
549
+ const result = await openInEditor(editorCmd, currentText);
550
+ if (result !== null) {
551
+ editor.setText(result);
552
+ }
553
+ } finally {
554
+ this.#tui.start();
555
+ this.#tui.requestRender(true);
556
+ }
557
+ }
558
+
455
559
  dispose(): void {
456
560
  this.#countdown?.dispose();
457
561
  }
@@ -29,6 +29,7 @@ const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
29
29
  const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
30
30
  const HOOK_SELECTOR_CHROME_ROWS = 7;
31
31
  const HOOK_SELECTOR_OUTLINE_ROWS = 2;
32
+ const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
32
33
 
33
34
  export class ExtensionUiController {
34
35
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
@@ -601,8 +602,11 @@ export class ExtensionUiController {
601
602
  const maxVisible =
602
603
  requestedTitleRows === undefined ? baseMaxVisible : Math.min(15, Math.max(3, scrollOptionRows + 1));
603
604
  const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
605
+ // Reserve rows for the inline custom-input editor so opening it doesn't
606
+ // push the scrollable title past the viewport into terminal scrollback.
607
+ const inlineInputRows = dialogOptions?.customInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
604
608
  const availableTitleRows =
605
- this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - HOOK_SELECTOR_CHROME_ROWS;
609
+ this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
606
610
  const scrollTitleRows =
607
611
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
608
612
  if (scrollTitleRows !== undefined) {
@@ -645,6 +649,17 @@ export class ExtensionUiController {
645
649
  wrapFocused: dialogOptions?.wrapFocused,
646
650
  scrollTitleRows,
647
651
  maxVisible,
652
+ customInput: dialogOptions?.customInput
653
+ ? {
654
+ optionLabel: dialogOptions.customInput.optionLabel,
655
+ onSubmit: text => {
656
+ const optionLabel = dialogOptions.customInput?.optionLabel;
657
+ this.hideHookSelector();
658
+ dialogOptions.customInput?.onSubmit(text);
659
+ finish(optionLabel);
660
+ },
661
+ }
662
+ : undefined,
648
663
  },
649
664
  );
650
665
  this.ctx.editorContainer.clear();
@@ -64,6 +64,11 @@ export class InputController {
64
64
  this.ctx.autoCompactionEscapeHandler ||
65
65
  this.ctx.retryEscapeHandler,
66
66
  );
67
+ // An open btw panel must stay dismissable with Esc even while another
68
+ // controller (auto-compaction, auto-retry, manual compaction, etc.) has
69
+ // temporarily replaced editor.onEscape. This priority hook is never
70
+ // swapped out, so it always wins for the interrupt key.
71
+ this.ctx.editor.onInterruptPriority = () => (this.ctx.hasActiveBtw() ? this.ctx.handleBtwEscape() : false);
67
72
  this.ctx.editor.onEscape = () => {
68
73
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
69
74
  return;
@@ -289,11 +294,12 @@ export class InputController {
289
294
  text = slashResult;
290
295
  }
291
296
 
292
- // Handle skill commands (/skill:name [args]). Enter steer (matches the
293
- // free-text Enter semantics applied a few lines below at the streaming
294
- // branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
295
- // same helper with `"followUp"`.
296
- if (await this.#invokeSkillCommand(text, "steer")) {
297
+ // Handle skill commands (/skill:name [args]). While streaming, Enter
298
+ // honors `busyPromptMode`: "steer" interrupts the active turn, "queue"
299
+ // runs after it completes (matches the free-text Enter semantics applied
300
+ // a few lines below at the streaming branch). Ctrl+Enter always routes
301
+ // through `handleFollowUp` and dispatches the same helper with `"followUp"`.
302
+ if (await this.#invokeSkillCommand(text, this.#busyStreamingBehavior())) {
297
303
  return;
298
304
  }
299
305
 
@@ -344,7 +350,9 @@ export class InputController {
344
350
  return;
345
351
  }
346
352
 
347
- // If streaming, use prompt() with steer behavior
353
+ // If streaming, use prompt() with the busy-prompt behavior the user
354
+ // selected: "steer" interrupts the active turn, "queue" defers the
355
+ // prompt to run after the active turn completes (in submission order).
348
356
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
349
357
  if (this.ctx.session.isStreaming) {
350
358
  this.ctx.editor.addToHistory(text);
@@ -355,9 +363,10 @@ export class InputController {
355
363
  // (a user-role `message_start` event) leaves any draft the user has
356
364
  // typed since queuing intact. Same protection as #783, applied to
357
365
  // the streaming/queue path.
366
+ const streamingBehavior = this.#busyStreamingBehavior();
358
367
  await this.ctx.withLocalSubmission(
359
368
  text,
360
- () => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
369
+ () => this.ctx.session.prompt(text, { streamingBehavior, images }),
361
370
  { imageCount: images?.length ?? 0 },
362
371
  );
363
372
  this.ctx.updatePendingMessagesDisplay();
@@ -450,6 +459,17 @@ export class InputController {
450
459
  }
451
460
  }
452
461
 
462
+ /**
463
+ * Resolve how a prompt submitted while the agent is busy should be delivered.
464
+ * Driven by the `busyPromptMode` setting and kept distinct from the
465
+ * follow-up keybinding: "steer" interrupts the active turn, "queue" defers
466
+ * the prompt to the follow-up queue so it runs after the active turn
467
+ * completes (in submission order). Only consulted while streaming.
468
+ */
469
+ #busyStreamingBehavior(): "steer" | "followUp" {
470
+ return this.ctx.settings.get("busyPromptMode") === "queue" ? "followUp" : "steer";
471
+ }
472
+
453
473
  /**
454
474
  * Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
455
475
  * using the supplied `streamingBehavior`. Returns true if the text was a
@@ -506,7 +506,13 @@ export class SelectorController {
506
506
  }
507
507
  break;
508
508
  case "providers.image":
509
- if (value === "auto" || value === "openai" || value === "gemini" || value === "openrouter") {
509
+ if (
510
+ value === "auto" ||
511
+ value === "openai" ||
512
+ value === "gemini" ||
513
+ value === "openrouter" ||
514
+ value === "antigravity"
515
+ ) {
510
516
  setPreferredImageProvider(value);
511
517
  }
512
518
  break;
@@ -1156,7 +1156,9 @@ export class InteractiveMode implements InteractiveModeContext {
1156
1156
  this.#updateGoalModeStatus();
1157
1157
  return;
1158
1158
  }
1159
- const pendingGoal = goalEnabled ? await consumePendingGoalModeRequest(this.sessionManager.getCwd()) : null;
1159
+ const pendingGoal = goalEnabled
1160
+ ? await consumePendingGoalModeRequest(this.sessionManager.getCwd(), this.sessionManager.getSessionId())
1161
+ : null;
1160
1162
  if (pendingGoal) {
1161
1163
  await this.#enterGoalMode({ objective: pendingGoal.objective, silent: true });
1162
1164
  this.#scheduleGoalContinuation();
@@ -150,6 +150,18 @@ function isRpcWorkflowGate(value: unknown): value is RpcWorkflowGate {
150
150
  );
151
151
  }
152
152
 
153
+ /**
154
+ * Unwrap a canonical agent-wire `event` frame `{ type:"event", payload:{ event_type, event } }`
155
+ * to its inner `AgentSessionEvent`. Returns null for any frame that is not a
156
+ * canonical event frame (session events are only delivered wrapped).
157
+ */
158
+ function unwrapAgentWireEventFrame(value: unknown): unknown {
159
+ if (isRecord(value) && value.type === "event" && isRecord(value.payload)) {
160
+ return value.payload.event;
161
+ }
162
+ return null;
163
+ }
164
+
153
165
  function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
154
166
  if (typeof result === "string") {
155
167
  return {
@@ -763,11 +775,12 @@ export class RpcClient {
763
775
  return;
764
776
  }
765
777
 
766
- if (!isAgentEvent(data)) return;
778
+ // Canonical agent-wire event frame: { type:"event", payload:{ event_type, event } }.
779
+ const event = unwrapAgentWireEventFrame(data);
780
+ if (!isAgentEvent(event)) return;
767
781
 
768
- // Otherwise it's an event
769
782
  for (const listener of this.#eventListeners) {
770
- listener(data);
783
+ listener(event);
771
784
  }
772
785
  }
773
786
 
@@ -21,6 +21,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
21
21
  import type { AgentSession } from "../../session/agent-session";
22
22
  import { initializeExtensions } from "../runtime-init";
23
23
  import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
24
+ import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
24
25
  import { rpcError as error } from "../shared/agent-wire/responses";
25
26
  import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
26
27
  import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
@@ -471,9 +472,11 @@ export async function runRpcMode(
471
472
  uiContext: rpcUiContext,
472
473
  });
473
474
 
474
- // Output all agent events as JSON
475
+ // Output all agent events as canonical agent-wire `event` frames (docs/rpc.md):
476
+ // { type:"event", protocol_version, session_id, seq, frame_id, payload:{ event_type, event } }.
477
+ const eventSequencer = new AgentWireFrameSequencer(session.sessionId);
475
478
  session.subscribe(event => {
476
- output(event);
479
+ output(toAgentWireEventFrame(event, eventSequencer));
477
480
  });
478
481
 
479
482
  // Handle a single command through the shared agent-wire dispatcher so RPC
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Canonical command-surface boundary for the agent-wire adapters.
3
+ *
4
+ * RPC and Bridge SHARE the JSONL `RpcCommand` grammar and dispatch it through
5
+ * the single `dispatchRpcCommand` entry in `command-dispatch.ts`. This module
6
+ * re-exports that command surface so the shared contract has one documented home.
7
+ *
8
+ * ACP does NOT use `RpcCommand`. It keeps its richer `@agentclientprotocol/sdk`
9
+ * command surface (fork/resume/elicitation/session-mode/session-model) and only
10
+ * shares the lower session/event layer (`AgentWireEventPayload`). ACP must never
11
+ * import `dispatchRpcCommand`.
12
+ *
13
+ * Event semantics are intentionally elsewhere: `event-contract.ts` owns the event
14
+ * types + registry and `event-observation.ts` owns the single semantic mapping.
15
+ */
16
+ export type { RpcCommand, RpcResponse } from "../../rpc/rpc-types";
17
+ export { dispatchRpcCommand, type RpcCommandDispatchContext } from "./command-dispatch";
18
+ export { isRpcCommandType, RPC_COMMAND_TYPES, type RpcCommandType } from "./scopes";