@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.
- package/CHANGELOG.md +13 -0
- package/dist/types/async/job-manager.d.ts +44 -1
- package/dist/types/cli/setup-cli.d.ts +14 -1
- package/dist/types/commands/coordinator.d.ts +19 -0
- package/dist/types/commands/mcp-serve.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +41 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/coordinator/contract.d.ts +4 -0
- package/dist/types/coordinator-mcp/policy.d.ts +24 -0
- package/dist/types/coordinator-mcp/safety.d.ts +26 -0
- package/dist/types/coordinator-mcp/server.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +13 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/harness-control-plane/types.d.ts +7 -2
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/session/agent-session.d.ts +12 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/setup/hermes-setup.d.ts +71 -0
- package/dist/types/task/render.d.ts +7 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/subagent-render.d.ts +25 -0
- package/dist/types/tools/subagent.d.ts +5 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +163 -2
- package/src/cli/setup-cli.ts +86 -2
- package/src/cli.ts +2 -0
- package/src/commands/coordinator.ts +70 -0
- package/src/commands/mcp-serve.ts +62 -0
- package/src/commands/setup.ts +30 -1
- package/src/commands/ultragoal.ts +7 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -1
- package/src/coordinator/contract.ts +20 -0
- package/src/coordinator-mcp/policy.ts +160 -0
- package/src/coordinator-mcp/safety.ts +80 -0
- package/src/coordinator-mcp/server.ts +1316 -0
- package/src/extensibility/extensions/types.ts +13 -0
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/gjc-runtime/session-state-sidecar.ts +79 -0
- package/src/harness-control-plane/owner.ts +3 -3
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +8 -11
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/components/hook-selector.ts +109 -5
- package/src/modes/controllers/extension-ui-controller.ts +16 -1
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +3 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/architect.md +6 -0
- package/src/prompts/agents/critic.md +6 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +66 -4
- package/src/session/session-manager.ts +1 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
- package/src/setup/hermes-setup.ts +429 -0
- package/src/task/agents.ts +1 -1
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +14 -0
- package/src/tools/ask.ts +30 -10
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/tools/renderers.ts +2 -0
- package/src/tools/subagent-render.ts +160 -0
- package/src/tools/subagent.ts +49 -7
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
package/src/memories/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
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")
|
|
295
|
+
if (this.#matchesAction(data, "app.interrupt")) {
|
|
289
296
|
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
|
290
|
-
|
|
291
|
-
|
|
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 {
|
|
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
|
-
|
|
369
|
-
this
|
|
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)
|
|
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]).
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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";
|