@oh-my-pi/pi-coding-agent 13.18.0 → 14.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 +316 -1
- package/package.json +86 -24
- package/scripts/format-prompts.ts +2 -2
- package/src/autoresearch/apply-contract-to-state.ts +24 -0
- package/src/autoresearch/contract.ts +0 -44
- package/src/autoresearch/dashboard.ts +1 -2
- package/src/autoresearch/git.ts +116 -30
- package/src/autoresearch/helpers.ts +49 -0
- package/src/autoresearch/index.ts +28 -187
- package/src/autoresearch/prompt.md +26 -9
- package/src/autoresearch/state.ts +0 -6
- package/src/autoresearch/tools/init-experiment.ts +202 -117
- package/src/autoresearch/tools/log-experiment.ts +123 -178
- package/src/autoresearch/tools/run-experiment.ts +48 -10
- package/src/autoresearch/types.ts +2 -2
- package/src/capability/index.ts +4 -2
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/grep-cli.ts +8 -8
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/read-cli.ts +67 -0
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/update-cli.ts +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/grep.ts +6 -1
- package/src/commands/grievances.ts +20 -0
- package/src/commands/read.ts +33 -0
- package/src/commit/agentic/agent.ts +5 -8
- package/src/commit/agentic/index.ts +22 -26
- package/src/commit/agentic/tools/analyze-file.ts +3 -3
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/agentic/validation.ts +1 -1
- package/src/commit/analysis/conventional.ts +4 -4
- package/src/commit/analysis/summary.ts +3 -3
- package/src/commit/changelog/generate.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/map-reduce/map-phase.ts +4 -4
- package/src/commit/map-reduce/reduce-phase.ts +4 -4
- package/src/commit/pipeline.ts +13 -16
- package/src/config/keybindings.ts +7 -6
- package/src/config/prompt-templates.ts +44 -226
- package/src/config/resolve-config-value.ts +4 -2
- package/src/config/settings-schema.ts +98 -2
- package/src/config/settings.ts +25 -26
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/log-viewer.ts +3 -2
- package/src/discovery/builtin.ts +1 -2
- package/src/discovery/codex.ts +2 -2
- package/src/discovery/github.ts +2 -1
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/opencode.ts +2 -2
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +309 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +454 -0
- package/src/{patch → edit/modes}/hashline.ts +741 -361
- package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
- package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
- package/src/{patch → edit}/normalize.ts +97 -76
- package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
- package/src/exec/bash-executor.ts +4 -2
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +5 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
- package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
- package/src/extensibility/custom-commands/loader.ts +1 -2
- package/src/extensibility/custom-tools/loader.ts +34 -11
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +9 -4
- package/src/extensibility/extensions/runner.ts +24 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/hooks/loader.ts +5 -6
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/plugins/doctor.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/extensibility/slash-commands.ts +3 -7
- package/src/index.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/ipy/executor.ts +58 -17
- package/src/ipy/gateway-coordinator.ts +6 -4
- package/src/ipy/kernel.ts +45 -22
- package/src/ipy/runtime.ts +2 -2
- package/src/lsp/client.ts +7 -4
- package/src/lsp/clients/lsp-linter-client.ts +4 -4
- package/src/lsp/config.ts +2 -2
- package/src/lsp/defaults.json +688 -154
- package/src/lsp/index.ts +234 -45
- package/src/lsp/lspmux.ts +2 -2
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +12 -1
- package/src/lsp/utils.ts +8 -1
- package/src/main.ts +125 -47
- package/src/memories/index.ts +4 -5
- package/src/modes/acp/acp-agent.ts +563 -163
- package/src/modes/acp/acp-event-mapper.ts +9 -1
- package/src/modes/acp/acp-mode.ts +4 -2
- package/src/modes/components/agent-dashboard.ts +3 -4
- package/src/modes/components/diff.ts +6 -7
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/read-tool-group.ts +6 -12
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +24 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/btw-controller.ts +2 -2
- package/src/modes/controllers/command-controller.ts +4 -2
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +15 -8
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/index.ts +20 -2
- package/src/modes/interactive-mode.ts +278 -69
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +178 -13
- package/src/modes/rpc/rpc-mode.ts +73 -3
- package/src/modes/rpc/rpc-types.ts +53 -1
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/theme/theme.ts +80 -8
- package/src/modes/types.ts +4 -2
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +8 -1
- package/src/prompts/tools/chunk-edit.md +219 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/grep.md +3 -0
- package/src/prompts/tools/lsp.md +5 -5
- package/src/prompts/tools/read-chunk.md +17 -0
- package/src/prompts/tools/read.md +19 -5
- package/src/sdk.ts +216 -165
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +25 -17
- package/src/session/agent-session.ts +381 -286
- package/src/session/agent-storage.ts +12 -12
- package/src/session/compaction/branch-summarization.ts +3 -3
- package/src/session/compaction/compaction.ts +5 -6
- package/src/session/compaction/utils.ts +3 -3
- package/src/session/history-storage.ts +62 -19
- package/src/session/messages.ts +3 -3
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +15 -5
- package/src/session/session-storage.ts +4 -2
- package/src/session/streaming-output.ts +1 -1
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +56 -8
- package/src/ssh/connection-manager.ts +2 -2
- package/src/ssh/sshfs-mount.ts +5 -5
- package/src/stt/downloader.ts +4 -4
- package/src/stt/recorder.ts +4 -4
- package/src/stt/transcriber.ts +2 -2
- package/src/system-prompt.ts +25 -13
- package/src/task/agents.ts +5 -6
- package/src/task/commands.ts +2 -5
- package/src/task/executor.ts +32 -4
- package/src/task/index.ts +91 -82
- package/src/task/template.ts +2 -2
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +131 -149
- package/src/tools/ask.ts +2 -3
- package/src/tools/ast-edit.ts +7 -7
- package/src/tools/ast-grep.ts +7 -7
- package/src/tools/auto-generated-guard.ts +36 -41
- package/src/tools/await-tool.ts +2 -2
- package/src/tools/bash.ts +5 -23
- package/src/tools/browser.ts +4 -5
- package/src/tools/calculator.ts +2 -3
- package/src/tools/cancel-job.ts +2 -2
- package/src/tools/checkpoint.ts +3 -3
- package/src/tools/debug.ts +1007 -0
- package/src/tools/exit-plan-mode.ts +3 -3
- package/src/tools/fetch.ts +67 -3
- package/src/tools/find.ts +4 -5
- package/src/tools/fs-cache-invalidation.ts +5 -0
- package/src/tools/gemini-image.ts +13 -5
- package/src/tools/gh.ts +130 -308
- package/src/tools/grep.ts +57 -9
- package/src/tools/index.ts +44 -22
- package/src/tools/inspect-image.ts +4 -4
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/python.ts +19 -6
- package/src/tools/read.ts +211 -146
- package/src/tools/render-mermaid.ts +2 -3
- package/src/tools/render-utils.ts +20 -6
- package/src/tools/renderers.ts +3 -1
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +70 -39
- package/src/tools/search-tool-bm25.ts +2 -2
- package/src/tools/ssh.ts +2 -2
- package/src/tools/todo-write.ts +2 -2
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/write.ts +5 -6
- package/src/tui/tree-list.ts +3 -1
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/commit-message-generator.ts +2 -3
- package/src/utils/edit-mode.ts +49 -0
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/file-display-mode.ts +6 -5
- package/src/utils/file-mentions.ts +8 -7
- package/src/utils/git.ts +1400 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/title-generator.ts +2 -3
- package/src/utils/tools-manager.ts +6 -6
- package/src/web/scrapers/choosealicense.ts +1 -1
- package/src/web/search/index.ts +3 -3
- package/src/web/search/render.ts +6 -4
- package/src/autoresearch/command-initialize.md +0 -34
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/patch/diff.ts +0 -433
- package/src/patch/index.ts +0 -888
- package/src/patch/parser.ts +0 -532
- package/src/patch/types.ts +0 -292
- package/src/prompts/agents/oracle.md +0 -77
- package/src/tools/gh-cli.ts +0 -125
- package/src/tools/pending-action.ts +0 -49
- package/src/utils/child-process.ts +0 -88
- package/src/utils/frontmatter.ts +0 -117
- package/src/utils/image-input.ts +0 -274
- package/src/utils/mime.ts +0 -53
- package/src/utils/prompt-format.ts +0 -170
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
* Interactive mode for the coding agent.
|
|
3
3
|
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
|
4
4
|
*/
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
type AssistantMessage,
|
|
10
|
+
type ImageContent,
|
|
11
|
+
type Message,
|
|
12
|
+
type Model,
|
|
13
|
+
modelsAreEqual,
|
|
14
|
+
type UsageReport,
|
|
15
|
+
} from "@oh-my-pi/pi-ai";
|
|
8
16
|
import type { Component, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
9
17
|
import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
10
|
-
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
|
|
11
19
|
import chalk from "chalk";
|
|
12
20
|
import { KeybindingsManager } from "../config/keybindings";
|
|
13
|
-
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
14
21
|
import { type Settings, settings } from "../config/settings";
|
|
15
22
|
import type {
|
|
16
23
|
ExtensionUIContext,
|
|
@@ -21,6 +28,7 @@ import type {
|
|
|
21
28
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
22
29
|
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
|
23
30
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
31
|
+
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
24
32
|
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
|
25
33
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
26
34
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
@@ -28,7 +36,9 @@ import { HistoryStorage } from "../session/history-storage";
|
|
|
28
36
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
29
37
|
import { getRecentSessions } from "../session/session-manager";
|
|
30
38
|
import { STTController, type SttState } from "../stt";
|
|
31
|
-
import type { ExitPlanModeDetails } from "../tools";
|
|
39
|
+
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
|
|
40
|
+
import type { EventBus } from "../utils/event-bus";
|
|
41
|
+
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
32
42
|
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
|
33
43
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
34
44
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -40,7 +50,7 @@ import type { HookSelectorComponent } from "./components/hook-selector";
|
|
|
40
50
|
import type { PythonExecutionComponent } from "./components/python-execution";
|
|
41
51
|
import { StatusLineComponent } from "./components/status-line";
|
|
42
52
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
43
|
-
import { WelcomeComponent } from "./components/welcome";
|
|
53
|
+
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
|
44
54
|
import { BtwController } from "./controllers/btw-controller";
|
|
45
55
|
import { CommandController } from "./controllers/command-controller";
|
|
46
56
|
import { EventController } from "./controllers/event-controller";
|
|
@@ -50,6 +60,7 @@ import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
|
50
60
|
import { SelectorController } from "./controllers/selector-controller";
|
|
51
61
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
52
62
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
63
|
+
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
53
64
|
import { setMermaidRenderCallback } from "./theme/mermaid-cache";
|
|
54
65
|
import type { Theme } from "./theme/theme";
|
|
55
66
|
import {
|
|
@@ -151,12 +162,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
151
162
|
readonly #version: string;
|
|
152
163
|
readonly #changelogMarkdown: string | undefined;
|
|
153
164
|
#planModePreviousTools: string[] | undefined;
|
|
154
|
-
#
|
|
155
|
-
#pendingModelSwitch: Model | undefined;
|
|
165
|
+
#planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
166
|
+
#pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
156
167
|
#planModeHasEntered = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
| undefined = undefined;
|
|
168
|
+
#planReviewContainer: Container | undefined;
|
|
169
|
+
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
|
|
160
170
|
mcpManager?: import("../mcp").MCPManager;
|
|
161
171
|
readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
162
172
|
|
|
@@ -173,16 +183,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
173
183
|
#voicePreviousShowHardwareCursor: boolean | null = null;
|
|
174
184
|
#voicePreviousUseTerminalCursor: boolean | null = null;
|
|
175
185
|
#resizeHandler?: () => void;
|
|
186
|
+
#observerRegistry: SessionObserverRegistry;
|
|
187
|
+
#eventBus?: EventBus;
|
|
188
|
+
#eventBusUnsubscribers: Array<() => void> = [];
|
|
189
|
+
#welcomeComponent?: WelcomeComponent;
|
|
176
190
|
|
|
177
191
|
constructor(
|
|
178
192
|
session: AgentSession,
|
|
179
193
|
version: string,
|
|
180
194
|
changelogMarkdown: string | undefined = undefined,
|
|
181
195
|
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
|
182
|
-
lspServers:
|
|
183
|
-
| Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
|
|
184
|
-
| undefined = undefined,
|
|
196
|
+
lspServers: LspStartupServerInfo[] | undefined = undefined,
|
|
185
197
|
mcpManager?: import("../mcp").MCPManager,
|
|
198
|
+
eventBus?: EventBus,
|
|
186
199
|
) {
|
|
187
200
|
this.session = session;
|
|
188
201
|
this.sessionManager = session.sessionManager;
|
|
@@ -194,6 +207,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
194
207
|
this.#toolUiContextSetter = setToolUIContext;
|
|
195
208
|
this.lspServers = lspServers;
|
|
196
209
|
this.mcpManager = mcpManager;
|
|
210
|
+
this.#eventBus = eventBus;
|
|
211
|
+
if (eventBus) {
|
|
212
|
+
this.#eventBusUnsubscribers.push(
|
|
213
|
+
eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
|
|
214
|
+
this.#handleLspStartupEvent(data as LspStartupEvent);
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
197
218
|
|
|
198
219
|
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
|
199
220
|
this.ui.setClearOnShrink(settings.get("clearOnShrink"));
|
|
@@ -268,18 +289,22 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
268
289
|
this.#commandController = new CommandController(this);
|
|
269
290
|
this.#selectorController = new SelectorController(this);
|
|
270
291
|
this.#inputController = new InputController(this);
|
|
292
|
+
this.#observerRegistry = new SessionObserverRegistry();
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
async init(): Promise<void> {
|
|
274
296
|
if (this.isInitialized) return;
|
|
275
297
|
|
|
276
|
-
|
|
298
|
+
logger.time("InteractiveMode.init:keybindings");
|
|
299
|
+
this.keybindings = KeybindingsManager.create();
|
|
277
300
|
|
|
278
301
|
// Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
|
|
279
302
|
this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
|
|
280
303
|
|
|
281
|
-
await logger.
|
|
282
|
-
|
|
304
|
+
await logger.time(
|
|
305
|
+
"InteractiveMode.init:slashCommands",
|
|
306
|
+
this.refreshSlashCommandState.bind(this),
|
|
307
|
+
getProjectDir(),
|
|
283
308
|
);
|
|
284
309
|
|
|
285
310
|
// Get current model info for welcome screen
|
|
@@ -287,7 +312,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
287
312
|
const providerName = this.session.model?.provider ?? "Unknown";
|
|
288
313
|
|
|
289
314
|
// Get recent sessions
|
|
290
|
-
const recentSessions = await logger.
|
|
315
|
+
const recentSessions = await logger.time("InteractiveMode.init:recentSessions", () =>
|
|
291
316
|
getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
|
|
292
317
|
sessions.map(s => ({
|
|
293
318
|
name: s.name,
|
|
@@ -296,15 +321,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
296
321
|
),
|
|
297
322
|
);
|
|
298
323
|
|
|
299
|
-
// Convert LSP servers to welcome format
|
|
300
|
-
const lspServerInfo =
|
|
301
|
-
this.lspServers?.map(s => ({
|
|
302
|
-
name: s.name,
|
|
303
|
-
status: s.status as "ready" | "error" | "connecting",
|
|
304
|
-
fileTypes: s.fileTypes,
|
|
305
|
-
})) ?? [];
|
|
306
|
-
|
|
307
324
|
const startupQuiet = settings.get("startup.quiet");
|
|
325
|
+
this.#welcomeComponent = undefined;
|
|
308
326
|
|
|
309
327
|
for (const warning of this.session.configWarnings) {
|
|
310
328
|
this.ui.addChild(new Text(theme.fg("warning", `Warning: ${warning}`), 1, 0));
|
|
@@ -313,11 +331,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
313
331
|
|
|
314
332
|
if (!startupQuiet) {
|
|
315
333
|
// Add welcome header
|
|
316
|
-
|
|
334
|
+
this.#welcomeComponent = new WelcomeComponent(
|
|
335
|
+
this.#version,
|
|
336
|
+
modelName,
|
|
337
|
+
providerName,
|
|
338
|
+
recentSessions,
|
|
339
|
+
this.#getWelcomeLspServers(),
|
|
340
|
+
);
|
|
317
341
|
|
|
318
342
|
// Setup UI layout
|
|
319
343
|
this.ui.addChild(new Spacer(1));
|
|
320
|
-
this.ui.addChild(
|
|
344
|
+
this.ui.addChild(this.#welcomeComponent);
|
|
321
345
|
this.ui.addChild(new Spacer(1));
|
|
322
346
|
|
|
323
347
|
// Add changelog if provided
|
|
@@ -352,6 +376,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
352
376
|
this.#inputController.setupKeyHandlers();
|
|
353
377
|
this.#inputController.setupEditorSubmitHandler();
|
|
354
378
|
|
|
379
|
+
// Wire observer registry to EventBus
|
|
380
|
+
if (this.#eventBus) {
|
|
381
|
+
this.#observerRegistry.subscribeToEventBus(this.#eventBus);
|
|
382
|
+
}
|
|
383
|
+
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
384
|
+
this.#observerRegistry.onChange(() => {
|
|
385
|
+
this.statusLine.setSubagentCount(this.#observerRegistry.getActiveSubagentCount());
|
|
386
|
+
this.ui.requestRender();
|
|
387
|
+
});
|
|
388
|
+
|
|
355
389
|
// Load initial todos
|
|
356
390
|
await this.#loadTodoList();
|
|
357
391
|
|
|
@@ -512,7 +546,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
512
546
|
|
|
513
547
|
rebuildChatFromMessages(): void {
|
|
514
548
|
this.chatContainer.clear();
|
|
515
|
-
const context = this.
|
|
549
|
+
const context = this.session.buildDisplaySessionContext();
|
|
516
550
|
this.renderSessionContext(context);
|
|
517
551
|
}
|
|
518
552
|
|
|
@@ -614,33 +648,41 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
614
648
|
}
|
|
615
649
|
|
|
616
650
|
async #applyPlanModeModel(): Promise<void> {
|
|
617
|
-
const
|
|
618
|
-
if (!
|
|
651
|
+
const resolved = this.session.resolveRoleModelWithThinking("plan");
|
|
652
|
+
if (!resolved.model) return;
|
|
653
|
+
|
|
619
654
|
const currentModel = this.session.model;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
this.#
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
655
|
+
const sameModel = modelsAreEqual(currentModel, resolved.model);
|
|
656
|
+
const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
|
|
657
|
+
|
|
658
|
+
this.#planModePreviousModelState = currentModel
|
|
659
|
+
? { model: currentModel, thinkingLevel: this.session.thinkingLevel }
|
|
660
|
+
: undefined;
|
|
661
|
+
|
|
662
|
+
if (!sameModel) {
|
|
663
|
+
if (this.session.isStreaming) {
|
|
664
|
+
this.#pendingModelSwitch = { model: resolved.model, thinkingLevel: planThinkingLevel };
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
await this.session.setModelTemporary(resolved.model, planThinkingLevel);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
this.showWarning(
|
|
671
|
+
`Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
} else if (planThinkingLevel) {
|
|
675
|
+
this.session.setThinkingLevel(planThinkingLevel);
|
|
634
676
|
}
|
|
635
677
|
}
|
|
636
678
|
|
|
637
679
|
/** Apply any deferred model switch after the current stream ends. */
|
|
638
680
|
async flushPendingModelSwitch(): Promise<void> {
|
|
639
|
-
const
|
|
640
|
-
if (!
|
|
681
|
+
const pending = this.#pendingModelSwitch;
|
|
682
|
+
if (!pending) return;
|
|
641
683
|
this.#pendingModelSwitch = undefined;
|
|
642
684
|
try {
|
|
643
|
-
await this.session.setModelTemporary(model);
|
|
685
|
+
await this.session.setModelTemporary(pending.model, pending.thinkingLevel);
|
|
644
686
|
} catch (error) {
|
|
645
687
|
this.showWarning(
|
|
646
688
|
`Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -704,20 +746,25 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
704
746
|
if (previousTools && previousTools.length > 0) {
|
|
705
747
|
await this.session.setActiveToolsByName(previousTools);
|
|
706
748
|
}
|
|
707
|
-
if (this.#
|
|
708
|
-
|
|
709
|
-
|
|
749
|
+
if (this.#planModePreviousModelState) {
|
|
750
|
+
const prev = this.#planModePreviousModelState;
|
|
751
|
+
if (modelsAreEqual(this.session.model, prev.model)) {
|
|
752
|
+
// Same model — only thinking level may differ. Avoid setModelTemporary()
|
|
753
|
+
// which would reset provider-side sessions (openai-responses/Codex) and
|
|
754
|
+
// break conversation continuity.
|
|
755
|
+
this.session.setThinkingLevel(prev.thinkingLevel);
|
|
756
|
+
} else if (this.session.isStreaming) {
|
|
757
|
+
this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
|
|
710
758
|
} else {
|
|
711
|
-
await this.session.setModelTemporary(
|
|
759
|
+
await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
|
|
712
760
|
}
|
|
713
761
|
}
|
|
714
|
-
|
|
715
762
|
this.session.setPlanModeState(undefined);
|
|
716
763
|
this.planModeEnabled = false;
|
|
717
764
|
this.planModePaused = options?.paused ?? false;
|
|
718
765
|
this.planModePlanFilePath = undefined;
|
|
719
766
|
this.#planModePreviousTools = undefined;
|
|
720
|
-
this.#
|
|
767
|
+
this.#planModePreviousModelState = undefined;
|
|
721
768
|
this.#updatePlanModeStatus();
|
|
722
769
|
const paused = options?.paused ?? false;
|
|
723
770
|
this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
|
|
@@ -739,15 +786,101 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
739
786
|
}
|
|
740
787
|
|
|
741
788
|
#renderPlanPreview(planContent: string): void {
|
|
742
|
-
this
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
789
|
+
const planReviewContainer = this.#planReviewContainer ?? new Container();
|
|
790
|
+
if (this.#planReviewContainer) {
|
|
791
|
+
// Re-append the preview so repeated plan-review refreshes stay adjacent to the
|
|
792
|
+
// active selector instead of updating an older off-screen preview in place.
|
|
793
|
+
this.chatContainer.removeChild(this.#planReviewContainer);
|
|
794
|
+
}
|
|
795
|
+
planReviewContainer.clear();
|
|
796
|
+
planReviewContainer.addChild(new Spacer(1));
|
|
797
|
+
planReviewContainer.addChild(new DynamicBorder());
|
|
798
|
+
planReviewContainer.addChild(new Text(theme.bold(theme.fg("accent", "Plan Review")), 1, 1));
|
|
799
|
+
planReviewContainer.addChild(new Spacer(1));
|
|
800
|
+
planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
|
|
801
|
+
planReviewContainer.addChild(new DynamicBorder());
|
|
802
|
+
this.chatContainer.addChild(planReviewContainer);
|
|
803
|
+
this.#planReviewContainer = planReviewContainer;
|
|
748
804
|
this.ui.requestRender();
|
|
749
805
|
}
|
|
750
806
|
|
|
807
|
+
#getEditorTerminalPath(): string | null {
|
|
808
|
+
if (process.platform === "win32") {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
return "/dev/tty";
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async #openEditorTerminalHandle(): Promise<fs.FileHandle | null> {
|
|
815
|
+
const terminalPath = this.#getEditorTerminalPath();
|
|
816
|
+
if (!terminalPath) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
return await fs.open(terminalPath, "r+");
|
|
821
|
+
} catch {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#getPlanReviewHelpText(): string {
|
|
827
|
+
const externalEditorKey = this.keybindings.getDisplayString("app.editor.external");
|
|
828
|
+
if (!externalEditorKey) {
|
|
829
|
+
return "up/down navigate enter select esc cancel";
|
|
830
|
+
}
|
|
831
|
+
return `up/down navigate enter select ${externalEditorKey.toLowerCase()} open in editor esc cancel`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
|
|
835
|
+
const editorCmd = getEditorCommand();
|
|
836
|
+
if (!editorCmd) {
|
|
837
|
+
this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const resolvedPath = this.#resolvePlanFilePath(planFilePath);
|
|
842
|
+
let currentText: string;
|
|
843
|
+
try {
|
|
844
|
+
currentText = await Bun.file(resolvedPath).text();
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (isEnoent(error)) {
|
|
847
|
+
this.showError(`Plan file not found at ${planFilePath}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let ttyHandle: fs.FileHandle | null = null;
|
|
855
|
+
try {
|
|
856
|
+
ttyHandle = await this.#openEditorTerminalHandle();
|
|
857
|
+
this.ui.stop();
|
|
858
|
+
|
|
859
|
+
const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
|
|
860
|
+
? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
|
|
861
|
+
: ["inherit", "inherit", "inherit"];
|
|
862
|
+
|
|
863
|
+
const result = await openInEditor(editorCmd, currentText, {
|
|
864
|
+
extension: path.extname(resolvedPath) || ".md",
|
|
865
|
+
stdio,
|
|
866
|
+
trimTrailingNewline: false,
|
|
867
|
+
});
|
|
868
|
+
if (result !== null) {
|
|
869
|
+
await Bun.write(resolvedPath, result);
|
|
870
|
+
this.#renderPlanPreview(result);
|
|
871
|
+
this.showStatus("Plan updated in external editor.");
|
|
872
|
+
}
|
|
873
|
+
} catch (error) {
|
|
874
|
+
this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
875
|
+
} finally {
|
|
876
|
+
if (ttyHandle) {
|
|
877
|
+
await ttyHandle.close();
|
|
878
|
+
}
|
|
879
|
+
this.ui.start();
|
|
880
|
+
this.ui.requestRender(true);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
751
884
|
async #approvePlan(
|
|
752
885
|
planContent: string,
|
|
753
886
|
options: { planFilePath: string; finalPlanFilePath: string },
|
|
@@ -773,11 +906,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
773
906
|
}
|
|
774
907
|
this.session.setPlanReferencePath(options.finalPlanFilePath);
|
|
775
908
|
this.session.markPlanReferenceSent();
|
|
776
|
-
const
|
|
909
|
+
const planModePrompt = prompt.render(planModeApprovedPrompt, {
|
|
777
910
|
planContent,
|
|
778
911
|
finalPlanFilePath: options.finalPlanFilePath,
|
|
779
912
|
});
|
|
780
|
-
await this.session.prompt(
|
|
913
|
+
await this.session.prompt(planModePrompt, { synthetic: true });
|
|
781
914
|
}
|
|
782
915
|
|
|
783
916
|
async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
|
|
@@ -817,16 +950,24 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
817
950
|
}
|
|
818
951
|
|
|
819
952
|
this.#renderPlanPreview(planContent);
|
|
820
|
-
const choice = await this.showHookSelector(
|
|
821
|
-
"
|
|
822
|
-
"Refine plan",
|
|
823
|
-
|
|
824
|
-
|
|
953
|
+
const choice = await this.showHookSelector(
|
|
954
|
+
"Plan mode - next step",
|
|
955
|
+
["Approve and execute", "Refine plan", "Stay in plan mode"],
|
|
956
|
+
{
|
|
957
|
+
helpText: this.#getPlanReviewHelpText(),
|
|
958
|
+
onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
|
|
959
|
+
},
|
|
960
|
+
);
|
|
825
961
|
|
|
826
962
|
if (choice === "Approve and execute") {
|
|
827
963
|
const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
|
|
828
964
|
try {
|
|
829
|
-
await this.#
|
|
965
|
+
const latestPlanContent = await this.#readPlanFile(planFilePath);
|
|
966
|
+
if (!latestPlanContent) {
|
|
967
|
+
this.showError(`Plan file not found at ${planFilePath}`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
await this.#approvePlan(latestPlanContent, { planFilePath, finalPlanFilePath });
|
|
830
971
|
} catch (error) {
|
|
831
972
|
this.showError(
|
|
832
973
|
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -835,9 +976,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
835
976
|
return;
|
|
836
977
|
}
|
|
837
978
|
if (choice === "Refine plan") {
|
|
838
|
-
const refinement = await this.showHookInput("What should be refined?");
|
|
979
|
+
const refinement = (await this.showHookInput("What should be refined?"))?.trim();
|
|
839
980
|
if (refinement) {
|
|
840
|
-
this.
|
|
981
|
+
if (this.onInputCallback) {
|
|
982
|
+
this.onInputCallback(this.startPendingSubmission({ text: refinement }));
|
|
983
|
+
} else {
|
|
984
|
+
this.editor.setText(refinement);
|
|
985
|
+
}
|
|
841
986
|
}
|
|
842
987
|
}
|
|
843
988
|
}
|
|
@@ -854,6 +999,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
854
999
|
}
|
|
855
1000
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
856
1001
|
this.#extensionUiController.clearHookWidgets();
|
|
1002
|
+
for (const unsubscribe of this.#eventBusUnsubscribers) {
|
|
1003
|
+
unsubscribe();
|
|
1004
|
+
}
|
|
1005
|
+
this.#eventBusUnsubscribers = [];
|
|
1006
|
+
this.#observerRegistry.dispose();
|
|
1007
|
+
this.#eventController.dispose();
|
|
857
1008
|
this.statusLine.dispose();
|
|
858
1009
|
if (this.#resizeHandler) {
|
|
859
1010
|
process.stdout.removeListener("resize", this.#resizeHandler);
|
|
@@ -950,6 +1101,48 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
950
1101
|
this.#uiHelpers.showWarning(message);
|
|
951
1102
|
}
|
|
952
1103
|
|
|
1104
|
+
#handleLspStartupEvent(event: LspStartupEvent): void {
|
|
1105
|
+
this.#updateWelcomeLspServers();
|
|
1106
|
+
|
|
1107
|
+
if (event.type === "failed") {
|
|
1108
|
+
this.showWarning(`LSP startup failed: ${event.error}. It will retry lazily on write.`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const failedServers = event.servers.filter(server => server.status === "error");
|
|
1113
|
+
|
|
1114
|
+
if (failedServers.length === 1) {
|
|
1115
|
+
const failedServer = failedServers[0];
|
|
1116
|
+
const detail = failedServer.error ? `: ${failedServer.error}` : "";
|
|
1117
|
+
this.showWarning(`LSP startup failed for ${failedServer.name}${detail}. It will retry lazily on write.`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (failedServers.length > 1) {
|
|
1122
|
+
const failedNames = failedServers.map(server => server.name).join(", ");
|
|
1123
|
+
this.showWarning(`LSP startup failed for ${failedNames}. It will retry lazily on write.`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
#getWelcomeLspServers(): WelcomeLspServerInfo[] {
|
|
1128
|
+
return (
|
|
1129
|
+
this.lspServers?.map(server => ({
|
|
1130
|
+
name: server.name,
|
|
1131
|
+
status: server.status,
|
|
1132
|
+
fileTypes: server.fileTypes,
|
|
1133
|
+
})) ?? []
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
#updateWelcomeLspServers(): void {
|
|
1138
|
+
if (!this.#welcomeComponent) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
|
|
1143
|
+
this.ui.requestRender();
|
|
1144
|
+
}
|
|
1145
|
+
|
|
953
1146
|
ensureLoadingAnimation(): void {
|
|
954
1147
|
if (!this.loadingAnimation) {
|
|
955
1148
|
this.statusContainer.clear();
|
|
@@ -1096,6 +1289,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1096
1289
|
handleClearCommand(): Promise<void> {
|
|
1097
1290
|
this.#btwController.dispose();
|
|
1098
1291
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
1292
|
+
this.#planReviewContainer = undefined;
|
|
1099
1293
|
return this.#commandController.handleClearCommand();
|
|
1100
1294
|
}
|
|
1101
1295
|
|
|
@@ -1192,6 +1386,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1192
1386
|
this.#selectorController.showDebugSelector();
|
|
1193
1387
|
}
|
|
1194
1388
|
|
|
1389
|
+
showSessionObserver(): void {
|
|
1390
|
+
const sessions = this.#observerRegistry.getSessions();
|
|
1391
|
+
if (sessions.length <= 1) {
|
|
1392
|
+
this.showStatus("No active subagent sessions");
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
this.#selectorController.showSessionObserver(this.#observerRegistry);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
resetObserverRegistry(): void {
|
|
1399
|
+
this.#observerRegistry.resetSessions();
|
|
1400
|
+
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1195
1403
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void> {
|
|
1196
1404
|
return this.#commandController.handleBashCommand(command, excludeFromContext);
|
|
1197
1405
|
}
|
|
@@ -1265,6 +1473,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1265
1473
|
|
|
1266
1474
|
handleResumeSession(sessionPath: string): Promise<void> {
|
|
1267
1475
|
this.#btwController.dispose();
|
|
1476
|
+
this.resetObserverRegistry();
|
|
1268
1477
|
return this.#selectorController.handleResumeSession(sessionPath);
|
|
1269
1478
|
}
|
|
1270
1479
|
|