@mariozechner/pi-coding-agent 0.14.2 → 0.16.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 +18 -0
- package/README.md +415 -1098
- package/dist/cli/args.d.ts +30 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +179 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/file-processor.d.ts +11 -0
- package/dist/cli/file-processor.d.ts.map +1 -0
- package/dist/cli/file-processor.js +82 -0
- package/dist/cli/file-processor.js.map +1 -0
- package/dist/cli/session-picker.d.ts +7 -0
- package/dist/cli/session-picker.d.ts.map +1 -0
- package/dist/cli/session-picker.js +29 -0
- package/dist/cli/session-picker.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -18
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +287 -0
- package/dist/core/agent-session.d.ts.map +1 -0
- package/dist/core/agent-session.js +735 -0
- package/dist/core/agent-session.js.map +1 -0
- package/dist/core/bash-executor.d.ts +41 -0
- package/dist/core/bash-executor.d.ts.map +1 -0
- package/dist/core/bash-executor.js +132 -0
- package/dist/core/bash-executor.js.map +1 -0
- package/dist/{compaction.d.ts → core/compaction.d.ts} +5 -1
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/{compaction.js → core/compaction.js} +23 -1
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/export-html.d.ts.map +1 -0
- package/dist/{export-html.js → core/export-html.js} +1 -1
- package/dist/{export-html.d.ts.map → core/export-html.js.map} +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/{model-config.js → core/model-config.js} +1 -1
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/model-resolver.d.ts +48 -0
- package/dist/core/model-resolver.d.ts.map +1 -0
- package/dist/core/model-resolver.js +244 -0
- package/dist/core/model-resolver.js.map +1 -0
- package/dist/core/oauth/anthropic.d.ts.map +1 -0
- package/dist/core/oauth/anthropic.js.map +1 -0
- package/dist/core/oauth/index.d.ts.map +1 -0
- package/dist/{oauth/index.d.ts.map → core/oauth/index.js.map} +1 -1
- package/dist/core/oauth/storage.d.ts.map +1 -0
- package/dist/{oauth → core/oauth}/storage.js +1 -1
- package/dist/core/oauth/storage.js.map +1 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/{session-manager.js → core/session-manager.js} +1 -1
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -0
- package/dist/{settings-manager.js → core/settings-manager.js} +1 -1
- package/dist/core/settings-manager.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -0
- package/dist/{slash-commands.js → core/slash-commands.js} +1 -1
- package/dist/core/slash-commands.js.map +1 -0
- package/dist/core/system-prompt.d.ts +17 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +203 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -0
- package/dist/{tools → core/tools}/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -0
- package/dist/core/tools/edit.js.map +1 -0
- package/dist/core/tools/find.d.ts.map +1 -0
- package/dist/{tools → core/tools}/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/{tools → core/tools}/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/ls.d.ts.map +1 -0
- package/dist/core/tools/ls.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -0
- package/dist/core/tools/read.js.map +1 -0
- package/dist/core/tools/truncate.d.ts.map +1 -0
- package/dist/core/tools/truncate.js.map +1 -0
- package/dist/core/tools/write.d.ts.map +1 -0
- package/dist/core/tools/write.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +176 -1082
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +9 -0
- package/dist/modes/index.d.ts.map +1 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/index.js.map +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/bash-execution.d.ts +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/bash-execution.js +1 -1
- package/dist/modes/interactive/components/bash-execution.js.map +1 -0
- package/dist/modes/interactive/components/compaction.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/model-selector.js +3 -3
- package/dist/modes/interactive/components/model-selector.js.map +1 -0
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/oauth-selector.js +2 -2
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -0
- package/dist/modes/interactive/components/queue-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/queue-mode-selector.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/session-selector.d.ts +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/session-selector.js +1 -1
- package/dist/modes/interactive/components/session-selector.js.map +1 -0
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -0
- package/dist/{tui/theme-selector.d.ts.map → modes/interactive/components/theme-selector.js.map} +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -0
- package/dist/{tui/tui-renderer.d.ts → modes/interactive/interactive-mode.d.ts} +36 -38
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-mode.js +1217 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/{theme → modes/interactive/theme}/theme.js +1 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/modes/print-mode.d.ts +21 -0
- package/dist/modes/print-mode.d.ts.map +1 -0
- package/dist/modes/print-mode.js +53 -0
- package/dist/modes/print-mode.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +182 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-client.js +362 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -0
- package/dist/modes/rpc/rpc-mode.d.ts +19 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-mode.js +204 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -0
- package/dist/modes/rpc/rpc-types.d.ts +254 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/modes/rpc/rpc-types.js.map +1 -0
- package/dist/{changelog.d.ts → utils/changelog.d.ts} +1 -1
- package/dist/{changelog.js.map → utils/changelog.d.ts.map} +1 -1
- package/dist/{changelog.js → utils/changelog.js} +1 -1
- package/dist/utils/changelog.js.map +1 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js.map +1 -0
- package/dist/utils/fuzzy.d.ts.map +1 -0
- package/dist/utils/fuzzy.js.map +1 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/{shell.js → utils/shell.js} +1 -1
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -0
- package/dist/{tools-manager.js → utils/tools-manager.js} +1 -1
- package/dist/utils/tools-manager.js.map +1 -0
- package/package.json +6 -6
- package/dist/changelog.d.ts.map +0 -1
- package/dist/clipboard.d.ts.map +0 -1
- package/dist/clipboard.js.map +0 -1
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js.map +0 -1
- package/dist/export-html.js.map +0 -1
- package/dist/fuzzy.d.ts.map +0 -1
- package/dist/fuzzy.js.map +0 -1
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js.map +0 -1
- package/dist/model-config.d.ts.map +0 -1
- package/dist/model-config.js.map +0 -1
- package/dist/oauth/anthropic.d.ts.map +0 -1
- package/dist/oauth/anthropic.js.map +0 -1
- package/dist/oauth/index.js.map +0 -1
- package/dist/oauth/storage.d.ts.map +0 -1
- package/dist/oauth/storage.js.map +0 -1
- package/dist/session-manager.d.ts.map +0 -1
- package/dist/session-manager.js.map +0 -1
- package/dist/settings-manager.d.ts.map +0 -1
- package/dist/settings-manager.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/slash-commands.d.ts.map +0 -1
- package/dist/slash-commands.js.map +0 -1
- package/dist/theme/theme.d.ts.map +0 -1
- package/dist/theme/theme.js.map +0 -1
- package/dist/tools/bash.d.ts.map +0 -1
- package/dist/tools/bash.js.map +0 -1
- package/dist/tools/edit.d.ts.map +0 -1
- package/dist/tools/edit.js.map +0 -1
- package/dist/tools/find.d.ts.map +0 -1
- package/dist/tools/find.js.map +0 -1
- package/dist/tools/grep.d.ts.map +0 -1
- package/dist/tools/grep.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/ls.d.ts.map +0 -1
- package/dist/tools/ls.js.map +0 -1
- package/dist/tools/read.d.ts.map +0 -1
- package/dist/tools/read.js.map +0 -1
- package/dist/tools/truncate.d.ts.map +0 -1
- package/dist/tools/truncate.js.map +0 -1
- package/dist/tools/write.d.ts.map +0 -1
- package/dist/tools/write.js.map +0 -1
- package/dist/tools-manager.d.ts.map +0 -1
- package/dist/tools-manager.js.map +0 -1
- package/dist/tui/assistant-message.d.ts.map +0 -1
- package/dist/tui/assistant-message.js.map +0 -1
- package/dist/tui/bash-execution.d.ts.map +0 -1
- package/dist/tui/bash-execution.js.map +0 -1
- package/dist/tui/compaction.d.ts.map +0 -1
- package/dist/tui/compaction.js.map +0 -1
- package/dist/tui/custom-editor.d.ts.map +0 -1
- package/dist/tui/custom-editor.js.map +0 -1
- package/dist/tui/dynamic-border.d.ts.map +0 -1
- package/dist/tui/dynamic-border.js.map +0 -1
- package/dist/tui/footer.d.ts.map +0 -1
- package/dist/tui/footer.js.map +0 -1
- package/dist/tui/model-selector.d.ts.map +0 -1
- package/dist/tui/model-selector.js.map +0 -1
- package/dist/tui/oauth-selector.d.ts.map +0 -1
- package/dist/tui/oauth-selector.js.map +0 -1
- package/dist/tui/queue-mode-selector.d.ts.map +0 -1
- package/dist/tui/queue-mode-selector.js.map +0 -1
- package/dist/tui/session-selector.d.ts.map +0 -1
- package/dist/tui/session-selector.js.map +0 -1
- package/dist/tui/theme-selector.js.map +0 -1
- package/dist/tui/thinking-selector.d.ts.map +0 -1
- package/dist/tui/thinking-selector.js.map +0 -1
- package/dist/tui/tool-execution.d.ts.map +0 -1
- package/dist/tui/tool-execution.js.map +0 -1
- package/dist/tui/tui-renderer.d.ts.map +0 -1
- package/dist/tui/tui-renderer.js +0 -1937
- package/dist/tui/tui-renderer.js.map +0 -1
- package/dist/tui/user-message-selector.d.ts.map +0 -1
- package/dist/tui/user-message-selector.js.map +0 -1
- package/dist/tui/user-message.d.ts.map +0 -1
- package/dist/tui/user-message.js.map +0 -1
- /package/dist/{export-html.d.ts → core/export-html.d.ts} +0 -0
- /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
- /package/dist/{messages.js → core/messages.js} +0 -0
- /package/dist/{model-config.d.ts → core/model-config.d.ts} +0 -0
- /package/dist/{oauth → core/oauth}/anthropic.d.ts +0 -0
- /package/dist/{oauth → core/oauth}/anthropic.js +0 -0
- /package/dist/{oauth → core/oauth}/index.d.ts +0 -0
- /package/dist/{oauth → core/oauth}/index.js +0 -0
- /package/dist/{oauth → core/oauth}/storage.d.ts +0 -0
- /package/dist/{session-manager.d.ts → core/session-manager.d.ts} +0 -0
- /package/dist/{settings-manager.d.ts → core/settings-manager.d.ts} +0 -0
- /package/dist/{slash-commands.d.ts → core/slash-commands.d.ts} +0 -0
- /package/dist/{tools → core/tools}/bash.d.ts +0 -0
- /package/dist/{tools → core/tools}/edit.d.ts +0 -0
- /package/dist/{tools → core/tools}/edit.js +0 -0
- /package/dist/{tools → core/tools}/find.d.ts +0 -0
- /package/dist/{tools → core/tools}/grep.d.ts +0 -0
- /package/dist/{tools → core/tools}/index.d.ts +0 -0
- /package/dist/{tools → core/tools}/index.js +0 -0
- /package/dist/{tools → core/tools}/ls.d.ts +0 -0
- /package/dist/{tools → core/tools}/ls.js +0 -0
- /package/dist/{tools → core/tools}/read.d.ts +0 -0
- /package/dist/{tools → core/tools}/read.js +0 -0
- /package/dist/{tools → core/tools}/truncate.d.ts +0 -0
- /package/dist/{tools → core/tools}/truncate.js +0 -0
- /package/dist/{tools → core/tools}/write.d.ts +0 -0
- /package/dist/{tools → core/tools}/write.js +0 -0
- /package/dist/{tui → modes/interactive/components}/assistant-message.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/assistant-message.js +0 -0
- /package/dist/{tui → modes/interactive/components}/compaction.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/compaction.js +0 -0
- /package/dist/{tui → modes/interactive/components}/custom-editor.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/custom-editor.js +0 -0
- /package/dist/{tui → modes/interactive/components}/dynamic-border.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/dynamic-border.js +0 -0
- /package/dist/{tui → modes/interactive/components}/footer.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/oauth-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/queue-mode-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/queue-mode-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/theme-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/theme-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/thinking-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/thinking-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/tool-execution.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/tool-execution.js +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message.js +0 -0
- /package/dist/{theme → modes/interactive/theme}/dark.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/light.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/theme-schema.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/theme.d.ts +0 -0
- /package/dist/{clipboard.d.ts → utils/clipboard.d.ts} +0 -0
- /package/dist/{clipboard.js → utils/clipboard.js} +0 -0
- /package/dist/{fuzzy.d.ts → utils/fuzzy.d.ts} +0 -0
- /package/dist/{fuzzy.js → utils/fuzzy.js} +0 -0
- /package/dist/{shell.d.ts → utils/shell.d.ts} +0 -0
- /package/dist/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive mode for the coding agent.
|
|
3
|
+
* Handles TUI rendering and user interaction, delegating business logic to AgentSession.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
import { APP_NAME, getDebugLogPath, getOAuthPath } from "../../config.js";
|
|
10
|
+
import { isBashExecutionMessage } from "../../core/messages.js";
|
|
11
|
+
import { invalidateOAuthCache } from "../../core/model-config.js";
|
|
12
|
+
import { listOAuthProviders, login, logout } from "../../core/oauth/index.js";
|
|
13
|
+
import { getLatestCompactionEntry, SUMMARY_PREFIX, SUMMARY_SUFFIX } from "../../core/session-manager.js";
|
|
14
|
+
import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
|
|
15
|
+
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
16
|
+
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
17
|
+
import { BashExecutionComponent } from "./components/bash-execution.js";
|
|
18
|
+
import { CompactionComponent } from "./components/compaction.js";
|
|
19
|
+
import { CustomEditor } from "./components/custom-editor.js";
|
|
20
|
+
import { DynamicBorder } from "./components/dynamic-border.js";
|
|
21
|
+
import { FooterComponent } from "./components/footer.js";
|
|
22
|
+
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
23
|
+
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
24
|
+
import { QueueModeSelectorComponent } from "./components/queue-mode-selector.js";
|
|
25
|
+
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
26
|
+
import { ThemeSelectorComponent } from "./components/theme-selector.js";
|
|
27
|
+
import { ThinkingSelectorComponent } from "./components/thinking-selector.js";
|
|
28
|
+
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
29
|
+
import { UserMessageComponent } from "./components/user-message.js";
|
|
30
|
+
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
31
|
+
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
|
|
32
|
+
export class InteractiveMode {
|
|
33
|
+
session;
|
|
34
|
+
ui;
|
|
35
|
+
chatContainer;
|
|
36
|
+
pendingMessagesContainer;
|
|
37
|
+
statusContainer;
|
|
38
|
+
editor;
|
|
39
|
+
editorContainer;
|
|
40
|
+
footer;
|
|
41
|
+
version;
|
|
42
|
+
isInitialized = false;
|
|
43
|
+
onInputCallback;
|
|
44
|
+
loadingAnimation = null;
|
|
45
|
+
lastSigintTime = 0;
|
|
46
|
+
lastEscapeTime = 0;
|
|
47
|
+
changelogMarkdown = null;
|
|
48
|
+
// Streaming message tracking
|
|
49
|
+
streamingComponent = null;
|
|
50
|
+
// Tool execution tracking: toolCallId -> component
|
|
51
|
+
pendingTools = new Map();
|
|
52
|
+
// Track if this is the first user message (to skip spacer)
|
|
53
|
+
isFirstUserMessage = true;
|
|
54
|
+
// Tool output expansion state
|
|
55
|
+
toolOutputExpanded = false;
|
|
56
|
+
// Thinking block visibility state
|
|
57
|
+
hideThinkingBlock = false;
|
|
58
|
+
// Agent subscription unsubscribe function
|
|
59
|
+
unsubscribe;
|
|
60
|
+
// Track if editor is in bash mode (text starts with !)
|
|
61
|
+
isBashMode = false;
|
|
62
|
+
// Track current bash execution component
|
|
63
|
+
bashComponent = null;
|
|
64
|
+
// Track pending bash components (shown in pending area, moved to chat on submit)
|
|
65
|
+
pendingBashComponents = [];
|
|
66
|
+
// Auto-compaction state
|
|
67
|
+
autoCompactionLoader = null;
|
|
68
|
+
autoCompactionEscapeHandler;
|
|
69
|
+
// Convenience accessors
|
|
70
|
+
get agent() {
|
|
71
|
+
return this.session.agent;
|
|
72
|
+
}
|
|
73
|
+
get sessionManager() {
|
|
74
|
+
return this.session.sessionManager;
|
|
75
|
+
}
|
|
76
|
+
get settingsManager() {
|
|
77
|
+
return this.session.settingsManager;
|
|
78
|
+
}
|
|
79
|
+
constructor(session, version, changelogMarkdown = null, fdPath = null) {
|
|
80
|
+
this.session = session;
|
|
81
|
+
this.version = version;
|
|
82
|
+
this.changelogMarkdown = changelogMarkdown;
|
|
83
|
+
this.ui = new TUI(new ProcessTerminal());
|
|
84
|
+
this.chatContainer = new Container();
|
|
85
|
+
this.pendingMessagesContainer = new Container();
|
|
86
|
+
this.statusContainer = new Container();
|
|
87
|
+
this.editor = new CustomEditor(getEditorTheme());
|
|
88
|
+
this.editorContainer = new Container();
|
|
89
|
+
this.editorContainer.addChild(this.editor);
|
|
90
|
+
this.footer = new FooterComponent(session.state);
|
|
91
|
+
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
92
|
+
// Define slash commands for autocomplete
|
|
93
|
+
const slashCommands = [
|
|
94
|
+
{ name: "thinking", description: "Select reasoning level (opens selector UI)" },
|
|
95
|
+
{ name: "model", description: "Select model (opens selector UI)" },
|
|
96
|
+
{ name: "export", description: "Export session to HTML file" },
|
|
97
|
+
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
98
|
+
{ name: "session", description: "Show session info and stats" },
|
|
99
|
+
{ name: "changelog", description: "Show changelog entries" },
|
|
100
|
+
{ name: "branch", description: "Create a new branch from a previous message" },
|
|
101
|
+
{ name: "login", description: "Login with OAuth provider" },
|
|
102
|
+
{ name: "logout", description: "Logout from OAuth provider" },
|
|
103
|
+
{ name: "queue", description: "Select message queue mode (opens selector UI)" },
|
|
104
|
+
{ name: "theme", description: "Select color theme (opens selector UI)" },
|
|
105
|
+
{ name: "clear", description: "Clear context and start a fresh session" },
|
|
106
|
+
{ name: "compact", description: "Manually compact the session context" },
|
|
107
|
+
{ name: "autocompact", description: "Toggle automatic context compaction" },
|
|
108
|
+
{ name: "resume", description: "Resume a different session" },
|
|
109
|
+
];
|
|
110
|
+
// Load hide thinking block setting
|
|
111
|
+
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
112
|
+
// Convert file commands to SlashCommand format
|
|
113
|
+
const fileSlashCommands = this.session.fileCommands.map((cmd) => ({
|
|
114
|
+
name: cmd.name,
|
|
115
|
+
description: cmd.description,
|
|
116
|
+
}));
|
|
117
|
+
// Setup autocomplete
|
|
118
|
+
const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands], process.cwd(), fdPath);
|
|
119
|
+
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
120
|
+
}
|
|
121
|
+
async init() {
|
|
122
|
+
if (this.isInitialized)
|
|
123
|
+
return;
|
|
124
|
+
// Add header
|
|
125
|
+
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
126
|
+
const instructions = theme.fg("dim", "esc") +
|
|
127
|
+
theme.fg("muted", " to interrupt") +
|
|
128
|
+
"\n" +
|
|
129
|
+
theme.fg("dim", "ctrl+c") +
|
|
130
|
+
theme.fg("muted", " to clear") +
|
|
131
|
+
"\n" +
|
|
132
|
+
theme.fg("dim", "ctrl+c twice") +
|
|
133
|
+
theme.fg("muted", " to exit") +
|
|
134
|
+
"\n" +
|
|
135
|
+
theme.fg("dim", "ctrl+k") +
|
|
136
|
+
theme.fg("muted", " to delete line") +
|
|
137
|
+
"\n" +
|
|
138
|
+
theme.fg("dim", "shift+tab") +
|
|
139
|
+
theme.fg("muted", " to cycle thinking") +
|
|
140
|
+
"\n" +
|
|
141
|
+
theme.fg("dim", "ctrl+p") +
|
|
142
|
+
theme.fg("muted", " to cycle models") +
|
|
143
|
+
"\n" +
|
|
144
|
+
theme.fg("dim", "ctrl+o") +
|
|
145
|
+
theme.fg("muted", " to expand tools") +
|
|
146
|
+
"\n" +
|
|
147
|
+
theme.fg("dim", "ctrl+t") +
|
|
148
|
+
theme.fg("muted", " to toggle thinking") +
|
|
149
|
+
"\n" +
|
|
150
|
+
theme.fg("dim", "/") +
|
|
151
|
+
theme.fg("muted", " for commands") +
|
|
152
|
+
"\n" +
|
|
153
|
+
theme.fg("dim", "!") +
|
|
154
|
+
theme.fg("muted", " to run bash") +
|
|
155
|
+
"\n" +
|
|
156
|
+
theme.fg("dim", "drop files") +
|
|
157
|
+
theme.fg("muted", " to attach");
|
|
158
|
+
const header = new Text(logo + "\n" + instructions, 1, 0);
|
|
159
|
+
// Setup UI layout
|
|
160
|
+
this.ui.addChild(new Spacer(1));
|
|
161
|
+
this.ui.addChild(header);
|
|
162
|
+
this.ui.addChild(new Spacer(1));
|
|
163
|
+
// Add changelog if provided
|
|
164
|
+
if (this.changelogMarkdown) {
|
|
165
|
+
this.ui.addChild(new DynamicBorder());
|
|
166
|
+
if (this.settingsManager.getCollapseChangelog()) {
|
|
167
|
+
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
168
|
+
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
|
169
|
+
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
|
170
|
+
this.ui.addChild(new Text(condensedText, 1, 0));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
174
|
+
this.ui.addChild(new Spacer(1));
|
|
175
|
+
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
|
176
|
+
this.ui.addChild(new Spacer(1));
|
|
177
|
+
}
|
|
178
|
+
this.ui.addChild(new DynamicBorder());
|
|
179
|
+
}
|
|
180
|
+
this.ui.addChild(this.chatContainer);
|
|
181
|
+
this.ui.addChild(this.pendingMessagesContainer);
|
|
182
|
+
this.ui.addChild(this.statusContainer);
|
|
183
|
+
this.ui.addChild(new Spacer(1));
|
|
184
|
+
this.ui.addChild(this.editorContainer);
|
|
185
|
+
this.ui.addChild(this.footer);
|
|
186
|
+
this.ui.setFocus(this.editor);
|
|
187
|
+
this.setupKeyHandlers();
|
|
188
|
+
this.setupEditorSubmitHandler();
|
|
189
|
+
// Start the UI
|
|
190
|
+
this.ui.start();
|
|
191
|
+
this.isInitialized = true;
|
|
192
|
+
// Subscribe to agent events
|
|
193
|
+
this.subscribeToAgent();
|
|
194
|
+
// Set up theme file watcher
|
|
195
|
+
onThemeChange(() => {
|
|
196
|
+
this.ui.invalidate();
|
|
197
|
+
this.updateEditorBorderColor();
|
|
198
|
+
this.ui.requestRender();
|
|
199
|
+
});
|
|
200
|
+
// Set up git branch watcher
|
|
201
|
+
this.footer.watchBranch(() => {
|
|
202
|
+
this.ui.requestRender();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
setupKeyHandlers() {
|
|
206
|
+
this.editor.onEscape = () => {
|
|
207
|
+
if (this.loadingAnimation) {
|
|
208
|
+
// Abort and restore queued messages to editor
|
|
209
|
+
const queuedMessages = this.session.clearQueue();
|
|
210
|
+
const queuedText = queuedMessages.join("\n\n");
|
|
211
|
+
const currentText = this.editor.getText();
|
|
212
|
+
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
213
|
+
this.editor.setText(combinedText);
|
|
214
|
+
this.updatePendingMessagesDisplay();
|
|
215
|
+
this.agent.abort();
|
|
216
|
+
}
|
|
217
|
+
else if (this.session.isBashRunning) {
|
|
218
|
+
this.session.abortBash();
|
|
219
|
+
}
|
|
220
|
+
else if (this.isBashMode) {
|
|
221
|
+
this.editor.setText("");
|
|
222
|
+
this.isBashMode = false;
|
|
223
|
+
this.updateEditorBorderColor();
|
|
224
|
+
}
|
|
225
|
+
else if (!this.editor.getText().trim()) {
|
|
226
|
+
// Double-escape with empty editor triggers /branch
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
if (now - this.lastEscapeTime < 500) {
|
|
229
|
+
this.showUserMessageSelector();
|
|
230
|
+
this.lastEscapeTime = 0;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
this.lastEscapeTime = now;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
this.editor.onCtrlC = () => this.handleCtrlC();
|
|
238
|
+
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
239
|
+
this.editor.onCtrlP = () => this.cycleModel();
|
|
240
|
+
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
241
|
+
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
242
|
+
this.editor.onChange = (text) => {
|
|
243
|
+
const wasBashMode = this.isBashMode;
|
|
244
|
+
this.isBashMode = text.trimStart().startsWith("!");
|
|
245
|
+
if (wasBashMode !== this.isBashMode) {
|
|
246
|
+
this.updateEditorBorderColor();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
setupEditorSubmitHandler() {
|
|
251
|
+
this.editor.onSubmit = async (text) => {
|
|
252
|
+
text = text.trim();
|
|
253
|
+
if (!text)
|
|
254
|
+
return;
|
|
255
|
+
// Handle slash commands
|
|
256
|
+
if (text === "/thinking") {
|
|
257
|
+
this.showThinkingSelector();
|
|
258
|
+
this.editor.setText("");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (text === "/model") {
|
|
262
|
+
this.showModelSelector();
|
|
263
|
+
this.editor.setText("");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (text.startsWith("/export")) {
|
|
267
|
+
this.handleExportCommand(text);
|
|
268
|
+
this.editor.setText("");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (text === "/copy") {
|
|
272
|
+
this.handleCopyCommand();
|
|
273
|
+
this.editor.setText("");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (text === "/session") {
|
|
277
|
+
this.handleSessionCommand();
|
|
278
|
+
this.editor.setText("");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (text === "/changelog") {
|
|
282
|
+
this.handleChangelogCommand();
|
|
283
|
+
this.editor.setText("");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (text === "/branch") {
|
|
287
|
+
this.showUserMessageSelector();
|
|
288
|
+
this.editor.setText("");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (text === "/login") {
|
|
292
|
+
this.showOAuthSelector("login");
|
|
293
|
+
this.editor.setText("");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (text === "/logout") {
|
|
297
|
+
this.showOAuthSelector("logout");
|
|
298
|
+
this.editor.setText("");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (text === "/queue") {
|
|
302
|
+
this.showQueueModeSelector();
|
|
303
|
+
this.editor.setText("");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (text === "/theme") {
|
|
307
|
+
this.showThemeSelector();
|
|
308
|
+
this.editor.setText("");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (text === "/clear") {
|
|
312
|
+
await this.handleClearCommand();
|
|
313
|
+
this.editor.setText("");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
317
|
+
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
318
|
+
await this.handleCompactCommand(customInstructions);
|
|
319
|
+
this.editor.setText("");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (text === "/autocompact") {
|
|
323
|
+
this.handleAutocompactCommand();
|
|
324
|
+
this.editor.setText("");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (text === "/debug") {
|
|
328
|
+
this.handleDebugCommand();
|
|
329
|
+
this.editor.setText("");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (text === "/resume") {
|
|
333
|
+
this.showSessionSelector();
|
|
334
|
+
this.editor.setText("");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Handle bash command
|
|
338
|
+
if (text.startsWith("!")) {
|
|
339
|
+
const command = text.slice(1).trim();
|
|
340
|
+
if (command) {
|
|
341
|
+
if (this.session.isBashRunning) {
|
|
342
|
+
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
343
|
+
this.editor.setText(text);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.editor.addToHistory(text);
|
|
347
|
+
await this.handleBashCommand(command);
|
|
348
|
+
this.isBashMode = false;
|
|
349
|
+
this.updateEditorBorderColor();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Queue message if agent is streaming
|
|
354
|
+
if (this.session.isStreaming) {
|
|
355
|
+
await this.session.queueMessage(text);
|
|
356
|
+
this.updatePendingMessagesDisplay();
|
|
357
|
+
this.editor.addToHistory(text);
|
|
358
|
+
this.editor.setText("");
|
|
359
|
+
this.ui.requestRender();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Normal message submission
|
|
363
|
+
// First, move any pending bash components to chat
|
|
364
|
+
this.flushPendingBashComponents();
|
|
365
|
+
if (this.onInputCallback) {
|
|
366
|
+
this.onInputCallback(text);
|
|
367
|
+
}
|
|
368
|
+
this.editor.addToHistory(text);
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
subscribeToAgent() {
|
|
372
|
+
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
373
|
+
await this.handleEvent(event, this.session.state);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
async handleEvent(event, state) {
|
|
377
|
+
if (!this.isInitialized) {
|
|
378
|
+
await this.init();
|
|
379
|
+
}
|
|
380
|
+
this.footer.updateState(state);
|
|
381
|
+
switch (event.type) {
|
|
382
|
+
case "agent_start":
|
|
383
|
+
if (this.loadingAnimation) {
|
|
384
|
+
this.loadingAnimation.stop();
|
|
385
|
+
}
|
|
386
|
+
this.statusContainer.clear();
|
|
387
|
+
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Working... (esc to interrupt)");
|
|
388
|
+
this.statusContainer.addChild(this.loadingAnimation);
|
|
389
|
+
this.ui.requestRender();
|
|
390
|
+
break;
|
|
391
|
+
case "message_start":
|
|
392
|
+
if (event.message.role === "user") {
|
|
393
|
+
this.addMessageToChat(event.message);
|
|
394
|
+
this.editor.setText("");
|
|
395
|
+
this.updatePendingMessagesDisplay();
|
|
396
|
+
this.ui.requestRender();
|
|
397
|
+
}
|
|
398
|
+
else if (event.message.role === "assistant") {
|
|
399
|
+
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
|
400
|
+
this.chatContainer.addChild(this.streamingComponent);
|
|
401
|
+
this.streamingComponent.updateContent(event.message);
|
|
402
|
+
this.ui.requestRender();
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
case "message_update":
|
|
406
|
+
if (this.streamingComponent && event.message.role === "assistant") {
|
|
407
|
+
const assistantMsg = event.message;
|
|
408
|
+
this.streamingComponent.updateContent(assistantMsg);
|
|
409
|
+
for (const content of assistantMsg.content) {
|
|
410
|
+
if (content.type === "toolCall") {
|
|
411
|
+
if (!this.pendingTools.has(content.id)) {
|
|
412
|
+
this.chatContainer.addChild(new Text("", 0, 0));
|
|
413
|
+
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
414
|
+
this.chatContainer.addChild(component);
|
|
415
|
+
this.pendingTools.set(content.id, component);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const component = this.pendingTools.get(content.id);
|
|
419
|
+
if (component) {
|
|
420
|
+
component.updateArgs(content.arguments);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
this.ui.requestRender();
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case "message_end":
|
|
429
|
+
if (event.message.role === "user")
|
|
430
|
+
break;
|
|
431
|
+
if (this.streamingComponent && event.message.role === "assistant") {
|
|
432
|
+
const assistantMsg = event.message;
|
|
433
|
+
this.streamingComponent.updateContent(assistantMsg);
|
|
434
|
+
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
|
435
|
+
const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
|
|
436
|
+
for (const [, component] of this.pendingTools.entries()) {
|
|
437
|
+
component.updateResult({
|
|
438
|
+
content: [{ type: "text", text: errorMessage }],
|
|
439
|
+
isError: true,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
this.pendingTools.clear();
|
|
443
|
+
}
|
|
444
|
+
this.streamingComponent = null;
|
|
445
|
+
this.footer.invalidate();
|
|
446
|
+
}
|
|
447
|
+
this.ui.requestRender();
|
|
448
|
+
break;
|
|
449
|
+
case "tool_execution_start": {
|
|
450
|
+
if (!this.pendingTools.has(event.toolCallId)) {
|
|
451
|
+
const component = new ToolExecutionComponent(event.toolName, event.args);
|
|
452
|
+
this.chatContainer.addChild(component);
|
|
453
|
+
this.pendingTools.set(event.toolCallId, component);
|
|
454
|
+
this.ui.requestRender();
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case "tool_execution_end": {
|
|
459
|
+
const component = this.pendingTools.get(event.toolCallId);
|
|
460
|
+
if (component) {
|
|
461
|
+
const resultData = typeof event.result === "string"
|
|
462
|
+
? {
|
|
463
|
+
content: [{ type: "text", text: event.result }],
|
|
464
|
+
details: undefined,
|
|
465
|
+
isError: event.isError,
|
|
466
|
+
}
|
|
467
|
+
: { content: event.result.content, details: event.result.details, isError: event.isError };
|
|
468
|
+
component.updateResult(resultData);
|
|
469
|
+
this.pendingTools.delete(event.toolCallId);
|
|
470
|
+
this.ui.requestRender();
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "agent_end":
|
|
475
|
+
if (this.loadingAnimation) {
|
|
476
|
+
this.loadingAnimation.stop();
|
|
477
|
+
this.loadingAnimation = null;
|
|
478
|
+
this.statusContainer.clear();
|
|
479
|
+
}
|
|
480
|
+
if (this.streamingComponent) {
|
|
481
|
+
this.chatContainer.removeChild(this.streamingComponent);
|
|
482
|
+
this.streamingComponent = null;
|
|
483
|
+
}
|
|
484
|
+
this.pendingTools.clear();
|
|
485
|
+
this.ui.requestRender();
|
|
486
|
+
break;
|
|
487
|
+
case "auto_compaction_start":
|
|
488
|
+
// Set up escape to abort auto-compaction
|
|
489
|
+
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
|
490
|
+
this.editor.onEscape = () => {
|
|
491
|
+
this.session.abortCompaction();
|
|
492
|
+
};
|
|
493
|
+
// Show compacting indicator
|
|
494
|
+
this.statusContainer.clear();
|
|
495
|
+
this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Auto-compacting... (esc to cancel)");
|
|
496
|
+
this.statusContainer.addChild(this.autoCompactionLoader);
|
|
497
|
+
this.ui.requestRender();
|
|
498
|
+
break;
|
|
499
|
+
case "auto_compaction_end":
|
|
500
|
+
// Restore escape handler
|
|
501
|
+
if (this.autoCompactionEscapeHandler) {
|
|
502
|
+
this.editor.onEscape = this.autoCompactionEscapeHandler;
|
|
503
|
+
this.autoCompactionEscapeHandler = undefined;
|
|
504
|
+
}
|
|
505
|
+
// Stop loader
|
|
506
|
+
if (this.autoCompactionLoader) {
|
|
507
|
+
this.autoCompactionLoader.stop();
|
|
508
|
+
this.autoCompactionLoader = null;
|
|
509
|
+
this.statusContainer.clear();
|
|
510
|
+
}
|
|
511
|
+
// Handle result
|
|
512
|
+
if (event.aborted) {
|
|
513
|
+
this.showStatus("Auto-compaction cancelled");
|
|
514
|
+
}
|
|
515
|
+
else if (event.result) {
|
|
516
|
+
// Rebuild chat to show compacted state
|
|
517
|
+
this.chatContainer.clear();
|
|
518
|
+
this.rebuildChatFromMessages();
|
|
519
|
+
// Add compaction component (same as manual /compact)
|
|
520
|
+
const compactionComponent = new CompactionComponent(event.result.tokensBefore, event.result.summary);
|
|
521
|
+
compactionComponent.setExpanded(this.toolOutputExpanded);
|
|
522
|
+
this.chatContainer.addChild(compactionComponent);
|
|
523
|
+
this.footer.updateState(this.session.state);
|
|
524
|
+
}
|
|
525
|
+
this.ui.requestRender();
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/** Extract text content from a user message */
|
|
530
|
+
getUserMessageText(message) {
|
|
531
|
+
if (message.role !== "user")
|
|
532
|
+
return "";
|
|
533
|
+
const textBlocks = typeof message.content === "string"
|
|
534
|
+
? [{ type: "text", text: message.content }]
|
|
535
|
+
: message.content.filter((c) => c.type === "text");
|
|
536
|
+
return textBlocks.map((c) => c.text).join("");
|
|
537
|
+
}
|
|
538
|
+
/** Show a status message in the chat */
|
|
539
|
+
showStatus(message) {
|
|
540
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
541
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
542
|
+
this.ui.requestRender();
|
|
543
|
+
}
|
|
544
|
+
addMessageToChat(message) {
|
|
545
|
+
if (isBashExecutionMessage(message)) {
|
|
546
|
+
const component = new BashExecutionComponent(message.command, this.ui);
|
|
547
|
+
if (message.output) {
|
|
548
|
+
component.appendOutput(message.output);
|
|
549
|
+
}
|
|
550
|
+
component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
|
|
551
|
+
this.chatContainer.addChild(component);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (message.role === "user") {
|
|
555
|
+
const textContent = this.getUserMessageText(message);
|
|
556
|
+
if (textContent) {
|
|
557
|
+
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
558
|
+
this.chatContainer.addChild(userComponent);
|
|
559
|
+
this.isFirstUserMessage = false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (message.role === "assistant") {
|
|
563
|
+
const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
|
|
564
|
+
this.chatContainer.addChild(assistantComponent);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Render messages to chat. Used for initial load and rebuild after compaction.
|
|
569
|
+
* @param messages Messages to render
|
|
570
|
+
* @param options.updateFooter Update footer state
|
|
571
|
+
* @param options.populateHistory Add user messages to editor history
|
|
572
|
+
*/
|
|
573
|
+
renderMessages(messages, options = {}) {
|
|
574
|
+
this.isFirstUserMessage = true;
|
|
575
|
+
this.pendingTools.clear();
|
|
576
|
+
if (options.updateFooter) {
|
|
577
|
+
this.footer.updateState(this.session.state);
|
|
578
|
+
this.updateEditorBorderColor();
|
|
579
|
+
}
|
|
580
|
+
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
581
|
+
for (const message of messages) {
|
|
582
|
+
if (isBashExecutionMessage(message)) {
|
|
583
|
+
this.addMessageToChat(message);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (message.role === "user") {
|
|
587
|
+
const textContent = this.getUserMessageText(message);
|
|
588
|
+
if (textContent) {
|
|
589
|
+
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
590
|
+
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
591
|
+
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
592
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
593
|
+
this.chatContainer.addChild(component);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
597
|
+
this.chatContainer.addChild(userComponent);
|
|
598
|
+
this.isFirstUserMessage = false;
|
|
599
|
+
if (options.populateHistory) {
|
|
600
|
+
this.editor.addToHistory(textContent);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else if (message.role === "assistant") {
|
|
606
|
+
const assistantMsg = message;
|
|
607
|
+
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
|
608
|
+
this.chatContainer.addChild(assistantComponent);
|
|
609
|
+
for (const content of assistantMsg.content) {
|
|
610
|
+
if (content.type === "toolCall") {
|
|
611
|
+
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
612
|
+
this.chatContainer.addChild(component);
|
|
613
|
+
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
|
614
|
+
const errorMessage = assistantMsg.stopReason === "aborted"
|
|
615
|
+
? "Operation aborted"
|
|
616
|
+
: assistantMsg.errorMessage || "Error";
|
|
617
|
+
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
this.pendingTools.set(content.id, component);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else if (message.role === "toolResult") {
|
|
626
|
+
const component = this.pendingTools.get(message.toolCallId);
|
|
627
|
+
if (component) {
|
|
628
|
+
component.updateResult({
|
|
629
|
+
content: message.content,
|
|
630
|
+
details: message.details,
|
|
631
|
+
isError: message.isError,
|
|
632
|
+
});
|
|
633
|
+
this.pendingTools.delete(message.toolCallId);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.pendingTools.clear();
|
|
638
|
+
this.ui.requestRender();
|
|
639
|
+
}
|
|
640
|
+
renderInitialMessages(state) {
|
|
641
|
+
this.renderMessages(state.messages, { updateFooter: true, populateHistory: true });
|
|
642
|
+
}
|
|
643
|
+
async getUserInput() {
|
|
644
|
+
return new Promise((resolve) => {
|
|
645
|
+
this.onInputCallback = (text) => {
|
|
646
|
+
this.onInputCallback = undefined;
|
|
647
|
+
resolve(text);
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
rebuildChatFromMessages() {
|
|
652
|
+
this.renderMessages(this.session.messages);
|
|
653
|
+
}
|
|
654
|
+
// =========================================================================
|
|
655
|
+
// Key handlers
|
|
656
|
+
// =========================================================================
|
|
657
|
+
handleCtrlC() {
|
|
658
|
+
const now = Date.now();
|
|
659
|
+
if (now - this.lastSigintTime < 500) {
|
|
660
|
+
this.stop();
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
this.clearEditor();
|
|
665
|
+
this.lastSigintTime = now;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
updateEditorBorderColor() {
|
|
669
|
+
if (this.isBashMode) {
|
|
670
|
+
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
const level = this.session.thinkingLevel || "off";
|
|
674
|
+
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
675
|
+
}
|
|
676
|
+
this.ui.requestRender();
|
|
677
|
+
}
|
|
678
|
+
cycleThinkingLevel() {
|
|
679
|
+
const newLevel = this.session.cycleThinkingLevel();
|
|
680
|
+
if (newLevel === null) {
|
|
681
|
+
this.showStatus("Current model does not support thinking");
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
this.updateEditorBorderColor();
|
|
685
|
+
this.showStatus(`Thinking level: ${newLevel}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async cycleModel() {
|
|
689
|
+
try {
|
|
690
|
+
const result = await this.session.cycleModel();
|
|
691
|
+
if (result === null) {
|
|
692
|
+
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
693
|
+
this.showStatus(msg);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
this.updateEditorBorderColor();
|
|
697
|
+
const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
698
|
+
this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
toggleToolOutputExpansion() {
|
|
706
|
+
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
707
|
+
for (const child of this.chatContainer.children) {
|
|
708
|
+
if (child instanceof ToolExecutionComponent) {
|
|
709
|
+
child.setExpanded(this.toolOutputExpanded);
|
|
710
|
+
}
|
|
711
|
+
else if (child instanceof CompactionComponent) {
|
|
712
|
+
child.setExpanded(this.toolOutputExpanded);
|
|
713
|
+
}
|
|
714
|
+
else if (child instanceof BashExecutionComponent) {
|
|
715
|
+
child.setExpanded(this.toolOutputExpanded);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
this.ui.requestRender();
|
|
719
|
+
}
|
|
720
|
+
toggleThinkingBlockVisibility() {
|
|
721
|
+
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
722
|
+
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
723
|
+
for (const child of this.chatContainer.children) {
|
|
724
|
+
if (child instanceof AssistantMessageComponent) {
|
|
725
|
+
child.setHideThinkingBlock(this.hideThinkingBlock);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
this.chatContainer.clear();
|
|
729
|
+
this.rebuildChatFromMessages();
|
|
730
|
+
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
731
|
+
}
|
|
732
|
+
// =========================================================================
|
|
733
|
+
// UI helpers
|
|
734
|
+
// =========================================================================
|
|
735
|
+
clearEditor() {
|
|
736
|
+
this.editor.setText("");
|
|
737
|
+
this.ui.requestRender();
|
|
738
|
+
}
|
|
739
|
+
showError(errorMessage) {
|
|
740
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
741
|
+
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
742
|
+
this.ui.requestRender();
|
|
743
|
+
}
|
|
744
|
+
showWarning(warningMessage) {
|
|
745
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
746
|
+
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
747
|
+
this.ui.requestRender();
|
|
748
|
+
}
|
|
749
|
+
showNewVersionNotification(newVersion) {
|
|
750
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
751
|
+
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
752
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available")) +
|
|
753
|
+
"\n" +
|
|
754
|
+
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
755
|
+
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
|
|
756
|
+
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
757
|
+
this.ui.requestRender();
|
|
758
|
+
}
|
|
759
|
+
updatePendingMessagesDisplay() {
|
|
760
|
+
this.pendingMessagesContainer.clear();
|
|
761
|
+
const queuedMessages = this.session.getQueuedMessages();
|
|
762
|
+
if (queuedMessages.length > 0) {
|
|
763
|
+
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
764
|
+
for (const message of queuedMessages) {
|
|
765
|
+
const queuedText = theme.fg("dim", "Queued: " + message);
|
|
766
|
+
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/** Move pending bash components from pending area to chat */
|
|
771
|
+
flushPendingBashComponents() {
|
|
772
|
+
for (const component of this.pendingBashComponents) {
|
|
773
|
+
this.pendingMessagesContainer.removeChild(component);
|
|
774
|
+
this.chatContainer.addChild(component);
|
|
775
|
+
}
|
|
776
|
+
this.pendingBashComponents = [];
|
|
777
|
+
}
|
|
778
|
+
// =========================================================================
|
|
779
|
+
// Selectors
|
|
780
|
+
// =========================================================================
|
|
781
|
+
/**
|
|
782
|
+
* Shows a selector component in place of the editor.
|
|
783
|
+
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
784
|
+
*/
|
|
785
|
+
showSelector(create) {
|
|
786
|
+
const done = () => {
|
|
787
|
+
this.editorContainer.clear();
|
|
788
|
+
this.editorContainer.addChild(this.editor);
|
|
789
|
+
this.ui.setFocus(this.editor);
|
|
790
|
+
};
|
|
791
|
+
const { component, focus } = create(done);
|
|
792
|
+
this.editorContainer.clear();
|
|
793
|
+
this.editorContainer.addChild(component);
|
|
794
|
+
this.ui.setFocus(focus);
|
|
795
|
+
this.ui.requestRender();
|
|
796
|
+
}
|
|
797
|
+
showThinkingSelector() {
|
|
798
|
+
this.showSelector((done) => {
|
|
799
|
+
const selector = new ThinkingSelectorComponent(this.session.thinkingLevel, (level) => {
|
|
800
|
+
this.session.setThinkingLevel(level);
|
|
801
|
+
this.updateEditorBorderColor();
|
|
802
|
+
done();
|
|
803
|
+
this.showStatus(`Thinking level: ${level}`);
|
|
804
|
+
}, () => {
|
|
805
|
+
done();
|
|
806
|
+
this.ui.requestRender();
|
|
807
|
+
});
|
|
808
|
+
return { component: selector, focus: selector.getSelectList() };
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
showQueueModeSelector() {
|
|
812
|
+
this.showSelector((done) => {
|
|
813
|
+
const selector = new QueueModeSelectorComponent(this.session.queueMode, (mode) => {
|
|
814
|
+
this.session.setQueueMode(mode);
|
|
815
|
+
done();
|
|
816
|
+
this.showStatus(`Queue mode: ${mode}`);
|
|
817
|
+
}, () => {
|
|
818
|
+
done();
|
|
819
|
+
this.ui.requestRender();
|
|
820
|
+
});
|
|
821
|
+
return { component: selector, focus: selector.getSelectList() };
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
showThemeSelector() {
|
|
825
|
+
const currentTheme = this.settingsManager.getTheme() || "dark";
|
|
826
|
+
this.showSelector((done) => {
|
|
827
|
+
const selector = new ThemeSelectorComponent(currentTheme, (themeName) => {
|
|
828
|
+
const result = setTheme(themeName);
|
|
829
|
+
this.settingsManager.setTheme(themeName);
|
|
830
|
+
this.ui.invalidate();
|
|
831
|
+
done();
|
|
832
|
+
if (result.success) {
|
|
833
|
+
this.showStatus(`Theme: ${themeName}`);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
|
|
837
|
+
}
|
|
838
|
+
}, () => {
|
|
839
|
+
done();
|
|
840
|
+
this.ui.requestRender();
|
|
841
|
+
}, (themeName) => {
|
|
842
|
+
const result = setTheme(themeName);
|
|
843
|
+
if (result.success) {
|
|
844
|
+
this.ui.invalidate();
|
|
845
|
+
this.ui.requestRender();
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
return { component: selector, focus: selector.getSelectList() };
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
showModelSelector() {
|
|
852
|
+
this.showSelector((done) => {
|
|
853
|
+
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, (model) => {
|
|
854
|
+
this.agent.setModel(model);
|
|
855
|
+
this.sessionManager.saveModelChange(model.provider, model.id);
|
|
856
|
+
done();
|
|
857
|
+
this.showStatus(`Model: ${model.id}`);
|
|
858
|
+
}, () => {
|
|
859
|
+
done();
|
|
860
|
+
this.ui.requestRender();
|
|
861
|
+
});
|
|
862
|
+
return { component: selector, focus: selector };
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
showUserMessageSelector() {
|
|
866
|
+
const userMessages = this.session.getUserMessagesForBranching();
|
|
867
|
+
if (userMessages.length <= 1) {
|
|
868
|
+
this.showStatus("No messages to branch from");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
this.showSelector((done) => {
|
|
872
|
+
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ index: m.entryIndex, text: m.text })), (entryIndex) => {
|
|
873
|
+
const selectedText = this.session.branch(entryIndex);
|
|
874
|
+
this.chatContainer.clear();
|
|
875
|
+
this.isFirstUserMessage = true;
|
|
876
|
+
this.renderInitialMessages(this.session.state);
|
|
877
|
+
this.editor.setText(selectedText);
|
|
878
|
+
done();
|
|
879
|
+
this.showStatus("Branched to new session");
|
|
880
|
+
}, () => {
|
|
881
|
+
done();
|
|
882
|
+
this.ui.requestRender();
|
|
883
|
+
});
|
|
884
|
+
return { component: selector, focus: selector.getMessageList() };
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
showSessionSelector() {
|
|
888
|
+
this.showSelector((done) => {
|
|
889
|
+
const selector = new SessionSelectorComponent(this.sessionManager, async (sessionPath) => {
|
|
890
|
+
done();
|
|
891
|
+
await this.handleResumeSession(sessionPath);
|
|
892
|
+
}, () => {
|
|
893
|
+
done();
|
|
894
|
+
this.ui.requestRender();
|
|
895
|
+
});
|
|
896
|
+
return { component: selector, focus: selector.getSessionList() };
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
async handleResumeSession(sessionPath) {
|
|
900
|
+
// Stop loading animation
|
|
901
|
+
if (this.loadingAnimation) {
|
|
902
|
+
this.loadingAnimation.stop();
|
|
903
|
+
this.loadingAnimation = null;
|
|
904
|
+
}
|
|
905
|
+
this.statusContainer.clear();
|
|
906
|
+
// Clear UI state
|
|
907
|
+
this.pendingMessagesContainer.clear();
|
|
908
|
+
this.streamingComponent = null;
|
|
909
|
+
this.pendingTools.clear();
|
|
910
|
+
// Switch session via AgentSession
|
|
911
|
+
await this.session.switchSession(sessionPath);
|
|
912
|
+
// Clear and re-render the chat
|
|
913
|
+
this.chatContainer.clear();
|
|
914
|
+
this.isFirstUserMessage = true;
|
|
915
|
+
this.renderInitialMessages(this.session.state);
|
|
916
|
+
this.showStatus("Resumed session");
|
|
917
|
+
}
|
|
918
|
+
async showOAuthSelector(mode) {
|
|
919
|
+
if (mode === "logout") {
|
|
920
|
+
const loggedInProviders = listOAuthProviders();
|
|
921
|
+
if (loggedInProviders.length === 0) {
|
|
922
|
+
this.showStatus("No OAuth providers logged in. Use /login first.");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
this.showSelector((done) => {
|
|
927
|
+
const selector = new OAuthSelectorComponent(mode, async (providerId) => {
|
|
928
|
+
done();
|
|
929
|
+
if (mode === "login") {
|
|
930
|
+
this.showStatus(`Logging in to ${providerId}...`);
|
|
931
|
+
try {
|
|
932
|
+
await login(providerId, (url) => {
|
|
933
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
934
|
+
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
|
|
935
|
+
this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
|
936
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
937
|
+
this.chatContainer.addChild(new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0));
|
|
938
|
+
this.ui.requestRender();
|
|
939
|
+
const openCmd = process.platform === "darwin"
|
|
940
|
+
? "open"
|
|
941
|
+
: process.platform === "win32"
|
|
942
|
+
? "start"
|
|
943
|
+
: "xdg-open";
|
|
944
|
+
exec(`${openCmd} "${url}"`);
|
|
945
|
+
}, async () => {
|
|
946
|
+
return new Promise((resolve) => {
|
|
947
|
+
const codeInput = new Input();
|
|
948
|
+
codeInput.onSubmit = () => {
|
|
949
|
+
const code = codeInput.getValue();
|
|
950
|
+
this.editorContainer.clear();
|
|
951
|
+
this.editorContainer.addChild(this.editor);
|
|
952
|
+
this.ui.setFocus(this.editor);
|
|
953
|
+
resolve(code);
|
|
954
|
+
};
|
|
955
|
+
this.editorContainer.clear();
|
|
956
|
+
this.editorContainer.addChild(codeInput);
|
|
957
|
+
this.ui.setFocus(codeInput);
|
|
958
|
+
this.ui.requestRender();
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
invalidateOAuthCache();
|
|
962
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
963
|
+
this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
|
|
964
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
|
|
965
|
+
this.ui.requestRender();
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
this.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
try {
|
|
973
|
+
await logout(providerId);
|
|
974
|
+
invalidateOAuthCache();
|
|
975
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
976
|
+
this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
|
|
977
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0));
|
|
978
|
+
this.ui.requestRender();
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}, () => {
|
|
985
|
+
done();
|
|
986
|
+
this.ui.requestRender();
|
|
987
|
+
});
|
|
988
|
+
return { component: selector, focus: selector };
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
// =========================================================================
|
|
992
|
+
// Command handlers
|
|
993
|
+
// =========================================================================
|
|
994
|
+
handleExportCommand(text) {
|
|
995
|
+
const parts = text.split(/\s+/);
|
|
996
|
+
const outputPath = parts.length > 1 ? parts[1] : undefined;
|
|
997
|
+
try {
|
|
998
|
+
const filePath = this.session.exportToHtml(outputPath);
|
|
999
|
+
this.showStatus(`Session exported to: ${filePath}`);
|
|
1000
|
+
}
|
|
1001
|
+
catch (error) {
|
|
1002
|
+
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
handleCopyCommand() {
|
|
1006
|
+
const text = this.session.getLastAssistantText();
|
|
1007
|
+
if (!text) {
|
|
1008
|
+
this.showError("No agent messages to copy yet.");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
copyToClipboard(text);
|
|
1013
|
+
this.showStatus("Copied last agent message to clipboard");
|
|
1014
|
+
}
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
handleSessionCommand() {
|
|
1020
|
+
const stats = this.session.getSessionStats();
|
|
1021
|
+
let info = `${theme.bold("Session Info")}\n\n`;
|
|
1022
|
+
info += `${theme.fg("dim", "File:")} ${stats.sessionFile}\n`;
|
|
1023
|
+
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
1024
|
+
info += `${theme.bold("Messages")}\n`;
|
|
1025
|
+
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
1026
|
+
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
|
|
1027
|
+
info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
|
|
1028
|
+
info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
|
|
1029
|
+
info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
|
|
1030
|
+
info += `${theme.bold("Tokens")}\n`;
|
|
1031
|
+
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
|
|
1032
|
+
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
1033
|
+
if (stats.tokens.cacheRead > 0) {
|
|
1034
|
+
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
1035
|
+
}
|
|
1036
|
+
if (stats.tokens.cacheWrite > 0) {
|
|
1037
|
+
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
1038
|
+
}
|
|
1039
|
+
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
1040
|
+
if (stats.cost > 0) {
|
|
1041
|
+
info += `\n${theme.bold("Cost")}\n`;
|
|
1042
|
+
info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
|
|
1043
|
+
}
|
|
1044
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1045
|
+
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
1046
|
+
this.ui.requestRender();
|
|
1047
|
+
}
|
|
1048
|
+
handleChangelogCommand() {
|
|
1049
|
+
const changelogPath = getChangelogPath();
|
|
1050
|
+
const allEntries = parseChangelog(changelogPath);
|
|
1051
|
+
const changelogMarkdown = allEntries.length > 0
|
|
1052
|
+
? allEntries
|
|
1053
|
+
.reverse()
|
|
1054
|
+
.map((e) => e.content)
|
|
1055
|
+
.join("\n\n")
|
|
1056
|
+
: "No changelog entries found.";
|
|
1057
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1058
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
1059
|
+
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
1060
|
+
this.ui.addChild(new Spacer(1));
|
|
1061
|
+
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
1062
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
1063
|
+
this.ui.requestRender();
|
|
1064
|
+
}
|
|
1065
|
+
async handleClearCommand() {
|
|
1066
|
+
// Stop loading animation
|
|
1067
|
+
if (this.loadingAnimation) {
|
|
1068
|
+
this.loadingAnimation.stop();
|
|
1069
|
+
this.loadingAnimation = null;
|
|
1070
|
+
}
|
|
1071
|
+
this.statusContainer.clear();
|
|
1072
|
+
// Reset via session
|
|
1073
|
+
await this.session.reset();
|
|
1074
|
+
// Clear UI state
|
|
1075
|
+
this.chatContainer.clear();
|
|
1076
|
+
this.pendingMessagesContainer.clear();
|
|
1077
|
+
this.streamingComponent = null;
|
|
1078
|
+
this.pendingTools.clear();
|
|
1079
|
+
this.isFirstUserMessage = true;
|
|
1080
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1081
|
+
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1));
|
|
1082
|
+
this.ui.requestRender();
|
|
1083
|
+
}
|
|
1084
|
+
handleDebugCommand() {
|
|
1085
|
+
const width = this.ui.terminal.columns;
|
|
1086
|
+
const allLines = this.ui.render(width);
|
|
1087
|
+
const debugLogPath = getDebugLogPath();
|
|
1088
|
+
const debugData = [
|
|
1089
|
+
`Debug output at ${new Date().toISOString()}`,
|
|
1090
|
+
`Terminal width: ${width}`,
|
|
1091
|
+
`Total lines: ${allLines.length}`,
|
|
1092
|
+
"",
|
|
1093
|
+
"=== All rendered lines with visible widths ===",
|
|
1094
|
+
...allLines.map((line, idx) => {
|
|
1095
|
+
const vw = visibleWidth(line);
|
|
1096
|
+
const escaped = JSON.stringify(line);
|
|
1097
|
+
return `[${idx}] (w=${vw}) ${escaped}`;
|
|
1098
|
+
}),
|
|
1099
|
+
"",
|
|
1100
|
+
"=== Agent messages (JSONL) ===",
|
|
1101
|
+
...this.session.messages.map((msg) => JSON.stringify(msg)),
|
|
1102
|
+
"",
|
|
1103
|
+
].join("\n");
|
|
1104
|
+
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
1105
|
+
fs.writeFileSync(debugLogPath, debugData);
|
|
1106
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1107
|
+
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
|
|
1108
|
+
this.ui.requestRender();
|
|
1109
|
+
}
|
|
1110
|
+
async handleBashCommand(command) {
|
|
1111
|
+
const isDeferred = this.session.isStreaming;
|
|
1112
|
+
this.bashComponent = new BashExecutionComponent(command, this.ui);
|
|
1113
|
+
if (isDeferred) {
|
|
1114
|
+
// Show in pending area when agent is streaming
|
|
1115
|
+
this.pendingMessagesContainer.addChild(this.bashComponent);
|
|
1116
|
+
this.pendingBashComponents.push(this.bashComponent);
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
// Show in chat immediately when agent is idle
|
|
1120
|
+
this.chatContainer.addChild(this.bashComponent);
|
|
1121
|
+
}
|
|
1122
|
+
this.ui.requestRender();
|
|
1123
|
+
try {
|
|
1124
|
+
const result = await this.session.executeBash(command, (chunk) => {
|
|
1125
|
+
if (this.bashComponent) {
|
|
1126
|
+
this.bashComponent.appendOutput(chunk);
|
|
1127
|
+
this.ui.requestRender();
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
if (this.bashComponent) {
|
|
1131
|
+
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
if (this.bashComponent) {
|
|
1136
|
+
this.bashComponent.setComplete(null, false);
|
|
1137
|
+
}
|
|
1138
|
+
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1139
|
+
}
|
|
1140
|
+
this.bashComponent = null;
|
|
1141
|
+
this.ui.requestRender();
|
|
1142
|
+
}
|
|
1143
|
+
async handleCompactCommand(customInstructions) {
|
|
1144
|
+
const entries = this.sessionManager.loadEntries();
|
|
1145
|
+
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
1146
|
+
if (messageCount < 2) {
|
|
1147
|
+
this.showWarning("Nothing to compact (no messages yet)");
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
await this.executeCompaction(customInstructions, false);
|
|
1151
|
+
}
|
|
1152
|
+
handleAutocompactCommand() {
|
|
1153
|
+
const newState = !this.session.autoCompactionEnabled;
|
|
1154
|
+
this.session.setAutoCompactionEnabled(newState);
|
|
1155
|
+
this.footer.setAutoCompactEnabled(newState);
|
|
1156
|
+
this.showStatus(`Auto-compaction: ${newState ? "on" : "off"}`);
|
|
1157
|
+
}
|
|
1158
|
+
async executeCompaction(customInstructions, isAuto = false) {
|
|
1159
|
+
// Stop loading animation
|
|
1160
|
+
if (this.loadingAnimation) {
|
|
1161
|
+
this.loadingAnimation.stop();
|
|
1162
|
+
this.loadingAnimation = null;
|
|
1163
|
+
}
|
|
1164
|
+
this.statusContainer.clear();
|
|
1165
|
+
// Set up escape handler during compaction
|
|
1166
|
+
const originalOnEscape = this.editor.onEscape;
|
|
1167
|
+
this.editor.onEscape = () => {
|
|
1168
|
+
this.session.abortCompaction();
|
|
1169
|
+
};
|
|
1170
|
+
// Show compacting status
|
|
1171
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1172
|
+
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
1173
|
+
const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
|
|
1174
|
+
this.statusContainer.addChild(compactingLoader);
|
|
1175
|
+
this.ui.requestRender();
|
|
1176
|
+
try {
|
|
1177
|
+
const result = await this.session.compact(customInstructions);
|
|
1178
|
+
// Rebuild UI
|
|
1179
|
+
this.chatContainer.clear();
|
|
1180
|
+
this.rebuildChatFromMessages();
|
|
1181
|
+
// Add compaction component
|
|
1182
|
+
const compactionComponent = new CompactionComponent(result.tokensBefore, result.summary);
|
|
1183
|
+
compactionComponent.setExpanded(this.toolOutputExpanded);
|
|
1184
|
+
this.chatContainer.addChild(compactionComponent);
|
|
1185
|
+
this.footer.updateState(this.session.state);
|
|
1186
|
+
}
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1189
|
+
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
1190
|
+
this.showError("Compaction cancelled");
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
this.showError(`Compaction failed: ${message}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
finally {
|
|
1197
|
+
compactingLoader.stop();
|
|
1198
|
+
this.statusContainer.clear();
|
|
1199
|
+
this.editor.onEscape = originalOnEscape;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
stop() {
|
|
1203
|
+
if (this.loadingAnimation) {
|
|
1204
|
+
this.loadingAnimation.stop();
|
|
1205
|
+
this.loadingAnimation = null;
|
|
1206
|
+
}
|
|
1207
|
+
this.footer.dispose();
|
|
1208
|
+
if (this.unsubscribe) {
|
|
1209
|
+
this.unsubscribe();
|
|
1210
|
+
}
|
|
1211
|
+
if (this.isInitialized) {
|
|
1212
|
+
this.ui.stop();
|
|
1213
|
+
this.isInitialized = false;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
//# sourceMappingURL=interactive-mode.js.map
|