@mariozechner/pi-coding-agent 0.30.2 → 0.31.1
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 +251 -1
- package/README.md +105 -84
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts +3 -3
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +7 -10
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +73 -34
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +464 -210
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +2 -2
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +2 -2
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -2
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +84 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/core/compaction/branch-summarization.js +233 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -0
- package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
- package/dist/core/compaction/compaction.d.ts.map +1 -0
- package/dist/core/compaction/compaction.js +558 -0
- package/dist/core/compaction/compaction.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/utils.d.ts +35 -0
- package/dist/core/compaction/utils.d.ts.map +1 -0
- package/dist/core/compaction/utils.js +138 -0
- package/dist/core/compaction/utils.js.map +1 -0
- package/dist/core/custom-tools/index.d.ts +2 -1
- package/dist/core/custom-tools/index.d.ts.map +1 -1
- package/dist/core/custom-tools/index.js +1 -0
- package/dist/core/custom-tools/index.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +13 -80
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/custom-tools/types.d.ts +84 -59
- package/dist/core/custom-tools/types.d.ts.map +1 -1
- package/dist/core/custom-tools/types.js.map +1 -1
- package/dist/core/custom-tools/wrapper.d.ts +15 -0
- package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
- package/dist/core/custom-tools/wrapper.js +23 -0
- package/dist/core/custom-tools/wrapper.js.map +1 -0
- package/dist/core/exec.d.ts +29 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/export-html/index.d.ts +17 -0
- package/dist/core/export-html/index.d.ts.map +1 -0
- package/dist/core/export-html/index.js +171 -0
- package/dist/core/export-html/index.js.map +1 -0
- package/dist/core/export-html/template.css +781 -0
- package/dist/core/export-html/template.html +54 -0
- package/dist/core/export-html/template.js +1185 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/hooks/index.d.ts +4 -4
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +4 -3
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +40 -5
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +43 -10
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/hooks/runner.d.ts +94 -18
- package/dist/core/hooks/runner.d.ts.map +1 -1
- package/dist/core/hooks/runner.js +199 -120
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
- package/dist/core/hooks/tool-wrapper.js +36 -19
- package/dist/core/hooks/tool-wrapper.js.map +1 -1
- package/dist/core/hooks/types.d.ts +407 -96
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/messages.d.ts +44 -12
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +82 -34
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +5 -5
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +7 -7
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +7 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +45 -14
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +7 -10
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +88 -32
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +202 -36
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +565 -133
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +33 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -0
- package/dist/core/tools/edit-diff.js +171 -0
- package/dist/core/tools/edit-diff.js.map +1 -0
- package/dist/core/tools/edit.d.ts +7 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +20 -95
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -21
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -4
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +6 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
- package/dist/modes/interactive/components/bordered-loader.js +30 -0
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.js +35 -0
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js +5 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +12 -6
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +57 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
- package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-editor.js +95 -0
- package/dist/modes/interactive/components/hook-editor.js.map +1 -0
- package/dist/modes/interactive/components/hook-message.d.ts +18 -0
- package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-message.js +80 -0
- package/dist/modes/interactive/components/hook-message.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +70 -21
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/tree-selector.js +745 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -5
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +29 -12
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +589 -208
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +13 -1
- package/dist/modes/interactive/theme/light.json +13 -1
- package/dist/modes/interactive/theme/theme-schema.json +34 -0
- package/dist/modes/interactive/theme/theme.d.ts +20 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +135 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +3 -3
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +26 -20
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +13 -10
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +11 -10
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +88 -35
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +30 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts +4 -2
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +36 -7
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +388 -0
- package/docs/custom-tools.md +146 -43
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +562 -596
- package/docs/rpc.md +33 -19
- package/docs/sdk.md +93 -21
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +172 -21
- package/docs/skills.md +2 -0
- package/docs/theme.md +31 -2
- package/docs/tree.md +197 -0
- package/docs/tui.md +343 -0
- package/examples/README.md +1 -9
- package/examples/custom-tools/hello/index.ts +4 -3
- package/examples/custom-tools/question/index.ts +4 -4
- package/examples/custom-tools/subagent/index.ts +7 -6
- package/examples/custom-tools/todo/index.ts +11 -5
- package/examples/hooks/README.md +29 -71
- package/examples/hooks/auto-commit-on-exit.ts +8 -9
- package/examples/hooks/confirm-destructive.ts +29 -30
- package/examples/hooks/custom-compaction.ts +20 -21
- package/examples/hooks/dirty-repo-guard.ts +41 -40
- package/examples/hooks/file-trigger.ts +10 -5
- package/examples/hooks/git-checkpoint.ts +16 -12
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +1 -1
- package/examples/hooks/protected-paths.ts +1 -1
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +4 -4
- package/examples/sdk/06-hooks.ts +1 -1
- package/examples/sdk/07-context-files.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +6 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
- package/examples/sdk/10-settings.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/examples/sdk/12-full-control.ts +4 -7
- package/package.json +6 -6
- package/dist/core/compaction.d.ts.map +0 -1
- package/dist/core/compaction.js +0 -412
- package/dist/core/compaction.js.map +0 -1
- package/dist/core/export-html.d.ts +0 -23
- package/dist/core/export-html.d.ts.map +0 -1
- package/dist/core/export-html.js +0 -1185
- package/dist/core/export-html.js.map +0 -1
- package/dist/modes/interactive/components/compaction.d.ts +0 -15
- package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
- package/dist/modes/interactive/components/compaction.js +0 -41
- package/dist/modes/interactive/components/compaction.js.map +0 -1
- package/docs/hooks-v2.md +0 -385
- package/docs/session-tree.md +0 -452
|
@@ -6,10 +6,10 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
9
|
-
import { exec, spawnSync } from "child_process";
|
|
9
|
+
import { exec, spawn, spawnSync } from "child_process";
|
|
10
10
|
import { APP_NAME, getAuthPath, getDebugLogPath } from "../../config.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
12
|
+
import { SessionManager } from "../../core/session-manager.js";
|
|
13
13
|
import { loadSkills } from "../../core/skills.js";
|
|
14
14
|
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
|
15
15
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
|
|
@@ -17,20 +17,28 @@ import { copyToClipboard } from "../../utils/clipboard.js";
|
|
|
17
17
|
import { ArminComponent } from "./components/armin.js";
|
|
18
18
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
|
19
19
|
import { BashExecutionComponent } from "./components/bash-execution.js";
|
|
20
|
-
import {
|
|
20
|
+
import { BorderedLoader } from "./components/bordered-loader.js";
|
|
21
|
+
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
|
|
22
|
+
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
|
|
21
23
|
import { CustomEditor } from "./components/custom-editor.js";
|
|
22
24
|
import { DynamicBorder } from "./components/dynamic-border.js";
|
|
23
25
|
import { FooterComponent } from "./components/footer.js";
|
|
26
|
+
import { HookEditorComponent } from "./components/hook-editor.js";
|
|
24
27
|
import { HookInputComponent } from "./components/hook-input.js";
|
|
28
|
+
import { HookMessageComponent } from "./components/hook-message.js";
|
|
25
29
|
import { HookSelectorComponent } from "./components/hook-selector.js";
|
|
26
30
|
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
27
31
|
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
28
32
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
29
33
|
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
|
30
34
|
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
35
|
+
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
31
36
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
32
37
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
33
|
-
import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
|
|
38
|
+
import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme, } from "./theme/theme.js";
|
|
39
|
+
function isExpandable(obj) {
|
|
40
|
+
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
41
|
+
}
|
|
34
42
|
export class InteractiveMode {
|
|
35
43
|
setToolUIContext;
|
|
36
44
|
session;
|
|
@@ -44,16 +52,18 @@ export class InteractiveMode {
|
|
|
44
52
|
version;
|
|
45
53
|
isInitialized = false;
|
|
46
54
|
onInputCallback;
|
|
47
|
-
loadingAnimation =
|
|
55
|
+
loadingAnimation = undefined;
|
|
48
56
|
lastSigintTime = 0;
|
|
49
57
|
lastEscapeTime = 0;
|
|
50
|
-
changelogMarkdown =
|
|
58
|
+
changelogMarkdown = undefined;
|
|
59
|
+
// Status line tracking (for mutating immediately-sequential status updates)
|
|
60
|
+
lastStatusSpacer = undefined;
|
|
61
|
+
lastStatusText = undefined;
|
|
51
62
|
// Streaming message tracking
|
|
52
|
-
streamingComponent =
|
|
63
|
+
streamingComponent = undefined;
|
|
64
|
+
streamingMessage = undefined;
|
|
53
65
|
// Tool execution tracking: toolCallId -> component
|
|
54
66
|
pendingTools = new Map();
|
|
55
|
-
// Track if this is the first user message (to skip spacer)
|
|
56
|
-
isFirstUserMessage = true;
|
|
57
67
|
// Tool output expansion state
|
|
58
68
|
toolOutputExpanded = false;
|
|
59
69
|
// Thinking block visibility state
|
|
@@ -63,18 +73,19 @@ export class InteractiveMode {
|
|
|
63
73
|
// Track if editor is in bash mode (text starts with !)
|
|
64
74
|
isBashMode = false;
|
|
65
75
|
// Track current bash execution component
|
|
66
|
-
bashComponent =
|
|
76
|
+
bashComponent = undefined;
|
|
67
77
|
// Track pending bash components (shown in pending area, moved to chat on submit)
|
|
68
78
|
pendingBashComponents = [];
|
|
69
79
|
// Auto-compaction state
|
|
70
|
-
autoCompactionLoader =
|
|
80
|
+
autoCompactionLoader = undefined;
|
|
71
81
|
autoCompactionEscapeHandler;
|
|
72
82
|
// Auto-retry state
|
|
73
|
-
retryLoader =
|
|
83
|
+
retryLoader = undefined;
|
|
74
84
|
retryEscapeHandler;
|
|
75
85
|
// Hook UI state
|
|
76
|
-
hookSelector =
|
|
77
|
-
hookInput =
|
|
86
|
+
hookSelector = undefined;
|
|
87
|
+
hookInput = undefined;
|
|
88
|
+
hookEditor = undefined;
|
|
78
89
|
// Custom tools for custom rendering
|
|
79
90
|
customTools;
|
|
80
91
|
// Convenience accessors
|
|
@@ -87,7 +98,7 @@ export class InteractiveMode {
|
|
|
87
98
|
get settingsManager() {
|
|
88
99
|
return this.session.settingsManager;
|
|
89
100
|
}
|
|
90
|
-
constructor(session, version, changelogMarkdown =
|
|
101
|
+
constructor(session, version, changelogMarkdown = undefined, customTools = [], setToolUIContext = () => { }, fdPath = undefined) {
|
|
91
102
|
this.setToolUIContext = setToolUIContext;
|
|
92
103
|
this.session = session;
|
|
93
104
|
this.version = version;
|
|
@@ -100,18 +111,20 @@ export class InteractiveMode {
|
|
|
100
111
|
this.editor = new CustomEditor(getEditorTheme());
|
|
101
112
|
this.editorContainer = new Container();
|
|
102
113
|
this.editorContainer.addChild(this.editor);
|
|
103
|
-
this.footer = new FooterComponent(session
|
|
114
|
+
this.footer = new FooterComponent(session);
|
|
104
115
|
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
105
116
|
// Define slash commands for autocomplete
|
|
106
117
|
const slashCommands = [
|
|
107
118
|
{ name: "settings", description: "Open settings menu" },
|
|
108
119
|
{ name: "model", description: "Select model (opens selector UI)" },
|
|
109
120
|
{ name: "export", description: "Export session to HTML file" },
|
|
121
|
+
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
110
122
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
111
123
|
{ name: "session", description: "Show session info and stats" },
|
|
112
124
|
{ name: "changelog", description: "Show changelog entries" },
|
|
113
125
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
114
126
|
{ name: "branch", description: "Create a new branch from a previous message" },
|
|
127
|
+
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
115
128
|
{ name: "login", description: "Login with OAuth provider" },
|
|
116
129
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
117
130
|
{ name: "new", description: "Start a new session" },
|
|
@@ -125,8 +138,13 @@ export class InteractiveMode {
|
|
|
125
138
|
name: cmd.name,
|
|
126
139
|
description: cmd.description,
|
|
127
140
|
}));
|
|
141
|
+
// Convert hook commands to SlashCommand format
|
|
142
|
+
const hookCommands = (this.session.hookRunner?.getRegisteredCommands() ?? []).map((cmd) => ({
|
|
143
|
+
name: cmd.name,
|
|
144
|
+
description: cmd.description ?? "(hook command)",
|
|
145
|
+
}));
|
|
128
146
|
// Setup autocomplete
|
|
129
|
-
const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands], process.cwd(), fdPath);
|
|
147
|
+
const autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...fileSlashCommands, ...hookCommands], process.cwd(), fdPath);
|
|
130
148
|
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
131
149
|
}
|
|
132
150
|
async init() {
|
|
@@ -267,33 +285,117 @@ export class InteractiveMode {
|
|
|
267
285
|
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded custom tools:\n") + toolList, 0, 0));
|
|
268
286
|
this.chatContainer.addChild(new Spacer(1));
|
|
269
287
|
}
|
|
270
|
-
//
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
288
|
+
// Create and set hook & tool UI context
|
|
289
|
+
const uiContext = {
|
|
290
|
+
select: (title, options) => this.showHookSelector(title, options),
|
|
291
|
+
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
292
|
+
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
293
|
+
notify: (message, type) => this.showHookNotify(message, type),
|
|
294
|
+
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
295
|
+
custom: (factory) => this.showHookCustom(factory),
|
|
296
|
+
setEditorText: (text) => this.editor.setText(text),
|
|
297
|
+
getEditorText: () => this.editor.getText(),
|
|
298
|
+
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
299
|
+
get theme() {
|
|
300
|
+
return theme;
|
|
301
|
+
},
|
|
302
|
+
};
|
|
274
303
|
this.setToolUIContext(uiContext, true);
|
|
275
304
|
// Notify custom tools of session start
|
|
276
|
-
await this.
|
|
277
|
-
entries,
|
|
278
|
-
sessionFile: this.session.sessionFile,
|
|
279
|
-
previousSessionFile: null,
|
|
305
|
+
await this.emitCustomToolSessionEvent({
|
|
280
306
|
reason: "start",
|
|
307
|
+
previousSessionFile: undefined,
|
|
281
308
|
});
|
|
282
309
|
const hookRunner = this.session.hookRunner;
|
|
283
310
|
if (!hookRunner) {
|
|
284
311
|
return; // No hooks loaded
|
|
285
312
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
313
|
+
hookRunner.initialize({
|
|
314
|
+
getModel: () => this.session.model,
|
|
315
|
+
sendMessageHandler: (message, triggerTurn) => {
|
|
316
|
+
const wasStreaming = this.session.isStreaming;
|
|
317
|
+
this.session
|
|
318
|
+
.sendHookMessage(message, triggerTurn)
|
|
319
|
+
.then(() => {
|
|
320
|
+
// For non-streaming cases with display=true, update UI
|
|
321
|
+
// (streaming cases update via message_end event)
|
|
322
|
+
if (!wasStreaming && message.display) {
|
|
323
|
+
this.rebuildChatFromMessages();
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
.catch((err) => {
|
|
327
|
+
this.showError(`Hook sendMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
appendEntryHandler: (customType, data) => {
|
|
331
|
+
this.sessionManager.appendCustomEntry(customType, data);
|
|
332
|
+
},
|
|
333
|
+
newSessionHandler: async (options) => {
|
|
334
|
+
// Stop any loading animation
|
|
335
|
+
if (this.loadingAnimation) {
|
|
336
|
+
this.loadingAnimation.stop();
|
|
337
|
+
this.loadingAnimation = undefined;
|
|
338
|
+
}
|
|
339
|
+
this.statusContainer.clear();
|
|
340
|
+
// Create new session
|
|
341
|
+
const success = await this.session.newSession({ parentSession: options?.parentSession });
|
|
342
|
+
if (!success) {
|
|
343
|
+
return { cancelled: true };
|
|
344
|
+
}
|
|
345
|
+
// Call setup callback if provided
|
|
346
|
+
if (options?.setup) {
|
|
347
|
+
await options.setup(this.sessionManager);
|
|
348
|
+
}
|
|
349
|
+
// Clear UI state
|
|
350
|
+
this.chatContainer.clear();
|
|
351
|
+
this.pendingMessagesContainer.clear();
|
|
352
|
+
this.streamingComponent = undefined;
|
|
353
|
+
this.streamingMessage = undefined;
|
|
354
|
+
this.pendingTools.clear();
|
|
355
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
356
|
+
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
357
|
+
this.ui.requestRender();
|
|
358
|
+
return { cancelled: false };
|
|
359
|
+
},
|
|
360
|
+
branchHandler: async (entryId) => {
|
|
361
|
+
const result = await this.session.branch(entryId);
|
|
362
|
+
if (result.cancelled) {
|
|
363
|
+
return { cancelled: true };
|
|
364
|
+
}
|
|
365
|
+
// Update UI
|
|
366
|
+
this.chatContainer.clear();
|
|
367
|
+
this.renderInitialMessages();
|
|
368
|
+
this.editor.setText(result.selectedText);
|
|
369
|
+
this.showStatus("Branched to new session");
|
|
370
|
+
return { cancelled: false };
|
|
371
|
+
},
|
|
372
|
+
navigateTreeHandler: async (targetId, options) => {
|
|
373
|
+
const result = await this.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
374
|
+
if (result.cancelled) {
|
|
375
|
+
return { cancelled: true };
|
|
376
|
+
}
|
|
377
|
+
// Update UI
|
|
378
|
+
this.chatContainer.clear();
|
|
379
|
+
this.renderInitialMessages();
|
|
380
|
+
if (result.editorText) {
|
|
381
|
+
this.editor.setText(result.editorText);
|
|
382
|
+
}
|
|
383
|
+
this.showStatus("Navigated to selected point");
|
|
384
|
+
return { cancelled: false };
|
|
385
|
+
},
|
|
386
|
+
isIdle: () => !this.session.isStreaming,
|
|
387
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
388
|
+
abort: () => {
|
|
389
|
+
this.session.abort();
|
|
390
|
+
},
|
|
391
|
+
hasQueuedMessages: () => this.session.queuedMessageCount > 0,
|
|
392
|
+
uiContext,
|
|
393
|
+
hasUI: true,
|
|
394
|
+
});
|
|
289
395
|
// Subscribe to hook errors
|
|
290
396
|
hookRunner.onError((error) => {
|
|
291
397
|
this.showHookError(error.hookPath, error.error);
|
|
292
398
|
});
|
|
293
|
-
// Set up send handler for pi.send()
|
|
294
|
-
hookRunner.setSendHandler((text, attachments) => {
|
|
295
|
-
this.handleHookSend(text, attachments);
|
|
296
|
-
});
|
|
297
399
|
// Show loaded hooks
|
|
298
400
|
const hookPaths = hookRunner.getHookPaths();
|
|
299
401
|
if (hookPaths.length > 0) {
|
|
@@ -301,23 +403,28 @@ export class InteractiveMode {
|
|
|
301
403
|
this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded hooks:\n") + hookList, 0, 0));
|
|
302
404
|
this.chatContainer.addChild(new Spacer(1));
|
|
303
405
|
}
|
|
304
|
-
// Emit
|
|
406
|
+
// Emit session_start event
|
|
305
407
|
await hookRunner.emit({
|
|
306
|
-
type: "
|
|
307
|
-
entries,
|
|
308
|
-
sessionFile: this.session.sessionFile,
|
|
309
|
-
previousSessionFile: null,
|
|
310
|
-
reason: "start",
|
|
408
|
+
type: "session_start",
|
|
311
409
|
});
|
|
312
410
|
}
|
|
313
411
|
/**
|
|
314
412
|
* Emit session event to all custom tools.
|
|
315
413
|
*/
|
|
316
|
-
async
|
|
414
|
+
async emitCustomToolSessionEvent(event) {
|
|
317
415
|
for (const { tool } of this.customTools.values()) {
|
|
318
416
|
if (tool.onSession) {
|
|
319
417
|
try {
|
|
320
|
-
await tool.onSession(event
|
|
418
|
+
await tool.onSession(event, {
|
|
419
|
+
sessionManager: this.session.sessionManager,
|
|
420
|
+
modelRegistry: this.session.modelRegistry,
|
|
421
|
+
model: this.session.model,
|
|
422
|
+
isIdle: () => !this.session.isStreaming,
|
|
423
|
+
hasQueuedMessages: () => this.session.queuedMessageCount > 0,
|
|
424
|
+
abort: () => {
|
|
425
|
+
this.session.abort();
|
|
426
|
+
},
|
|
427
|
+
});
|
|
321
428
|
}
|
|
322
429
|
catch (err) {
|
|
323
430
|
this.showToolError(tool.name, err instanceof Error ? err.message : String(err));
|
|
@@ -334,15 +441,11 @@ export class InteractiveMode {
|
|
|
334
441
|
this.ui.requestRender();
|
|
335
442
|
}
|
|
336
443
|
/**
|
|
337
|
-
*
|
|
444
|
+
* Set hook status text in the footer.
|
|
338
445
|
*/
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
confirm: (title, message) => this.showHookConfirm(title, message),
|
|
343
|
-
input: (title, placeholder) => this.showHookInput(title, placeholder),
|
|
344
|
-
notify: (message, type) => this.showHookNotify(message, type),
|
|
345
|
-
};
|
|
446
|
+
setHookStatus(key, text) {
|
|
447
|
+
this.footer.setHookStatus(key, text);
|
|
448
|
+
this.ui.requestRender();
|
|
346
449
|
}
|
|
347
450
|
/**
|
|
348
451
|
* Show a selector for hooks.
|
|
@@ -354,7 +457,7 @@ export class InteractiveMode {
|
|
|
354
457
|
resolve(option);
|
|
355
458
|
}, () => {
|
|
356
459
|
this.hideHookSelector();
|
|
357
|
-
resolve(
|
|
460
|
+
resolve(undefined);
|
|
358
461
|
});
|
|
359
462
|
this.editorContainer.clear();
|
|
360
463
|
this.editorContainer.addChild(this.hookSelector);
|
|
@@ -368,7 +471,7 @@ export class InteractiveMode {
|
|
|
368
471
|
hideHookSelector() {
|
|
369
472
|
this.editorContainer.clear();
|
|
370
473
|
this.editorContainer.addChild(this.editor);
|
|
371
|
-
this.hookSelector =
|
|
474
|
+
this.hookSelector = undefined;
|
|
372
475
|
this.ui.setFocus(this.editor);
|
|
373
476
|
this.ui.requestRender();
|
|
374
477
|
}
|
|
@@ -389,7 +492,7 @@ export class InteractiveMode {
|
|
|
389
492
|
resolve(value);
|
|
390
493
|
}, () => {
|
|
391
494
|
this.hideHookInput();
|
|
392
|
-
resolve(
|
|
495
|
+
resolve(undefined);
|
|
393
496
|
});
|
|
394
497
|
this.editorContainer.clear();
|
|
395
498
|
this.editorContainer.addChild(this.hookInput);
|
|
@@ -403,7 +506,35 @@ export class InteractiveMode {
|
|
|
403
506
|
hideHookInput() {
|
|
404
507
|
this.editorContainer.clear();
|
|
405
508
|
this.editorContainer.addChild(this.editor);
|
|
406
|
-
this.hookInput =
|
|
509
|
+
this.hookInput = undefined;
|
|
510
|
+
this.ui.setFocus(this.editor);
|
|
511
|
+
this.ui.requestRender();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Show a multi-line editor for hooks (with Ctrl+G support).
|
|
515
|
+
*/
|
|
516
|
+
showHookEditor(title, prefill) {
|
|
517
|
+
return new Promise((resolve) => {
|
|
518
|
+
this.hookEditor = new HookEditorComponent(this.ui, title, prefill, (value) => {
|
|
519
|
+
this.hideHookEditor();
|
|
520
|
+
resolve(value);
|
|
521
|
+
}, () => {
|
|
522
|
+
this.hideHookEditor();
|
|
523
|
+
resolve(undefined);
|
|
524
|
+
});
|
|
525
|
+
this.editorContainer.clear();
|
|
526
|
+
this.editorContainer.addChild(this.hookEditor);
|
|
527
|
+
this.ui.setFocus(this.hookEditor);
|
|
528
|
+
this.ui.requestRender();
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Hide the hook editor.
|
|
533
|
+
*/
|
|
534
|
+
hideHookEditor() {
|
|
535
|
+
this.editorContainer.clear();
|
|
536
|
+
this.editorContainer.addChild(this.editor);
|
|
537
|
+
this.hookEditor = undefined;
|
|
407
538
|
this.ui.setFocus(this.editor);
|
|
408
539
|
this.ui.requestRender();
|
|
409
540
|
}
|
|
@@ -421,6 +552,31 @@ export class InteractiveMode {
|
|
|
421
552
|
this.showStatus(message);
|
|
422
553
|
}
|
|
423
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Show a custom component with keyboard focus.
|
|
557
|
+
*/
|
|
558
|
+
async showHookCustom(factory) {
|
|
559
|
+
const savedText = this.editor.getText();
|
|
560
|
+
return new Promise((resolve) => {
|
|
561
|
+
let component;
|
|
562
|
+
const close = (result) => {
|
|
563
|
+
component.dispose?.();
|
|
564
|
+
this.editorContainer.clear();
|
|
565
|
+
this.editorContainer.addChild(this.editor);
|
|
566
|
+
this.editor.setText(savedText);
|
|
567
|
+
this.ui.setFocus(this.editor);
|
|
568
|
+
this.ui.requestRender();
|
|
569
|
+
resolve(result);
|
|
570
|
+
};
|
|
571
|
+
Promise.resolve(factory(this.ui, theme, close)).then((c) => {
|
|
572
|
+
component = c;
|
|
573
|
+
this.editorContainer.clear();
|
|
574
|
+
this.editorContainer.addChild(component);
|
|
575
|
+
this.ui.setFocus(component);
|
|
576
|
+
this.ui.requestRender();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
}
|
|
424
580
|
/**
|
|
425
581
|
* Show a hook error in the UI.
|
|
426
582
|
*/
|
|
@@ -433,19 +589,6 @@ export class InteractiveMode {
|
|
|
433
589
|
* Handle pi.send() from hooks.
|
|
434
590
|
* If streaming, queue the message. Otherwise, start a new agent loop.
|
|
435
591
|
*/
|
|
436
|
-
handleHookSend(text, attachments) {
|
|
437
|
-
if (this.session.isStreaming) {
|
|
438
|
-
// Queue the message for later (note: attachments are lost when queuing)
|
|
439
|
-
this.session.queueMessage(text);
|
|
440
|
-
this.updatePendingMessagesDisplay();
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
// Start a new agent loop immediately
|
|
444
|
-
this.session.prompt(text, { attachments }).catch((err) => {
|
|
445
|
-
this.showError(err instanceof Error ? err.message : String(err));
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
592
|
// =========================================================================
|
|
450
593
|
// Key Handlers
|
|
451
594
|
// =========================================================================
|
|
@@ -487,6 +630,8 @@ export class InteractiveMode {
|
|
|
487
630
|
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
488
631
|
this.editor.onCtrlP = () => this.cycleModel("forward");
|
|
489
632
|
this.editor.onShiftCtrlP = () => this.cycleModel("backward");
|
|
633
|
+
// Global debug handler on TUI (works regardless of focus)
|
|
634
|
+
this.ui.onDebug = () => this.handleDebugCommand();
|
|
490
635
|
this.editor.onCtrlL = () => this.showModelSelector();
|
|
491
636
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
492
637
|
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
@@ -520,6 +665,11 @@ export class InteractiveMode {
|
|
|
520
665
|
this.editor.setText("");
|
|
521
666
|
return;
|
|
522
667
|
}
|
|
668
|
+
if (text === "/share") {
|
|
669
|
+
await this.handleShareCommand();
|
|
670
|
+
this.editor.setText("");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
523
673
|
if (text === "/copy") {
|
|
524
674
|
this.handleCopyCommand();
|
|
525
675
|
this.editor.setText("");
|
|
@@ -545,6 +695,11 @@ export class InteractiveMode {
|
|
|
545
695
|
this.editor.setText("");
|
|
546
696
|
return;
|
|
547
697
|
}
|
|
698
|
+
if (text === "/tree") {
|
|
699
|
+
this.showTreeSelector();
|
|
700
|
+
this.editor.setText("");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
548
703
|
if (text === "/login") {
|
|
549
704
|
this.showOAuthSelector("login");
|
|
550
705
|
this.editor.setText("");
|
|
@@ -607,7 +762,20 @@ export class InteractiveMode {
|
|
|
607
762
|
if (this.session.isCompacting) {
|
|
608
763
|
return;
|
|
609
764
|
}
|
|
610
|
-
//
|
|
765
|
+
// Hook commands always run immediately, even during streaming
|
|
766
|
+
// (if they need to interact with LLM, they use pi.sendMessage which handles queueing)
|
|
767
|
+
if (text.startsWith("/") && this.session.hookRunner) {
|
|
768
|
+
const spaceIndex = text.indexOf(" ");
|
|
769
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
770
|
+
const command = this.session.hookRunner.getCommand(commandName);
|
|
771
|
+
if (command) {
|
|
772
|
+
this.editor.addToHistory(text);
|
|
773
|
+
this.editor.setText("");
|
|
774
|
+
await this.session.prompt(text);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Queue regular messages if agent is streaming
|
|
611
779
|
if (this.session.isStreaming) {
|
|
612
780
|
await this.session.queueMessage(text);
|
|
613
781
|
this.updatePendingMessagesDisplay();
|
|
@@ -627,14 +795,14 @@ export class InteractiveMode {
|
|
|
627
795
|
}
|
|
628
796
|
subscribeToAgent() {
|
|
629
797
|
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
630
|
-
await this.handleEvent(event
|
|
798
|
+
await this.handleEvent(event);
|
|
631
799
|
});
|
|
632
800
|
}
|
|
633
|
-
async handleEvent(event
|
|
801
|
+
async handleEvent(event) {
|
|
634
802
|
if (!this.isInitialized) {
|
|
635
803
|
await this.init();
|
|
636
804
|
}
|
|
637
|
-
this.footer.
|
|
805
|
+
this.footer.invalidate();
|
|
638
806
|
switch (event.type) {
|
|
639
807
|
case "agent_start":
|
|
640
808
|
if (this.loadingAnimation) {
|
|
@@ -646,7 +814,11 @@ export class InteractiveMode {
|
|
|
646
814
|
this.ui.requestRender();
|
|
647
815
|
break;
|
|
648
816
|
case "message_start":
|
|
649
|
-
if (event.message.role === "
|
|
817
|
+
if (event.message.role === "hookMessage") {
|
|
818
|
+
this.addMessageToChat(event.message);
|
|
819
|
+
this.ui.requestRender();
|
|
820
|
+
}
|
|
821
|
+
else if (event.message.role === "user") {
|
|
650
822
|
this.addMessageToChat(event.message);
|
|
651
823
|
this.editor.setText("");
|
|
652
824
|
this.updatePendingMessagesDisplay();
|
|
@@ -654,22 +826,24 @@ export class InteractiveMode {
|
|
|
654
826
|
}
|
|
655
827
|
else if (event.message.role === "assistant") {
|
|
656
828
|
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
|
|
829
|
+
this.streamingMessage = event.message;
|
|
657
830
|
this.chatContainer.addChild(this.streamingComponent);
|
|
658
|
-
this.streamingComponent.updateContent(
|
|
831
|
+
this.streamingComponent.updateContent(this.streamingMessage);
|
|
659
832
|
this.ui.requestRender();
|
|
660
833
|
}
|
|
661
834
|
break;
|
|
662
835
|
case "message_update":
|
|
663
836
|
if (this.streamingComponent && event.message.role === "assistant") {
|
|
664
|
-
|
|
665
|
-
this.streamingComponent.updateContent(
|
|
666
|
-
for (const content of
|
|
837
|
+
this.streamingMessage = event.message;
|
|
838
|
+
this.streamingComponent.updateContent(this.streamingMessage);
|
|
839
|
+
for (const content of this.streamingMessage.content) {
|
|
667
840
|
if (content.type === "toolCall") {
|
|
668
841
|
if (!this.pendingTools.has(content.id)) {
|
|
669
842
|
this.chatContainer.addChild(new Text("", 0, 0));
|
|
670
843
|
const component = new ToolExecutionComponent(content.name, content.arguments, {
|
|
671
844
|
showImages: this.settingsManager.getShowImages(),
|
|
672
845
|
}, this.customTools.get(content.name)?.tool, this.ui);
|
|
846
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
673
847
|
this.chatContainer.addChild(component);
|
|
674
848
|
this.pendingTools.set(content.id, component);
|
|
675
849
|
}
|
|
@@ -688,10 +862,12 @@ export class InteractiveMode {
|
|
|
688
862
|
if (event.message.role === "user")
|
|
689
863
|
break;
|
|
690
864
|
if (this.streamingComponent && event.message.role === "assistant") {
|
|
691
|
-
|
|
692
|
-
this.streamingComponent.updateContent(
|
|
693
|
-
if (
|
|
694
|
-
const errorMessage =
|
|
865
|
+
this.streamingMessage = event.message;
|
|
866
|
+
this.streamingComponent.updateContent(this.streamingMessage);
|
|
867
|
+
if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
|
|
868
|
+
const errorMessage = this.streamingMessage.stopReason === "aborted"
|
|
869
|
+
? "Operation aborted"
|
|
870
|
+
: this.streamingMessage.errorMessage || "Error";
|
|
695
871
|
for (const [, component] of this.pendingTools.entries()) {
|
|
696
872
|
component.updateResult({
|
|
697
873
|
content: [{ type: "text", text: errorMessage }],
|
|
@@ -700,7 +876,14 @@ export class InteractiveMode {
|
|
|
700
876
|
}
|
|
701
877
|
this.pendingTools.clear();
|
|
702
878
|
}
|
|
703
|
-
|
|
879
|
+
else {
|
|
880
|
+
// Args are now complete - trigger diff computation for edit tools
|
|
881
|
+
for (const [, component] of this.pendingTools.entries()) {
|
|
882
|
+
component.setArgsComplete();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
this.streamingComponent = undefined;
|
|
886
|
+
this.streamingMessage = undefined;
|
|
704
887
|
this.footer.invalidate();
|
|
705
888
|
}
|
|
706
889
|
this.ui.requestRender();
|
|
@@ -710,6 +893,7 @@ export class InteractiveMode {
|
|
|
710
893
|
const component = new ToolExecutionComponent(event.toolName, event.args, {
|
|
711
894
|
showImages: this.settingsManager.getShowImages(),
|
|
712
895
|
}, this.customTools.get(event.toolName)?.tool, this.ui);
|
|
896
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
713
897
|
this.chatContainer.addChild(component);
|
|
714
898
|
this.pendingTools.set(event.toolCallId, component);
|
|
715
899
|
this.ui.requestRender();
|
|
@@ -736,12 +920,13 @@ export class InteractiveMode {
|
|
|
736
920
|
case "agent_end":
|
|
737
921
|
if (this.loadingAnimation) {
|
|
738
922
|
this.loadingAnimation.stop();
|
|
739
|
-
this.loadingAnimation =
|
|
923
|
+
this.loadingAnimation = undefined;
|
|
740
924
|
this.statusContainer.clear();
|
|
741
925
|
}
|
|
742
926
|
if (this.streamingComponent) {
|
|
743
927
|
this.chatContainer.removeChild(this.streamingComponent);
|
|
744
|
-
this.streamingComponent =
|
|
928
|
+
this.streamingComponent = undefined;
|
|
929
|
+
this.streamingMessage = undefined;
|
|
745
930
|
}
|
|
746
931
|
this.pendingTools.clear();
|
|
747
932
|
this.ui.requestRender();
|
|
@@ -773,7 +958,7 @@ export class InteractiveMode {
|
|
|
773
958
|
// Stop loader
|
|
774
959
|
if (this.autoCompactionLoader) {
|
|
775
960
|
this.autoCompactionLoader.stop();
|
|
776
|
-
this.autoCompactionLoader =
|
|
961
|
+
this.autoCompactionLoader = undefined;
|
|
777
962
|
this.statusContainer.clear();
|
|
778
963
|
}
|
|
779
964
|
// Handle result
|
|
@@ -784,11 +969,14 @@ export class InteractiveMode {
|
|
|
784
969
|
// Rebuild chat to show compacted state
|
|
785
970
|
this.chatContainer.clear();
|
|
786
971
|
this.rebuildChatFromMessages();
|
|
787
|
-
// Add compaction component
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
972
|
+
// Add compaction component at bottom so user sees it without scrolling
|
|
973
|
+
this.addMessageToChat({
|
|
974
|
+
role: "compactionSummary",
|
|
975
|
+
tokensBefore: event.result.tokensBefore,
|
|
976
|
+
summary: event.result.summary,
|
|
977
|
+
timestamp: Date.now(),
|
|
978
|
+
});
|
|
979
|
+
this.footer.invalidate();
|
|
792
980
|
}
|
|
793
981
|
this.ui.requestRender();
|
|
794
982
|
break;
|
|
@@ -816,7 +1004,7 @@ export class InteractiveMode {
|
|
|
816
1004
|
// Stop loader
|
|
817
1005
|
if (this.retryLoader) {
|
|
818
1006
|
this.retryLoader.stop();
|
|
819
|
-
this.retryLoader =
|
|
1007
|
+
this.retryLoader = undefined;
|
|
820
1008
|
this.statusContainer.clear();
|
|
821
1009
|
}
|
|
822
1010
|
// Show error only on final failure (success shows normal response)
|
|
@@ -837,87 +1025,110 @@ export class InteractiveMode {
|
|
|
837
1025
|
: message.content.filter((c) => c.type === "text");
|
|
838
1026
|
return textBlocks.map((c) => c.text).join("");
|
|
839
1027
|
}
|
|
840
|
-
/**
|
|
1028
|
+
/**
|
|
1029
|
+
* Show a status message in the chat.
|
|
1030
|
+
*
|
|
1031
|
+
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
1032
|
+
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
1033
|
+
*/
|
|
841
1034
|
showStatus(message) {
|
|
842
|
-
this.chatContainer.
|
|
843
|
-
|
|
1035
|
+
const children = this.chatContainer.children;
|
|
1036
|
+
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1037
|
+
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
1038
|
+
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
1039
|
+
this.lastStatusText.setText(theme.fg("dim", message));
|
|
1040
|
+
this.ui.requestRender();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const spacer = new Spacer(1);
|
|
1044
|
+
const text = new Text(theme.fg("dim", message), 1, 0);
|
|
1045
|
+
this.chatContainer.addChild(spacer);
|
|
1046
|
+
this.chatContainer.addChild(text);
|
|
1047
|
+
this.lastStatusSpacer = spacer;
|
|
1048
|
+
this.lastStatusText = text;
|
|
844
1049
|
this.ui.requestRender();
|
|
845
1050
|
}
|
|
846
|
-
addMessageToChat(message) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1051
|
+
addMessageToChat(message, options) {
|
|
1052
|
+
switch (message.role) {
|
|
1053
|
+
case "bashExecution": {
|
|
1054
|
+
const component = new BashExecutionComponent(message.command, this.ui);
|
|
1055
|
+
if (message.output) {
|
|
1056
|
+
component.appendOutput(message.output);
|
|
1057
|
+
}
|
|
1058
|
+
component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
|
|
1059
|
+
this.chatContainer.addChild(component);
|
|
1060
|
+
break;
|
|
851
1061
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
this.chatContainer.addChild(
|
|
861
|
-
|
|
1062
|
+
case "hookMessage": {
|
|
1063
|
+
if (message.display) {
|
|
1064
|
+
const renderer = this.session.hookRunner?.getMessageRenderer(message.customType);
|
|
1065
|
+
this.chatContainer.addChild(new HookMessageComponent(message, renderer));
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
case "compactionSummary": {
|
|
1070
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1071
|
+
const component = new CompactionSummaryMessageComponent(message);
|
|
1072
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
1073
|
+
this.chatContainer.addChild(component);
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
case "branchSummary": {
|
|
1077
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1078
|
+
const component = new BranchSummaryMessageComponent(message);
|
|
1079
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
1080
|
+
this.chatContainer.addChild(component);
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
case "user": {
|
|
1084
|
+
const textContent = this.getUserMessageText(message);
|
|
1085
|
+
if (textContent) {
|
|
1086
|
+
const userComponent = new UserMessageComponent(textContent);
|
|
1087
|
+
this.chatContainer.addChild(userComponent);
|
|
1088
|
+
if (options?.populateHistory) {
|
|
1089
|
+
this.editor.addToHistory(textContent);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
case "assistant": {
|
|
1095
|
+
const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
|
|
1096
|
+
this.chatContainer.addChild(assistantComponent);
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "toolResult": {
|
|
1100
|
+
// Tool results are rendered inline with tool calls, handled separately
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
default: {
|
|
1104
|
+
const _exhaustive = message;
|
|
862
1105
|
}
|
|
863
|
-
}
|
|
864
|
-
else if (message.role === "assistant") {
|
|
865
|
-
const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock);
|
|
866
|
-
this.chatContainer.addChild(assistantComponent);
|
|
867
1106
|
}
|
|
868
1107
|
}
|
|
869
1108
|
/**
|
|
870
|
-
* Render
|
|
871
|
-
* @param
|
|
1109
|
+
* Render session context to chat. Used for initial load and rebuild after compaction.
|
|
1110
|
+
* @param sessionContext Session context to render
|
|
872
1111
|
* @param options.updateFooter Update footer state
|
|
873
1112
|
* @param options.populateHistory Add user messages to editor history
|
|
874
1113
|
*/
|
|
875
|
-
|
|
876
|
-
this.isFirstUserMessage = true;
|
|
1114
|
+
renderSessionContext(sessionContext, options = {}) {
|
|
877
1115
|
this.pendingTools.clear();
|
|
878
1116
|
if (options.updateFooter) {
|
|
879
|
-
this.footer.
|
|
1117
|
+
this.footer.invalidate();
|
|
880
1118
|
this.updateEditorBorderColor();
|
|
881
1119
|
}
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
if (
|
|
1120
|
+
for (const message of sessionContext.messages) {
|
|
1121
|
+
// Assistant messages need special handling for tool calls
|
|
1122
|
+
if (message.role === "assistant") {
|
|
885
1123
|
this.addMessageToChat(message);
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (message.role === "user") {
|
|
889
|
-
const textContent = this.getUserMessageText(message);
|
|
890
|
-
if (textContent) {
|
|
891
|
-
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
892
|
-
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
893
|
-
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
894
|
-
component.setExpanded(this.toolOutputExpanded);
|
|
895
|
-
this.chatContainer.addChild(component);
|
|
896
|
-
}
|
|
897
|
-
else {
|
|
898
|
-
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
899
|
-
this.chatContainer.addChild(userComponent);
|
|
900
|
-
this.isFirstUserMessage = false;
|
|
901
|
-
if (options.populateHistory) {
|
|
902
|
-
this.editor.addToHistory(textContent);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
else if (message.role === "assistant") {
|
|
908
|
-
const assistantMsg = message;
|
|
909
|
-
const assistantComponent = new AssistantMessageComponent(assistantMsg, this.hideThinkingBlock);
|
|
910
|
-
this.chatContainer.addChild(assistantComponent);
|
|
911
|
-
for (const content of assistantMsg.content) {
|
|
1124
|
+
// Render tool call components
|
|
1125
|
+
for (const content of message.content) {
|
|
912
1126
|
if (content.type === "toolCall") {
|
|
913
|
-
const component = new ToolExecutionComponent(content.name, content.arguments, {
|
|
914
|
-
|
|
915
|
-
}, this.customTools.get(content.name)?.tool, this.ui);
|
|
1127
|
+
const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.customTools.get(content.name)?.tool, this.ui);
|
|
1128
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
916
1129
|
this.chatContainer.addChild(component);
|
|
917
|
-
if (
|
|
918
|
-
const errorMessage =
|
|
919
|
-
? "Operation aborted"
|
|
920
|
-
: assistantMsg.errorMessage || "Error";
|
|
1130
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
1131
|
+
const errorMessage = message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
|
|
921
1132
|
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
922
1133
|
}
|
|
923
1134
|
else {
|
|
@@ -927,21 +1138,31 @@ export class InteractiveMode {
|
|
|
927
1138
|
}
|
|
928
1139
|
}
|
|
929
1140
|
else if (message.role === "toolResult") {
|
|
1141
|
+
// Match tool results to pending tool components
|
|
930
1142
|
const component = this.pendingTools.get(message.toolCallId);
|
|
931
1143
|
if (component) {
|
|
932
1144
|
component.updateResult(message);
|
|
933
1145
|
this.pendingTools.delete(message.toolCallId);
|
|
934
1146
|
}
|
|
935
1147
|
}
|
|
1148
|
+
else {
|
|
1149
|
+
// All other messages use standard rendering
|
|
1150
|
+
this.addMessageToChat(message, options);
|
|
1151
|
+
}
|
|
936
1152
|
}
|
|
937
1153
|
this.pendingTools.clear();
|
|
938
1154
|
this.ui.requestRender();
|
|
939
1155
|
}
|
|
940
|
-
renderInitialMessages(
|
|
941
|
-
|
|
1156
|
+
renderInitialMessages() {
|
|
1157
|
+
// Get aligned messages and entries from session context
|
|
1158
|
+
const context = this.sessionManager.buildSessionContext();
|
|
1159
|
+
this.renderSessionContext(context, {
|
|
1160
|
+
updateFooter: true,
|
|
1161
|
+
populateHistory: true,
|
|
1162
|
+
});
|
|
942
1163
|
// Show compaction info if session was compacted
|
|
943
|
-
const
|
|
944
|
-
const compactionCount =
|
|
1164
|
+
const allEntries = this.sessionManager.getEntries();
|
|
1165
|
+
const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
|
|
945
1166
|
if (compactionCount > 0) {
|
|
946
1167
|
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
947
1168
|
this.showStatus(`Session compacted ${times}`);
|
|
@@ -956,7 +1177,9 @@ export class InteractiveMode {
|
|
|
956
1177
|
});
|
|
957
1178
|
}
|
|
958
1179
|
rebuildChatFromMessages() {
|
|
959
|
-
this.
|
|
1180
|
+
this.chatContainer.clear();
|
|
1181
|
+
const context = this.sessionManager.buildSessionContext();
|
|
1182
|
+
this.renderSessionContext(context);
|
|
960
1183
|
}
|
|
961
1184
|
// =========================================================================
|
|
962
1185
|
// Key handlers
|
|
@@ -977,21 +1200,18 @@ export class InteractiveMode {
|
|
|
977
1200
|
}
|
|
978
1201
|
/**
|
|
979
1202
|
* Gracefully shutdown the agent.
|
|
980
|
-
* Emits shutdown event to hooks, then exits.
|
|
1203
|
+
* Emits shutdown event to hooks and tools, then exits.
|
|
981
1204
|
*/
|
|
982
1205
|
async shutdown() {
|
|
983
1206
|
// Emit shutdown event to hooks
|
|
984
1207
|
const hookRunner = this.session.hookRunner;
|
|
985
|
-
if (hookRunner?.hasHandlers("
|
|
986
|
-
const entries = this.sessionManager.getEntries();
|
|
1208
|
+
if (hookRunner?.hasHandlers("session_shutdown")) {
|
|
987
1209
|
await hookRunner.emit({
|
|
988
|
-
type: "
|
|
989
|
-
entries,
|
|
990
|
-
sessionFile: this.session.sessionFile,
|
|
991
|
-
previousSessionFile: null,
|
|
992
|
-
reason: "shutdown",
|
|
1210
|
+
type: "session_shutdown",
|
|
993
1211
|
});
|
|
994
1212
|
}
|
|
1213
|
+
// Emit shutdown event to custom tools
|
|
1214
|
+
await this.session.emitCustomToolSessionEvent("shutdown");
|
|
995
1215
|
this.stop();
|
|
996
1216
|
process.exit(0);
|
|
997
1217
|
}
|
|
@@ -1018,11 +1238,11 @@ export class InteractiveMode {
|
|
|
1018
1238
|
}
|
|
1019
1239
|
cycleThinkingLevel() {
|
|
1020
1240
|
const newLevel = this.session.cycleThinkingLevel();
|
|
1021
|
-
if (newLevel ===
|
|
1241
|
+
if (newLevel === undefined) {
|
|
1022
1242
|
this.showStatus("Current model does not support thinking");
|
|
1023
1243
|
}
|
|
1024
1244
|
else {
|
|
1025
|
-
this.footer.
|
|
1245
|
+
this.footer.invalidate();
|
|
1026
1246
|
this.updateEditorBorderColor();
|
|
1027
1247
|
this.showStatus(`Thinking level: ${newLevel}`);
|
|
1028
1248
|
}
|
|
@@ -1030,12 +1250,12 @@ export class InteractiveMode {
|
|
|
1030
1250
|
async cycleModel(direction) {
|
|
1031
1251
|
try {
|
|
1032
1252
|
const result = await this.session.cycleModel(direction);
|
|
1033
|
-
if (result ===
|
|
1253
|
+
if (result === undefined) {
|
|
1034
1254
|
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
|
1035
1255
|
this.showStatus(msg);
|
|
1036
1256
|
}
|
|
1037
1257
|
else {
|
|
1038
|
-
this.footer.
|
|
1258
|
+
this.footer.invalidate();
|
|
1039
1259
|
this.updateEditorBorderColor();
|
|
1040
1260
|
const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
1041
1261
|
this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
|
|
@@ -1048,13 +1268,7 @@ export class InteractiveMode {
|
|
|
1048
1268
|
toggleToolOutputExpansion() {
|
|
1049
1269
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
1050
1270
|
for (const child of this.chatContainer.children) {
|
|
1051
|
-
if (child
|
|
1052
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
1053
|
-
}
|
|
1054
|
-
else if (child instanceof CompactionComponent) {
|
|
1055
|
-
child.setExpanded(this.toolOutputExpanded);
|
|
1056
|
-
}
|
|
1057
|
-
else if (child instanceof BashExecutionComponent) {
|
|
1271
|
+
if (isExpandable(child)) {
|
|
1058
1272
|
child.setExpanded(this.toolOutputExpanded);
|
|
1059
1273
|
}
|
|
1060
1274
|
}
|
|
@@ -1063,13 +1277,15 @@ export class InteractiveMode {
|
|
|
1063
1277
|
toggleThinkingBlockVisibility() {
|
|
1064
1278
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
1065
1279
|
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
1066
|
-
|
|
1067
|
-
if (child instanceof AssistantMessageComponent) {
|
|
1068
|
-
child.setHideThinkingBlock(this.hideThinkingBlock);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1280
|
+
// Rebuild chat from session messages
|
|
1071
1281
|
this.chatContainer.clear();
|
|
1072
1282
|
this.rebuildChatFromMessages();
|
|
1283
|
+
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
1284
|
+
if (this.streamingComponent && this.streamingMessage) {
|
|
1285
|
+
this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock);
|
|
1286
|
+
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1287
|
+
this.chatContainer.addChild(this.streamingComponent);
|
|
1288
|
+
}
|
|
1073
1289
|
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
1074
1290
|
}
|
|
1075
1291
|
openExternalEditor() {
|
|
@@ -1207,7 +1423,7 @@ export class InteractiveMode {
|
|
|
1207
1423
|
},
|
|
1208
1424
|
onThinkingLevelChange: (level) => {
|
|
1209
1425
|
this.session.setThinkingLevel(level);
|
|
1210
|
-
this.footer.
|
|
1426
|
+
this.footer.invalidate();
|
|
1211
1427
|
this.updateEditorBorderColor();
|
|
1212
1428
|
},
|
|
1213
1429
|
onThemeChange: (themeName) => {
|
|
@@ -1252,7 +1468,7 @@ export class InteractiveMode {
|
|
|
1252
1468
|
const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
|
|
1253
1469
|
try {
|
|
1254
1470
|
await this.session.setModel(model);
|
|
1255
|
-
this.footer.
|
|
1471
|
+
this.footer.invalidate();
|
|
1256
1472
|
this.updateEditorBorderColor();
|
|
1257
1473
|
done();
|
|
1258
1474
|
this.showStatus(`Model: ${model.id}`);
|
|
@@ -1275,8 +1491,8 @@ export class InteractiveMode {
|
|
|
1275
1491
|
return;
|
|
1276
1492
|
}
|
|
1277
1493
|
this.showSelector((done) => {
|
|
1278
|
-
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({
|
|
1279
|
-
const result = await this.session.branch(
|
|
1494
|
+
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
|
|
1495
|
+
const result = await this.session.branch(entryId);
|
|
1280
1496
|
if (result.cancelled) {
|
|
1281
1497
|
// Hook cancelled the branch
|
|
1282
1498
|
done();
|
|
@@ -1284,8 +1500,7 @@ export class InteractiveMode {
|
|
|
1284
1500
|
return;
|
|
1285
1501
|
}
|
|
1286
1502
|
this.chatContainer.clear();
|
|
1287
|
-
this.
|
|
1288
|
-
this.renderInitialMessages(this.session.state);
|
|
1503
|
+
this.renderInitialMessages();
|
|
1289
1504
|
this.editor.setText(result.selectedText);
|
|
1290
1505
|
done();
|
|
1291
1506
|
this.showStatus("Branched to new session");
|
|
@@ -1296,6 +1511,86 @@ export class InteractiveMode {
|
|
|
1296
1511
|
return { component: selector, focus: selector.getMessageList() };
|
|
1297
1512
|
});
|
|
1298
1513
|
}
|
|
1514
|
+
showTreeSelector() {
|
|
1515
|
+
const tree = this.sessionManager.getTree();
|
|
1516
|
+
const realLeafId = this.sessionManager.getLeafId();
|
|
1517
|
+
// Find the visible leaf for display (skip metadata entries like labels)
|
|
1518
|
+
let visibleLeafId = realLeafId;
|
|
1519
|
+
while (visibleLeafId) {
|
|
1520
|
+
const entry = this.sessionManager.getEntry(visibleLeafId);
|
|
1521
|
+
if (!entry)
|
|
1522
|
+
break;
|
|
1523
|
+
if (entry.type !== "label" && entry.type !== "custom")
|
|
1524
|
+
break;
|
|
1525
|
+
visibleLeafId = entry.parentId ?? null;
|
|
1526
|
+
}
|
|
1527
|
+
if (tree.length === 0) {
|
|
1528
|
+
this.showStatus("No entries in session");
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
this.showSelector((done) => {
|
|
1532
|
+
const selector = new TreeSelectorComponent(tree, visibleLeafId, this.ui.terminal.rows, async (entryId) => {
|
|
1533
|
+
// Selecting the visible leaf is a no-op (already there)
|
|
1534
|
+
if (entryId === visibleLeafId) {
|
|
1535
|
+
done();
|
|
1536
|
+
this.showStatus("Already at this point");
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
// Ask about summarization
|
|
1540
|
+
done(); // Close selector first
|
|
1541
|
+
const wantsSummary = await this.showHookConfirm("Summarize branch?", "Create a summary of the branch you're leaving?");
|
|
1542
|
+
// Set up escape handler and loader if summarizing
|
|
1543
|
+
let summaryLoader;
|
|
1544
|
+
const originalOnEscape = this.editor.onEscape;
|
|
1545
|
+
if (wantsSummary) {
|
|
1546
|
+
this.editor.onEscape = () => {
|
|
1547
|
+
this.session.abortBranchSummary();
|
|
1548
|
+
};
|
|
1549
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1550
|
+
summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), "Summarizing branch... (esc to cancel)");
|
|
1551
|
+
this.statusContainer.addChild(summaryLoader);
|
|
1552
|
+
this.ui.requestRender();
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
const result = await this.session.navigateTree(entryId, { summarize: wantsSummary });
|
|
1556
|
+
if (result.aborted) {
|
|
1557
|
+
// Summarization aborted - re-show tree selector
|
|
1558
|
+
this.showStatus("Branch summarization cancelled");
|
|
1559
|
+
this.showTreeSelector();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (result.cancelled) {
|
|
1563
|
+
this.showStatus("Navigation cancelled");
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
// Update UI
|
|
1567
|
+
this.chatContainer.clear();
|
|
1568
|
+
this.renderInitialMessages();
|
|
1569
|
+
if (result.editorText) {
|
|
1570
|
+
this.editor.setText(result.editorText);
|
|
1571
|
+
}
|
|
1572
|
+
this.showStatus("Navigated to selected point");
|
|
1573
|
+
}
|
|
1574
|
+
catch (error) {
|
|
1575
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
1576
|
+
}
|
|
1577
|
+
finally {
|
|
1578
|
+
if (summaryLoader) {
|
|
1579
|
+
summaryLoader.stop();
|
|
1580
|
+
this.statusContainer.clear();
|
|
1581
|
+
}
|
|
1582
|
+
this.editor.onEscape = originalOnEscape;
|
|
1583
|
+
}
|
|
1584
|
+
}, () => {
|
|
1585
|
+
done();
|
|
1586
|
+
this.ui.requestRender();
|
|
1587
|
+
}, (entryId, label) => {
|
|
1588
|
+
this.sessionManager.appendLabelChange(entryId, label);
|
|
1589
|
+
this.ui.requestRender();
|
|
1590
|
+
});
|
|
1591
|
+
return { component: selector, focus: selector };
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1299
1594
|
showSessionSelector() {
|
|
1300
1595
|
this.showSelector((done) => {
|
|
1301
1596
|
const sessions = SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir());
|
|
@@ -1315,19 +1610,19 @@ export class InteractiveMode {
|
|
|
1315
1610
|
// Stop loading animation
|
|
1316
1611
|
if (this.loadingAnimation) {
|
|
1317
1612
|
this.loadingAnimation.stop();
|
|
1318
|
-
this.loadingAnimation =
|
|
1613
|
+
this.loadingAnimation = undefined;
|
|
1319
1614
|
}
|
|
1320
1615
|
this.statusContainer.clear();
|
|
1321
1616
|
// Clear UI state
|
|
1322
1617
|
this.pendingMessagesContainer.clear();
|
|
1323
|
-
this.streamingComponent =
|
|
1618
|
+
this.streamingComponent = undefined;
|
|
1619
|
+
this.streamingMessage = undefined;
|
|
1324
1620
|
this.pendingTools.clear();
|
|
1325
1621
|
// Switch session via AgentSession (emits hook and tool session events)
|
|
1326
1622
|
await this.session.switchSession(sessionPath);
|
|
1327
1623
|
// Clear and re-render the chat
|
|
1328
1624
|
this.chatContainer.clear();
|
|
1329
|
-
this.
|
|
1330
|
-
this.renderInitialMessages(this.session.state);
|
|
1625
|
+
this.renderInitialMessages();
|
|
1331
1626
|
this.showStatus("Resumed session");
|
|
1332
1627
|
}
|
|
1333
1628
|
async showOAuthSelector(mode) {
|
|
@@ -1348,8 +1643,9 @@ export class InteractiveMode {
|
|
|
1348
1643
|
await this.session.modelRegistry.authStorage.login(providerId, {
|
|
1349
1644
|
onAuth: (info) => {
|
|
1350
1645
|
this.chatContainer.addChild(new Spacer(1));
|
|
1351
|
-
|
|
1352
|
-
|
|
1646
|
+
// Use OSC 8 hyperlink escape sequence for clickable link
|
|
1647
|
+
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
1648
|
+
this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
1353
1649
|
if (info.instructions) {
|
|
1354
1650
|
this.chatContainer.addChild(new Spacer(1));
|
|
1355
1651
|
this.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
@@ -1435,6 +1731,93 @@ export class InteractiveMode {
|
|
|
1435
1731
|
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1436
1732
|
}
|
|
1437
1733
|
}
|
|
1734
|
+
async handleShareCommand() {
|
|
1735
|
+
// Check if gh is available and logged in
|
|
1736
|
+
try {
|
|
1737
|
+
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
|
|
1738
|
+
if (authResult.status !== 0) {
|
|
1739
|
+
this.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
this.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
// Export to a temp file
|
|
1748
|
+
const tmpFile = path.join(os.tmpdir(), "session.html");
|
|
1749
|
+
try {
|
|
1750
|
+
this.session.exportToHtml(tmpFile);
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
// Show cancellable loader, replacing the editor
|
|
1757
|
+
const loader = new BorderedLoader(this.ui, theme, "Creating gist...");
|
|
1758
|
+
this.editorContainer.clear();
|
|
1759
|
+
this.editorContainer.addChild(loader);
|
|
1760
|
+
this.ui.setFocus(loader);
|
|
1761
|
+
this.ui.requestRender();
|
|
1762
|
+
const restoreEditor = () => {
|
|
1763
|
+
loader.dispose();
|
|
1764
|
+
this.editorContainer.clear();
|
|
1765
|
+
this.editorContainer.addChild(this.editor);
|
|
1766
|
+
this.ui.setFocus(this.editor);
|
|
1767
|
+
try {
|
|
1768
|
+
fs.unlinkSync(tmpFile);
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
// Ignore cleanup errors
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
// Create a secret gist asynchronously
|
|
1775
|
+
let proc = null;
|
|
1776
|
+
loader.onAbort = () => {
|
|
1777
|
+
proc?.kill();
|
|
1778
|
+
restoreEditor();
|
|
1779
|
+
this.showStatus("Share cancelled");
|
|
1780
|
+
};
|
|
1781
|
+
try {
|
|
1782
|
+
const result = await new Promise((resolve) => {
|
|
1783
|
+
proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]);
|
|
1784
|
+
let stdout = "";
|
|
1785
|
+
let stderr = "";
|
|
1786
|
+
proc.stdout?.on("data", (data) => {
|
|
1787
|
+
stdout += data.toString();
|
|
1788
|
+
});
|
|
1789
|
+
proc.stderr?.on("data", (data) => {
|
|
1790
|
+
stderr += data.toString();
|
|
1791
|
+
});
|
|
1792
|
+
proc.on("close", (code) => resolve({ stdout, stderr, code }));
|
|
1793
|
+
});
|
|
1794
|
+
if (loader.signal.aborted)
|
|
1795
|
+
return;
|
|
1796
|
+
restoreEditor();
|
|
1797
|
+
if (result.code !== 0) {
|
|
1798
|
+
const errorMsg = result.stderr?.trim() || "Unknown error";
|
|
1799
|
+
this.showError(`Failed to create gist: ${errorMsg}`);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
// Extract gist ID from the URL returned by gh
|
|
1803
|
+
// gh returns something like: https://gist.github.com/username/GIST_ID
|
|
1804
|
+
const gistUrl = result.stdout?.trim();
|
|
1805
|
+
const gistId = gistUrl?.split("/").pop();
|
|
1806
|
+
if (!gistId) {
|
|
1807
|
+
this.showError("Failed to parse gist ID from gh output");
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
// Create the preview URL
|
|
1811
|
+
const previewUrl = `https://shittycodingagent.ai/session?${gistId}`;
|
|
1812
|
+
this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
|
|
1813
|
+
}
|
|
1814
|
+
catch (error) {
|
|
1815
|
+
if (!loader.signal.aborted) {
|
|
1816
|
+
restoreEditor();
|
|
1817
|
+
this.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1438
1821
|
handleCopyCommand() {
|
|
1439
1822
|
const text = this.session.getLastAssistantText();
|
|
1440
1823
|
if (!text) {
|
|
@@ -1542,17 +1925,17 @@ export class InteractiveMode {
|
|
|
1542
1925
|
// Stop loading animation
|
|
1543
1926
|
if (this.loadingAnimation) {
|
|
1544
1927
|
this.loadingAnimation.stop();
|
|
1545
|
-
this.loadingAnimation =
|
|
1928
|
+
this.loadingAnimation = undefined;
|
|
1546
1929
|
}
|
|
1547
1930
|
this.statusContainer.clear();
|
|
1548
|
-
//
|
|
1549
|
-
await this.session.
|
|
1931
|
+
// New session via session (emits hook and tool session events)
|
|
1932
|
+
await this.session.newSession();
|
|
1550
1933
|
// Clear UI state
|
|
1551
1934
|
this.chatContainer.clear();
|
|
1552
1935
|
this.pendingMessagesContainer.clear();
|
|
1553
|
-
this.streamingComponent =
|
|
1936
|
+
this.streamingComponent = undefined;
|
|
1937
|
+
this.streamingMessage = undefined;
|
|
1554
1938
|
this.pendingTools.clear();
|
|
1555
|
-
this.isFirstUserMessage = true;
|
|
1556
1939
|
this.chatContainer.addChild(new Spacer(1));
|
|
1557
1940
|
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
|
1558
1941
|
this.ui.requestRender();
|
|
@@ -1614,11 +1997,11 @@ export class InteractiveMode {
|
|
|
1614
1997
|
}
|
|
1615
1998
|
catch (error) {
|
|
1616
1999
|
if (this.bashComponent) {
|
|
1617
|
-
this.bashComponent.setComplete(
|
|
2000
|
+
this.bashComponent.setComplete(undefined, false);
|
|
1618
2001
|
}
|
|
1619
2002
|
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1620
2003
|
}
|
|
1621
|
-
this.bashComponent =
|
|
2004
|
+
this.bashComponent = undefined;
|
|
1622
2005
|
this.ui.requestRender();
|
|
1623
2006
|
}
|
|
1624
2007
|
async handleCompactCommand(customInstructions) {
|
|
@@ -1634,7 +2017,7 @@ export class InteractiveMode {
|
|
|
1634
2017
|
// Stop loading animation
|
|
1635
2018
|
if (this.loadingAnimation) {
|
|
1636
2019
|
this.loadingAnimation.stop();
|
|
1637
|
-
this.loadingAnimation =
|
|
2020
|
+
this.loadingAnimation = undefined;
|
|
1638
2021
|
}
|
|
1639
2022
|
this.statusContainer.clear();
|
|
1640
2023
|
// Set up escape handler during compaction
|
|
@@ -1651,13 +2034,11 @@ export class InteractiveMode {
|
|
|
1651
2034
|
try {
|
|
1652
2035
|
const result = await this.session.compact(customInstructions);
|
|
1653
2036
|
// Rebuild UI
|
|
1654
|
-
this.chatContainer.clear();
|
|
1655
2037
|
this.rebuildChatFromMessages();
|
|
1656
|
-
// Add compaction component
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
this.
|
|
1660
|
-
this.footer.updateState(this.session.state);
|
|
2038
|
+
// Add compaction component at bottom so user sees it without scrolling
|
|
2039
|
+
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
2040
|
+
this.addMessageToChat(msg);
|
|
2041
|
+
this.footer.invalidate();
|
|
1661
2042
|
}
|
|
1662
2043
|
catch (error) {
|
|
1663
2044
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1677,7 +2058,7 @@ export class InteractiveMode {
|
|
|
1677
2058
|
stop() {
|
|
1678
2059
|
if (this.loadingAnimation) {
|
|
1679
2060
|
this.loadingAnimation.stop();
|
|
1680
|
-
this.loadingAnimation =
|
|
2061
|
+
this.loadingAnimation = undefined;
|
|
1681
2062
|
}
|
|
1682
2063
|
this.footer.dispose();
|
|
1683
2064
|
if (this.unsubscribe) {
|