@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.0
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 +115 -7
- package/dist/cli.js +5996 -5945
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/models-config-schema.ts +13 -0
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/ssh.ts +8 -1
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
TUI,
|
|
37
37
|
visibleWidth,
|
|
38
38
|
} from "@oh-my-pi/pi-tui";
|
|
39
|
+
import { isInsideTerminalMultiplexer } from "@oh-my-pi/pi-tui/terminal-capabilities";
|
|
39
40
|
import {
|
|
40
41
|
APP_NAME,
|
|
41
42
|
adjustHsv,
|
|
@@ -65,6 +66,7 @@ import type {
|
|
|
65
66
|
ExtensionWidgetOptions,
|
|
66
67
|
} from "../extensibility/extensions";
|
|
67
68
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
69
|
+
import type { Skill } from "../extensibility/skills";
|
|
68
70
|
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
69
71
|
import { type GuidedGoalMessage, runGuidedGoalTurn } from "../goals/guided-setup";
|
|
70
72
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
@@ -88,7 +90,12 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
|
|
|
88
90
|
type: "text",
|
|
89
91
|
};
|
|
90
92
|
import type { AgentRegistry } from "../registry/agent-registry";
|
|
91
|
-
import
|
|
93
|
+
import {
|
|
94
|
+
type AgentSession,
|
|
95
|
+
type AgentSessionEvent,
|
|
96
|
+
type ResolvedRoleModel,
|
|
97
|
+
SHUTDOWN_CONSOLIDATE_BUDGET_MS,
|
|
98
|
+
} from "../session/agent-session";
|
|
92
99
|
import type { CompactMode } from "../session/compact-modes";
|
|
93
100
|
import { HistoryStorage } from "../session/history-storage";
|
|
94
101
|
import type { SessionContext } from "../session/session-context";
|
|
@@ -155,6 +162,7 @@ import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
|
155
162
|
import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
|
|
156
163
|
import type { ObservableSession } from "./session-observer-registry";
|
|
157
164
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
165
|
+
import { createSessionTeardown, type SessionTeardown } from "./session-teardown";
|
|
158
166
|
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
159
167
|
import { interruptHint } from "./shared";
|
|
160
168
|
import { clearMermaidCache } from "./theme/mermaid-cache";
|
|
@@ -328,8 +336,10 @@ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
|
|
|
328
336
|
|
|
329
337
|
/**
|
|
330
338
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
331
|
-
* one
|
|
339
|
+
* one tree row per running agent in the same `Id: description` shape the
|
|
332
340
|
* inline task rows use (muted task preview when no description was given).
|
|
341
|
+
* Layout mirrors the Todos HUD exactly: unindented header, then
|
|
342
|
+
* `renderTreeList` rows (dim connectors) shifted right by one space.
|
|
333
343
|
* Only detached background spawns are listed: a sync task call blocks the
|
|
334
344
|
* parent turn and its inline tool block already renders progress live, and
|
|
335
345
|
* eval `agent()` spawns are rendered by their own eval cell tree.
|
|
@@ -341,29 +351,32 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
|
|
341
351
|
);
|
|
342
352
|
if (running.length === 0) return [];
|
|
343
353
|
|
|
344
|
-
const indent = " ";
|
|
345
|
-
const hook = theme.tree.hook;
|
|
346
354
|
const dot = theme.styledSymbol("status.done", "accent");
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
355
|
+
const rows = renderTreeList(
|
|
356
|
+
{
|
|
357
|
+
items: running,
|
|
358
|
+
expanded: true,
|
|
359
|
+
renderItem: session => {
|
|
360
|
+
const displayId = formatTaskId(session.id);
|
|
361
|
+
let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}`;
|
|
362
|
+
const description = session.description?.trim() || session.progress?.description?.trim();
|
|
363
|
+
if (description) {
|
|
364
|
+
const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(displayId) - 10);
|
|
365
|
+
line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`;
|
|
366
|
+
} else {
|
|
367
|
+
// No spawn description: fall back to a muted task preview, same as
|
|
368
|
+
// the inline task rows when a row has no label.
|
|
369
|
+
const taskPreview = session.progress?.task?.trim();
|
|
370
|
+
if (taskPreview) {
|
|
371
|
+
line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return line;
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
theme,
|
|
378
|
+
);
|
|
379
|
+
return ["", theme.bold(theme.fg("accent", "Subagents")), ...rows.map(line => ` ${line}`)];
|
|
367
380
|
}
|
|
368
381
|
|
|
369
382
|
export class InteractiveMode implements InteractiveModeContext {
|
|
@@ -373,7 +386,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
373
386
|
keybindings: KeybindingsManager;
|
|
374
387
|
agent: Agent;
|
|
375
388
|
historyStorage?: HistoryStorage;
|
|
376
|
-
titleSystemPrompt?: string;
|
|
377
389
|
|
|
378
390
|
ui: TUI;
|
|
379
391
|
chatContainer: TranscriptContainer;
|
|
@@ -478,13 +490,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
478
490
|
lastStatusSpacer: Spacer | undefined = undefined;
|
|
479
491
|
lastStatusText: Text | undefined = undefined;
|
|
480
492
|
fileSlashCommands: Set<string> = new Set();
|
|
481
|
-
skillCommands: Map<string,
|
|
493
|
+
skillCommands: Map<string, Skill> = new Map();
|
|
482
494
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
483
495
|
collabHost?: CollabHost;
|
|
484
496
|
collabGuest?: CollabGuestLink;
|
|
485
497
|
|
|
486
498
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
487
499
|
#cleanupUnsubscribe?: () => void;
|
|
500
|
+
#signalTeardown?: SessionTeardown;
|
|
488
501
|
readonly #version: string;
|
|
489
502
|
readonly #changelogMarkdown: string | undefined;
|
|
490
503
|
#planModePreviousTools: string[] | undefined;
|
|
@@ -583,7 +596,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
583
596
|
lspServers: LspStartupServerInfo[] | undefined = undefined,
|
|
584
597
|
mcpManager?: MCPManager,
|
|
585
598
|
eventBus?: EventBus,
|
|
586
|
-
titleSystemPrompt?: string,
|
|
587
599
|
) {
|
|
588
600
|
this.session = session;
|
|
589
601
|
this.sessionManager = session.sessionManager;
|
|
@@ -596,7 +608,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
596
608
|
this.lspServers = lspServers;
|
|
597
609
|
this.mcpManager = mcpManager;
|
|
598
610
|
this.#eventBus = eventBus;
|
|
599
|
-
this.titleSystemPrompt = titleSystemPrompt;
|
|
600
611
|
if (eventBus) {
|
|
601
612
|
this.#eventBusUnsubscribers.push(
|
|
602
613
|
eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
|
|
@@ -645,7 +656,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
645
656
|
this.#syncEditorMaxHeight();
|
|
646
657
|
this.#resizeHandler = () => {
|
|
647
658
|
this.#syncEditorMaxHeight();
|
|
648
|
-
this.
|
|
659
|
+
this.ui.requestRender();
|
|
649
660
|
};
|
|
650
661
|
process.stdout.on("resize", this.#resizeHandler);
|
|
651
662
|
try {
|
|
@@ -662,6 +673,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
662
673
|
this.editorContainer.addChild(this.editor);
|
|
663
674
|
this.statusLine = new StatusLineComponent(session);
|
|
664
675
|
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
676
|
+
// Lazy provider — the top border rebuild coalesces to at most one
|
|
677
|
+
// invocation per painted frame instead of firing on every session event
|
|
678
|
+
// (#4145). The TUI throttles renders at ~30fps, so a long-running eval
|
|
679
|
+
// spraying events no longer runs `getTopBorder` synchronously in the
|
|
680
|
+
// hot path where the render never gets to paint the result.
|
|
681
|
+
this.editor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
|
|
665
682
|
|
|
666
683
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
667
684
|
this.proseOnlyThinking = settings.get("proseOnlyThinking");
|
|
@@ -685,7 +702,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
685
702
|
if (settings.get("skills.enableSkillCommands")) {
|
|
686
703
|
for (const skill of this.session.skills) {
|
|
687
704
|
const commandName = `skill:${skill.name}`;
|
|
688
|
-
this.skillCommands.set(commandName, skill
|
|
705
|
+
this.skillCommands.set(commandName, skill);
|
|
689
706
|
skillCommandList.push({ name: commandName, description: skill.description });
|
|
690
707
|
}
|
|
691
708
|
}
|
|
@@ -768,8 +785,29 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
768
785
|
|
|
769
786
|
this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
|
|
770
787
|
|
|
771
|
-
//
|
|
772
|
-
|
|
788
|
+
// Route SIGINT/SIGTERM/SIGHUP/uncaughtException through the same teardown
|
|
789
|
+
// the TUI Ctrl+C keypress path performs: persist the in-progress editor
|
|
790
|
+
// draft for `--resume`, then dispose the session (which emits the extension
|
|
791
|
+
// `session_shutdown` event, cancels the owned async job manager, disposes
|
|
792
|
+
// eval kernels, releases owned browser tabs, and closes the session
|
|
793
|
+
// manager). Without this callback a real kernel signal would drop the
|
|
794
|
+
// draft, skip the `session_shutdown` contract from `shared-events.ts`,
|
|
795
|
+
// and orphan background bash/task processes (issue #4080). The registered
|
|
796
|
+
// callback and `shutdown()` share one promise-memoized teardown, so a
|
|
797
|
+
// signal arriving mid-Ctrl+C no-ops instead of racing a second dispose.
|
|
798
|
+
this.#signalTeardown = createSessionTeardown({
|
|
799
|
+
getDraftText: () => this.editor.getText(),
|
|
800
|
+
beginDispose: () => this.session.beginDispose(),
|
|
801
|
+
saveDraft: text => this.sessionManager.saveDraft(text),
|
|
802
|
+
disposeSession: reason =>
|
|
803
|
+
this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS, reason }),
|
|
804
|
+
});
|
|
805
|
+
// Forward the postmortem reason (SIGTERM/SIGHUP/uncaughtException/…) so the
|
|
806
|
+
// persisted `session_exit` diagnostic carries the real trigger. Postmortem
|
|
807
|
+
// runs callbacks in REVERSE registration order — this callback (registered
|
|
808
|
+
// after the AgentSession constructor's `agent-session:<id>` recorder) runs
|
|
809
|
+
// FIRST and its dispose() would otherwise persist the generic "dispose".
|
|
810
|
+
this.#cleanupUnsubscribe = postmortem.register("session-teardown", reason => this.#signalTeardown!(reason));
|
|
773
811
|
|
|
774
812
|
// Wire the report_tool_issue consent gate to the Yes/No dialog popup.
|
|
775
813
|
// The handler is process-global — subagent tools (which can't reach
|
|
@@ -958,13 +996,22 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
958
996
|
);
|
|
959
997
|
// Set up theme file watcher
|
|
960
998
|
this.#eventBusUnsubscribers.push(
|
|
961
|
-
onThemeChange(
|
|
999
|
+
onThemeChange(event => {
|
|
962
1000
|
this.#clearWorkingMessageAccentCache();
|
|
963
1001
|
clearRenderCache();
|
|
964
1002
|
clearMermaidCache();
|
|
965
1003
|
this.ui.invalidate();
|
|
966
1004
|
this.updateEditorBorderColor();
|
|
967
|
-
|
|
1005
|
+
if (event.ephemeral || isInsideTerminalMultiplexer()) {
|
|
1006
|
+
// Theme previews and multiplexer panes cannot safely replace native
|
|
1007
|
+
// scrollback: previews must stay non-destructive, and multiplexers
|
|
1008
|
+
// suppress ED3 so a forced replay would duplicate transcript history.
|
|
1009
|
+
this.ui.requestRender();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
// Rows already committed to native scrollback are immutable; replay them
|
|
1013
|
+
// after a theme swap so a reader scrolled up sees the same palette.
|
|
1014
|
+
this.ui.requestRender(true, { clearScrollback: true });
|
|
968
1015
|
}),
|
|
969
1016
|
);
|
|
970
1017
|
|
|
@@ -975,21 +1022,24 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
975
1022
|
onTerminalAppearanceChange(mode);
|
|
976
1023
|
});
|
|
977
1024
|
|
|
978
|
-
//
|
|
1025
|
+
// A branch change (checkout, worktree switch, `git switch`) invalidates
|
|
1026
|
+
// the status-line git segments; the lazy top-border provider picks up
|
|
1027
|
+
// the fresh branch on the next painted frame.
|
|
979
1028
|
this.statusLine.watchBranch(() => {
|
|
980
|
-
this.updateEditorTopBorder();
|
|
981
1029
|
this.ui.requestRender();
|
|
982
1030
|
});
|
|
983
|
-
|
|
984
|
-
// Initial top border update
|
|
985
|
-
this.updateEditorTopBorder();
|
|
986
1031
|
}
|
|
987
1032
|
|
|
988
|
-
/** Reload the title-generation system prompt override for the provided working
|
|
1033
|
+
/** Reload the title-generation system prompt override for the provided working
|
|
1034
|
+
* directory and stash it on the session so first-input titling
|
|
1035
|
+
* ({@link input-controller}) and replan-driven refresh
|
|
1036
|
+
* ({@link AgentSession.#refreshTitleAfterReplan}) share one source
|
|
1037
|
+
* ({@link discoverTitleSystemPromptFile}; issue #3734). */
|
|
989
1038
|
async refreshTitleSystemPrompt(cwd?: string): Promise<void> {
|
|
990
1039
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
991
1040
|
const titleSystemPromptSource = discoverTitleSystemPromptFile(basePath);
|
|
992
|
-
|
|
1041
|
+
const resolved = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
|
|
1042
|
+
this.session.setTitleSystemPrompt(resolved);
|
|
993
1043
|
}
|
|
994
1044
|
|
|
995
1045
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
@@ -1059,7 +1109,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1059
1109
|
await this.session.refreshSshTool({ activateIfAvailable: true });
|
|
1060
1110
|
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1061
1111
|
this.statusLine.invalidate();
|
|
1062
|
-
this.
|
|
1112
|
+
this.ui.requestRender();
|
|
1063
1113
|
}
|
|
1064
1114
|
|
|
1065
1115
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
@@ -1200,7 +1250,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1200
1250
|
this.loopLimit = undefined;
|
|
1201
1251
|
this.#cancelLoopAutoSubmit();
|
|
1202
1252
|
this.statusLine.setLoopModeStatus(undefined);
|
|
1203
|
-
this.updateEditorTopBorder();
|
|
1204
1253
|
this.ui.requestRender();
|
|
1205
1254
|
if (wasEnabled) {
|
|
1206
1255
|
this.showStatus(message);
|
|
@@ -1231,7 +1280,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1231
1280
|
this.loopPrompt = undefined;
|
|
1232
1281
|
this.loopLimit = createLoopLimitRuntime(parsed.limit);
|
|
1233
1282
|
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
1234
|
-
this.updateEditorTopBorder();
|
|
1235
1283
|
this.ui.requestRender();
|
|
1236
1284
|
const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
|
|
1237
1285
|
const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
|
|
@@ -1452,7 +1500,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1452
1500
|
const base = this.editor.borderColor;
|
|
1453
1501
|
this.editor.borderColor = (str: string) => `\x1b[2m${base(str)}\x1b[22m`;
|
|
1454
1502
|
}
|
|
1455
|
-
this.updateEditorTopBorder();
|
|
1456
1503
|
this.ui.requestRender();
|
|
1457
1504
|
}
|
|
1458
1505
|
|
|
@@ -1469,13 +1516,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1469
1516
|
}
|
|
1470
1517
|
const count = countRunningSubagentBadgeAgents(registry);
|
|
1471
1518
|
this.statusLine.setSubagentCount(count);
|
|
1472
|
-
this.
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
updateEditorTopBorder(): void {
|
|
1476
|
-
const availableWidth = this.editor.getTopBorderAvailableWidth(this.ui.terminal.columns);
|
|
1477
|
-
const topBorder = this.statusLine.getTopBorder(availableWidth);
|
|
1478
|
-
this.editor.setTopBorder(topBorder);
|
|
1519
|
+
this.ui.requestRender();
|
|
1479
1520
|
}
|
|
1480
1521
|
|
|
1481
1522
|
rebuildChatFromMessages(): void {
|
|
@@ -1806,7 +1847,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1806
1847
|
}
|
|
1807
1848
|
: undefined;
|
|
1808
1849
|
this.statusLine.setPlanModeStatus(status);
|
|
1809
|
-
this.updateEditorTopBorder();
|
|
1810
1850
|
this.ui.requestRender();
|
|
1811
1851
|
}
|
|
1812
1852
|
|
|
@@ -1816,7 +1856,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1816
1856
|
? { enabled: this.goalModeEnabled, paused: this.goalModePaused }
|
|
1817
1857
|
: undefined;
|
|
1818
1858
|
this.statusLine.setGoalModeStatus(status);
|
|
1819
|
-
this.updateEditorTopBorder();
|
|
1820
1859
|
this.ui.requestRender();
|
|
1821
1860
|
}
|
|
1822
1861
|
|
|
@@ -2579,11 +2618,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2579
2618
|
this.session.clearPlanInternalAbortPending();
|
|
2580
2619
|
}
|
|
2581
2620
|
|
|
2582
|
-
//
|
|
2583
|
-
//
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2621
|
+
// Restore the execution tool set, but force-enable `read`: approved-plan
|
|
2622
|
+
// prompts now require loading the durable local:// plan file before work.
|
|
2623
|
+
const executionTools = previousTools.includes("read") ? previousTools : [...previousTools, "read"];
|
|
2624
|
+
await this.session.setActiveToolsByName(executionTools);
|
|
2587
2625
|
this.session.setPlanReferencePath(options.planFilePath);
|
|
2588
2626
|
|
|
2589
2627
|
// Resolve the deferred plan-approval model transition. On the compact path
|
|
@@ -2631,7 +2669,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2631
2669
|
// plan-approved prompt is the source of the reference injection.
|
|
2632
2670
|
this.session.markPlanReferenceSent();
|
|
2633
2671
|
const planModePrompt = prompt.render(planModeApprovedPrompt, {
|
|
2634
|
-
planContent,
|
|
2635
2672
|
planFilePath: options.planFilePath,
|
|
2636
2673
|
contextPreserved: options.preserveContext === true,
|
|
2637
2674
|
});
|
|
@@ -3105,10 +3142,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3105
3142
|
|
|
3106
3143
|
if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
|
|
3107
3144
|
try {
|
|
3108
|
-
// Prefer in-overlay edits (already in memory) over a disk re-read
|
|
3109
|
-
//
|
|
3110
|
-
//
|
|
3145
|
+
// Prefer in-overlay edits (already in memory) over a disk re-read. The
|
|
3146
|
+
// overlay mirrors edits as they happen, and approval awaits one final
|
|
3147
|
+
// write so the durable plan file and synthetic prompt carry the same text.
|
|
3111
3148
|
const latestPlanContent = editedContent ?? (await this.#readPlanFile(planFilePath));
|
|
3149
|
+
if (editedContent !== undefined) {
|
|
3150
|
+
await Bun.write(this.#resolvePlanFilePath(planFilePath), editedContent);
|
|
3151
|
+
}
|
|
3112
3152
|
if (!latestPlanContent) {
|
|
3113
3153
|
this.showError(`Plan file not found at ${planFilePath}`);
|
|
3114
3154
|
return;
|
|
@@ -3277,24 +3317,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3277
3317
|
if (this.#isShuttingDown) return;
|
|
3278
3318
|
this.#isShuttingDown = true;
|
|
3279
3319
|
|
|
3280
|
-
// Snapshot the editor before any teardown empties it. Persisting the draft
|
|
3281
|
-
// here covers Ctrl+D shutdown with non-empty text; for /exit the editor is
|
|
3282
|
-
// already cleared so saveDraft("") just removes any stale sidecar.
|
|
3283
|
-
const draftText = this.editor.getText();
|
|
3284
|
-
|
|
3285
|
-
// Flush pending session writes before shutdown
|
|
3286
|
-
await this.sessionManager.flush();
|
|
3287
|
-
try {
|
|
3288
|
-
await this.sessionManager.saveDraft(draftText);
|
|
3289
|
-
} catch (err) {
|
|
3290
|
-
logger.warn("Failed to save session draft", { error: String(err) });
|
|
3291
|
-
}
|
|
3292
3320
|
this.#btwController.dispose();
|
|
3293
3321
|
this.#omfgController.dispose();
|
|
3294
3322
|
this.#focusController.dispose();
|
|
3295
3323
|
|
|
3296
|
-
//
|
|
3297
|
-
|
|
3324
|
+
// Surface an explicit "Closing session…" line so the user sees a reason
|
|
3325
|
+
// for the pause while `session.dispose()` flushes memory consolidate and
|
|
3326
|
+
// other cleanups (issue #3641). The await on the next line yields the
|
|
3327
|
+
// event loop, giving requestRender() a tick to paint the status before
|
|
3328
|
+
// dispose blocks.
|
|
3329
|
+
this.showStatus("Closing session…");
|
|
3330
|
+
|
|
3331
|
+
// Persist the draft and dispose the session through the shared teardown
|
|
3332
|
+
// so a signal that arrives mid-shutdown cannot fire a second dispose.
|
|
3333
|
+
// The teardown is a promise-memoized singleton; whichever path calls it
|
|
3334
|
+
// first runs the work, the other awaits the same settled promise.
|
|
3335
|
+
// The teardown is registered lazily in `init()` — a `/exit` reached
|
|
3336
|
+
// before `init()` completed falls back to a direct dispose.
|
|
3337
|
+
if (this.#signalTeardown) {
|
|
3338
|
+
await this.#signalTeardown();
|
|
3339
|
+
} else {
|
|
3340
|
+
await this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
3341
|
+
}
|
|
3298
3342
|
|
|
3299
3343
|
// Do not force a final render during teardown: disposed session/UI state can
|
|
3300
3344
|
// collapse to an empty frame, clearing the viewport and leaving the parent
|
|
@@ -3348,6 +3392,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3348
3392
|
this.ui.requestRender();
|
|
3349
3393
|
};
|
|
3350
3394
|
nextEditor.setShimmerRepaintHandler(() => this.ui.requestComponentRender(this.editor));
|
|
3395
|
+
nextEditor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
|
|
3351
3396
|
nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
3352
3397
|
if (this.historyStorage) {
|
|
3353
3398
|
nextEditor.setHistoryStorage(this.historyStorage);
|
|
@@ -3367,7 +3412,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3367
3412
|
});
|
|
3368
3413
|
|
|
3369
3414
|
this.updateEditorBorderColor();
|
|
3370
|
-
this.updateEditorTopBorder();
|
|
3371
3415
|
this.ui.requestRender();
|
|
3372
3416
|
}
|
|
3373
3417
|
|
|
@@ -3767,7 +3811,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3767
3811
|
} else {
|
|
3768
3812
|
this.#cleanupMicAnimation();
|
|
3769
3813
|
}
|
|
3770
|
-
this.updateEditorTopBorder();
|
|
3771
3814
|
this.ui.requestRender();
|
|
3772
3815
|
},
|
|
3773
3816
|
});
|
|
@@ -225,12 +225,21 @@ export class RpcClient {
|
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
227
|
* Start the RPC agent process.
|
|
228
|
+
*
|
|
229
|
+
* Safe to call again after {@link stop} on the same instance: a fresh
|
|
230
|
+
* {@link AbortController} is minted for each start, and any failure after
|
|
231
|
+
* the child spawn kills the child and clears internal state so callers may
|
|
232
|
+
* retry without leaking processes.
|
|
228
233
|
*/
|
|
229
234
|
async start(): Promise<void> {
|
|
230
235
|
if (this.#process) {
|
|
231
236
|
throw new Error("Client already started");
|
|
232
237
|
}
|
|
233
238
|
|
|
239
|
+
// Mint a fresh controller so a previous stop()'s abort does not
|
|
240
|
+
// short-circuit the new stdout reader (issue #4079).
|
|
241
|
+
this.#abortController = new AbortController();
|
|
242
|
+
|
|
234
243
|
const cliPath = this.options.cliPath ?? "dist/cli.js";
|
|
235
244
|
const args = ["--mode", "rpc"];
|
|
236
245
|
|
|
@@ -304,6 +313,18 @@ export class RpcClient {
|
|
|
304
313
|
if (this.#customTools.length > 0) {
|
|
305
314
|
await this.setCustomTools(this.#customTools);
|
|
306
315
|
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
// Startup failed after we spawned the child. Kill it and clear
|
|
318
|
+
// state so the caller (or a retry via start() again) does not
|
|
319
|
+
// leak the abandoned process (issue #4079).
|
|
320
|
+
try {
|
|
321
|
+
this.#process?.kill();
|
|
322
|
+
} catch {
|
|
323
|
+
// best-effort cleanup
|
|
324
|
+
}
|
|
325
|
+
this.#abortController.abort();
|
|
326
|
+
this.#process = null;
|
|
327
|
+
throw err;
|
|
307
328
|
} finally {
|
|
308
329
|
clearTimeout(readyTimeout);
|
|
309
330
|
}
|