@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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 +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- 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 +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- 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/advisor-config.d.ts +8 -1
- 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 +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -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/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- 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/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- 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/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- 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/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- 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/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- 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 +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- 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 +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- 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/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- 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/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- 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/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
|
@@ -56,8 +56,15 @@ import { reset as resetCapabilities } from "../capability";
|
|
|
56
56
|
import type { CollabGuestLink } from "../collab/guest";
|
|
57
57
|
import type { CollabHost } from "../collab/host";
|
|
58
58
|
import { KeybindingsManager } from "../config/keybindings";
|
|
59
|
+
import type { ResolvedModelRoleValue } from "../config/model-resolver";
|
|
59
60
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
60
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
isSettingsInitialized,
|
|
63
|
+
onModelRolesChanged,
|
|
64
|
+
onStatusLineSessionAccentChanged,
|
|
65
|
+
Settings,
|
|
66
|
+
settings,
|
|
67
|
+
} from "../config/settings";
|
|
61
68
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
62
69
|
import type {
|
|
63
70
|
AutocompleteProviderFactory,
|
|
@@ -88,6 +95,7 @@ import {
|
|
|
88
95
|
resolveApprovedPlan,
|
|
89
96
|
resolvePlanTitle,
|
|
90
97
|
} from "../plan-mode/approved-plan";
|
|
98
|
+
import { resolvePlanModelTransition } from "../plan-mode/model-transition";
|
|
91
99
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
92
100
|
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
93
101
|
type: "text",
|
|
@@ -519,6 +527,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
519
527
|
collabHost?: CollabHost;
|
|
520
528
|
collabGuest?: CollabGuestLink;
|
|
521
529
|
|
|
530
|
+
#pendingCommandOutput: Component[] = [];
|
|
531
|
+
#pendingCommandOutputSessionId: string | undefined;
|
|
522
532
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
523
533
|
/** Built-in editor autocomplete provider, before extension wrapping. */
|
|
524
534
|
#baseAutocompleteProvider: AutocompleteProvider | undefined;
|
|
@@ -537,6 +547,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
537
547
|
#goalSuppressNextContinuation = false;
|
|
538
548
|
#planModePreviousModelState: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
|
|
539
549
|
#pendingModelSwitch: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
|
|
550
|
+
/** Whether #pendingModelSwitch was queued by the live plan-role reconciler. */
|
|
551
|
+
#pendingPlanModelSwitch = false;
|
|
540
552
|
#planModeHasEntered = false;
|
|
541
553
|
#planReviewOverlay: PlanReviewOverlay | undefined;
|
|
542
554
|
#planReviewOverlayHandle: OverlayHandle | undefined;
|
|
@@ -733,15 +745,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
733
745
|
description: `${loaded.command.description} (${loaded.source})`,
|
|
734
746
|
}));
|
|
735
747
|
|
|
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
|
-
}
|
|
748
|
+
const skillCommandList = this.#rebuildSkillCommandsFromSession();
|
|
745
749
|
|
|
746
750
|
const builtinCommands = buildTuiBuiltinSlashCommands({ ctx: this });
|
|
747
751
|
// Store pending commands for init() where file commands are loaded async
|
|
@@ -1029,6 +1033,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1029
1033
|
this.#handleSessionAccentInputsChanged();
|
|
1030
1034
|
}),
|
|
1031
1035
|
);
|
|
1036
|
+
this.#eventBusUnsubscribers.push(
|
|
1037
|
+
onModelRolesChanged(() => {
|
|
1038
|
+
void this.#reapplyPlanModeModelOnRoleChange();
|
|
1039
|
+
}),
|
|
1040
|
+
);
|
|
1041
|
+
this.#eventBusUnsubscribers.push(
|
|
1042
|
+
this.session.subscribeCommandMetadataChanged(() => {
|
|
1043
|
+
const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
|
|
1044
|
+
const skillCommands = this.#rebuildSkillCommandsFromSession();
|
|
1045
|
+
this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
|
|
1046
|
+
}),
|
|
1047
|
+
);
|
|
1032
1048
|
// Set up theme file watcher
|
|
1033
1049
|
this.#eventBusUnsubscribers.push(
|
|
1034
1050
|
onThemeChange(event => {
|
|
@@ -1077,6 +1093,27 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1077
1093
|
this.session.setTitleSystemPrompt(resolved);
|
|
1078
1094
|
}
|
|
1079
1095
|
|
|
1096
|
+
#rebuildSkillCommandsFromSession(): SlashCommand[] {
|
|
1097
|
+
const commands: SlashCommand[] = [];
|
|
1098
|
+
this.skillCommands.clear();
|
|
1099
|
+
if (this.session.skillsSettings?.enableSkillCommands !== false) {
|
|
1100
|
+
for (const skill of this.session.skills) {
|
|
1101
|
+
const commandName = `skill:${skill.name}`;
|
|
1102
|
+
this.skillCommands.set(commandName, skill);
|
|
1103
|
+
commands.push({ name: commandName, description: skill.description });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return commands;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** Reload session skills and the `/skill:<name>` command list. */
|
|
1110
|
+
async refreshSkillState(): Promise<void> {
|
|
1111
|
+
await this.session.refreshSkills();
|
|
1112
|
+
const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
|
|
1113
|
+
const skillCommands = this.#rebuildSkillCommandsFromSession();
|
|
1114
|
+
this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1080
1117
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
1081
1118
|
async refreshSlashCommandState(cwd?: string): Promise<void> {
|
|
1082
1119
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
@@ -1175,6 +1212,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1175
1212
|
clearClaudePluginRootsCache();
|
|
1176
1213
|
await this.refreshTitleSystemPrompt(newCwd);
|
|
1177
1214
|
resetCapabilities();
|
|
1215
|
+
await this.refreshSkillState();
|
|
1178
1216
|
await this.refreshSlashCommandState(newCwd);
|
|
1179
1217
|
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1180
1218
|
this.statusLine.invalidate();
|
|
@@ -2057,35 +2095,81 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2057
2095
|
if (!resolved.model) return;
|
|
2058
2096
|
|
|
2059
2097
|
const currentModel = this.session.model;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2098
|
+
// Capture the pre-plan model so #exitPlanMode can restore it. Only the
|
|
2099
|
+
// entry path records this — a mid-planning role change (below) leaves the
|
|
2100
|
+
// active model on the plan role, so overwriting here would restore the old
|
|
2101
|
+
// plan model instead of the user's real pre-plan model.
|
|
2063
2102
|
this.#planModePreviousModelState = currentModel
|
|
2064
2103
|
? { model: currentModel, thinkingLevel: this.session.configuredThinkingLevel() }
|
|
2065
2104
|
: undefined;
|
|
2066
2105
|
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2106
|
+
await this.#applyPlanModelTransition(currentModel, resolved);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* Re-resolve the `plan` role and move the active model onto it. Fires when
|
|
2111
|
+
* the plan role is reassigned while plan mode is active: the active model IS
|
|
2112
|
+
* the plan model there, so a settings-only change would otherwise leave the
|
|
2113
|
+
* current turn on the model plan mode was entered with (issue #5657). No-op
|
|
2114
|
+
* outside plan mode — role reassignment for an inactive role only touches
|
|
2115
|
+
* settings.
|
|
2116
|
+
*/
|
|
2117
|
+
async #reapplyPlanModeModelOnRoleChange(): Promise<void> {
|
|
2118
|
+
if (!this.planModeEnabled) return;
|
|
2119
|
+
const resolved = this.session.resolveRoleModelWithThinking("plan");
|
|
2120
|
+
if (!resolved.model) {
|
|
2121
|
+
this.#clearPendingPlanModelSwitch();
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
await this.#applyPlanModelTransition(this.session.model, resolved);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* Drop a stale deferred switch that was queued for a previous plan-role
|
|
2129
|
+
* assignment. Other deferred switches (such as restoring the pre-plan
|
|
2130
|
+
* model) remain intact.
|
|
2131
|
+
*/
|
|
2132
|
+
#clearPendingPlanModelSwitch(): void {
|
|
2133
|
+
if (!this.#pendingPlanModelSwitch) return;
|
|
2134
|
+
this.#pendingModelSwitch = undefined;
|
|
2135
|
+
this.#pendingPlanModelSwitch = false;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/** Apply (or defer) the model/thinking change implied by the resolved plan role. */
|
|
2139
|
+
async #applyPlanModelTransition(currentModel: Model | undefined, resolved: ResolvedModelRoleValue): Promise<void> {
|
|
2140
|
+
const transition = resolvePlanModelTransition(currentModel, resolved, this.session.isStreaming);
|
|
2141
|
+
if (transition.kind !== "apply" || !transition.deferred) {
|
|
2142
|
+
this.#clearPendingPlanModelSwitch();
|
|
2143
|
+
}
|
|
2144
|
+
switch (transition.kind) {
|
|
2145
|
+
case "none":
|
|
2146
|
+
return;
|
|
2147
|
+
case "thinking":
|
|
2148
|
+
this.session.setThinkingLevel(transition.thinkingLevel);
|
|
2149
|
+
return;
|
|
2150
|
+
case "apply":
|
|
2151
|
+
if (transition.deferred) {
|
|
2152
|
+
this.#pendingModelSwitch = { model: transition.model, thinkingLevel: transition.thinkingLevel };
|
|
2153
|
+
this.#pendingPlanModelSwitch = true;
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
await this.session.setModelTemporary(transition.model, transition.thinkingLevel);
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
this.showWarning(
|
|
2160
|
+
`Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2070
2163
|
return;
|
|
2071
|
-
}
|
|
2072
|
-
try {
|
|
2073
|
-
await this.session.setModelTemporary(resolved.model, planThinkingLevel);
|
|
2074
|
-
} catch (error) {
|
|
2075
|
-
this.showWarning(
|
|
2076
|
-
`Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
|
|
2077
|
-
);
|
|
2078
|
-
}
|
|
2079
|
-
} else if (planThinkingLevel) {
|
|
2080
|
-
this.session.setThinkingLevel(planThinkingLevel);
|
|
2081
2164
|
}
|
|
2082
2165
|
}
|
|
2083
2166
|
|
|
2084
2167
|
/** Apply any deferred model switch after the current stream ends. */
|
|
2085
2168
|
async flushPendingModelSwitch(): Promise<void> {
|
|
2086
2169
|
const pending = this.#pendingModelSwitch;
|
|
2087
|
-
if (!pending) return;
|
|
2088
2170
|
this.#pendingModelSwitch = undefined;
|
|
2171
|
+
this.#pendingPlanModelSwitch = false;
|
|
2172
|
+
if (!pending) return;
|
|
2089
2173
|
try {
|
|
2090
2174
|
await this.session.setModelTemporary(pending.model, pending.thinkingLevel);
|
|
2091
2175
|
} catch (error) {
|
|
@@ -2097,19 +2181,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2097
2181
|
|
|
2098
2182
|
async #clearTransientModeState(): Promise<void> {
|
|
2099
2183
|
if (this.planModeEnabled || this.planModePaused) {
|
|
2100
|
-
if (this.#planModePreviousTools !== undefined) {
|
|
2101
|
-
await this.session.setActiveToolsByName(this.#planModePreviousTools);
|
|
2102
|
-
}
|
|
2103
|
-
this.session.setPlanProposalHandler?.(null);
|
|
2104
2184
|
this.session.setPlanModeState(undefined);
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2185
|
+
try {
|
|
2186
|
+
if (this.#planModePreviousTools !== undefined) {
|
|
2187
|
+
await this.session.setActiveToolsByName(this.#planModePreviousTools);
|
|
2188
|
+
}
|
|
2189
|
+
} finally {
|
|
2190
|
+
this.session.setPlanProposalHandler?.(null);
|
|
2191
|
+
this.planModeEnabled = false;
|
|
2192
|
+
this.planModePaused = false;
|
|
2193
|
+
this.planModePlanFilePath = undefined;
|
|
2194
|
+
this.#planModePreviousTools = undefined;
|
|
2195
|
+
this.#planModePreviousModelState = undefined;
|
|
2196
|
+
this.#pendingModelSwitch = undefined;
|
|
2197
|
+
this.#pendingPlanModelSwitch = false;
|
|
2198
|
+
this.#planModeHasEntered = false;
|
|
2199
|
+
this.#updatePlanModeStatus();
|
|
2200
|
+
}
|
|
2113
2201
|
}
|
|
2114
2202
|
|
|
2115
2203
|
if (this.goalModeEnabled || this.goalModePaused) {
|
|
@@ -2291,6 +2379,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2291
2379
|
this.session.setThinkingLevel(prev.thinkingLevel);
|
|
2292
2380
|
} else if (this.session.isStreaming) {
|
|
2293
2381
|
this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
|
|
2382
|
+
this.#pendingPlanModelSwitch = false;
|
|
2294
2383
|
} else {
|
|
2295
2384
|
await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
|
|
2296
2385
|
}
|
|
@@ -2322,33 +2411,30 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2322
2411
|
return;
|
|
2323
2412
|
}
|
|
2324
2413
|
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
if (!options?.deferModelRestore) {
|
|
2331
|
-
await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
|
|
2414
|
+
const planModeState = this.session.getPlanModeState();
|
|
2415
|
+
this.session.setPlanModeState(undefined);
|
|
2416
|
+
try {
|
|
2417
|
+
if (this.#planModePreviousTools !== undefined) {
|
|
2418
|
+
await this.session.setActiveToolsByName(this.#planModePreviousTools);
|
|
2332
2419
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
// session on the plan-role model after the user has exited plan mode
|
|
2337
|
-
// (issue #816). This runs even when deferModelRestore is set
|
|
2338
|
-
// (compact-approval path): otherwise the stale plan switch survives and
|
|
2339
|
-
// flushPendingModelSwitch() later clobbers the restored/execution model.
|
|
2340
|
-
// Only clear when the pending target matches the plan-role model — leave
|
|
2341
|
-
// any unrelated user-queued switch intact.
|
|
2342
|
-
const pending = this.#pendingModelSwitch;
|
|
2343
|
-
if (pending) {
|
|
2344
|
-
const planResolution = this.session.resolveRoleModelWithThinking("plan");
|
|
2345
|
-
if (planResolution.model && modelsAreEqual(pending.model, planResolution.model)) {
|
|
2346
|
-
this.#pendingModelSwitch = undefined;
|
|
2420
|
+
if (this.#planModePreviousModelState) {
|
|
2421
|
+
if (!options?.deferModelRestore) {
|
|
2422
|
+
await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
|
|
2347
2423
|
}
|
|
2424
|
+
// If #applyPlanModeModel queued a deferred switch to the plan-role model
|
|
2425
|
+
// (because the session was streaming on entry), drop it now: we are
|
|
2426
|
+
// leaving plan mode, so flushing it on the next agent_end would land the
|
|
2427
|
+
// session on the plan-role model after the user has exited plan mode
|
|
2428
|
+
// (issue #816). This runs even when deferModelRestore is set
|
|
2429
|
+
// (compact-approval path): otherwise the stale plan switch survives and
|
|
2430
|
+
// flushPendingModelSwitch() later clobbers the restored/execution model.
|
|
2431
|
+
this.#clearPendingPlanModelSwitch();
|
|
2348
2432
|
}
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
this.session.setPlanModeState(planModeState);
|
|
2435
|
+
throw error;
|
|
2349
2436
|
}
|
|
2350
2437
|
this.session.setPlanProposalHandler?.(null);
|
|
2351
|
-
this.session.setPlanModeState(undefined);
|
|
2352
2438
|
this.planModeEnabled = false;
|
|
2353
2439
|
// Suppress cache-miss marker on the next turn: plan exit changes the system
|
|
2354
2440
|
// prompt, which predictably invalidates the cache.
|
|
@@ -2498,8 +2584,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2498
2584
|
const finish = (choice: string | undefined): void => {
|
|
2499
2585
|
if (settled) return;
|
|
2500
2586
|
settled = true;
|
|
2501
|
-
this.#hidePlanReview();
|
|
2502
|
-
this.ui.requestRender();
|
|
2503
2587
|
resolve(choice);
|
|
2504
2588
|
};
|
|
2505
2589
|
const overlay = new PlanReviewOverlay(
|
|
@@ -2530,6 +2614,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2530
2614
|
maxHeight: "100%",
|
|
2531
2615
|
margin: 0,
|
|
2532
2616
|
fullscreen: true,
|
|
2617
|
+
mouseTracking: false,
|
|
2533
2618
|
});
|
|
2534
2619
|
this.ui.setFocus(overlay);
|
|
2535
2620
|
this.ui.requestRender();
|
|
@@ -2860,6 +2945,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2860
2945
|
planFilePath: options.planFilePath,
|
|
2861
2946
|
contextPreserved: options.preserveContext === true,
|
|
2862
2947
|
});
|
|
2948
|
+
// Close the review overlay only now — after the async title write and plan
|
|
2949
|
+
// prompt are prepared, immediately before the execution turn is queued. The
|
|
2950
|
+
// synthetic prompt below blocks in `session.prompt` for the whole run, so
|
|
2951
|
+
// hiding here (rather than after #approvePlan returns) keeps the operator off
|
|
2952
|
+
// the stale plan-review screen (issue #5688) while #5319's stale-buffer guard
|
|
2953
|
+
// stays intact. Deferring the hide past the awaited `setSessionName` also
|
|
2954
|
+
// prevents restored editor focus from letting operator keystrokes submit a
|
|
2955
|
+
// normal turn ahead of the approved execution turn (PR #5689 review).
|
|
2956
|
+
// `#hidePlanReview` is idempotent, so the caller's trailing `closePlanReview()`
|
|
2957
|
+
// — and the cancelled/error early returns above — stay safe no-ops.
|
|
2958
|
+
this.#hidePlanReview();
|
|
2959
|
+
this.ui.requestRender();
|
|
2863
2960
|
// A user turn queued during compaction was already fired by
|
|
2864
2961
|
// `flushCompactionQueue` before we returned from `handleCompactCommand`; the
|
|
2865
2962
|
// old abort-then-prompt path would have discarded that operator turn AND
|
|
@@ -3423,6 +3520,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3423
3520
|
},
|
|
3424
3521
|
{ slider },
|
|
3425
3522
|
);
|
|
3523
|
+
const closePlanReview = (): void => {
|
|
3524
|
+
this.#hidePlanReview();
|
|
3525
|
+
this.ui.requestRender();
|
|
3526
|
+
};
|
|
3426
3527
|
|
|
3427
3528
|
if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
|
|
3428
3529
|
try {
|
|
@@ -3435,6 +3536,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3435
3536
|
}
|
|
3436
3537
|
if (!latestPlanContent) {
|
|
3437
3538
|
this.showError(`Plan file not found at ${planFilePath}`);
|
|
3539
|
+
closePlanReview();
|
|
3438
3540
|
return;
|
|
3439
3541
|
}
|
|
3440
3542
|
// Capture the operator's tier choice and hand it to #approvePlan, which
|
|
@@ -3477,6 +3579,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3477
3579
|
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
3478
3580
|
);
|
|
3479
3581
|
}
|
|
3582
|
+
closePlanReview();
|
|
3480
3583
|
return;
|
|
3481
3584
|
}
|
|
3482
3585
|
|
|
@@ -3495,8 +3598,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3495
3598
|
} catch (error) {
|
|
3496
3599
|
this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
|
|
3497
3600
|
}
|
|
3601
|
+
closePlanReview();
|
|
3498
3602
|
return;
|
|
3499
3603
|
}
|
|
3604
|
+
closePlanReview();
|
|
3500
3605
|
}
|
|
3501
3606
|
|
|
3502
3607
|
/**
|
|
@@ -3711,6 +3816,32 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3711
3816
|
this.ui.requestRender();
|
|
3712
3817
|
}
|
|
3713
3818
|
|
|
3819
|
+
/** Defer transcript command panels until the active turn can no longer grow above them. */
|
|
3820
|
+
presentCommandOutput(content: Component | readonly Component[]): void {
|
|
3821
|
+
if (!this.session.isStreaming) {
|
|
3822
|
+
this.present(content);
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
const sessionId = this.sessionManager.getSessionId();
|
|
3826
|
+
if (this.#pendingCommandOutput.length > 0 && this.#pendingCommandOutputSessionId !== sessionId) {
|
|
3827
|
+
this.#pendingCommandOutput = [];
|
|
3828
|
+
}
|
|
3829
|
+
this.#pendingCommandOutputSessionId = sessionId;
|
|
3830
|
+
const items = Array.isArray(content) ? content : [content as Component];
|
|
3831
|
+
this.#pendingCommandOutput.push(...items);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
/** Mount every command panel queued for the current session while the agent was streaming. */
|
|
3835
|
+
flushPendingCommandOutput(): void {
|
|
3836
|
+
if (this.#pendingCommandOutput.length === 0) return;
|
|
3837
|
+
const pending = this.#pendingCommandOutput;
|
|
3838
|
+
const pendingSessionId = this.#pendingCommandOutputSessionId;
|
|
3839
|
+
this.#pendingCommandOutput = [];
|
|
3840
|
+
this.#pendingCommandOutputSessionId = undefined;
|
|
3841
|
+
if (pendingSessionId !== this.sessionManager.getSessionId()) return;
|
|
3842
|
+
this.present(pending);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3714
3845
|
#mountChatChild(item: Component): void {
|
|
3715
3846
|
this.chatContainer.addChild(item);
|
|
3716
3847
|
if (item instanceof ChatBlock) item.mount(this.#chatHost);
|
|
@@ -4255,11 +4386,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4255
4386
|
this.#selectorController.showSessionSelector();
|
|
4256
4387
|
}
|
|
4257
4388
|
|
|
4258
|
-
handleResumeSession(sessionPath: string): Promise<void> {
|
|
4389
|
+
async handleResumeSession(sessionPath: string): Promise<void> {
|
|
4390
|
+
// Flush pending settings writes *before* disposing controllers or resetting
|
|
4391
|
+
// observers: a save failure must leave the session, process project dir,
|
|
4392
|
+
// and Settings in the source scope with all UI intact.
|
|
4393
|
+
try {
|
|
4394
|
+
await this.settings.flush();
|
|
4395
|
+
} catch (err) {
|
|
4396
|
+
this.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4259
4399
|
this.#btwController.dispose();
|
|
4260
4400
|
this.#omfgController.dispose();
|
|
4261
4401
|
this.resetObserverRegistry();
|
|
4262
|
-
|
|
4402
|
+
await this.#selectorController.handleResumeSession(sessionPath, { settingsFlushed: true });
|
|
4263
4403
|
}
|
|
4264
4404
|
|
|
4265
4405
|
handleSessionDeleteCommand(): Promise<void> {
|
|
@@ -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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
|
-
import type
|
|
11
|
+
import { type AgentSession, type AgentSessionEvent, SHUTDOWN_CONSOLIDATE_BUDGET_MS } from "../session/agent-session";
|
|
12
12
|
import { isSilentAbort } from "../session/messages";
|
|
13
13
|
import { flushTelemetryExport } from "../telemetry-export";
|
|
14
14
|
import { initializeExtensions } from "./runtime-init";
|
|
@@ -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({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
151
156
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
152
157
|
if (flushed) {
|
|
153
158
|
process.exit(1);
|
|
@@ -184,5 +189,5 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
184
189
|
});
|
|
185
190
|
});
|
|
186
191
|
|
|
187
|
-
await session.dispose();
|
|
192
|
+
await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
188
193
|
}
|
|
@@ -3,6 +3,7 @@ import type { Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { applyToolProxy } from "../../extensibility/tool-proxy";
|
|
5
5
|
import type { Theme } from "../../modes/theme/theme";
|
|
6
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
6
7
|
import type {
|
|
7
8
|
RpcHostToolCallRequest,
|
|
8
9
|
RpcHostToolCancelRequest,
|
|
@@ -54,7 +55,7 @@ class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme
|
|
|
54
55
|
this.#definition = definition;
|
|
55
56
|
this.#bridge = bridge;
|
|
56
57
|
applyToolProxy(definition, this);
|
|
57
|
-
this.loadMode = definition.loadMode
|
|
58
|
+
this.loadMode = defaultLoadModeForToolName(definition.name, definition.loadMode);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
execute(
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
14
14
|
import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
15
|
-
import { $env, isRecord,
|
|
15
|
+
import { $env, isRecord, readLines, Snowflake } from "@oh-my-pi/pi-utils";
|
|
16
16
|
import { reset as resetCapabilities } from "../../capability";
|
|
17
17
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
18
18
|
import {
|
|
@@ -29,6 +29,7 @@ import type { AgentSession } from "../../session/agent-session";
|
|
|
29
29
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
30
30
|
import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
31
31
|
import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
|
|
32
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
32
33
|
import type { EventBus } from "../../utils/event-bus";
|
|
33
34
|
import { initializeExtensions } from "../runtime-init";
|
|
34
35
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
@@ -507,7 +508,7 @@ function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostTo
|
|
|
507
508
|
description,
|
|
508
509
|
parameters: tool.parameters,
|
|
509
510
|
hidden: tool.hidden === true,
|
|
510
|
-
loadMode: tool.loadMode
|
|
511
|
+
loadMode: defaultLoadModeForToolName(name, tool.loadMode),
|
|
511
512
|
};
|
|
512
513
|
});
|
|
513
514
|
}
|
|
@@ -914,6 +915,7 @@ export async function runRpcMode(
|
|
|
914
915
|
const projectPath = await resolveActiveProjectRegistryPath(cwd);
|
|
915
916
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
916
917
|
resetCapabilities();
|
|
918
|
+
await session.refreshSkills();
|
|
917
919
|
session.setSlashCommands(await loadSlashCommands({ cwd }));
|
|
918
920
|
await emitAvailableCommandsUpdate();
|
|
919
921
|
};
|
|
@@ -1346,9 +1348,12 @@ export async function runRpcMode(
|
|
|
1346
1348
|
const shutdownCoordinator = new RpcShutdownCoordinator({
|
|
1347
1349
|
isShutdownRequested: () => shutdownState.requested,
|
|
1348
1350
|
performShutdown: async () => {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1351
|
+
// Route through the idempotent session.dispose() so the browser
|
|
1352
|
+
// reaper (releaseTabsForOwner) and other bounded teardown run before
|
|
1353
|
+
// the process exits. dispose() also emits `session_shutdown`, so we
|
|
1354
|
+
// must NOT emit it separately here or the event fires twice. Skipping
|
|
1355
|
+
// dispose left OMP-owned Chromium alive after RPC shutdown (#5643).
|
|
1356
|
+
await session.dispose();
|
|
1352
1357
|
process.exit(0);
|
|
1353
1358
|
},
|
|
1354
1359
|
});
|
|
@@ -1371,8 +1376,22 @@ export async function runRpcMode(
|
|
|
1371
1376
|
|
|
1372
1377
|
// Keep the stdin reader moving: side-channel frames dispatch immediately,
|
|
1373
1378
|
// ordinary commands serialize through inputDispatcher, and bash remains
|
|
1374
|
-
// background-dispatched so abort_bash can overtake it.
|
|
1375
|
-
|
|
1379
|
+
// background-dispatched so abort_bash can overtake it. Frames are read
|
|
1380
|
+
// line-by-line and parsed here (not via readJsonl) so a single malformed
|
|
1381
|
+
// line is reported as an error frame and the loop keeps running instead of
|
|
1382
|
+
// throwing out of the generator and killing the whole process (issue #5194).
|
|
1383
|
+
const decoder = new TextDecoder();
|
|
1384
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
1385
|
+
const text = decoder.decode(line).trim();
|
|
1386
|
+
if (!text) continue;
|
|
1387
|
+
let parsed: unknown;
|
|
1388
|
+
try {
|
|
1389
|
+
parsed = JSON.parse(text);
|
|
1390
|
+
} catch (e: unknown) {
|
|
1391
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1392
|
+
output(error(undefined, "parse", `Failed to parse command: ${message}`));
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1376
1395
|
inputDispatcher.dispatch(parsed);
|
|
1377
1396
|
}
|
|
1378
1397
|
|
|
@@ -1384,5 +1403,10 @@ export async function runRpcMode(
|
|
|
1384
1403
|
await inputDispatcher.drain();
|
|
1385
1404
|
await shutdownCoordinator.drain();
|
|
1386
1405
|
subagentRegistry?.dispose();
|
|
1406
|
+
// Dispose the main session before exiting so the browser reaper and other
|
|
1407
|
+
// bounded teardown run on the stdin-EOF path too (#5643). Idempotent: a
|
|
1408
|
+
// prior pi.shutdown() through the coordinator makes this await settle
|
|
1409
|
+
// immediately.
|
|
1410
|
+
await session.dispose();
|
|
1387
1411
|
process.exit(0);
|
|
1388
1412
|
}
|
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
|