@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/cli.js +3305 -3302
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +9 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +9 -1
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +22 -12
- package/src/advisor/advise-tool.ts +7 -3
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/config/model-registry.ts +9 -0
- package/src/config/model-resolver.ts +43 -5
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -1
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/export/html/template.js +2 -0
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/lib/xai-http.ts +28 -2
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +17 -2
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +6 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +19 -17
- package/src/modes/interactive-mode.ts +66 -11
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +8 -3
- package/src/modes/rpc/rpc-mode.ts +12 -3
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/sdk.ts +6 -2
- package/src/session/agent-session.ts +79 -34
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +2 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/eval.ts +3 -0
- package/src/tools/image-gen.ts +561 -512
- package/src/tools/index.ts +3 -1
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/xai.ts +40 -9
|
@@ -83,7 +83,7 @@ import { UserMessageSelectorComponent } from "../components/user-message-selecto
|
|
|
83
83
|
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
84
84
|
import { buildCopyTargets } from "../utils/copy-targets";
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL), then press Enter:";
|
|
87
87
|
|
|
88
88
|
export class SelectorController {
|
|
89
89
|
constructor(private ctx: InteractiveModeContext) {}
|
|
@@ -188,6 +188,7 @@ export class SelectorController {
|
|
|
188
188
|
onPluginsChanged: async () => {
|
|
189
189
|
const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
|
|
190
190
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
191
|
+
await this.ctx.refreshSkillState();
|
|
191
192
|
await this.ctx.refreshSlashCommandState();
|
|
192
193
|
resetCapabilities();
|
|
193
194
|
this.ctx.ui.requestRender();
|
|
@@ -1109,12 +1110,10 @@ export class SelectorController {
|
|
|
1109
1110
|
// every project's history when the cwd has nothing to resume. See #3099.
|
|
1110
1111
|
const historyStorage = this.ctx.historyStorage;
|
|
1111
1112
|
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1115
|
-
//
|
|
1116
|
-
// directly to a rendered line (the overlay paints from screen row 0), and
|
|
1117
|
-
// `fillHeight` pads the body so the footer pins to the screen bottom.
|
|
1113
|
+
// Keep the fullscreen picker on the alternate buffer while a selected
|
|
1114
|
+
// session is loaded and its transcript is rebuilt. Closing it first exposes
|
|
1115
|
+
// the stale normal buffer for the entire async switch on terminals without
|
|
1116
|
+
// effective synchronized output.
|
|
1118
1117
|
let overlayHandle: OverlayHandle | undefined;
|
|
1119
1118
|
const done = () => {
|
|
1120
1119
|
overlayHandle?.hide();
|
|
@@ -1124,8 +1123,12 @@ export class SelectorController {
|
|
|
1124
1123
|
const selector = new SessionSelectorComponent(
|
|
1125
1124
|
sessions,
|
|
1126
1125
|
async (session: SessionInfo) => {
|
|
1127
|
-
|
|
1128
|
-
|
|
1126
|
+
selector.lockInput();
|
|
1127
|
+
try {
|
|
1128
|
+
await this.handleResumeSession(session.path);
|
|
1129
|
+
} finally {
|
|
1130
|
+
done();
|
|
1131
|
+
}
|
|
1129
1132
|
},
|
|
1130
1133
|
() => {
|
|
1131
1134
|
done();
|
|
@@ -1272,7 +1275,6 @@ export class SelectorController {
|
|
|
1272
1275
|
*/
|
|
1273
1276
|
async #handleOAuthLogin(providerId: string): Promise<boolean> {
|
|
1274
1277
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1275
|
-
const manualInput = this.ctx.oauthManualInput;
|
|
1276
1278
|
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
1277
1279
|
let restored = false;
|
|
1278
1280
|
const restoreEditor = () => {
|
|
@@ -1300,16 +1302,19 @@ export class SelectorController {
|
|
|
1300
1302
|
// The dialog renders the full URL (SSH-safe copy target) and
|
|
1301
1303
|
// opens the browser best-effort.
|
|
1302
1304
|
dialog.showAuth(info.url, info.instructions, info.launchUrl);
|
|
1303
|
-
if (useManualInput) {
|
|
1304
|
-
dialog.showProgress(MANUAL_LOGIN_TIP);
|
|
1305
|
-
}
|
|
1306
1305
|
},
|
|
1307
1306
|
onPrompt: (prompt: { message: string; placeholder?: string }) =>
|
|
1308
1307
|
dialog.showPrompt(prompt.message, prompt.placeholder),
|
|
1309
1308
|
onProgress: (message: string) => {
|
|
1310
1309
|
dialog.showProgress(message);
|
|
1311
1310
|
},
|
|
1312
|
-
|
|
1311
|
+
// Paste-code providers (e.g. Codex) may need the user to paste the
|
|
1312
|
+
// fallback redirect URL when the loopback callback can't complete
|
|
1313
|
+
// (headless/remote/Windows). Mount a focused input in the dialog so
|
|
1314
|
+
// the paste lands somewhere the OAuth flow consumes — the hidden
|
|
1315
|
+
// editor's `/login <url>` path is unreachable while the dialog holds
|
|
1316
|
+
// focus (#5339).
|
|
1317
|
+
onManualCodeInput: useManualInput ? () => dialog.showManualInput(MANUAL_LOGIN_PROMPT) : undefined,
|
|
1313
1318
|
});
|
|
1314
1319
|
this.ctx.session.modelRegistry.refreshInBackground();
|
|
1315
1320
|
const block = new TranscriptBlock();
|
|
@@ -1338,9 +1343,6 @@ export class SelectorController {
|
|
|
1338
1343
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1339
1344
|
return false;
|
|
1340
1345
|
} finally {
|
|
1341
|
-
if (useManualInput) {
|
|
1342
|
-
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
1343
|
-
}
|
|
1344
1346
|
restoreEditor();
|
|
1345
1347
|
}
|
|
1346
1348
|
}
|
|
@@ -519,6 +519,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
519
519
|
collabHost?: CollabHost;
|
|
520
520
|
collabGuest?: CollabGuestLink;
|
|
521
521
|
|
|
522
|
+
#pendingCommandOutput: Component[] = [];
|
|
523
|
+
#pendingCommandOutputSessionId: string | undefined;
|
|
522
524
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
523
525
|
/** Built-in editor autocomplete provider, before extension wrapping. */
|
|
524
526
|
#baseAutocompleteProvider: AutocompleteProvider | undefined;
|
|
@@ -733,15 +735,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
733
735
|
description: `${loaded.command.description} (${loaded.source})`,
|
|
734
736
|
}));
|
|
735
737
|
|
|
736
|
-
|
|
737
|
-
const skillCommandList: SlashCommand[] = [];
|
|
738
|
-
if (settings.get("skills.enableSkillCommands")) {
|
|
739
|
-
for (const skill of this.session.skills) {
|
|
740
|
-
const commandName = `skill:${skill.name}`;
|
|
741
|
-
this.skillCommands.set(commandName, skill);
|
|
742
|
-
skillCommandList.push({ name: commandName, description: skill.description });
|
|
743
|
-
}
|
|
744
|
-
}
|
|
738
|
+
const skillCommandList = this.#rebuildSkillCommandsFromSession();
|
|
745
739
|
|
|
746
740
|
const builtinCommands = buildTuiBuiltinSlashCommands({ ctx: this });
|
|
747
741
|
// Store pending commands for init() where file commands are loaded async
|
|
@@ -1029,6 +1023,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1029
1023
|
this.#handleSessionAccentInputsChanged();
|
|
1030
1024
|
}),
|
|
1031
1025
|
);
|
|
1026
|
+
this.#eventBusUnsubscribers.push(
|
|
1027
|
+
this.session.subscribeCommandMetadataChanged(() => {
|
|
1028
|
+
const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
|
|
1029
|
+
const skillCommands = this.#rebuildSkillCommandsFromSession();
|
|
1030
|
+
this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
|
|
1031
|
+
}),
|
|
1032
|
+
);
|
|
1032
1033
|
// Set up theme file watcher
|
|
1033
1034
|
this.#eventBusUnsubscribers.push(
|
|
1034
1035
|
onThemeChange(event => {
|
|
@@ -1077,6 +1078,27 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1077
1078
|
this.session.setTitleSystemPrompt(resolved);
|
|
1078
1079
|
}
|
|
1079
1080
|
|
|
1081
|
+
#rebuildSkillCommandsFromSession(): SlashCommand[] {
|
|
1082
|
+
const commands: SlashCommand[] = [];
|
|
1083
|
+
this.skillCommands.clear();
|
|
1084
|
+
if (this.session.skillsSettings?.enableSkillCommands !== false) {
|
|
1085
|
+
for (const skill of this.session.skills) {
|
|
1086
|
+
const commandName = `skill:${skill.name}`;
|
|
1087
|
+
this.skillCommands.set(commandName, skill);
|
|
1088
|
+
commands.push({ name: commandName, description: skill.description });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return commands;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Reload session skills and the `/skill:<name>` command list. */
|
|
1095
|
+
async refreshSkillState(): Promise<void> {
|
|
1096
|
+
await this.session.refreshSkills();
|
|
1097
|
+
const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
|
|
1098
|
+
const skillCommands = this.#rebuildSkillCommandsFromSession();
|
|
1099
|
+
this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1080
1102
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
1081
1103
|
async refreshSlashCommandState(cwd?: string): Promise<void> {
|
|
1082
1104
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
@@ -1175,6 +1197,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1175
1197
|
clearClaudePluginRootsCache();
|
|
1176
1198
|
await this.refreshTitleSystemPrompt(newCwd);
|
|
1177
1199
|
resetCapabilities();
|
|
1200
|
+
await this.refreshSkillState();
|
|
1178
1201
|
await this.refreshSlashCommandState(newCwd);
|
|
1179
1202
|
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1180
1203
|
this.statusLine.invalidate();
|
|
@@ -2498,8 +2521,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2498
2521
|
const finish = (choice: string | undefined): void => {
|
|
2499
2522
|
if (settled) return;
|
|
2500
2523
|
settled = true;
|
|
2501
|
-
this.#hidePlanReview();
|
|
2502
|
-
this.ui.requestRender();
|
|
2503
2524
|
resolve(choice);
|
|
2504
2525
|
};
|
|
2505
2526
|
const overlay = new PlanReviewOverlay(
|
|
@@ -3423,6 +3444,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3423
3444
|
},
|
|
3424
3445
|
{ slider },
|
|
3425
3446
|
);
|
|
3447
|
+
const closePlanReview = (): void => {
|
|
3448
|
+
this.#hidePlanReview();
|
|
3449
|
+
this.ui.requestRender();
|
|
3450
|
+
};
|
|
3426
3451
|
|
|
3427
3452
|
if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
|
|
3428
3453
|
try {
|
|
@@ -3435,6 +3460,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3435
3460
|
}
|
|
3436
3461
|
if (!latestPlanContent) {
|
|
3437
3462
|
this.showError(`Plan file not found at ${planFilePath}`);
|
|
3463
|
+
closePlanReview();
|
|
3438
3464
|
return;
|
|
3439
3465
|
}
|
|
3440
3466
|
// Capture the operator's tier choice and hand it to #approvePlan, which
|
|
@@ -3477,6 +3503,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3477
3503
|
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
3478
3504
|
);
|
|
3479
3505
|
}
|
|
3506
|
+
closePlanReview();
|
|
3480
3507
|
return;
|
|
3481
3508
|
}
|
|
3482
3509
|
|
|
@@ -3495,8 +3522,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3495
3522
|
} catch (error) {
|
|
3496
3523
|
this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
|
|
3497
3524
|
}
|
|
3525
|
+
closePlanReview();
|
|
3498
3526
|
return;
|
|
3499
3527
|
}
|
|
3528
|
+
closePlanReview();
|
|
3500
3529
|
}
|
|
3501
3530
|
|
|
3502
3531
|
/**
|
|
@@ -3711,6 +3740,32 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3711
3740
|
this.ui.requestRender();
|
|
3712
3741
|
}
|
|
3713
3742
|
|
|
3743
|
+
/** Defer transcript command panels until the active turn can no longer grow above them. */
|
|
3744
|
+
presentCommandOutput(content: Component | readonly Component[]): void {
|
|
3745
|
+
if (!this.session.isStreaming) {
|
|
3746
|
+
this.present(content);
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
3750
|
+
if (this.#pendingCommandOutput.length > 0 && this.#pendingCommandOutputSessionId !== sessionId) {
|
|
3751
|
+
this.#pendingCommandOutput = [];
|
|
3752
|
+
}
|
|
3753
|
+
this.#pendingCommandOutputSessionId = sessionId;
|
|
3754
|
+
const items = Array.isArray(content) ? content : [content as Component];
|
|
3755
|
+
this.#pendingCommandOutput.push(...items);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
/** Mount every command panel queued for the current session while the agent was streaming. */
|
|
3759
|
+
flushPendingCommandOutput(): void {
|
|
3760
|
+
if (this.#pendingCommandOutput.length === 0) return;
|
|
3761
|
+
const pending = this.#pendingCommandOutput;
|
|
3762
|
+
const pendingSessionId = this.#pendingCommandOutputSessionId;
|
|
3763
|
+
this.#pendingCommandOutput = [];
|
|
3764
|
+
this.#pendingCommandOutputSessionId = undefined;
|
|
3765
|
+
if (pendingSessionId !== this.sessionManager.getSessionId()) return;
|
|
3766
|
+
this.present(pending);
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3714
3769
|
#mountChatChild(item: Component): void {
|
|
3715
3770
|
this.chatContainer.addChild(item);
|
|
3716
3771
|
if (item instanceof ChatBlock) item.mount(this.#chatHost);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract: the print-mode assistant-error/aborted exit path MUST run the
|
|
3
|
+
* awaited `session.dispose()` (which contains the bounded browser reaper
|
|
4
|
+
* `releaseTabsForOwner`) before terminating the process. It previously called
|
|
5
|
+
* `process.exit(1)` ahead of the `dispose()` at the end of `runPrintMode`, so
|
|
6
|
+
* an OMP-owned Chromium survived the exit (issue #5643).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it, spyOn } from "bun:test";
|
|
9
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { AgentSession } from "../session/agent-session";
|
|
11
|
+
import { runPrintMode } from "./print-mode";
|
|
12
|
+
|
|
13
|
+
/** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
|
|
14
|
+
class ProcessExit extends Error {
|
|
15
|
+
constructor(readonly code: number) {
|
|
16
|
+
super(`process.exit(${code})`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("print-mode error exit disposes the session before exit", () => {
|
|
21
|
+
it("disposes on the assistant-error path before process.exit(1)", async () => {
|
|
22
|
+
const order: string[] = [];
|
|
23
|
+
const errorMsg: AssistantMessage = {
|
|
24
|
+
role: "assistant",
|
|
25
|
+
content: [],
|
|
26
|
+
api: "openai-responses",
|
|
27
|
+
provider: "openai",
|
|
28
|
+
model: "gpt-test",
|
|
29
|
+
usage: {} as AssistantMessage["usage"],
|
|
30
|
+
stopReason: "error",
|
|
31
|
+
errorMessage: "boom",
|
|
32
|
+
timestamp: 1,
|
|
33
|
+
};
|
|
34
|
+
const session = {
|
|
35
|
+
extensionRunner: undefined,
|
|
36
|
+
subscribe: () => {},
|
|
37
|
+
state: { messages: [errorMsg] },
|
|
38
|
+
dispose: async () => {
|
|
39
|
+
order.push("dispose");
|
|
40
|
+
},
|
|
41
|
+
} as unknown as AgentSession;
|
|
42
|
+
|
|
43
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
|
|
44
|
+
order.push("exit");
|
|
45
|
+
throw new ProcessExit(code);
|
|
46
|
+
}) as never);
|
|
47
|
+
const stderrSpy = spyOn(process.stderr, "write").mockImplementation((() => true) as never);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await runPrintMode(session, { mode: "text" });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (!(err instanceof ProcessExit)) throw err;
|
|
53
|
+
} finally {
|
|
54
|
+
exitSpy.mockRestore();
|
|
55
|
+
stderrSpy.mockRestore();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(order).toEqual(["dispose", "exit"]);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -144,10 +144,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
144
144
|
!isSilentAbort(assistantMsg)
|
|
145
145
|
) {
|
|
146
146
|
const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
147
|
+
// This branch hard-exits, bypassing the `await session.dispose()` at
|
|
148
|
+
// the end of runPrintMode. Flush telemetry and dispose the session
|
|
149
|
+
// HERE so error spans reach the exporter (the postmortem `exit`
|
|
150
|
+
// handler can't await) and the browser reaper installed in
|
|
151
|
+
// `dispose()` (releaseTabsForOwner) actually runs — otherwise an
|
|
152
|
+
// OMP-owned Chromium survives this exit (issue #5643). `dispose()`
|
|
153
|
+
// is idempotent, so the unreachable call below is a harmless no-op.
|
|
150
154
|
await flushTelemetryExport();
|
|
155
|
+
await session.dispose();
|
|
151
156
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
152
157
|
if (flushed) {
|
|
153
158
|
process.exit(1);
|
|
@@ -914,6 +914,7 @@ export async function runRpcMode(
|
|
|
914
914
|
const projectPath = await resolveActiveProjectRegistryPath(cwd);
|
|
915
915
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
916
916
|
resetCapabilities();
|
|
917
|
+
await session.refreshSkills();
|
|
917
918
|
session.setSlashCommands(await loadSlashCommands({ cwd }));
|
|
918
919
|
await emitAvailableCommandsUpdate();
|
|
919
920
|
};
|
|
@@ -1346,9 +1347,12 @@ export async function runRpcMode(
|
|
|
1346
1347
|
const shutdownCoordinator = new RpcShutdownCoordinator({
|
|
1347
1348
|
isShutdownRequested: () => shutdownState.requested,
|
|
1348
1349
|
performShutdown: async () => {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1350
|
+
// Route through the idempotent session.dispose() so the browser
|
|
1351
|
+
// reaper (releaseTabsForOwner) and other bounded teardown run before
|
|
1352
|
+
// the process exits. dispose() also emits `session_shutdown`, so we
|
|
1353
|
+
// must NOT emit it separately here or the event fires twice. Skipping
|
|
1354
|
+
// dispose left OMP-owned Chromium alive after RPC shutdown (#5643).
|
|
1355
|
+
await session.dispose();
|
|
1352
1356
|
process.exit(0);
|
|
1353
1357
|
},
|
|
1354
1358
|
});
|
|
@@ -1384,5 +1388,10 @@ export async function runRpcMode(
|
|
|
1384
1388
|
await inputDispatcher.drain();
|
|
1385
1389
|
await shutdownCoordinator.drain();
|
|
1386
1390
|
subagentRegistry?.dispose();
|
|
1391
|
+
// Dispose the main session before exiting so the browser reaper and other
|
|
1392
|
+
// bounded teardown run on the stdin-EOF path too (#5643). Idempotent: a
|
|
1393
|
+
// prior pi.shutdown() through the coordinator makes this await settle
|
|
1394
|
+
// immediately.
|
|
1395
|
+
await session.dispose();
|
|
1387
1396
|
process.exit(0);
|
|
1388
1397
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -614,8 +614,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
614
614
|
"icon.throughput": "\uf0e4",
|
|
615
615
|
// pick: | alt:
|
|
616
616
|
"icon.host": "\uf109",
|
|
617
|
-
// pick:
|
|
618
|
-
"icon.session": "\
|
|
617
|
+
// pick: (nf-md-arrow_left_bold_hexagon_outline) | alt:
|
|
618
|
+
"icon.session": "\u{f0051}",
|
|
619
619
|
// pick: | alt:
|
|
620
620
|
"icon.package": "\uf487",
|
|
621
621
|
// pick: | alt:
|
package/src/modes/types.ts
CHANGED
|
@@ -237,6 +237,14 @@ export interface InteractiveModeContext {
|
|
|
237
237
|
* runs) so their timers/subscriptions start.
|
|
238
238
|
*/
|
|
239
239
|
present(content: Component | readonly Component[]): void;
|
|
240
|
+
/**
|
|
241
|
+
* Mount command output immediately while idle, or defer it until the active
|
|
242
|
+
* agent turn ends so a growing live block cannot push duplicate rows into
|
|
243
|
+
* native scrollback.
|
|
244
|
+
*/
|
|
245
|
+
presentCommandOutput(content: Component | readonly Component[]): void;
|
|
246
|
+
/** Mount command output deferred by {@link presentCommandOutput}. */
|
|
247
|
+
flushPendingCommandOutput(): void;
|
|
240
248
|
/**
|
|
241
249
|
* Dispose every live block in the transcript (stopping timers/subscriptions)
|
|
242
250
|
* and clear it. Used before a full rebuild so animated/streaming blocks do not
|
|
@@ -347,6 +355,8 @@ export interface InteractiveModeContext {
|
|
|
347
355
|
): Promise<CompactionOutcome>;
|
|
348
356
|
openInBrowser(urlOrPath: string): void;
|
|
349
357
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
358
|
+
/** Reload session skills and derived `/skill:<name>` commands. */
|
|
359
|
+
refreshSkillState(): Promise<void>;
|
|
350
360
|
applyCwdChange(newCwd: string): Promise<void>;
|
|
351
361
|
|
|
352
362
|
// Selector handling
|
|
@@ -16,7 +16,7 @@ export function createAssistantMessageComponent(
|
|
|
16
16
|
ctx: InteractiveModeContext,
|
|
17
17
|
message?: AssistantMessage,
|
|
18
18
|
): AssistantMessageComponent {
|
|
19
|
-
|
|
19
|
+
const component = new AssistantMessageComponent(
|
|
20
20
|
message,
|
|
21
21
|
ctx.effectiveHideThinkingBlock,
|
|
22
22
|
() => ctx.ui.requestRender(),
|
|
@@ -24,4 +24,6 @@ export function createAssistantMessageComponent(
|
|
|
24
24
|
ctx.ui.imageBudget,
|
|
25
25
|
ctx.proseOnlyThinking,
|
|
26
26
|
);
|
|
27
|
+
component.setImagesVisible(ctx.settings.get("terminal.showImages"));
|
|
28
|
+
return component;
|
|
27
29
|
}
|
|
@@ -125,12 +125,13 @@ export function buildFileMentionBlock(files: FileMentionMessage["files"], indent
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Whether an assistant turn has visible text or
|
|
129
|
-
*
|
|
128
|
+
* Whether an assistant turn has visible text, thinking, or image content — i.e.
|
|
129
|
+
* content that closes the current read-tool run.
|
|
130
130
|
*/
|
|
131
131
|
export function assistantHasVisibleContent(message: AssistantAgentMessage): boolean {
|
|
132
132
|
return message.content.some(
|
|
133
133
|
content =>
|
|
134
|
+
content.type === "image" ||
|
|
134
135
|
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
135
136
|
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
136
137
|
);
|
|
@@ -56,7 +56,7 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
|
|
|
56
56
|
{{#if hasMemoryRoot}}
|
|
57
57
|
- `memory://root`: project memory summary
|
|
58
58
|
{{/if}}
|
|
59
|
-
- `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field
|
|
59
|
+
- `agent://<id>`: agent output artifact; `/<child>` reads a nested subagent's output, else `/<path>` extracts a JSON field
|
|
60
60
|
- `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves any agent whose session file persists on disk, not just registered peers.
|
|
61
61
|
- `artifact://<id>`: artifact content
|
|
62
62
|
- `local://<name>.md`: plan artifacts or shared content for subagents
|
package/src/sdk.ts
CHANGED
|
@@ -1557,7 +1557,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1557
1557
|
skipPythonPreflight: options.skipPythonPreflight,
|
|
1558
1558
|
contextFiles,
|
|
1559
1559
|
workspaceTree: resolvedWorkspaceTree,
|
|
1560
|
-
skills
|
|
1560
|
+
get skills() {
|
|
1561
|
+
return session?.skills ?? skills;
|
|
1562
|
+
},
|
|
1563
|
+
refreshSkills: () => session.refreshSkills(),
|
|
1561
1564
|
rules: allRules,
|
|
1562
1565
|
eventBus,
|
|
1563
1566
|
outputSchema: options.outputSchema,
|
|
@@ -2341,7 +2344,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2341
2344
|
xdevDocs: toolSession.xdevRegistry?.docsAll() ?? "",
|
|
2342
2345
|
autoQaEnabled: isAutoQaEnabled(settings),
|
|
2343
2346
|
resolvedCustomPrompt: options.customSystemPrompt,
|
|
2344
|
-
skills,
|
|
2347
|
+
skills: session?.skills ?? skills,
|
|
2345
2348
|
contextFiles,
|
|
2346
2349
|
tools: promptTools,
|
|
2347
2350
|
toolNames,
|
|
@@ -2737,6 +2740,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2737
2740
|
customCommands: customCommandsResult.commands,
|
|
2738
2741
|
skills,
|
|
2739
2742
|
skillWarnings,
|
|
2743
|
+
skillsReloadable: options.skills === undefined,
|
|
2740
2744
|
skillsSettings: settings.getGroup("skills"),
|
|
2741
2745
|
modelRegistry,
|
|
2742
2746
|
toolRegistry,
|