@mariozechner/pi-coding-agent 0.14.1 → 0.15.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 +16 -0
- package/README.md +10 -1
- 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 +7 -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 +2 -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-mode.d.ts +21 -0
- package/dist/modes/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc-mode.js +77 -0
- package/dist/modes/rpc-mode.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/{shell.d.ts → utils/shell.d.ts} +8 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/{shell.js → utils/shell.js} +15 -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 -1934
- 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/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentSession - Core abstraction for agent lifecycle and session management.
|
|
3
|
+
*
|
|
4
|
+
* This class is shared between all run modes (interactive, print, rpc).
|
|
5
|
+
* It encapsulates:
|
|
6
|
+
* - Agent state access
|
|
7
|
+
* - Event subscription with automatic session persistence
|
|
8
|
+
* - Model and thinking level management
|
|
9
|
+
* - Compaction (manual and auto)
|
|
10
|
+
* - Bash execution
|
|
11
|
+
* - Session switching and branching
|
|
12
|
+
*
|
|
13
|
+
* Modes use this class and add their own I/O layer on top.
|
|
14
|
+
*/
|
|
15
|
+
import { getModelsPath } from "../config.js";
|
|
16
|
+
import { executeBash as executeBashCommand } from "./bash-executor.js";
|
|
17
|
+
import { calculateContextTokens, compact, shouldCompact } from "./compaction.js";
|
|
18
|
+
import { exportSessionToHtml } from "./export-html.js";
|
|
19
|
+
import { getApiKeyForModel, getAvailableModels } from "./model-config.js";
|
|
20
|
+
import { loadSessionFromEntries } from "./session-manager.js";
|
|
21
|
+
import { expandSlashCommand } from "./slash-commands.js";
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// AgentSession Class
|
|
24
|
+
// ============================================================================
|
|
25
|
+
export class AgentSession {
|
|
26
|
+
agent;
|
|
27
|
+
sessionManager;
|
|
28
|
+
settingsManager;
|
|
29
|
+
_scopedModels;
|
|
30
|
+
_fileCommands;
|
|
31
|
+
// Event subscription state
|
|
32
|
+
_unsubscribeAgent;
|
|
33
|
+
_eventListeners = [];
|
|
34
|
+
// Message queue state
|
|
35
|
+
_queuedMessages = [];
|
|
36
|
+
// Compaction state
|
|
37
|
+
_compactionAbortController = null;
|
|
38
|
+
_autoCompactionAbortController = null;
|
|
39
|
+
// Bash execution state
|
|
40
|
+
_bashAbortController = null;
|
|
41
|
+
_pendingBashMessages = [];
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.agent = config.agent;
|
|
44
|
+
this.sessionManager = config.sessionManager;
|
|
45
|
+
this.settingsManager = config.settingsManager;
|
|
46
|
+
this._scopedModels = config.scopedModels ?? [];
|
|
47
|
+
this._fileCommands = config.fileCommands ?? [];
|
|
48
|
+
}
|
|
49
|
+
// =========================================================================
|
|
50
|
+
// Event Subscription
|
|
51
|
+
// =========================================================================
|
|
52
|
+
/** Emit an event to all listeners */
|
|
53
|
+
_emit(event) {
|
|
54
|
+
for (const l of this._eventListeners) {
|
|
55
|
+
l(event);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Track last assistant message for auto-compaction check
|
|
59
|
+
_lastAssistantMessage = null;
|
|
60
|
+
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
61
|
+
_handleAgentEvent = async (event) => {
|
|
62
|
+
// Notify all listeners
|
|
63
|
+
this._emit(event);
|
|
64
|
+
// Handle session persistence
|
|
65
|
+
if (event.type === "message_end") {
|
|
66
|
+
this.sessionManager.saveMessage(event.message);
|
|
67
|
+
// Initialize session after first user+assistant exchange
|
|
68
|
+
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
|
69
|
+
this.sessionManager.startSession(this.agent.state);
|
|
70
|
+
}
|
|
71
|
+
// Track assistant message for auto-compaction (checked on agent_end)
|
|
72
|
+
if (event.message.role === "assistant") {
|
|
73
|
+
this._lastAssistantMessage = event.message;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check auto-compaction after agent completes (after agent_end clears UI)
|
|
77
|
+
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
|
78
|
+
const msg = this._lastAssistantMessage;
|
|
79
|
+
this._lastAssistantMessage = null;
|
|
80
|
+
this._runAutoCompaction(msg).catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe to agent events.
|
|
85
|
+
* Session persistence is handled internally (saves messages on message_end).
|
|
86
|
+
* Multiple listeners can be added. Returns unsubscribe function for this listener.
|
|
87
|
+
*/
|
|
88
|
+
subscribe(listener) {
|
|
89
|
+
this._eventListeners.push(listener);
|
|
90
|
+
// Set up agent subscription if not already done
|
|
91
|
+
if (!this._unsubscribeAgent) {
|
|
92
|
+
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
93
|
+
}
|
|
94
|
+
// Return unsubscribe function for this specific listener
|
|
95
|
+
return () => {
|
|
96
|
+
const index = this._eventListeners.indexOf(listener);
|
|
97
|
+
if (index !== -1) {
|
|
98
|
+
this._eventListeners.splice(index, 1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Temporarily disconnect from agent events.
|
|
104
|
+
* User listeners are preserved and will receive events again after resubscribe().
|
|
105
|
+
* Used internally during operations that need to pause event processing.
|
|
106
|
+
*/
|
|
107
|
+
_disconnectFromAgent() {
|
|
108
|
+
if (this._unsubscribeAgent) {
|
|
109
|
+
this._unsubscribeAgent();
|
|
110
|
+
this._unsubscribeAgent = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Reconnect to agent events after _disconnectFromAgent().
|
|
115
|
+
* Preserves all existing listeners.
|
|
116
|
+
*/
|
|
117
|
+
_reconnectToAgent() {
|
|
118
|
+
if (this._unsubscribeAgent)
|
|
119
|
+
return; // Already connected
|
|
120
|
+
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove all listeners and disconnect from agent.
|
|
124
|
+
* Call this when completely done with the session.
|
|
125
|
+
*/
|
|
126
|
+
dispose() {
|
|
127
|
+
this._disconnectFromAgent();
|
|
128
|
+
this._eventListeners = [];
|
|
129
|
+
}
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// Read-only State Access
|
|
132
|
+
// =========================================================================
|
|
133
|
+
/** Full agent state */
|
|
134
|
+
get state() {
|
|
135
|
+
return this.agent.state;
|
|
136
|
+
}
|
|
137
|
+
/** Current model (may be null if not yet selected) */
|
|
138
|
+
get model() {
|
|
139
|
+
return this.agent.state.model;
|
|
140
|
+
}
|
|
141
|
+
/** Current thinking level */
|
|
142
|
+
get thinkingLevel() {
|
|
143
|
+
return this.agent.state.thinkingLevel;
|
|
144
|
+
}
|
|
145
|
+
/** Whether agent is currently streaming a response */
|
|
146
|
+
get isStreaming() {
|
|
147
|
+
return this.agent.state.isStreaming;
|
|
148
|
+
}
|
|
149
|
+
/** All messages including custom types like BashExecutionMessage */
|
|
150
|
+
get messages() {
|
|
151
|
+
return this.agent.state.messages;
|
|
152
|
+
}
|
|
153
|
+
/** Current queue mode */
|
|
154
|
+
get queueMode() {
|
|
155
|
+
return this.agent.getQueueMode();
|
|
156
|
+
}
|
|
157
|
+
/** Current session file path */
|
|
158
|
+
get sessionFile() {
|
|
159
|
+
return this.sessionManager.getSessionFile();
|
|
160
|
+
}
|
|
161
|
+
/** Current session ID */
|
|
162
|
+
get sessionId() {
|
|
163
|
+
return this.sessionManager.getSessionId();
|
|
164
|
+
}
|
|
165
|
+
/** Scoped models for cycling (from --models flag) */
|
|
166
|
+
get scopedModels() {
|
|
167
|
+
return this._scopedModels;
|
|
168
|
+
}
|
|
169
|
+
/** File-based slash commands */
|
|
170
|
+
get fileCommands() {
|
|
171
|
+
return this._fileCommands;
|
|
172
|
+
}
|
|
173
|
+
// =========================================================================
|
|
174
|
+
// Prompting
|
|
175
|
+
// =========================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Send a prompt to the agent.
|
|
178
|
+
* - Validates model and API key before sending
|
|
179
|
+
* - Expands file-based slash commands by default
|
|
180
|
+
* @throws Error if no model selected or no API key available
|
|
181
|
+
*/
|
|
182
|
+
async prompt(text, options) {
|
|
183
|
+
// Flush any pending bash messages before the new prompt
|
|
184
|
+
this._flushPendingBashMessages();
|
|
185
|
+
const expandCommands = options?.expandSlashCommands ?? true;
|
|
186
|
+
// Validate model
|
|
187
|
+
if (!this.model) {
|
|
188
|
+
throw new Error("No model selected.\n\n" +
|
|
189
|
+
"Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" +
|
|
190
|
+
`or create ${getModelsPath()}\n\n` +
|
|
191
|
+
"Then use /model to select a model.");
|
|
192
|
+
}
|
|
193
|
+
// Validate API key
|
|
194
|
+
const apiKey = await getApiKeyForModel(this.model);
|
|
195
|
+
if (!apiKey) {
|
|
196
|
+
throw new Error(`No API key found for ${this.model.provider}.\n\n` +
|
|
197
|
+
`Set the appropriate environment variable or update ${getModelsPath()}`);
|
|
198
|
+
}
|
|
199
|
+
// Expand slash commands if requested
|
|
200
|
+
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
|
|
201
|
+
await this.agent.prompt(expandedText, options?.attachments);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Queue a message to be sent after the current response completes.
|
|
205
|
+
* Use when agent is currently streaming.
|
|
206
|
+
*/
|
|
207
|
+
async queueMessage(text) {
|
|
208
|
+
this._queuedMessages.push(text);
|
|
209
|
+
await this.agent.queueMessage({
|
|
210
|
+
role: "user",
|
|
211
|
+
content: [{ type: "text", text }],
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Clear queued messages and return them.
|
|
217
|
+
* Useful for restoring to editor when user aborts.
|
|
218
|
+
*/
|
|
219
|
+
clearQueue() {
|
|
220
|
+
const queued = [...this._queuedMessages];
|
|
221
|
+
this._queuedMessages = [];
|
|
222
|
+
this.agent.clearMessageQueue();
|
|
223
|
+
return queued;
|
|
224
|
+
}
|
|
225
|
+
/** Number of messages currently queued */
|
|
226
|
+
get queuedMessageCount() {
|
|
227
|
+
return this._queuedMessages.length;
|
|
228
|
+
}
|
|
229
|
+
/** Get queued messages (read-only) */
|
|
230
|
+
getQueuedMessages() {
|
|
231
|
+
return this._queuedMessages;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Abort current operation and wait for agent to become idle.
|
|
235
|
+
*/
|
|
236
|
+
async abort() {
|
|
237
|
+
this.agent.abort();
|
|
238
|
+
await this.agent.waitForIdle();
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Reset agent and session to start fresh.
|
|
242
|
+
* Clears all messages and starts a new session.
|
|
243
|
+
* Listeners are preserved and will continue receiving events.
|
|
244
|
+
*/
|
|
245
|
+
async reset() {
|
|
246
|
+
this._disconnectFromAgent();
|
|
247
|
+
await this.abort();
|
|
248
|
+
this.agent.reset();
|
|
249
|
+
this.sessionManager.reset();
|
|
250
|
+
this._queuedMessages = [];
|
|
251
|
+
this._reconnectToAgent();
|
|
252
|
+
}
|
|
253
|
+
// =========================================================================
|
|
254
|
+
// Model Management
|
|
255
|
+
// =========================================================================
|
|
256
|
+
/**
|
|
257
|
+
* Set model directly.
|
|
258
|
+
* Validates API key, saves to session and settings.
|
|
259
|
+
* @throws Error if no API key available for the model
|
|
260
|
+
*/
|
|
261
|
+
async setModel(model) {
|
|
262
|
+
const apiKey = await getApiKeyForModel(model);
|
|
263
|
+
if (!apiKey) {
|
|
264
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
265
|
+
}
|
|
266
|
+
this.agent.setModel(model);
|
|
267
|
+
this.sessionManager.saveModelChange(model.provider, model.id);
|
|
268
|
+
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Cycle to next model.
|
|
272
|
+
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
273
|
+
* @returns The new model info, or null if only one model available
|
|
274
|
+
*/
|
|
275
|
+
async cycleModel() {
|
|
276
|
+
if (this._scopedModels.length > 0) {
|
|
277
|
+
return this._cycleScopedModel();
|
|
278
|
+
}
|
|
279
|
+
return this._cycleAvailableModel();
|
|
280
|
+
}
|
|
281
|
+
async _cycleScopedModel() {
|
|
282
|
+
if (this._scopedModels.length <= 1)
|
|
283
|
+
return null;
|
|
284
|
+
const currentModel = this.model;
|
|
285
|
+
let currentIndex = this._scopedModels.findIndex((sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider);
|
|
286
|
+
if (currentIndex === -1)
|
|
287
|
+
currentIndex = 0;
|
|
288
|
+
const nextIndex = (currentIndex + 1) % this._scopedModels.length;
|
|
289
|
+
const next = this._scopedModels[nextIndex];
|
|
290
|
+
// Validate API key
|
|
291
|
+
const apiKey = await getApiKeyForModel(next.model);
|
|
292
|
+
if (!apiKey) {
|
|
293
|
+
throw new Error(`No API key for ${next.model.provider}/${next.model.id}`);
|
|
294
|
+
}
|
|
295
|
+
// Apply model
|
|
296
|
+
this.agent.setModel(next.model);
|
|
297
|
+
this.sessionManager.saveModelChange(next.model.provider, next.model.id);
|
|
298
|
+
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
299
|
+
// Apply thinking level (silently use "off" if not supported)
|
|
300
|
+
const effectiveThinking = next.model.reasoning ? next.thinkingLevel : "off";
|
|
301
|
+
this.agent.setThinkingLevel(effectiveThinking);
|
|
302
|
+
this.sessionManager.saveThinkingLevelChange(effectiveThinking);
|
|
303
|
+
this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
|
|
304
|
+
return { model: next.model, thinkingLevel: effectiveThinking, isScoped: true };
|
|
305
|
+
}
|
|
306
|
+
async _cycleAvailableModel() {
|
|
307
|
+
const { models: availableModels, error } = await getAvailableModels();
|
|
308
|
+
if (error)
|
|
309
|
+
throw new Error(`Failed to load models: ${error}`);
|
|
310
|
+
if (availableModels.length <= 1)
|
|
311
|
+
return null;
|
|
312
|
+
const currentModel = this.model;
|
|
313
|
+
let currentIndex = availableModels.findIndex((m) => m.id === currentModel?.id && m.provider === currentModel?.provider);
|
|
314
|
+
if (currentIndex === -1)
|
|
315
|
+
currentIndex = 0;
|
|
316
|
+
const nextIndex = (currentIndex + 1) % availableModels.length;
|
|
317
|
+
const nextModel = availableModels[nextIndex];
|
|
318
|
+
const apiKey = await getApiKeyForModel(nextModel);
|
|
319
|
+
if (!apiKey) {
|
|
320
|
+
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
321
|
+
}
|
|
322
|
+
this.agent.setModel(nextModel);
|
|
323
|
+
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
|
|
324
|
+
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
325
|
+
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get all available models with valid API keys.
|
|
329
|
+
*/
|
|
330
|
+
async getAvailableModels() {
|
|
331
|
+
const { models, error } = await getAvailableModels();
|
|
332
|
+
if (error)
|
|
333
|
+
throw new Error(error);
|
|
334
|
+
return models;
|
|
335
|
+
}
|
|
336
|
+
// =========================================================================
|
|
337
|
+
// Thinking Level Management
|
|
338
|
+
// =========================================================================
|
|
339
|
+
/**
|
|
340
|
+
* Set thinking level.
|
|
341
|
+
* Silently uses "off" if model doesn't support thinking.
|
|
342
|
+
* Saves to session and settings.
|
|
343
|
+
*/
|
|
344
|
+
setThinkingLevel(level) {
|
|
345
|
+
const effectiveLevel = this.supportsThinking() ? level : "off";
|
|
346
|
+
this.agent.setThinkingLevel(effectiveLevel);
|
|
347
|
+
this.sessionManager.saveThinkingLevelChange(effectiveLevel);
|
|
348
|
+
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Cycle to next thinking level.
|
|
352
|
+
* @returns New level, or null if model doesn't support thinking
|
|
353
|
+
*/
|
|
354
|
+
cycleThinkingLevel() {
|
|
355
|
+
if (!this.supportsThinking())
|
|
356
|
+
return null;
|
|
357
|
+
const modelId = this.model?.id || "";
|
|
358
|
+
const supportsXhigh = modelId.includes("codex-max");
|
|
359
|
+
const levels = supportsXhigh
|
|
360
|
+
? ["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
361
|
+
: ["off", "minimal", "low", "medium", "high"];
|
|
362
|
+
const currentIndex = levels.indexOf(this.thinkingLevel);
|
|
363
|
+
const nextIndex = (currentIndex + 1) % levels.length;
|
|
364
|
+
const nextLevel = levels[nextIndex];
|
|
365
|
+
this.setThinkingLevel(nextLevel);
|
|
366
|
+
return nextLevel;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check if current model supports thinking/reasoning.
|
|
370
|
+
*/
|
|
371
|
+
supportsThinking() {
|
|
372
|
+
return !!this.model?.reasoning;
|
|
373
|
+
}
|
|
374
|
+
// =========================================================================
|
|
375
|
+
// Queue Mode Management
|
|
376
|
+
// =========================================================================
|
|
377
|
+
/**
|
|
378
|
+
* Set message queue mode.
|
|
379
|
+
* Saves to settings.
|
|
380
|
+
*/
|
|
381
|
+
setQueueMode(mode) {
|
|
382
|
+
this.agent.setQueueMode(mode);
|
|
383
|
+
this.settingsManager.setQueueMode(mode);
|
|
384
|
+
}
|
|
385
|
+
// =========================================================================
|
|
386
|
+
// Compaction
|
|
387
|
+
// =========================================================================
|
|
388
|
+
/**
|
|
389
|
+
* Manually compact the session context.
|
|
390
|
+
* Aborts current agent operation first.
|
|
391
|
+
* @param customInstructions Optional instructions for the compaction summary
|
|
392
|
+
*/
|
|
393
|
+
async compact(customInstructions) {
|
|
394
|
+
// Abort any running operation
|
|
395
|
+
this._disconnectFromAgent();
|
|
396
|
+
await this.abort();
|
|
397
|
+
// Create abort controller
|
|
398
|
+
this._compactionAbortController = new AbortController();
|
|
399
|
+
try {
|
|
400
|
+
if (!this.model) {
|
|
401
|
+
throw new Error("No model selected");
|
|
402
|
+
}
|
|
403
|
+
const apiKey = await getApiKeyForModel(this.model);
|
|
404
|
+
if (!apiKey) {
|
|
405
|
+
throw new Error(`No API key for ${this.model.provider}`);
|
|
406
|
+
}
|
|
407
|
+
const entries = this.sessionManager.loadEntries();
|
|
408
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
409
|
+
const compactionEntry = await compact(entries, this.model, settings, apiKey, this._compactionAbortController.signal, customInstructions);
|
|
410
|
+
if (this._compactionAbortController.signal.aborted) {
|
|
411
|
+
throw new Error("Compaction cancelled");
|
|
412
|
+
}
|
|
413
|
+
// Save and reload
|
|
414
|
+
this.sessionManager.saveCompaction(compactionEntry);
|
|
415
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
416
|
+
this.agent.replaceMessages(loaded.messages);
|
|
417
|
+
return {
|
|
418
|
+
tokensBefore: compactionEntry.tokensBefore,
|
|
419
|
+
summary: compactionEntry.summary,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
this._compactionAbortController = null;
|
|
424
|
+
this._reconnectToAgent();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Cancel in-progress compaction (manual or auto).
|
|
429
|
+
*/
|
|
430
|
+
abortCompaction() {
|
|
431
|
+
this._compactionAbortController?.abort();
|
|
432
|
+
this._autoCompactionAbortController?.abort();
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Internal: Run auto-compaction with events.
|
|
436
|
+
* Called after assistant messages complete.
|
|
437
|
+
*/
|
|
438
|
+
async _runAutoCompaction(assistantMessage) {
|
|
439
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
440
|
+
if (!settings.enabled)
|
|
441
|
+
return;
|
|
442
|
+
// Skip if message was aborted
|
|
443
|
+
if (assistantMessage.stopReason === "aborted")
|
|
444
|
+
return;
|
|
445
|
+
const contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
446
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
447
|
+
if (!shouldCompact(contextTokens, contextWindow, settings))
|
|
448
|
+
return;
|
|
449
|
+
// Emit start event
|
|
450
|
+
this._emit({ type: "auto_compaction_start" });
|
|
451
|
+
this._autoCompactionAbortController = new AbortController();
|
|
452
|
+
try {
|
|
453
|
+
if (!this.model) {
|
|
454
|
+
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const apiKey = await getApiKeyForModel(this.model);
|
|
458
|
+
if (!apiKey) {
|
|
459
|
+
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Load entries (sync file read) then yield to let UI render
|
|
463
|
+
const entries = this.sessionManager.loadEntries();
|
|
464
|
+
const compactionEntry = await compact(entries, this.model, settings, apiKey, this._autoCompactionAbortController.signal);
|
|
465
|
+
if (this._autoCompactionAbortController.signal.aborted) {
|
|
466
|
+
this._emit({ type: "auto_compaction_end", result: null, aborted: true });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.sessionManager.saveCompaction(compactionEntry);
|
|
470
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
471
|
+
this.agent.replaceMessages(loaded.messages);
|
|
472
|
+
const result = {
|
|
473
|
+
tokensBefore: compactionEntry.tokensBefore,
|
|
474
|
+
summary: compactionEntry.summary,
|
|
475
|
+
};
|
|
476
|
+
this._emit({ type: "auto_compaction_end", result, aborted: false });
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Silently fail auto-compaction but emit end event
|
|
480
|
+
this._emit({ type: "auto_compaction_end", result: null, aborted: false });
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
this._autoCompactionAbortController = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Toggle auto-compaction setting.
|
|
488
|
+
*/
|
|
489
|
+
setAutoCompactionEnabled(enabled) {
|
|
490
|
+
this.settingsManager.setCompactionEnabled(enabled);
|
|
491
|
+
}
|
|
492
|
+
/** Whether auto-compaction is enabled */
|
|
493
|
+
get autoCompactionEnabled() {
|
|
494
|
+
return this.settingsManager.getCompactionEnabled();
|
|
495
|
+
}
|
|
496
|
+
// =========================================================================
|
|
497
|
+
// Bash Execution
|
|
498
|
+
// =========================================================================
|
|
499
|
+
/**
|
|
500
|
+
* Execute a bash command.
|
|
501
|
+
* Adds result to agent context and session.
|
|
502
|
+
* @param command The bash command to execute
|
|
503
|
+
* @param onChunk Optional streaming callback for output
|
|
504
|
+
*/
|
|
505
|
+
async executeBash(command, onChunk) {
|
|
506
|
+
this._bashAbortController = new AbortController();
|
|
507
|
+
try {
|
|
508
|
+
const result = await executeBashCommand(command, {
|
|
509
|
+
onChunk,
|
|
510
|
+
signal: this._bashAbortController.signal,
|
|
511
|
+
});
|
|
512
|
+
// Create and save message
|
|
513
|
+
const bashMessage = {
|
|
514
|
+
role: "bashExecution",
|
|
515
|
+
command,
|
|
516
|
+
output: result.output,
|
|
517
|
+
exitCode: result.exitCode,
|
|
518
|
+
cancelled: result.cancelled,
|
|
519
|
+
truncated: result.truncated,
|
|
520
|
+
fullOutputPath: result.fullOutputPath,
|
|
521
|
+
timestamp: Date.now(),
|
|
522
|
+
};
|
|
523
|
+
// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
|
|
524
|
+
if (this.isStreaming) {
|
|
525
|
+
// Queue for later - will be flushed on agent_end
|
|
526
|
+
this._pendingBashMessages.push(bashMessage);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Add to agent state immediately
|
|
530
|
+
this.agent.appendMessage(bashMessage);
|
|
531
|
+
// Save to session
|
|
532
|
+
this.sessionManager.saveMessage(bashMessage);
|
|
533
|
+
// Initialize session if needed
|
|
534
|
+
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
|
535
|
+
this.sessionManager.startSession(this.agent.state);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
this._bashAbortController = null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Cancel running bash command.
|
|
546
|
+
*/
|
|
547
|
+
abortBash() {
|
|
548
|
+
this._bashAbortController?.abort();
|
|
549
|
+
}
|
|
550
|
+
/** Whether a bash command is currently running */
|
|
551
|
+
get isBashRunning() {
|
|
552
|
+
return this._bashAbortController !== null;
|
|
553
|
+
}
|
|
554
|
+
/** Whether there are pending bash messages waiting to be flushed */
|
|
555
|
+
get hasPendingBashMessages() {
|
|
556
|
+
return this._pendingBashMessages.length > 0;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Flush pending bash messages to agent state and session.
|
|
560
|
+
* Called after agent turn completes to maintain proper message ordering.
|
|
561
|
+
*/
|
|
562
|
+
_flushPendingBashMessages() {
|
|
563
|
+
if (this._pendingBashMessages.length === 0)
|
|
564
|
+
return;
|
|
565
|
+
for (const bashMessage of this._pendingBashMessages) {
|
|
566
|
+
// Add to agent state
|
|
567
|
+
this.agent.appendMessage(bashMessage);
|
|
568
|
+
// Save to session
|
|
569
|
+
this.sessionManager.saveMessage(bashMessage);
|
|
570
|
+
}
|
|
571
|
+
// Initialize session if needed
|
|
572
|
+
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
|
573
|
+
this.sessionManager.startSession(this.agent.state);
|
|
574
|
+
}
|
|
575
|
+
this._pendingBashMessages = [];
|
|
576
|
+
}
|
|
577
|
+
// =========================================================================
|
|
578
|
+
// Session Management
|
|
579
|
+
// =========================================================================
|
|
580
|
+
/**
|
|
581
|
+
* Switch to a different session file.
|
|
582
|
+
* Aborts current operation, loads messages, restores model/thinking.
|
|
583
|
+
* Listeners are preserved and will continue receiving events.
|
|
584
|
+
*/
|
|
585
|
+
async switchSession(sessionPath) {
|
|
586
|
+
this._disconnectFromAgent();
|
|
587
|
+
await this.abort();
|
|
588
|
+
this._queuedMessages = [];
|
|
589
|
+
// Set new session
|
|
590
|
+
this.sessionManager.setSessionFile(sessionPath);
|
|
591
|
+
// Reload messages
|
|
592
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
593
|
+
this.agent.replaceMessages(loaded.messages);
|
|
594
|
+
// Restore model if saved
|
|
595
|
+
const savedModel = this.sessionManager.loadModel();
|
|
596
|
+
if (savedModel) {
|
|
597
|
+
const availableModels = (await getAvailableModels()).models;
|
|
598
|
+
const match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);
|
|
599
|
+
if (match) {
|
|
600
|
+
this.agent.setModel(match);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Restore thinking level if saved
|
|
604
|
+
const savedThinking = this.sessionManager.loadThinkingLevel();
|
|
605
|
+
if (savedThinking) {
|
|
606
|
+
this.agent.setThinkingLevel(savedThinking);
|
|
607
|
+
}
|
|
608
|
+
this._reconnectToAgent();
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Create a branch from a specific entry index.
|
|
612
|
+
* @param entryIndex Index into session entries to branch from
|
|
613
|
+
* @returns The text of the selected user message (for editor pre-fill)
|
|
614
|
+
*/
|
|
615
|
+
branch(entryIndex) {
|
|
616
|
+
const entries = this.sessionManager.loadEntries();
|
|
617
|
+
const selectedEntry = entries[entryIndex];
|
|
618
|
+
if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
|
|
619
|
+
throw new Error("Invalid entry index for branching");
|
|
620
|
+
}
|
|
621
|
+
const selectedText = this._extractUserMessageText(selectedEntry.message.content);
|
|
622
|
+
// Create branched session
|
|
623
|
+
const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
|
|
624
|
+
this.sessionManager.setSessionFile(newSessionFile);
|
|
625
|
+
// Reload
|
|
626
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
627
|
+
this.agent.replaceMessages(loaded.messages);
|
|
628
|
+
return selectedText;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get all user messages from session for branch selector.
|
|
632
|
+
*/
|
|
633
|
+
getUserMessagesForBranching() {
|
|
634
|
+
const entries = this.sessionManager.loadEntries();
|
|
635
|
+
const result = [];
|
|
636
|
+
for (let i = 0; i < entries.length; i++) {
|
|
637
|
+
const entry = entries[i];
|
|
638
|
+
if (entry.type !== "message")
|
|
639
|
+
continue;
|
|
640
|
+
if (entry.message.role !== "user")
|
|
641
|
+
continue;
|
|
642
|
+
const text = this._extractUserMessageText(entry.message.content);
|
|
643
|
+
if (text) {
|
|
644
|
+
result.push({ entryIndex: i, text });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
_extractUserMessageText(content) {
|
|
650
|
+
if (typeof content === "string")
|
|
651
|
+
return content;
|
|
652
|
+
if (Array.isArray(content)) {
|
|
653
|
+
return content
|
|
654
|
+
.filter((c) => c.type === "text")
|
|
655
|
+
.map((c) => c.text)
|
|
656
|
+
.join("");
|
|
657
|
+
}
|
|
658
|
+
return "";
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get session statistics.
|
|
662
|
+
*/
|
|
663
|
+
getSessionStats() {
|
|
664
|
+
const state = this.state;
|
|
665
|
+
const userMessages = state.messages.filter((m) => m.role === "user").length;
|
|
666
|
+
const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
|
|
667
|
+
const toolResults = state.messages.filter((m) => m.role === "toolResult").length;
|
|
668
|
+
let toolCalls = 0;
|
|
669
|
+
let totalInput = 0;
|
|
670
|
+
let totalOutput = 0;
|
|
671
|
+
let totalCacheRead = 0;
|
|
672
|
+
let totalCacheWrite = 0;
|
|
673
|
+
let totalCost = 0;
|
|
674
|
+
for (const message of state.messages) {
|
|
675
|
+
if (message.role === "assistant") {
|
|
676
|
+
const assistantMsg = message;
|
|
677
|
+
toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
|
678
|
+
totalInput += assistantMsg.usage.input;
|
|
679
|
+
totalOutput += assistantMsg.usage.output;
|
|
680
|
+
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
681
|
+
totalCacheWrite += assistantMsg.usage.cacheWrite;
|
|
682
|
+
totalCost += assistantMsg.usage.cost.total;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
sessionFile: this.sessionFile,
|
|
687
|
+
sessionId: this.sessionId,
|
|
688
|
+
userMessages,
|
|
689
|
+
assistantMessages,
|
|
690
|
+
toolCalls,
|
|
691
|
+
toolResults,
|
|
692
|
+
totalMessages: state.messages.length,
|
|
693
|
+
tokens: {
|
|
694
|
+
input: totalInput,
|
|
695
|
+
output: totalOutput,
|
|
696
|
+
cacheRead: totalCacheRead,
|
|
697
|
+
cacheWrite: totalCacheWrite,
|
|
698
|
+
total: totalInput + totalOutput + totalCacheRead + totalCacheWrite,
|
|
699
|
+
},
|
|
700
|
+
cost: totalCost,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Export session to HTML.
|
|
705
|
+
* @param outputPath Optional output path (defaults to session directory)
|
|
706
|
+
* @returns Path to exported file
|
|
707
|
+
*/
|
|
708
|
+
exportToHtml(outputPath) {
|
|
709
|
+
return exportSessionToHtml(this.sessionManager, this.state, outputPath);
|
|
710
|
+
}
|
|
711
|
+
// =========================================================================
|
|
712
|
+
// Utilities
|
|
713
|
+
// =========================================================================
|
|
714
|
+
/**
|
|
715
|
+
* Get text content of last assistant message.
|
|
716
|
+
* Useful for /copy command.
|
|
717
|
+
* @returns Text content, or null if no assistant message exists
|
|
718
|
+
*/
|
|
719
|
+
getLastAssistantText() {
|
|
720
|
+
const lastAssistant = this.messages
|
|
721
|
+
.slice()
|
|
722
|
+
.reverse()
|
|
723
|
+
.find((m) => m.role === "assistant");
|
|
724
|
+
if (!lastAssistant)
|
|
725
|
+
return null;
|
|
726
|
+
let text = "";
|
|
727
|
+
for (const content of lastAssistant.content) {
|
|
728
|
+
if (content.type === "text") {
|
|
729
|
+
text += content.text;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return text.trim() || null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=agent-session.js.map
|