@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
package/dist/tui/tui-renderer.js
DELETED
|
@@ -1,1937 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
8
|
-
import { exec, spawn } from "child_process";
|
|
9
|
-
import stripAnsi from "strip-ansi";
|
|
10
|
-
import { getChangelogPath, parseChangelog } from "../changelog.js";
|
|
11
|
-
import { copyToClipboard } from "../clipboard.js";
|
|
12
|
-
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
|
|
13
|
-
import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
|
|
14
|
-
import { exportSessionToHtml } from "../export-html.js";
|
|
15
|
-
import { isBashExecutionMessage } from "../messages.js";
|
|
16
|
-
import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
|
|
17
|
-
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
|
18
|
-
import { getLatestCompactionEntry, loadSessionFromEntries, SUMMARY_PREFIX, SUMMARY_SUFFIX, } from "../session-manager.js";
|
|
19
|
-
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../shell.js";
|
|
20
|
-
import { expandSlashCommand, loadSlashCommands } from "../slash-commands.js";
|
|
21
|
-
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
|
22
|
-
import { DEFAULT_MAX_BYTES, truncateTail } from "../tools/truncate.js";
|
|
23
|
-
import { AssistantMessageComponent } from "./assistant-message.js";
|
|
24
|
-
import { BashExecutionComponent } from "./bash-execution.js";
|
|
25
|
-
import { CompactionComponent } from "./compaction.js";
|
|
26
|
-
import { CustomEditor } from "./custom-editor.js";
|
|
27
|
-
import { DynamicBorder } from "./dynamic-border.js";
|
|
28
|
-
import { FooterComponent } from "./footer.js";
|
|
29
|
-
import { ModelSelectorComponent } from "./model-selector.js";
|
|
30
|
-
import { OAuthSelectorComponent } from "./oauth-selector.js";
|
|
31
|
-
import { QueueModeSelectorComponent } from "./queue-mode-selector.js";
|
|
32
|
-
import { SessionSelectorComponent } from "./session-selector.js";
|
|
33
|
-
import { ThemeSelectorComponent } from "./theme-selector.js";
|
|
34
|
-
import { ThinkingSelectorComponent } from "./thinking-selector.js";
|
|
35
|
-
import { ToolExecutionComponent } from "./tool-execution.js";
|
|
36
|
-
import { UserMessageComponent } from "./user-message.js";
|
|
37
|
-
import { UserMessageSelectorComponent } from "./user-message-selector.js";
|
|
38
|
-
/**
|
|
39
|
-
* TUI renderer for the coding agent
|
|
40
|
-
*/
|
|
41
|
-
export class TuiRenderer {
|
|
42
|
-
ui;
|
|
43
|
-
chatContainer;
|
|
44
|
-
pendingMessagesContainer;
|
|
45
|
-
statusContainer;
|
|
46
|
-
editor;
|
|
47
|
-
editorContainer; // Container to swap between editor and selector
|
|
48
|
-
footer;
|
|
49
|
-
agent;
|
|
50
|
-
sessionManager;
|
|
51
|
-
settingsManager;
|
|
52
|
-
version;
|
|
53
|
-
isInitialized = false;
|
|
54
|
-
onInputCallback;
|
|
55
|
-
loadingAnimation = null;
|
|
56
|
-
lastSigintTime = 0;
|
|
57
|
-
lastEscapeTime = 0;
|
|
58
|
-
changelogMarkdown = null;
|
|
59
|
-
collapseChangelog = false;
|
|
60
|
-
// Message queueing
|
|
61
|
-
queuedMessages = [];
|
|
62
|
-
// Streaming message tracking
|
|
63
|
-
streamingComponent = null;
|
|
64
|
-
// Tool execution tracking: toolCallId -> component
|
|
65
|
-
pendingTools = new Map();
|
|
66
|
-
// Thinking level selector
|
|
67
|
-
thinkingSelector = null;
|
|
68
|
-
// Queue mode selector
|
|
69
|
-
queueModeSelector = null;
|
|
70
|
-
// Theme selector
|
|
71
|
-
themeSelector = null;
|
|
72
|
-
// Model selector
|
|
73
|
-
modelSelector = null;
|
|
74
|
-
// User message selector (for branching)
|
|
75
|
-
userMessageSelector = null;
|
|
76
|
-
// Session selector (for resume)
|
|
77
|
-
sessionSelector = null;
|
|
78
|
-
// OAuth selector
|
|
79
|
-
oauthSelector = null;
|
|
80
|
-
// Track if this is the first user message (to skip spacer)
|
|
81
|
-
isFirstUserMessage = true;
|
|
82
|
-
// Model scope for quick cycling
|
|
83
|
-
scopedModels = [];
|
|
84
|
-
// Tool output expansion state
|
|
85
|
-
toolOutputExpanded = false;
|
|
86
|
-
// Thinking block visibility state
|
|
87
|
-
hideThinkingBlock = false;
|
|
88
|
-
// Agent subscription unsubscribe function
|
|
89
|
-
unsubscribe;
|
|
90
|
-
// File-based slash commands
|
|
91
|
-
fileCommands = [];
|
|
92
|
-
// Track if editor is in bash mode (text starts with !)
|
|
93
|
-
isBashMode = false;
|
|
94
|
-
// Track running bash command process for cancellation
|
|
95
|
-
bashProcess = null;
|
|
96
|
-
// Track current bash execution component
|
|
97
|
-
bashComponent = null;
|
|
98
|
-
constructor(agent, sessionManager, settingsManager, version, changelogMarkdown = null, collapseChangelog = false, scopedModels = [], fdPath = null) {
|
|
99
|
-
this.agent = agent;
|
|
100
|
-
this.sessionManager = sessionManager;
|
|
101
|
-
this.settingsManager = settingsManager;
|
|
102
|
-
this.version = version;
|
|
103
|
-
this.changelogMarkdown = changelogMarkdown;
|
|
104
|
-
this.collapseChangelog = collapseChangelog;
|
|
105
|
-
this.scopedModels = scopedModels;
|
|
106
|
-
this.ui = new TUI(new ProcessTerminal());
|
|
107
|
-
this.chatContainer = new Container();
|
|
108
|
-
this.pendingMessagesContainer = new Container();
|
|
109
|
-
this.statusContainer = new Container();
|
|
110
|
-
this.editor = new CustomEditor(getEditorTheme());
|
|
111
|
-
this.editorContainer = new Container(); // Container to hold editor or selector
|
|
112
|
-
this.editorContainer.addChild(this.editor); // Start with editor
|
|
113
|
-
this.footer = new FooterComponent(agent.state);
|
|
114
|
-
this.footer.setAutoCompactEnabled(this.settingsManager.getCompactionEnabled());
|
|
115
|
-
// Define slash commands
|
|
116
|
-
const thinkingCommand = {
|
|
117
|
-
name: "thinking",
|
|
118
|
-
description: "Select reasoning level (opens selector UI)",
|
|
119
|
-
};
|
|
120
|
-
const modelCommand = {
|
|
121
|
-
name: "model",
|
|
122
|
-
description: "Select model (opens selector UI)",
|
|
123
|
-
};
|
|
124
|
-
const exportCommand = {
|
|
125
|
-
name: "export",
|
|
126
|
-
description: "Export session to HTML file",
|
|
127
|
-
};
|
|
128
|
-
const copyCommand = {
|
|
129
|
-
name: "copy",
|
|
130
|
-
description: "Copy last agent message to clipboard",
|
|
131
|
-
};
|
|
132
|
-
const sessionCommand = {
|
|
133
|
-
name: "session",
|
|
134
|
-
description: "Show session info and stats",
|
|
135
|
-
};
|
|
136
|
-
const changelogCommand = {
|
|
137
|
-
name: "changelog",
|
|
138
|
-
description: "Show changelog entries",
|
|
139
|
-
};
|
|
140
|
-
const branchCommand = {
|
|
141
|
-
name: "branch",
|
|
142
|
-
description: "Create a new branch from a previous message",
|
|
143
|
-
};
|
|
144
|
-
const loginCommand = {
|
|
145
|
-
name: "login",
|
|
146
|
-
description: "Login with OAuth provider",
|
|
147
|
-
};
|
|
148
|
-
const logoutCommand = {
|
|
149
|
-
name: "logout",
|
|
150
|
-
description: "Logout from OAuth provider",
|
|
151
|
-
};
|
|
152
|
-
const queueCommand = {
|
|
153
|
-
name: "queue",
|
|
154
|
-
description: "Select message queue mode (opens selector UI)",
|
|
155
|
-
};
|
|
156
|
-
const themeCommand = {
|
|
157
|
-
name: "theme",
|
|
158
|
-
description: "Select color theme (opens selector UI)",
|
|
159
|
-
};
|
|
160
|
-
const clearCommand = {
|
|
161
|
-
name: "clear",
|
|
162
|
-
description: "Clear context and start a fresh session",
|
|
163
|
-
};
|
|
164
|
-
const compactCommand = {
|
|
165
|
-
name: "compact",
|
|
166
|
-
description: "Manually compact the session context",
|
|
167
|
-
};
|
|
168
|
-
const autocompactCommand = {
|
|
169
|
-
name: "autocompact",
|
|
170
|
-
description: "Toggle automatic context compaction",
|
|
171
|
-
};
|
|
172
|
-
const resumeCommand = {
|
|
173
|
-
name: "resume",
|
|
174
|
-
description: "Resume a different session",
|
|
175
|
-
};
|
|
176
|
-
// Load hide thinking block setting
|
|
177
|
-
this.hideThinkingBlock = settingsManager.getHideThinkingBlock();
|
|
178
|
-
// Load file-based slash commands
|
|
179
|
-
this.fileCommands = loadSlashCommands();
|
|
180
|
-
// Convert file commands to SlashCommand format
|
|
181
|
-
const fileSlashCommands = this.fileCommands.map((cmd) => ({
|
|
182
|
-
name: cmd.name,
|
|
183
|
-
description: cmd.description,
|
|
184
|
-
}));
|
|
185
|
-
// Setup autocomplete for file paths and slash commands
|
|
186
|
-
const autocompleteProvider = new CombinedAutocompleteProvider([
|
|
187
|
-
thinkingCommand,
|
|
188
|
-
modelCommand,
|
|
189
|
-
themeCommand,
|
|
190
|
-
exportCommand,
|
|
191
|
-
copyCommand,
|
|
192
|
-
sessionCommand,
|
|
193
|
-
changelogCommand,
|
|
194
|
-
branchCommand,
|
|
195
|
-
loginCommand,
|
|
196
|
-
logoutCommand,
|
|
197
|
-
queueCommand,
|
|
198
|
-
clearCommand,
|
|
199
|
-
compactCommand,
|
|
200
|
-
autocompactCommand,
|
|
201
|
-
resumeCommand,
|
|
202
|
-
...fileSlashCommands,
|
|
203
|
-
], process.cwd(), fdPath);
|
|
204
|
-
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
205
|
-
}
|
|
206
|
-
async init() {
|
|
207
|
-
if (this.isInitialized)
|
|
208
|
-
return;
|
|
209
|
-
// Add header with logo and instructions
|
|
210
|
-
const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
211
|
-
const instructions = theme.fg("dim", "esc") +
|
|
212
|
-
theme.fg("muted", " to interrupt") +
|
|
213
|
-
"\n" +
|
|
214
|
-
theme.fg("dim", "ctrl+c") +
|
|
215
|
-
theme.fg("muted", " to clear") +
|
|
216
|
-
"\n" +
|
|
217
|
-
theme.fg("dim", "ctrl+c twice") +
|
|
218
|
-
theme.fg("muted", " to exit") +
|
|
219
|
-
"\n" +
|
|
220
|
-
theme.fg("dim", "ctrl+k") +
|
|
221
|
-
theme.fg("muted", " to delete line") +
|
|
222
|
-
"\n" +
|
|
223
|
-
theme.fg("dim", "shift+tab") +
|
|
224
|
-
theme.fg("muted", " to cycle thinking") +
|
|
225
|
-
"\n" +
|
|
226
|
-
theme.fg("dim", "ctrl+p") +
|
|
227
|
-
theme.fg("muted", " to cycle models") +
|
|
228
|
-
"\n" +
|
|
229
|
-
theme.fg("dim", "ctrl+o") +
|
|
230
|
-
theme.fg("muted", " to expand tools") +
|
|
231
|
-
"\n" +
|
|
232
|
-
theme.fg("dim", "ctrl+t") +
|
|
233
|
-
theme.fg("muted", " to toggle thinking") +
|
|
234
|
-
"\n" +
|
|
235
|
-
theme.fg("dim", "/") +
|
|
236
|
-
theme.fg("muted", " for commands") +
|
|
237
|
-
"\n" +
|
|
238
|
-
theme.fg("dim", "!") +
|
|
239
|
-
theme.fg("muted", " to run bash") +
|
|
240
|
-
"\n" +
|
|
241
|
-
theme.fg("dim", "drop files") +
|
|
242
|
-
theme.fg("muted", " to attach");
|
|
243
|
-
const header = new Text(logo + "\n" + instructions, 1, 0);
|
|
244
|
-
// Setup UI layout
|
|
245
|
-
this.ui.addChild(new Spacer(1));
|
|
246
|
-
this.ui.addChild(header);
|
|
247
|
-
this.ui.addChild(new Spacer(1));
|
|
248
|
-
// Add changelog if provided
|
|
249
|
-
if (this.changelogMarkdown) {
|
|
250
|
-
this.ui.addChild(new DynamicBorder());
|
|
251
|
-
if (this.collapseChangelog) {
|
|
252
|
-
// Show condensed version with hint to use /changelog
|
|
253
|
-
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
254
|
-
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
|
255
|
-
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
|
256
|
-
this.ui.addChild(new Text(condensedText, 1, 0));
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
260
|
-
this.ui.addChild(new Spacer(1));
|
|
261
|
-
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
|
262
|
-
this.ui.addChild(new Spacer(1));
|
|
263
|
-
}
|
|
264
|
-
this.ui.addChild(new DynamicBorder());
|
|
265
|
-
}
|
|
266
|
-
this.ui.addChild(this.chatContainer);
|
|
267
|
-
this.ui.addChild(this.pendingMessagesContainer);
|
|
268
|
-
this.ui.addChild(this.statusContainer);
|
|
269
|
-
this.ui.addChild(new Spacer(1));
|
|
270
|
-
this.ui.addChild(this.editorContainer); // Use container that can hold editor or selector
|
|
271
|
-
this.ui.addChild(this.footer);
|
|
272
|
-
this.ui.setFocus(this.editor);
|
|
273
|
-
// Set up custom key handlers on the editor
|
|
274
|
-
this.editor.onEscape = () => {
|
|
275
|
-
// Intercept Escape key when processing
|
|
276
|
-
if (this.loadingAnimation) {
|
|
277
|
-
// Get all queued messages
|
|
278
|
-
const queuedText = this.queuedMessages.join("\n\n");
|
|
279
|
-
// Get current editor text
|
|
280
|
-
const currentText = this.editor.getText();
|
|
281
|
-
// Combine: queued messages + current editor text
|
|
282
|
-
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
283
|
-
// Put back in editor
|
|
284
|
-
this.editor.setText(combinedText);
|
|
285
|
-
// Clear queued messages
|
|
286
|
-
this.queuedMessages = [];
|
|
287
|
-
this.updatePendingMessagesDisplay();
|
|
288
|
-
// Clear agent's queue too
|
|
289
|
-
this.agent.clearMessageQueue();
|
|
290
|
-
// Abort
|
|
291
|
-
this.agent.abort();
|
|
292
|
-
}
|
|
293
|
-
else if (this.bashProcess) {
|
|
294
|
-
// Kill running bash command
|
|
295
|
-
if (this.bashProcess.pid) {
|
|
296
|
-
killProcessTree(this.bashProcess.pid);
|
|
297
|
-
}
|
|
298
|
-
this.bashProcess = null;
|
|
299
|
-
}
|
|
300
|
-
else if (this.isBashMode) {
|
|
301
|
-
// Cancel bash mode and clear editor
|
|
302
|
-
this.editor.setText("");
|
|
303
|
-
this.isBashMode = false;
|
|
304
|
-
this.updateEditorBorderColor();
|
|
305
|
-
}
|
|
306
|
-
else if (!this.editor.getText().trim()) {
|
|
307
|
-
// Double-escape with empty editor triggers /branch
|
|
308
|
-
const now = Date.now();
|
|
309
|
-
if (now - this.lastEscapeTime < 500) {
|
|
310
|
-
this.showUserMessageSelector();
|
|
311
|
-
this.lastEscapeTime = 0; // Reset to prevent triple-escape
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
this.lastEscapeTime = now;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
this.editor.onCtrlC = () => {
|
|
319
|
-
this.handleCtrlC();
|
|
320
|
-
};
|
|
321
|
-
this.editor.onShiftTab = () => {
|
|
322
|
-
this.cycleThinkingLevel();
|
|
323
|
-
};
|
|
324
|
-
this.editor.onCtrlP = () => {
|
|
325
|
-
this.cycleModel();
|
|
326
|
-
};
|
|
327
|
-
this.editor.onCtrlO = () => {
|
|
328
|
-
this.toggleToolOutputExpansion();
|
|
329
|
-
};
|
|
330
|
-
this.editor.onCtrlT = () => {
|
|
331
|
-
this.toggleThinkingBlockVisibility();
|
|
332
|
-
};
|
|
333
|
-
// Handle editor text changes for bash mode detection
|
|
334
|
-
this.editor.onChange = (text) => {
|
|
335
|
-
const wasBashMode = this.isBashMode;
|
|
336
|
-
this.isBashMode = text.trimStart().startsWith("!");
|
|
337
|
-
if (wasBashMode !== this.isBashMode) {
|
|
338
|
-
this.updateEditorBorderColor();
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
// Handle editor submission
|
|
342
|
-
this.editor.onSubmit = async (text) => {
|
|
343
|
-
text = text.trim();
|
|
344
|
-
if (!text)
|
|
345
|
-
return;
|
|
346
|
-
// Check for /thinking command
|
|
347
|
-
if (text === "/thinking") {
|
|
348
|
-
// Show thinking level selector
|
|
349
|
-
this.showThinkingSelector();
|
|
350
|
-
this.editor.setText("");
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
// Check for /model command
|
|
354
|
-
if (text === "/model") {
|
|
355
|
-
// Show model selector
|
|
356
|
-
this.showModelSelector();
|
|
357
|
-
this.editor.setText("");
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
// Check for /export command
|
|
361
|
-
if (text.startsWith("/export")) {
|
|
362
|
-
this.handleExportCommand(text);
|
|
363
|
-
this.editor.setText("");
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
// Check for /copy command
|
|
367
|
-
if (text === "/copy") {
|
|
368
|
-
this.handleCopyCommand();
|
|
369
|
-
this.editor.setText("");
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
// Check for /session command
|
|
373
|
-
if (text === "/session") {
|
|
374
|
-
this.handleSessionCommand();
|
|
375
|
-
this.editor.setText("");
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
// Check for /changelog command
|
|
379
|
-
if (text === "/changelog") {
|
|
380
|
-
this.handleChangelogCommand();
|
|
381
|
-
this.editor.setText("");
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
// Check for /branch command
|
|
385
|
-
if (text === "/branch") {
|
|
386
|
-
this.showUserMessageSelector();
|
|
387
|
-
this.editor.setText("");
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
// Check for /login command
|
|
391
|
-
if (text === "/login") {
|
|
392
|
-
this.showOAuthSelector("login");
|
|
393
|
-
this.editor.setText("");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
// Check for /logout command
|
|
397
|
-
if (text === "/logout") {
|
|
398
|
-
this.showOAuthSelector("logout");
|
|
399
|
-
this.editor.setText("");
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
// Check for /queue command
|
|
403
|
-
if (text === "/queue") {
|
|
404
|
-
this.showQueueModeSelector();
|
|
405
|
-
this.editor.setText("");
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
// Check for /theme command
|
|
409
|
-
if (text === "/theme") {
|
|
410
|
-
this.showThemeSelector();
|
|
411
|
-
this.editor.setText("");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
// Check for /clear command
|
|
415
|
-
if (text === "/clear") {
|
|
416
|
-
this.handleClearCommand();
|
|
417
|
-
this.editor.setText("");
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
// Check for /compact command
|
|
421
|
-
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
422
|
-
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
423
|
-
this.handleCompactCommand(customInstructions);
|
|
424
|
-
this.editor.setText("");
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
// Check for /autocompact command
|
|
428
|
-
if (text === "/autocompact") {
|
|
429
|
-
this.handleAutocompactCommand();
|
|
430
|
-
this.editor.setText("");
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
// Check for /debug command
|
|
434
|
-
if (text === "/debug") {
|
|
435
|
-
this.handleDebugCommand();
|
|
436
|
-
this.editor.setText("");
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
// Check for /resume command
|
|
440
|
-
if (text === "/resume") {
|
|
441
|
-
this.showSessionSelector();
|
|
442
|
-
this.editor.setText("");
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
// Check for bash command (!<command>)
|
|
446
|
-
if (text.startsWith("!")) {
|
|
447
|
-
const command = text.slice(1).trim();
|
|
448
|
-
if (command) {
|
|
449
|
-
// Block if bash already running
|
|
450
|
-
if (this.bashProcess) {
|
|
451
|
-
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
452
|
-
// Restore text since editor clears on submit
|
|
453
|
-
this.editor.setText(text);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
// Add to history for up/down arrow navigation
|
|
457
|
-
this.editor.addToHistory(text);
|
|
458
|
-
this.handleBashCommand(command);
|
|
459
|
-
// Reset bash mode since editor is now empty
|
|
460
|
-
this.isBashMode = false;
|
|
461
|
-
this.updateEditorBorderColor();
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// Check for file-based slash commands
|
|
466
|
-
text = expandSlashCommand(text, this.fileCommands);
|
|
467
|
-
// Normal message submission - validate model and API key first
|
|
468
|
-
const currentModel = this.agent.state.model;
|
|
469
|
-
if (!currentModel) {
|
|
470
|
-
this.showError("No model selected.\n\n" +
|
|
471
|
-
"Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" +
|
|
472
|
-
`or create ${getModelsPath()}\n\n` +
|
|
473
|
-
"Then use /model to select a model.");
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
// Validate API key (async)
|
|
477
|
-
const apiKey = await getApiKeyForModel(currentModel);
|
|
478
|
-
if (!apiKey) {
|
|
479
|
-
this.showError(`No API key found for ${currentModel.provider}.\n\n` +
|
|
480
|
-
`Set the appropriate environment variable or update ${getModelsPath()}`);
|
|
481
|
-
this.editor.setText(text);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// Check if agent is currently streaming
|
|
485
|
-
if (this.agent.state.isStreaming) {
|
|
486
|
-
// Queue the message instead of submitting
|
|
487
|
-
this.queuedMessages.push(text);
|
|
488
|
-
// Queue in agent
|
|
489
|
-
await this.agent.queueMessage({
|
|
490
|
-
role: "user",
|
|
491
|
-
content: [{ type: "text", text }],
|
|
492
|
-
timestamp: Date.now(),
|
|
493
|
-
});
|
|
494
|
-
// Update pending messages display
|
|
495
|
-
this.updatePendingMessagesDisplay();
|
|
496
|
-
// Add to history for up/down arrow navigation
|
|
497
|
-
this.editor.addToHistory(text);
|
|
498
|
-
// Clear editor
|
|
499
|
-
this.editor.setText("");
|
|
500
|
-
this.ui.requestRender();
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
// All good, proceed with submission
|
|
504
|
-
if (this.onInputCallback) {
|
|
505
|
-
this.onInputCallback(text);
|
|
506
|
-
}
|
|
507
|
-
// Add to history for up/down arrow navigation
|
|
508
|
-
this.editor.addToHistory(text);
|
|
509
|
-
};
|
|
510
|
-
// Start the UI
|
|
511
|
-
this.ui.start();
|
|
512
|
-
this.isInitialized = true;
|
|
513
|
-
// Subscribe to agent events for UI updates and session saving
|
|
514
|
-
this.subscribeToAgent();
|
|
515
|
-
// Set up theme file watcher for live reload
|
|
516
|
-
onThemeChange(() => {
|
|
517
|
-
this.ui.invalidate();
|
|
518
|
-
this.updateEditorBorderColor();
|
|
519
|
-
this.ui.requestRender();
|
|
520
|
-
});
|
|
521
|
-
// Set up git branch watcher
|
|
522
|
-
this.footer.watchBranch(() => {
|
|
523
|
-
this.ui.requestRender();
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
subscribeToAgent() {
|
|
527
|
-
this.unsubscribe = this.agent.subscribe(async (event) => {
|
|
528
|
-
// Handle UI updates
|
|
529
|
-
await this.handleEvent(event, this.agent.state);
|
|
530
|
-
// Save messages to session
|
|
531
|
-
if (event.type === "message_end") {
|
|
532
|
-
this.sessionManager.saveMessage(event.message);
|
|
533
|
-
// Check if we should initialize session now (after first user+assistant exchange)
|
|
534
|
-
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
|
535
|
-
this.sessionManager.startSession(this.agent.state);
|
|
536
|
-
}
|
|
537
|
-
// Check for auto-compaction after assistant messages
|
|
538
|
-
if (event.message.role === "assistant") {
|
|
539
|
-
await this.checkAutoCompaction();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
async checkAutoCompaction() {
|
|
545
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
546
|
-
if (!settings.enabled)
|
|
547
|
-
return;
|
|
548
|
-
// Get last non-aborted assistant message from agent state
|
|
549
|
-
const messages = this.agent.state.messages;
|
|
550
|
-
let lastAssistant = null;
|
|
551
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
552
|
-
const msg = messages[i];
|
|
553
|
-
if (msg.role === "assistant") {
|
|
554
|
-
const assistantMsg = msg;
|
|
555
|
-
if (assistantMsg.stopReason !== "aborted") {
|
|
556
|
-
lastAssistant = assistantMsg;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (!lastAssistant)
|
|
562
|
-
return;
|
|
563
|
-
const contextTokens = calculateContextTokens(lastAssistant.usage);
|
|
564
|
-
const contextWindow = this.agent.state.model.contextWindow;
|
|
565
|
-
if (!shouldCompact(contextTokens, contextWindow, settings))
|
|
566
|
-
return;
|
|
567
|
-
// Trigger auto-compaction
|
|
568
|
-
await this.executeCompaction(undefined, true);
|
|
569
|
-
}
|
|
570
|
-
async handleEvent(event, state) {
|
|
571
|
-
if (!this.isInitialized) {
|
|
572
|
-
await this.init();
|
|
573
|
-
}
|
|
574
|
-
// Update footer with current stats
|
|
575
|
-
this.footer.updateState(state);
|
|
576
|
-
switch (event.type) {
|
|
577
|
-
case "agent_start":
|
|
578
|
-
// Show loading animation
|
|
579
|
-
// Note: Don't disable submit - we handle queuing in onSubmit callback
|
|
580
|
-
// Stop old loader before clearing
|
|
581
|
-
if (this.loadingAnimation) {
|
|
582
|
-
this.loadingAnimation.stop();
|
|
583
|
-
}
|
|
584
|
-
this.statusContainer.clear();
|
|
585
|
-
this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Working... (esc to interrupt)");
|
|
586
|
-
this.statusContainer.addChild(this.loadingAnimation);
|
|
587
|
-
this.ui.requestRender();
|
|
588
|
-
break;
|
|
589
|
-
case "message_start":
|
|
590
|
-
if (event.message.role === "user") {
|
|
591
|
-
// Check if this is a queued message
|
|
592
|
-
const userMsg = event.message;
|
|
593
|
-
const textBlocks = typeof userMsg.content === "string"
|
|
594
|
-
? [{ type: "text", text: userMsg.content }]
|
|
595
|
-
: userMsg.content.filter((c) => c.type === "text");
|
|
596
|
-
const messageText = textBlocks.map((c) => c.text).join("");
|
|
597
|
-
const queuedIndex = this.queuedMessages.indexOf(messageText);
|
|
598
|
-
if (queuedIndex !== -1) {
|
|
599
|
-
// Remove from queued messages
|
|
600
|
-
this.queuedMessages.splice(queuedIndex, 1);
|
|
601
|
-
this.updatePendingMessagesDisplay();
|
|
602
|
-
}
|
|
603
|
-
// Show user message immediately and clear editor
|
|
604
|
-
this.addMessageToChat(event.message);
|
|
605
|
-
this.editor.setText("");
|
|
606
|
-
this.ui.requestRender();
|
|
607
|
-
}
|
|
608
|
-
else if (event.message.role === "assistant") {
|
|
609
|
-
// Create assistant component for streaming
|
|
610
|
-
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
|
611
|
-
this.chatContainer.addChild(this.streamingComponent);
|
|
612
|
-
this.streamingComponent.updateContent(event.message);
|
|
613
|
-
this.ui.requestRender();
|
|
614
|
-
}
|
|
615
|
-
break;
|
|
616
|
-
case "message_update":
|
|
617
|
-
// Update streaming component
|
|
618
|
-
if (this.streamingComponent && event.message.role === "assistant") {
|
|
619
|
-
const assistantMsg = event.message;
|
|
620
|
-
this.streamingComponent.updateContent(assistantMsg);
|
|
621
|
-
// Create tool execution components as soon as we see tool calls
|
|
622
|
-
for (const content of assistantMsg.content) {
|
|
623
|
-
if (content.type === "toolCall") {
|
|
624
|
-
// Only create if we haven't created it yet
|
|
625
|
-
if (!this.pendingTools.has(content.id)) {
|
|
626
|
-
this.chatContainer.addChild(new Text("", 0, 0));
|
|
627
|
-
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
628
|
-
this.chatContainer.addChild(component);
|
|
629
|
-
this.pendingTools.set(content.id, component);
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
// Update existing component with latest arguments as they stream
|
|
633
|
-
const component = this.pendingTools.get(content.id);
|
|
634
|
-
if (component) {
|
|
635
|
-
component.updateArgs(content.arguments);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
this.ui.requestRender();
|
|
641
|
-
}
|
|
642
|
-
break;
|
|
643
|
-
case "message_end":
|
|
644
|
-
// Skip user messages (already shown in message_start)
|
|
645
|
-
if (event.message.role === "user") {
|
|
646
|
-
break;
|
|
647
|
-
}
|
|
648
|
-
if (this.streamingComponent && event.message.role === "assistant") {
|
|
649
|
-
const assistantMsg = event.message;
|
|
650
|
-
// Update streaming component with final message (includes stopReason)
|
|
651
|
-
this.streamingComponent.updateContent(assistantMsg);
|
|
652
|
-
// If message was aborted or errored, mark all pending tool components as failed
|
|
653
|
-
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
|
654
|
-
const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
|
|
655
|
-
for (const [toolCallId, component] of this.pendingTools.entries()) {
|
|
656
|
-
component.updateResult({
|
|
657
|
-
content: [{ type: "text", text: errorMessage }],
|
|
658
|
-
isError: true,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
this.pendingTools.clear();
|
|
662
|
-
}
|
|
663
|
-
// Keep the streaming component - it's now the final assistant message
|
|
664
|
-
this.streamingComponent = null;
|
|
665
|
-
// Invalidate footer cache to refresh git branch (in case agent executed git commands)
|
|
666
|
-
this.footer.invalidate();
|
|
667
|
-
}
|
|
668
|
-
this.ui.requestRender();
|
|
669
|
-
break;
|
|
670
|
-
case "tool_execution_start": {
|
|
671
|
-
// Component should already exist from message_update, but create if missing
|
|
672
|
-
if (!this.pendingTools.has(event.toolCallId)) {
|
|
673
|
-
const component = new ToolExecutionComponent(event.toolName, event.args);
|
|
674
|
-
this.chatContainer.addChild(component);
|
|
675
|
-
this.pendingTools.set(event.toolCallId, component);
|
|
676
|
-
this.ui.requestRender();
|
|
677
|
-
}
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
case "tool_execution_end": {
|
|
681
|
-
// Update the existing tool component with the result
|
|
682
|
-
const component = this.pendingTools.get(event.toolCallId);
|
|
683
|
-
if (component) {
|
|
684
|
-
// Convert result to the format expected by updateResult
|
|
685
|
-
const resultData = typeof event.result === "string"
|
|
686
|
-
? {
|
|
687
|
-
content: [{ type: "text", text: event.result }],
|
|
688
|
-
details: undefined,
|
|
689
|
-
isError: event.isError,
|
|
690
|
-
}
|
|
691
|
-
: {
|
|
692
|
-
content: event.result.content,
|
|
693
|
-
details: event.result.details,
|
|
694
|
-
isError: event.isError,
|
|
695
|
-
};
|
|
696
|
-
component.updateResult(resultData);
|
|
697
|
-
this.pendingTools.delete(event.toolCallId);
|
|
698
|
-
this.ui.requestRender();
|
|
699
|
-
}
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
case "agent_end":
|
|
703
|
-
// Stop loading animation
|
|
704
|
-
if (this.loadingAnimation) {
|
|
705
|
-
this.loadingAnimation.stop();
|
|
706
|
-
this.loadingAnimation = null;
|
|
707
|
-
this.statusContainer.clear();
|
|
708
|
-
}
|
|
709
|
-
if (this.streamingComponent) {
|
|
710
|
-
this.chatContainer.removeChild(this.streamingComponent);
|
|
711
|
-
this.streamingComponent = null;
|
|
712
|
-
}
|
|
713
|
-
this.pendingTools.clear();
|
|
714
|
-
// Note: Don't need to re-enable submit - we never disable it
|
|
715
|
-
this.ui.requestRender();
|
|
716
|
-
break;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
addMessageToChat(message) {
|
|
720
|
-
// Handle bash execution messages
|
|
721
|
-
if (isBashExecutionMessage(message)) {
|
|
722
|
-
const bashMsg = message;
|
|
723
|
-
const component = new BashExecutionComponent(bashMsg.command, this.ui);
|
|
724
|
-
if (bashMsg.output) {
|
|
725
|
-
component.appendOutput(bashMsg.output);
|
|
726
|
-
}
|
|
727
|
-
component.setComplete(bashMsg.exitCode, bashMsg.cancelled, bashMsg.truncated ? { truncated: true } : undefined, bashMsg.fullOutputPath);
|
|
728
|
-
this.chatContainer.addChild(component);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
if (message.role === "user") {
|
|
732
|
-
const userMsg = message;
|
|
733
|
-
// Extract text content from content blocks
|
|
734
|
-
const textBlocks = typeof userMsg.content === "string"
|
|
735
|
-
? [{ type: "text", text: userMsg.content }]
|
|
736
|
-
: userMsg.content.filter((c) => c.type === "text");
|
|
737
|
-
const textContent = textBlocks.map((c) => c.text).join("");
|
|
738
|
-
if (textContent) {
|
|
739
|
-
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
740
|
-
this.chatContainer.addChild(userComponent);
|
|
741
|
-
this.isFirstUserMessage = false;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
else if (message.role === "assistant") {
|
|
745
|
-
const assistantMsg = message;
|
|
746
|
-
// Add assistant message component
|
|
747
|
-
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
|
748
|
-
this.chatContainer.addChild(assistantComponent);
|
|
749
|
-
}
|
|
750
|
-
// Note: tool calls and results are now handled via tool_execution_start/end events
|
|
751
|
-
}
|
|
752
|
-
renderInitialMessages(state) {
|
|
753
|
-
// Render all existing messages (for --continue mode)
|
|
754
|
-
// Reset first user message flag for initial render
|
|
755
|
-
this.isFirstUserMessage = true;
|
|
756
|
-
// Update footer with loaded state
|
|
757
|
-
this.footer.updateState(state);
|
|
758
|
-
// Update editor border color based on current thinking level
|
|
759
|
-
this.updateEditorBorderColor();
|
|
760
|
-
// Get compaction info if any
|
|
761
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
762
|
-
// Render messages
|
|
763
|
-
for (let i = 0; i < state.messages.length; i++) {
|
|
764
|
-
const message = state.messages[i];
|
|
765
|
-
// Handle bash execution messages
|
|
766
|
-
if (isBashExecutionMessage(message)) {
|
|
767
|
-
this.addMessageToChat(message);
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
if (message.role === "user") {
|
|
771
|
-
const userMsg = message;
|
|
772
|
-
const textBlocks = typeof userMsg.content === "string"
|
|
773
|
-
? [{ type: "text", text: userMsg.content }]
|
|
774
|
-
: userMsg.content.filter((c) => c.type === "text");
|
|
775
|
-
const textContent = textBlocks.map((c) => c.text).join("");
|
|
776
|
-
if (textContent) {
|
|
777
|
-
// Check if this is a compaction summary message
|
|
778
|
-
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
779
|
-
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
780
|
-
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
781
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
782
|
-
this.chatContainer.addChild(component);
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
786
|
-
this.chatContainer.addChild(userComponent);
|
|
787
|
-
this.isFirstUserMessage = false;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
else if (message.role === "assistant") {
|
|
792
|
-
const assistantMsg = message;
|
|
793
|
-
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
|
794
|
-
this.chatContainer.addChild(assistantComponent);
|
|
795
|
-
// Create tool execution components for any tool calls
|
|
796
|
-
for (const content of assistantMsg.content) {
|
|
797
|
-
if (content.type === "toolCall") {
|
|
798
|
-
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
799
|
-
this.chatContainer.addChild(component);
|
|
800
|
-
// If message was aborted/errored, immediately mark tool as failed
|
|
801
|
-
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
|
802
|
-
const errorMessage = assistantMsg.stopReason === "aborted"
|
|
803
|
-
? "Operation aborted"
|
|
804
|
-
: assistantMsg.errorMessage || "Error";
|
|
805
|
-
component.updateResult({
|
|
806
|
-
content: [{ type: "text", text: errorMessage }],
|
|
807
|
-
isError: true,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
// Store in map so we can update with results later
|
|
812
|
-
this.pendingTools.set(content.id, component);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
else if (message.role === "toolResult") {
|
|
818
|
-
// Update existing tool execution component with results ;
|
|
819
|
-
const component = this.pendingTools.get(message.toolCallId);
|
|
820
|
-
if (component) {
|
|
821
|
-
component.updateResult({
|
|
822
|
-
content: message.content,
|
|
823
|
-
details: message.details,
|
|
824
|
-
isError: message.isError,
|
|
825
|
-
});
|
|
826
|
-
// Remove from pending map since it's complete
|
|
827
|
-
this.pendingTools.delete(message.toolCallId);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
// Clear pending tools after rendering initial messages
|
|
832
|
-
this.pendingTools.clear();
|
|
833
|
-
// Populate editor history with user messages from the session (oldest first so newest is at index 0)
|
|
834
|
-
for (const message of state.messages) {
|
|
835
|
-
if (message.role === "user") {
|
|
836
|
-
const textBlocks = typeof message.content === "string"
|
|
837
|
-
? [{ type: "text", text: message.content }]
|
|
838
|
-
: message.content.filter((c) => c.type === "text");
|
|
839
|
-
const textContent = textBlocks.map((c) => c.text).join("");
|
|
840
|
-
// Skip compaction summary messages
|
|
841
|
-
if (textContent && !textContent.startsWith(SUMMARY_PREFIX)) {
|
|
842
|
-
this.editor.addToHistory(textContent);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
this.ui.requestRender();
|
|
847
|
-
}
|
|
848
|
-
async getUserInput() {
|
|
849
|
-
return new Promise((resolve) => {
|
|
850
|
-
this.onInputCallback = (text) => {
|
|
851
|
-
this.onInputCallback = undefined;
|
|
852
|
-
resolve(text);
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
rebuildChatFromMessages() {
|
|
857
|
-
// Reset state and re-render messages from agent state
|
|
858
|
-
this.isFirstUserMessage = true;
|
|
859
|
-
this.pendingTools.clear();
|
|
860
|
-
// Get compaction info if any
|
|
861
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
862
|
-
for (const message of this.agent.state.messages) {
|
|
863
|
-
// Handle bash execution messages
|
|
864
|
-
if (isBashExecutionMessage(message)) {
|
|
865
|
-
this.addMessageToChat(message);
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
if (message.role === "user") {
|
|
869
|
-
const userMsg = message;
|
|
870
|
-
const textBlocks = typeof userMsg.content === "string"
|
|
871
|
-
? [{ type: "text", text: userMsg.content }]
|
|
872
|
-
: userMsg.content.filter((c) => c.type === "text");
|
|
873
|
-
const textContent = textBlocks.map((c) => c.text).join("");
|
|
874
|
-
if (textContent) {
|
|
875
|
-
// Check if this is a compaction summary message
|
|
876
|
-
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
877
|
-
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
878
|
-
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
879
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
880
|
-
this.chatContainer.addChild(component);
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
884
|
-
this.chatContainer.addChild(userComponent);
|
|
885
|
-
this.isFirstUserMessage = false;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
else if (message.role === "assistant") {
|
|
890
|
-
const assistantMsg = message;
|
|
891
|
-
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
|
892
|
-
this.chatContainer.addChild(assistantComponent);
|
|
893
|
-
for (const content of assistantMsg.content) {
|
|
894
|
-
if (content.type === "toolCall") {
|
|
895
|
-
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
896
|
-
this.chatContainer.addChild(component);
|
|
897
|
-
this.pendingTools.set(content.id, component);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
else if (message.role === "toolResult") {
|
|
902
|
-
const component = this.pendingTools.get(message.toolCallId);
|
|
903
|
-
if (component) {
|
|
904
|
-
component.updateResult({
|
|
905
|
-
content: message.content,
|
|
906
|
-
details: message.details,
|
|
907
|
-
isError: message.isError,
|
|
908
|
-
});
|
|
909
|
-
this.pendingTools.delete(message.toolCallId);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
this.pendingTools.clear();
|
|
914
|
-
this.ui.requestRender();
|
|
915
|
-
}
|
|
916
|
-
handleCtrlC() {
|
|
917
|
-
// Handle Ctrl+C double-press logic
|
|
918
|
-
const now = Date.now();
|
|
919
|
-
const timeSinceLastCtrlC = now - this.lastSigintTime;
|
|
920
|
-
if (timeSinceLastCtrlC < 500) {
|
|
921
|
-
// Second Ctrl+C within 500ms - exit
|
|
922
|
-
this.stop();
|
|
923
|
-
process.exit(0);
|
|
924
|
-
}
|
|
925
|
-
else {
|
|
926
|
-
// First Ctrl+C - clear the editor
|
|
927
|
-
this.clearEditor();
|
|
928
|
-
this.lastSigintTime = now;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
updateEditorBorderColor() {
|
|
932
|
-
if (this.isBashMode) {
|
|
933
|
-
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
const level = this.agent.state.thinkingLevel || "off";
|
|
937
|
-
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
938
|
-
}
|
|
939
|
-
this.ui.requestRender();
|
|
940
|
-
}
|
|
941
|
-
cycleThinkingLevel() {
|
|
942
|
-
// Only cycle if model supports thinking
|
|
943
|
-
if (!this.agent.state.model?.reasoning) {
|
|
944
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
945
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Current model does not support thinking"), 1, 0));
|
|
946
|
-
this.ui.requestRender();
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
// xhigh is only available for codex-max models
|
|
950
|
-
const modelId = this.agent.state.model?.id || "";
|
|
951
|
-
const supportsXhigh = modelId.includes("codex-max");
|
|
952
|
-
const levels = supportsXhigh
|
|
953
|
-
? ["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
954
|
-
: ["off", "minimal", "low", "medium", "high"];
|
|
955
|
-
const currentLevel = this.agent.state.thinkingLevel || "off";
|
|
956
|
-
const currentIndex = levels.indexOf(currentLevel);
|
|
957
|
-
const nextIndex = (currentIndex + 1) % levels.length;
|
|
958
|
-
const nextLevel = levels[nextIndex];
|
|
959
|
-
// Apply the new thinking level
|
|
960
|
-
this.agent.setThinkingLevel(nextLevel);
|
|
961
|
-
// Save thinking level change to session and settings
|
|
962
|
-
this.sessionManager.saveThinkingLevelChange(nextLevel);
|
|
963
|
-
this.settingsManager.setDefaultThinkingLevel(nextLevel);
|
|
964
|
-
// Update border color
|
|
965
|
-
this.updateEditorBorderColor();
|
|
966
|
-
// Show brief notification
|
|
967
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
968
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${nextLevel}`), 1, 0));
|
|
969
|
-
this.ui.requestRender();
|
|
970
|
-
}
|
|
971
|
-
async cycleModel() {
|
|
972
|
-
// Use scoped models if available, otherwise all available models
|
|
973
|
-
if (this.scopedModels.length > 0) {
|
|
974
|
-
// Use scoped models with thinking levels
|
|
975
|
-
if (this.scopedModels.length === 1) {
|
|
976
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
977
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
|
|
978
|
-
this.ui.requestRender();
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
const currentModel = this.agent.state.model;
|
|
982
|
-
let currentIndex = this.scopedModels.findIndex((sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider);
|
|
983
|
-
// If current model not in scope, start from first
|
|
984
|
-
if (currentIndex === -1) {
|
|
985
|
-
currentIndex = 0;
|
|
986
|
-
}
|
|
987
|
-
const nextIndex = (currentIndex + 1) % this.scopedModels.length;
|
|
988
|
-
const nextEntry = this.scopedModels[nextIndex];
|
|
989
|
-
const nextModel = nextEntry.model;
|
|
990
|
-
const nextThinking = nextEntry.thinkingLevel;
|
|
991
|
-
// Validate API key
|
|
992
|
-
const apiKey = await getApiKeyForModel(nextModel);
|
|
993
|
-
if (!apiKey) {
|
|
994
|
-
this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
// Switch model
|
|
998
|
-
this.agent.setModel(nextModel);
|
|
999
|
-
// Save model change to session and settings
|
|
1000
|
-
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
|
|
1001
|
-
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1002
|
-
// Apply thinking level (silently use "off" if model doesn't support thinking)
|
|
1003
|
-
const effectiveThinking = nextModel.reasoning ? nextThinking : "off";
|
|
1004
|
-
this.agent.setThinkingLevel(effectiveThinking);
|
|
1005
|
-
this.sessionManager.saveThinkingLevelChange(effectiveThinking);
|
|
1006
|
-
this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
|
|
1007
|
-
this.updateEditorBorderColor();
|
|
1008
|
-
// Show notification
|
|
1009
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1010
|
-
const thinkingStr = nextModel.reasoning && nextThinking !== "off" ? ` (thinking: ${nextThinking})` : "";
|
|
1011
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}${thinkingStr}`), 1, 0));
|
|
1012
|
-
this.ui.requestRender();
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
// Fallback to all available models (no thinking level changes)
|
|
1016
|
-
const { models: availableModels, error } = await getAvailableModels();
|
|
1017
|
-
if (error) {
|
|
1018
|
-
this.showError(`Failed to load models: ${error}`);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
if (availableModels.length === 0) {
|
|
1022
|
-
this.showError("No models available to cycle");
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
if (availableModels.length === 1) {
|
|
1026
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1027
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model available"), 1, 0));
|
|
1028
|
-
this.ui.requestRender();
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const currentModel = this.agent.state.model;
|
|
1032
|
-
let currentIndex = availableModels.findIndex((m) => m.id === currentModel?.id && m.provider === currentModel?.provider);
|
|
1033
|
-
// If current model not in scope, start from first
|
|
1034
|
-
if (currentIndex === -1) {
|
|
1035
|
-
currentIndex = 0;
|
|
1036
|
-
}
|
|
1037
|
-
const nextIndex = (currentIndex + 1) % availableModels.length;
|
|
1038
|
-
const nextModel = availableModels[nextIndex];
|
|
1039
|
-
// Validate API key
|
|
1040
|
-
const apiKey = await getApiKeyForModel(nextModel);
|
|
1041
|
-
if (!apiKey) {
|
|
1042
|
-
this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
// Switch model
|
|
1046
|
-
this.agent.setModel(nextModel);
|
|
1047
|
-
// Save model change to session and settings
|
|
1048
|
-
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
|
|
1049
|
-
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
1050
|
-
// Show notification
|
|
1051
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1052
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
|
|
1053
|
-
this.ui.requestRender();
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
toggleToolOutputExpansion() {
|
|
1057
|
-
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
1058
|
-
// Update all tool execution, compaction, and bash execution components
|
|
1059
|
-
for (const child of this.chatContainer.children) {
|
|
1060
|
-
if (child instanceof ToolExecutionComponent) {
|
|
1061
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
1062
|
-
}
|
|
1063
|
-
else if (child instanceof CompactionComponent) {
|
|
1064
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
1065
|
-
}
|
|
1066
|
-
else if (child instanceof BashExecutionComponent) {
|
|
1067
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
this.ui.requestRender();
|
|
1071
|
-
}
|
|
1072
|
-
toggleThinkingBlockVisibility() {
|
|
1073
|
-
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
1074
|
-
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
1075
|
-
// Update all assistant message components and rebuild their content
|
|
1076
|
-
for (const child of this.chatContainer.children) {
|
|
1077
|
-
if (child instanceof AssistantMessageComponent) {
|
|
1078
|
-
child.setHideThinkingBlock(this.hideThinkingBlock);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
// Rebuild chat to apply visibility change
|
|
1082
|
-
this.chatContainer.clear();
|
|
1083
|
-
this.rebuildChatFromMessages();
|
|
1084
|
-
// Show brief notification
|
|
1085
|
-
const status = this.hideThinkingBlock ? "hidden" : "visible";
|
|
1086
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1087
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking blocks: ${status}`), 1, 0));
|
|
1088
|
-
this.ui.requestRender();
|
|
1089
|
-
}
|
|
1090
|
-
clearEditor() {
|
|
1091
|
-
this.editor.setText("");
|
|
1092
|
-
this.ui.requestRender();
|
|
1093
|
-
}
|
|
1094
|
-
showError(errorMessage) {
|
|
1095
|
-
// Show error message in the chat
|
|
1096
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1097
|
-
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
|
1098
|
-
this.ui.requestRender();
|
|
1099
|
-
}
|
|
1100
|
-
showWarning(warningMessage) {
|
|
1101
|
-
// Show warning message in the chat
|
|
1102
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1103
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
1104
|
-
this.ui.requestRender();
|
|
1105
|
-
}
|
|
1106
|
-
showNewVersionNotification(newVersion) {
|
|
1107
|
-
// Show new version notification in the chat
|
|
1108
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1109
|
-
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
1110
|
-
this.chatContainer.addChild(new Text(theme.bold(theme.fg("warning", "Update Available")) +
|
|
1111
|
-
"\n" +
|
|
1112
|
-
theme.fg("muted", `New version ${newVersion} is available. Run: `) +
|
|
1113
|
-
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"), 1, 0));
|
|
1114
|
-
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
|
1115
|
-
this.ui.requestRender();
|
|
1116
|
-
}
|
|
1117
|
-
showThinkingSelector() {
|
|
1118
|
-
// Create thinking selector with current level
|
|
1119
|
-
this.thinkingSelector = new ThinkingSelectorComponent(this.agent.state.thinkingLevel, (level) => {
|
|
1120
|
-
// Apply the selected thinking level
|
|
1121
|
-
this.agent.setThinkingLevel(level);
|
|
1122
|
-
// Save thinking level change to session and settings
|
|
1123
|
-
this.sessionManager.saveThinkingLevelChange(level);
|
|
1124
|
-
this.settingsManager.setDefaultThinkingLevel(level);
|
|
1125
|
-
// Update border color
|
|
1126
|
-
this.updateEditorBorderColor();
|
|
1127
|
-
// Show confirmation message with proper spacing
|
|
1128
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1129
|
-
const confirmText = new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0);
|
|
1130
|
-
this.chatContainer.addChild(confirmText);
|
|
1131
|
-
// Hide selector and show editor again
|
|
1132
|
-
this.hideThinkingSelector();
|
|
1133
|
-
this.ui.requestRender();
|
|
1134
|
-
}, () => {
|
|
1135
|
-
// Just hide the selector
|
|
1136
|
-
this.hideThinkingSelector();
|
|
1137
|
-
this.ui.requestRender();
|
|
1138
|
-
});
|
|
1139
|
-
// Replace editor with selector
|
|
1140
|
-
this.editorContainer.clear();
|
|
1141
|
-
this.editorContainer.addChild(this.thinkingSelector);
|
|
1142
|
-
this.ui.setFocus(this.thinkingSelector.getSelectList());
|
|
1143
|
-
this.ui.requestRender();
|
|
1144
|
-
}
|
|
1145
|
-
hideThinkingSelector() {
|
|
1146
|
-
// Replace selector with editor in the container
|
|
1147
|
-
this.editorContainer.clear();
|
|
1148
|
-
this.editorContainer.addChild(this.editor);
|
|
1149
|
-
this.thinkingSelector = null;
|
|
1150
|
-
this.ui.setFocus(this.editor);
|
|
1151
|
-
}
|
|
1152
|
-
showQueueModeSelector() {
|
|
1153
|
-
// Create queue mode selector with current mode
|
|
1154
|
-
this.queueModeSelector = new QueueModeSelectorComponent(this.agent.getQueueMode(), (mode) => {
|
|
1155
|
-
// Apply the selected queue mode
|
|
1156
|
-
this.agent.setQueueMode(mode);
|
|
1157
|
-
// Save queue mode to settings
|
|
1158
|
-
this.settingsManager.setQueueMode(mode);
|
|
1159
|
-
// Show confirmation message with proper spacing
|
|
1160
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1161
|
-
const confirmText = new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0);
|
|
1162
|
-
this.chatContainer.addChild(confirmText);
|
|
1163
|
-
// Hide selector and show editor again
|
|
1164
|
-
this.hideQueueModeSelector();
|
|
1165
|
-
this.ui.requestRender();
|
|
1166
|
-
}, () => {
|
|
1167
|
-
// Just hide the selector
|
|
1168
|
-
this.hideQueueModeSelector();
|
|
1169
|
-
this.ui.requestRender();
|
|
1170
|
-
});
|
|
1171
|
-
// Replace editor with selector
|
|
1172
|
-
this.editorContainer.clear();
|
|
1173
|
-
this.editorContainer.addChild(this.queueModeSelector);
|
|
1174
|
-
this.ui.setFocus(this.queueModeSelector.getSelectList());
|
|
1175
|
-
this.ui.requestRender();
|
|
1176
|
-
}
|
|
1177
|
-
hideQueueModeSelector() {
|
|
1178
|
-
// Replace selector with editor in the container
|
|
1179
|
-
this.editorContainer.clear();
|
|
1180
|
-
this.editorContainer.addChild(this.editor);
|
|
1181
|
-
this.queueModeSelector = null;
|
|
1182
|
-
this.ui.setFocus(this.editor);
|
|
1183
|
-
}
|
|
1184
|
-
showThemeSelector() {
|
|
1185
|
-
// Get current theme from settings
|
|
1186
|
-
const currentTheme = this.settingsManager.getTheme() || "dark";
|
|
1187
|
-
// Create theme selector
|
|
1188
|
-
this.themeSelector = new ThemeSelectorComponent(currentTheme, (themeName) => {
|
|
1189
|
-
// Apply the selected theme
|
|
1190
|
-
const result = setTheme(themeName);
|
|
1191
|
-
// Save theme to settings
|
|
1192
|
-
this.settingsManager.setTheme(themeName);
|
|
1193
|
-
// Invalidate all components to clear cached rendering
|
|
1194
|
-
this.ui.invalidate();
|
|
1195
|
-
// Show confirmation or error message
|
|
1196
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1197
|
-
if (result.success) {
|
|
1198
|
-
const confirmText = new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0);
|
|
1199
|
-
this.chatContainer.addChild(confirmText);
|
|
1200
|
-
}
|
|
1201
|
-
else {
|
|
1202
|
-
const errorText = new Text(theme.fg("error", `Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`), 1, 0);
|
|
1203
|
-
this.chatContainer.addChild(errorText);
|
|
1204
|
-
}
|
|
1205
|
-
// Hide selector and show editor again
|
|
1206
|
-
this.hideThemeSelector();
|
|
1207
|
-
this.ui.requestRender();
|
|
1208
|
-
}, () => {
|
|
1209
|
-
// Just hide the selector
|
|
1210
|
-
this.hideThemeSelector();
|
|
1211
|
-
this.ui.requestRender();
|
|
1212
|
-
}, (themeName) => {
|
|
1213
|
-
// Preview theme on selection change
|
|
1214
|
-
const result = setTheme(themeName);
|
|
1215
|
-
if (result.success) {
|
|
1216
|
-
this.ui.invalidate();
|
|
1217
|
-
this.ui.requestRender();
|
|
1218
|
-
}
|
|
1219
|
-
// If failed, theme already fell back to dark, just don't re-render
|
|
1220
|
-
});
|
|
1221
|
-
// Replace editor with selector
|
|
1222
|
-
this.editorContainer.clear();
|
|
1223
|
-
this.editorContainer.addChild(this.themeSelector);
|
|
1224
|
-
this.ui.setFocus(this.themeSelector.getSelectList());
|
|
1225
|
-
this.ui.requestRender();
|
|
1226
|
-
}
|
|
1227
|
-
hideThemeSelector() {
|
|
1228
|
-
// Replace selector with editor in the container
|
|
1229
|
-
this.editorContainer.clear();
|
|
1230
|
-
this.editorContainer.addChild(this.editor);
|
|
1231
|
-
this.themeSelector = null;
|
|
1232
|
-
this.ui.setFocus(this.editor);
|
|
1233
|
-
}
|
|
1234
|
-
showModelSelector() {
|
|
1235
|
-
// Create model selector with current model
|
|
1236
|
-
this.modelSelector = new ModelSelectorComponent(this.ui, this.agent.state.model, this.settingsManager, (model) => {
|
|
1237
|
-
// Apply the selected model
|
|
1238
|
-
this.agent.setModel(model);
|
|
1239
|
-
// Save model change to session
|
|
1240
|
-
this.sessionManager.saveModelChange(model.provider, model.id);
|
|
1241
|
-
// Show confirmation message with proper spacing
|
|
1242
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1243
|
-
const confirmText = new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0);
|
|
1244
|
-
this.chatContainer.addChild(confirmText);
|
|
1245
|
-
// Hide selector and show editor again
|
|
1246
|
-
this.hideModelSelector();
|
|
1247
|
-
this.ui.requestRender();
|
|
1248
|
-
}, () => {
|
|
1249
|
-
// Just hide the selector
|
|
1250
|
-
this.hideModelSelector();
|
|
1251
|
-
this.ui.requestRender();
|
|
1252
|
-
});
|
|
1253
|
-
// Replace editor with selector
|
|
1254
|
-
this.editorContainer.clear();
|
|
1255
|
-
this.editorContainer.addChild(this.modelSelector);
|
|
1256
|
-
this.ui.setFocus(this.modelSelector);
|
|
1257
|
-
this.ui.requestRender();
|
|
1258
|
-
}
|
|
1259
|
-
hideModelSelector() {
|
|
1260
|
-
// Replace selector with editor in the container
|
|
1261
|
-
this.editorContainer.clear();
|
|
1262
|
-
this.editorContainer.addChild(this.editor);
|
|
1263
|
-
this.modelSelector = null;
|
|
1264
|
-
this.ui.setFocus(this.editor);
|
|
1265
|
-
}
|
|
1266
|
-
showUserMessageSelector() {
|
|
1267
|
-
// Read from session file directly to see ALL historical user messages
|
|
1268
|
-
// (including those before compaction events)
|
|
1269
|
-
const entries = this.sessionManager.loadEntries();
|
|
1270
|
-
const userMessages = [];
|
|
1271
|
-
const getUserMessageText = (content) => {
|
|
1272
|
-
if (typeof content === "string")
|
|
1273
|
-
return content;
|
|
1274
|
-
if (Array.isArray(content)) {
|
|
1275
|
-
return content
|
|
1276
|
-
.filter((c) => c.type === "text")
|
|
1277
|
-
.map((c) => c.text)
|
|
1278
|
-
.join("");
|
|
1279
|
-
}
|
|
1280
|
-
return "";
|
|
1281
|
-
};
|
|
1282
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1283
|
-
const entry = entries[i];
|
|
1284
|
-
if (entry.type !== "message")
|
|
1285
|
-
continue;
|
|
1286
|
-
if (entry.message.role !== "user")
|
|
1287
|
-
continue;
|
|
1288
|
-
const textContent = getUserMessageText(entry.message.content);
|
|
1289
|
-
if (textContent) {
|
|
1290
|
-
userMessages.push({ index: i, text: textContent });
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
// Don't show selector if there are no messages or only one message
|
|
1294
|
-
if (userMessages.length <= 1) {
|
|
1295
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1296
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "No messages to branch from"), 1, 0));
|
|
1297
|
-
this.ui.requestRender();
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
// Create user message selector
|
|
1301
|
-
this.userMessageSelector = new UserMessageSelectorComponent(userMessages, (entryIndex) => {
|
|
1302
|
-
// Get the selected user message text to put in the editor
|
|
1303
|
-
const selectedEntry = entries[entryIndex];
|
|
1304
|
-
if (selectedEntry.type !== "message")
|
|
1305
|
-
return;
|
|
1306
|
-
if (selectedEntry.message.role !== "user")
|
|
1307
|
-
return;
|
|
1308
|
-
const selectedText = getUserMessageText(selectedEntry.message.content);
|
|
1309
|
-
// Create a branched session by copying entries up to (but not including) the selected entry
|
|
1310
|
-
const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
|
|
1311
|
-
// Set the new session file as active
|
|
1312
|
-
this.sessionManager.setSessionFile(newSessionFile);
|
|
1313
|
-
// Reload the session
|
|
1314
|
-
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
1315
|
-
this.agent.replaceMessages(loaded.messages);
|
|
1316
|
-
// Clear and re-render the chat
|
|
1317
|
-
this.chatContainer.clear();
|
|
1318
|
-
this.isFirstUserMessage = true;
|
|
1319
|
-
this.renderInitialMessages(this.agent.state);
|
|
1320
|
-
// Show confirmation message
|
|
1321
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1322
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Branched to new session"), 1, 0));
|
|
1323
|
-
// Put the selected message in the editor
|
|
1324
|
-
this.editor.setText(selectedText);
|
|
1325
|
-
// Hide selector and show editor again
|
|
1326
|
-
this.hideUserMessageSelector();
|
|
1327
|
-
this.ui.requestRender();
|
|
1328
|
-
}, () => {
|
|
1329
|
-
// Just hide the selector
|
|
1330
|
-
this.hideUserMessageSelector();
|
|
1331
|
-
this.ui.requestRender();
|
|
1332
|
-
});
|
|
1333
|
-
// Replace editor with selector
|
|
1334
|
-
this.editorContainer.clear();
|
|
1335
|
-
this.editorContainer.addChild(this.userMessageSelector);
|
|
1336
|
-
this.ui.setFocus(this.userMessageSelector.getMessageList());
|
|
1337
|
-
this.ui.requestRender();
|
|
1338
|
-
}
|
|
1339
|
-
hideUserMessageSelector() {
|
|
1340
|
-
// Replace selector with editor in the container
|
|
1341
|
-
this.editorContainer.clear();
|
|
1342
|
-
this.editorContainer.addChild(this.editor);
|
|
1343
|
-
this.userMessageSelector = null;
|
|
1344
|
-
this.ui.setFocus(this.editor);
|
|
1345
|
-
}
|
|
1346
|
-
showSessionSelector() {
|
|
1347
|
-
// Create session selector
|
|
1348
|
-
this.sessionSelector = new SessionSelectorComponent(this.sessionManager, async (sessionPath) => {
|
|
1349
|
-
this.hideSessionSelector();
|
|
1350
|
-
await this.handleResumeSession(sessionPath);
|
|
1351
|
-
}, () => {
|
|
1352
|
-
// Just hide the selector
|
|
1353
|
-
this.hideSessionSelector();
|
|
1354
|
-
this.ui.requestRender();
|
|
1355
|
-
});
|
|
1356
|
-
// Replace editor with selector
|
|
1357
|
-
this.editorContainer.clear();
|
|
1358
|
-
this.editorContainer.addChild(this.sessionSelector);
|
|
1359
|
-
this.ui.setFocus(this.sessionSelector.getSessionList());
|
|
1360
|
-
this.ui.requestRender();
|
|
1361
|
-
}
|
|
1362
|
-
async handleResumeSession(sessionPath) {
|
|
1363
|
-
// Unsubscribe first to prevent processing events during transition
|
|
1364
|
-
this.unsubscribe?.();
|
|
1365
|
-
// Abort and wait for completion
|
|
1366
|
-
this.agent.abort();
|
|
1367
|
-
await this.agent.waitForIdle();
|
|
1368
|
-
// Stop loading animation
|
|
1369
|
-
if (this.loadingAnimation) {
|
|
1370
|
-
this.loadingAnimation.stop();
|
|
1371
|
-
this.loadingAnimation = null;
|
|
1372
|
-
}
|
|
1373
|
-
this.statusContainer.clear();
|
|
1374
|
-
// Clear UI state
|
|
1375
|
-
this.queuedMessages = [];
|
|
1376
|
-
this.pendingMessagesContainer.clear();
|
|
1377
|
-
this.streamingComponent = null;
|
|
1378
|
-
this.pendingTools.clear();
|
|
1379
|
-
// Set the selected session as active
|
|
1380
|
-
this.sessionManager.setSessionFile(sessionPath);
|
|
1381
|
-
// Reload the session
|
|
1382
|
-
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
1383
|
-
this.agent.replaceMessages(loaded.messages);
|
|
1384
|
-
// Restore model if saved in session
|
|
1385
|
-
const savedModel = this.sessionManager.loadModel();
|
|
1386
|
-
if (savedModel) {
|
|
1387
|
-
const availableModels = (await getAvailableModels()).models;
|
|
1388
|
-
const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);
|
|
1389
|
-
if (match) {
|
|
1390
|
-
this.agent.setModel(match);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
// Restore thinking level if saved in session
|
|
1394
|
-
const savedThinking = this.sessionManager.loadThinkingLevel();
|
|
1395
|
-
if (savedThinking) {
|
|
1396
|
-
this.agent.setThinkingLevel(savedThinking);
|
|
1397
|
-
}
|
|
1398
|
-
// Resubscribe to agent
|
|
1399
|
-
this.subscribeToAgent();
|
|
1400
|
-
// Clear and re-render the chat
|
|
1401
|
-
this.chatContainer.clear();
|
|
1402
|
-
this.isFirstUserMessage = true;
|
|
1403
|
-
this.renderInitialMessages(this.agent.state);
|
|
1404
|
-
// Show confirmation message
|
|
1405
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1406
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Resumed session"), 1, 0));
|
|
1407
|
-
this.ui.requestRender();
|
|
1408
|
-
}
|
|
1409
|
-
hideSessionSelector() {
|
|
1410
|
-
// Replace selector with editor in the container
|
|
1411
|
-
this.editorContainer.clear();
|
|
1412
|
-
this.editorContainer.addChild(this.editor);
|
|
1413
|
-
this.sessionSelector = null;
|
|
1414
|
-
this.ui.setFocus(this.editor);
|
|
1415
|
-
}
|
|
1416
|
-
async showOAuthSelector(mode) {
|
|
1417
|
-
// For logout mode, filter to only show logged-in providers
|
|
1418
|
-
let providersToShow = [];
|
|
1419
|
-
if (mode === "logout") {
|
|
1420
|
-
const loggedInProviders = listOAuthProviders();
|
|
1421
|
-
if (loggedInProviders.length === 0) {
|
|
1422
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1423
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "No OAuth providers logged in. Use /login first."), 1, 0));
|
|
1424
|
-
this.ui.requestRender();
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
providersToShow = loggedInProviders;
|
|
1428
|
-
}
|
|
1429
|
-
// Create OAuth selector
|
|
1430
|
-
this.oauthSelector = new OAuthSelectorComponent(mode, async (providerId) => {
|
|
1431
|
-
// Hide selector first
|
|
1432
|
-
this.hideOAuthSelector();
|
|
1433
|
-
if (mode === "login") {
|
|
1434
|
-
// Handle login
|
|
1435
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1436
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0));
|
|
1437
|
-
this.ui.requestRender();
|
|
1438
|
-
try {
|
|
1439
|
-
await login(providerId, (url) => {
|
|
1440
|
-
// Show auth URL to user
|
|
1441
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1442
|
-
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
|
|
1443
|
-
this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
|
1444
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1445
|
-
this.chatContainer.addChild(new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0));
|
|
1446
|
-
this.ui.requestRender();
|
|
1447
|
-
// Open URL in browser
|
|
1448
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1449
|
-
exec(`${openCmd} "${url}"`);
|
|
1450
|
-
}, async () => {
|
|
1451
|
-
// Prompt for code with a simple Input
|
|
1452
|
-
return new Promise((resolve) => {
|
|
1453
|
-
const codeInput = new Input();
|
|
1454
|
-
codeInput.onSubmit = () => {
|
|
1455
|
-
const code = codeInput.getValue();
|
|
1456
|
-
// Restore editor
|
|
1457
|
-
this.editorContainer.clear();
|
|
1458
|
-
this.editorContainer.addChild(this.editor);
|
|
1459
|
-
this.ui.setFocus(this.editor);
|
|
1460
|
-
resolve(code);
|
|
1461
|
-
};
|
|
1462
|
-
this.editorContainer.clear();
|
|
1463
|
-
this.editorContainer.addChild(codeInput);
|
|
1464
|
-
this.ui.setFocus(codeInput);
|
|
1465
|
-
this.ui.requestRender();
|
|
1466
|
-
});
|
|
1467
|
-
});
|
|
1468
|
-
// Success - invalidate OAuth cache so footer updates
|
|
1469
|
-
invalidateOAuthCache();
|
|
1470
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1471
|
-
this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0));
|
|
1472
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0));
|
|
1473
|
-
this.ui.requestRender();
|
|
1474
|
-
}
|
|
1475
|
-
catch (error) {
|
|
1476
|
-
this.showError(`Login failed: ${error.message}`);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
else {
|
|
1480
|
-
// Handle logout
|
|
1481
|
-
try {
|
|
1482
|
-
await logout(providerId);
|
|
1483
|
-
// Invalidate OAuth cache so footer updates
|
|
1484
|
-
invalidateOAuthCache();
|
|
1485
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1486
|
-
this.chatContainer.addChild(new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0));
|
|
1487
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0));
|
|
1488
|
-
this.ui.requestRender();
|
|
1489
|
-
}
|
|
1490
|
-
catch (error) {
|
|
1491
|
-
this.showError(`Logout failed: ${error.message}`);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}, () => {
|
|
1495
|
-
// Cancel - just hide the selector
|
|
1496
|
-
this.hideOAuthSelector();
|
|
1497
|
-
this.ui.requestRender();
|
|
1498
|
-
});
|
|
1499
|
-
// Replace editor with selector
|
|
1500
|
-
this.editorContainer.clear();
|
|
1501
|
-
this.editorContainer.addChild(this.oauthSelector);
|
|
1502
|
-
this.ui.setFocus(this.oauthSelector);
|
|
1503
|
-
this.ui.requestRender();
|
|
1504
|
-
}
|
|
1505
|
-
hideOAuthSelector() {
|
|
1506
|
-
// Replace selector with editor in the container
|
|
1507
|
-
this.editorContainer.clear();
|
|
1508
|
-
this.editorContainer.addChild(this.editor);
|
|
1509
|
-
this.oauthSelector = null;
|
|
1510
|
-
this.ui.setFocus(this.editor);
|
|
1511
|
-
}
|
|
1512
|
-
handleExportCommand(text) {
|
|
1513
|
-
// Parse optional filename from command: /export [filename]
|
|
1514
|
-
const parts = text.split(/\s+/);
|
|
1515
|
-
const outputPath = parts.length > 1 ? parts[1] : undefined;
|
|
1516
|
-
try {
|
|
1517
|
-
// Export session to HTML
|
|
1518
|
-
const filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);
|
|
1519
|
-
// Show success message in chat - matching thinking level style
|
|
1520
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1521
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Session exported to: ${filePath}`), 1, 0));
|
|
1522
|
-
this.ui.requestRender();
|
|
1523
|
-
}
|
|
1524
|
-
catch (error) {
|
|
1525
|
-
// Show error message in chat
|
|
1526
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1527
|
-
this.chatContainer.addChild(new Text(theme.fg("error", `Failed to export session: ${error.message || "Unknown error"}`), 1, 0));
|
|
1528
|
-
this.ui.requestRender();
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
handleCopyCommand() {
|
|
1532
|
-
// Find the last assistant message
|
|
1533
|
-
const lastAssistantMessage = this.agent.state.messages
|
|
1534
|
-
.slice()
|
|
1535
|
-
.reverse()
|
|
1536
|
-
.find((m) => m.role === "assistant");
|
|
1537
|
-
if (!lastAssistantMessage) {
|
|
1538
|
-
this.showError("No agent messages to copy yet.");
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
// Extract raw text content from all text blocks
|
|
1542
|
-
let textContent = "";
|
|
1543
|
-
for (const content of lastAssistantMessage.content) {
|
|
1544
|
-
if (content.type === "text") {
|
|
1545
|
-
textContent += content.text;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
if (!textContent.trim()) {
|
|
1549
|
-
this.showError("Last agent message contains no text content.");
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
// Copy to clipboard using cross-platform compatible method
|
|
1553
|
-
try {
|
|
1554
|
-
copyToClipboard(textContent);
|
|
1555
|
-
}
|
|
1556
|
-
catch (error) {
|
|
1557
|
-
this.showError(error instanceof Error ? error.message : String(error));
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
// Show confirmation message
|
|
1561
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1562
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", "Copied last agent message to clipboard"), 1, 0));
|
|
1563
|
-
this.ui.requestRender();
|
|
1564
|
-
}
|
|
1565
|
-
handleSessionCommand() {
|
|
1566
|
-
// Get session info
|
|
1567
|
-
const sessionFile = this.sessionManager.getSessionFile();
|
|
1568
|
-
const state = this.agent.state;
|
|
1569
|
-
// Count messages
|
|
1570
|
-
const userMessages = state.messages.filter((m) => m.role === "user").length;
|
|
1571
|
-
const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
|
|
1572
|
-
const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
|
|
1573
|
-
const totalMessages = state.messages.length;
|
|
1574
|
-
// Count tool calls from assistant messages
|
|
1575
|
-
let toolCalls = 0;
|
|
1576
|
-
for (const message of state.messages) {
|
|
1577
|
-
if (message.role === "assistant") {
|
|
1578
|
-
const assistantMsg = message;
|
|
1579
|
-
toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
// Calculate cumulative usage from all assistant messages (same as footer)
|
|
1583
|
-
let totalInput = 0;
|
|
1584
|
-
let totalOutput = 0;
|
|
1585
|
-
let totalCacheRead = 0;
|
|
1586
|
-
let totalCacheWrite = 0;
|
|
1587
|
-
let totalCost = 0;
|
|
1588
|
-
for (const message of state.messages) {
|
|
1589
|
-
if (message.role === "assistant") {
|
|
1590
|
-
const assistantMsg = message;
|
|
1591
|
-
totalInput += assistantMsg.usage.input;
|
|
1592
|
-
totalOutput += assistantMsg.usage.output;
|
|
1593
|
-
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
1594
|
-
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
1595
|
-
totalCost += assistantMsg.usage.cost.total;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
|
|
1599
|
-
// Build info text
|
|
1600
|
-
let info = `${theme.bold("Session Info")}\n\n`;
|
|
1601
|
-
info += `${theme.fg("dim", "File:")} ${sessionFile}\n`;
|
|
1602
|
-
info += `${theme.fg("dim", "ID:")} ${this.sessionManager.getSessionId()}\n\n`;
|
|
1603
|
-
info += `${theme.bold("Messages")}\n`;
|
|
1604
|
-
info += `${theme.fg("dim", "User:")} ${userMessages}\n`;
|
|
1605
|
-
info += `${theme.fg("dim", "Assistant:")} ${assistantMessages}\n`;
|
|
1606
|
-
info += `${theme.fg("dim", "Tool Calls:")} ${toolCalls}\n`;
|
|
1607
|
-
info += `${theme.fg("dim", "Tool Results:")} ${toolResults}\n`;
|
|
1608
|
-
info += `${theme.fg("dim", "Total:")} ${totalMessages}\n\n`;
|
|
1609
|
-
info += `${theme.bold("Tokens")}\n`;
|
|
1610
|
-
info += `${theme.fg("dim", "Input:")} ${totalInput.toLocaleString()}\n`;
|
|
1611
|
-
info += `${theme.fg("dim", "Output:")} ${totalOutput.toLocaleString()}\n`;
|
|
1612
|
-
if (totalCacheRead > 0) {
|
|
1613
|
-
info += `${theme.fg("dim", "Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
|
|
1614
|
-
}
|
|
1615
|
-
if (totalCacheWrite > 0) {
|
|
1616
|
-
info += `${theme.fg("dim", "Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
|
|
1617
|
-
}
|
|
1618
|
-
info += `${theme.fg("dim", "Total:")} ${totalTokens.toLocaleString()}\n`;
|
|
1619
|
-
if (totalCost > 0) {
|
|
1620
|
-
info += `\n${theme.bold("Cost")}\n`;
|
|
1621
|
-
info += `${theme.fg("dim", "Total:")} ${totalCost.toFixed(4)}`;
|
|
1622
|
-
}
|
|
1623
|
-
// Show info in chat
|
|
1624
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1625
|
-
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
1626
|
-
this.ui.requestRender();
|
|
1627
|
-
}
|
|
1628
|
-
handleChangelogCommand() {
|
|
1629
|
-
const changelogPath = getChangelogPath();
|
|
1630
|
-
const allEntries = parseChangelog(changelogPath);
|
|
1631
|
-
// Show all entries in reverse order (oldest first, newest last)
|
|
1632
|
-
const changelogMarkdown = allEntries.length > 0
|
|
1633
|
-
? allEntries
|
|
1634
|
-
.reverse()
|
|
1635
|
-
.map((e) => e.content)
|
|
1636
|
-
.join("\n\n")
|
|
1637
|
-
: "No changelog entries found.";
|
|
1638
|
-
// Display in chat
|
|
1639
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1640
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
1641
|
-
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
1642
|
-
this.ui.addChild(new Spacer(1));
|
|
1643
|
-
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
1644
|
-
this.chatContainer.addChild(new DynamicBorder());
|
|
1645
|
-
this.ui.requestRender();
|
|
1646
|
-
}
|
|
1647
|
-
async handleClearCommand() {
|
|
1648
|
-
// Unsubscribe first to prevent processing abort events
|
|
1649
|
-
this.unsubscribe?.();
|
|
1650
|
-
// Abort and wait for completion
|
|
1651
|
-
this.agent.abort();
|
|
1652
|
-
await this.agent.waitForIdle();
|
|
1653
|
-
// Stop loading animation
|
|
1654
|
-
if (this.loadingAnimation) {
|
|
1655
|
-
this.loadingAnimation.stop();
|
|
1656
|
-
this.loadingAnimation = null;
|
|
1657
|
-
}
|
|
1658
|
-
this.statusContainer.clear();
|
|
1659
|
-
// Reset agent and session
|
|
1660
|
-
this.agent.reset();
|
|
1661
|
-
this.sessionManager.reset();
|
|
1662
|
-
// Resubscribe to agent
|
|
1663
|
-
this.subscribeToAgent();
|
|
1664
|
-
// Clear UI state
|
|
1665
|
-
this.chatContainer.clear();
|
|
1666
|
-
this.pendingMessagesContainer.clear();
|
|
1667
|
-
this.queuedMessages = [];
|
|
1668
|
-
this.streamingComponent = null;
|
|
1669
|
-
this.pendingTools.clear();
|
|
1670
|
-
this.isFirstUserMessage = true;
|
|
1671
|
-
// Show confirmation
|
|
1672
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1673
|
-
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1));
|
|
1674
|
-
this.ui.requestRender();
|
|
1675
|
-
}
|
|
1676
|
-
handleDebugCommand() {
|
|
1677
|
-
// Force a render and capture all lines with their widths
|
|
1678
|
-
const width = this.ui.terminal.columns;
|
|
1679
|
-
const allLines = this.ui.render(width);
|
|
1680
|
-
const debugLogPath = getDebugLogPath();
|
|
1681
|
-
const debugData = [
|
|
1682
|
-
`Debug output at ${new Date().toISOString()}`,
|
|
1683
|
-
`Terminal width: ${width}`,
|
|
1684
|
-
`Total lines: ${allLines.length}`,
|
|
1685
|
-
"",
|
|
1686
|
-
"=== All rendered lines with visible widths ===",
|
|
1687
|
-
...allLines.map((line, idx) => {
|
|
1688
|
-
const vw = visibleWidth(line);
|
|
1689
|
-
const escaped = JSON.stringify(line);
|
|
1690
|
-
return `[${idx}] (w=${vw}) ${escaped}`;
|
|
1691
|
-
}),
|
|
1692
|
-
"",
|
|
1693
|
-
"=== Agent messages (JSONL) ===",
|
|
1694
|
-
...this.agent.state.messages.map((msg) => JSON.stringify(msg)),
|
|
1695
|
-
"",
|
|
1696
|
-
].join("\n");
|
|
1697
|
-
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
1698
|
-
fs.writeFileSync(debugLogPath, debugData);
|
|
1699
|
-
// Show confirmation
|
|
1700
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1701
|
-
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
|
|
1702
|
-
this.ui.requestRender();
|
|
1703
|
-
}
|
|
1704
|
-
async handleBashCommand(command) {
|
|
1705
|
-
// Create component and add to chat
|
|
1706
|
-
this.bashComponent = new BashExecutionComponent(command, this.ui);
|
|
1707
|
-
this.chatContainer.addChild(this.bashComponent);
|
|
1708
|
-
this.ui.requestRender();
|
|
1709
|
-
try {
|
|
1710
|
-
const result = await this.executeBashCommand(command, (chunk) => {
|
|
1711
|
-
if (this.bashComponent) {
|
|
1712
|
-
this.bashComponent.appendOutput(chunk);
|
|
1713
|
-
this.ui.requestRender();
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1716
|
-
if (this.bashComponent) {
|
|
1717
|
-
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncationResult, result.fullOutputPath);
|
|
1718
|
-
// Create and save message (even if cancelled, for consistency with LLM aborts)
|
|
1719
|
-
const bashMessage = {
|
|
1720
|
-
role: "bashExecution",
|
|
1721
|
-
command,
|
|
1722
|
-
output: result.truncationResult?.content || this.bashComponent.getOutput(),
|
|
1723
|
-
exitCode: result.exitCode,
|
|
1724
|
-
cancelled: result.cancelled,
|
|
1725
|
-
truncated: result.truncationResult?.truncated || false,
|
|
1726
|
-
fullOutputPath: result.fullOutputPath,
|
|
1727
|
-
timestamp: Date.now(),
|
|
1728
|
-
};
|
|
1729
|
-
// Add to agent state
|
|
1730
|
-
this.agent.appendMessage(bashMessage);
|
|
1731
|
-
// Save to session
|
|
1732
|
-
this.sessionManager.saveMessage(bashMessage);
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
catch (error) {
|
|
1736
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1737
|
-
if (this.bashComponent) {
|
|
1738
|
-
this.bashComponent.setComplete(null, false);
|
|
1739
|
-
}
|
|
1740
|
-
this.showError(`Bash command failed: ${errorMessage}`);
|
|
1741
|
-
}
|
|
1742
|
-
this.bashComponent = null;
|
|
1743
|
-
this.ui.requestRender();
|
|
1744
|
-
}
|
|
1745
|
-
executeBashCommand(command, onChunk) {
|
|
1746
|
-
return new Promise((resolve, reject) => {
|
|
1747
|
-
const { shell, args } = getShellConfig();
|
|
1748
|
-
const child = spawn(shell, [...args, command], {
|
|
1749
|
-
detached: true,
|
|
1750
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1751
|
-
});
|
|
1752
|
-
this.bashProcess = child;
|
|
1753
|
-
// Track sanitized output for truncation
|
|
1754
|
-
const outputChunks = [];
|
|
1755
|
-
let outputBytes = 0;
|
|
1756
|
-
const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
|
|
1757
|
-
// Temp file for large output
|
|
1758
|
-
let tempFilePath;
|
|
1759
|
-
let tempFileStream;
|
|
1760
|
-
let totalBytes = 0;
|
|
1761
|
-
const handleData = (data) => {
|
|
1762
|
-
totalBytes += data.length;
|
|
1763
|
-
// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
|
|
1764
|
-
const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
|
|
1765
|
-
// Start writing to temp file if exceeds threshold
|
|
1766
|
-
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
1767
|
-
const id = randomBytes(8).toString("hex");
|
|
1768
|
-
tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
|
|
1769
|
-
tempFileStream = createWriteStream(tempFilePath);
|
|
1770
|
-
for (const chunk of outputChunks) {
|
|
1771
|
-
tempFileStream.write(chunk);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
if (tempFileStream) {
|
|
1775
|
-
tempFileStream.write(text);
|
|
1776
|
-
}
|
|
1777
|
-
// Keep rolling buffer of sanitized text
|
|
1778
|
-
outputChunks.push(text);
|
|
1779
|
-
outputBytes += text.length;
|
|
1780
|
-
while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
|
|
1781
|
-
const removed = outputChunks.shift();
|
|
1782
|
-
outputBytes -= removed.length;
|
|
1783
|
-
}
|
|
1784
|
-
// Stream to component
|
|
1785
|
-
onChunk(text);
|
|
1786
|
-
};
|
|
1787
|
-
child.stdout?.on("data", handleData);
|
|
1788
|
-
child.stderr?.on("data", handleData);
|
|
1789
|
-
child.on("close", (code) => {
|
|
1790
|
-
if (tempFileStream) {
|
|
1791
|
-
tempFileStream.end();
|
|
1792
|
-
}
|
|
1793
|
-
this.bashProcess = null;
|
|
1794
|
-
// Combine buffered chunks for truncation (already sanitized)
|
|
1795
|
-
const fullOutput = outputChunks.join("");
|
|
1796
|
-
const truncationResult = truncateTail(fullOutput);
|
|
1797
|
-
// code === null means killed (cancelled)
|
|
1798
|
-
const cancelled = code === null;
|
|
1799
|
-
resolve({
|
|
1800
|
-
exitCode: code,
|
|
1801
|
-
cancelled,
|
|
1802
|
-
truncationResult: truncationResult.truncated ? truncationResult : undefined,
|
|
1803
|
-
fullOutputPath: tempFilePath,
|
|
1804
|
-
});
|
|
1805
|
-
});
|
|
1806
|
-
child.on("error", (err) => {
|
|
1807
|
-
if (tempFileStream) {
|
|
1808
|
-
tempFileStream.end();
|
|
1809
|
-
}
|
|
1810
|
-
this.bashProcess = null;
|
|
1811
|
-
reject(err);
|
|
1812
|
-
});
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
compactionAbortController = null;
|
|
1816
|
-
/**
|
|
1817
|
-
* Shared logic to execute context compaction.
|
|
1818
|
-
* Handles aborting agent, showing loader, performing compaction, updating session/UI.
|
|
1819
|
-
*/
|
|
1820
|
-
async executeCompaction(customInstructions, isAuto = false) {
|
|
1821
|
-
// Unsubscribe first to prevent processing events during compaction
|
|
1822
|
-
this.unsubscribe?.();
|
|
1823
|
-
// Abort and wait for completion
|
|
1824
|
-
this.agent.abort();
|
|
1825
|
-
await this.agent.waitForIdle();
|
|
1826
|
-
// Stop loading animation
|
|
1827
|
-
if (this.loadingAnimation) {
|
|
1828
|
-
this.loadingAnimation.stop();
|
|
1829
|
-
this.loadingAnimation = null;
|
|
1830
|
-
}
|
|
1831
|
-
this.statusContainer.clear();
|
|
1832
|
-
// Create abort controller for compaction
|
|
1833
|
-
this.compactionAbortController = new AbortController();
|
|
1834
|
-
// Set up escape handler during compaction
|
|
1835
|
-
const originalOnEscape = this.editor.onEscape;
|
|
1836
|
-
this.editor.onEscape = () => {
|
|
1837
|
-
if (this.compactionAbortController) {
|
|
1838
|
-
this.compactionAbortController.abort();
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1841
|
-
// Show compacting status with loader
|
|
1842
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1843
|
-
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
1844
|
-
const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
|
|
1845
|
-
this.statusContainer.addChild(compactingLoader);
|
|
1846
|
-
this.ui.requestRender();
|
|
1847
|
-
try {
|
|
1848
|
-
// Get API key for current model
|
|
1849
|
-
const apiKey = await getApiKeyForModel(this.agent.state.model);
|
|
1850
|
-
if (!apiKey) {
|
|
1851
|
-
throw new Error(`No API key for ${this.agent.state.model.provider}`);
|
|
1852
|
-
}
|
|
1853
|
-
// Perform compaction with abort signal
|
|
1854
|
-
const entries = this.sessionManager.loadEntries();
|
|
1855
|
-
const settings = this.settingsManager.getCompactionSettings();
|
|
1856
|
-
const compactionEntry = await compact(entries, this.agent.state.model, settings, apiKey, this.compactionAbortController.signal, customInstructions);
|
|
1857
|
-
// Check if aborted after compact returned
|
|
1858
|
-
if (this.compactionAbortController.signal.aborted) {
|
|
1859
|
-
throw new Error("Compaction cancelled");
|
|
1860
|
-
}
|
|
1861
|
-
// Save compaction to session
|
|
1862
|
-
this.sessionManager.saveCompaction(compactionEntry);
|
|
1863
|
-
// Reload session
|
|
1864
|
-
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
1865
|
-
this.agent.replaceMessages(loaded.messages);
|
|
1866
|
-
// Rebuild UI
|
|
1867
|
-
this.chatContainer.clear();
|
|
1868
|
-
this.rebuildChatFromMessages();
|
|
1869
|
-
// Add compaction component at current position so user can see/expand the summary
|
|
1870
|
-
const compactionComponent = new CompactionComponent(compactionEntry.tokensBefore, compactionEntry.summary);
|
|
1871
|
-
compactionComponent.setExpanded(this.toolOutputExpanded);
|
|
1872
|
-
this.chatContainer.addChild(compactionComponent);
|
|
1873
|
-
// Update footer with new state (fixes context % display)
|
|
1874
|
-
this.footer.updateState(this.agent.state);
|
|
1875
|
-
}
|
|
1876
|
-
catch (error) {
|
|
1877
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1878
|
-
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
1879
|
-
this.showError("Compaction cancelled");
|
|
1880
|
-
}
|
|
1881
|
-
else {
|
|
1882
|
-
this.showError(`Compaction failed: ${message}`);
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
finally {
|
|
1886
|
-
// Clean up
|
|
1887
|
-
compactingLoader.stop();
|
|
1888
|
-
this.statusContainer.clear();
|
|
1889
|
-
this.compactionAbortController = null;
|
|
1890
|
-
this.editor.onEscape = originalOnEscape;
|
|
1891
|
-
}
|
|
1892
|
-
// Resubscribe to agent
|
|
1893
|
-
this.subscribeToAgent();
|
|
1894
|
-
}
|
|
1895
|
-
async handleCompactCommand(customInstructions) {
|
|
1896
|
-
// Check if there are any messages to compact
|
|
1897
|
-
const entries = this.sessionManager.loadEntries();
|
|
1898
|
-
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
1899
|
-
if (messageCount < 2) {
|
|
1900
|
-
this.showWarning("Nothing to compact (no messages yet)");
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
await this.executeCompaction(customInstructions, false);
|
|
1904
|
-
}
|
|
1905
|
-
handleAutocompactCommand() {
|
|
1906
|
-
const currentEnabled = this.settingsManager.getCompactionEnabled();
|
|
1907
|
-
const newState = !currentEnabled;
|
|
1908
|
-
this.settingsManager.setCompactionEnabled(newState);
|
|
1909
|
-
this.footer.setAutoCompactEnabled(newState);
|
|
1910
|
-
// Show brief notification (same style as thinking level toggle)
|
|
1911
|
-
this.chatContainer.addChild(new Spacer(1));
|
|
1912
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Auto-compaction: ${newState ? "on" : "off"}`), 1, 0));
|
|
1913
|
-
this.ui.requestRender();
|
|
1914
|
-
}
|
|
1915
|
-
updatePendingMessagesDisplay() {
|
|
1916
|
-
this.pendingMessagesContainer.clear();
|
|
1917
|
-
if (this.queuedMessages.length > 0) {
|
|
1918
|
-
this.pendingMessagesContainer.addChild(new Spacer(1));
|
|
1919
|
-
for (const message of this.queuedMessages) {
|
|
1920
|
-
const queuedText = theme.fg("dim", "Queued: " + message);
|
|
1921
|
-
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
stop() {
|
|
1926
|
-
if (this.loadingAnimation) {
|
|
1927
|
-
this.loadingAnimation.stop();
|
|
1928
|
-
this.loadingAnimation = null;
|
|
1929
|
-
}
|
|
1930
|
-
this.footer.dispose();
|
|
1931
|
-
if (this.isInitialized) {
|
|
1932
|
-
this.ui.stop();
|
|
1933
|
-
this.isInitialized = false;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
//# sourceMappingURL=tui-renderer.js.map
|