@mariozechner/pi-coding-agent 0.14.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +415 -1098
- package/dist/cli/args.d.ts +30 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +179 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/file-processor.d.ts +11 -0
- package/dist/cli/file-processor.d.ts.map +1 -0
- package/dist/cli/file-processor.js +82 -0
- package/dist/cli/file-processor.js.map +1 -0
- package/dist/cli/session-picker.d.ts +7 -0
- package/dist/cli/session-picker.d.ts.map +1 -0
- package/dist/cli/session-picker.js +29 -0
- package/dist/cli/session-picker.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -18
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +287 -0
- package/dist/core/agent-session.d.ts.map +1 -0
- package/dist/core/agent-session.js +735 -0
- package/dist/core/agent-session.js.map +1 -0
- package/dist/core/bash-executor.d.ts +41 -0
- package/dist/core/bash-executor.d.ts.map +1 -0
- package/dist/core/bash-executor.js +132 -0
- package/dist/core/bash-executor.js.map +1 -0
- package/dist/{compaction.d.ts → core/compaction.d.ts} +5 -1
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/{compaction.js → core/compaction.js} +23 -1
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/export-html.d.ts.map +1 -0
- package/dist/{export-html.js → core/export-html.js} +1 -1
- package/dist/{export-html.d.ts.map → core/export-html.js.map} +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/{model-config.js → core/model-config.js} +1 -1
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/model-resolver.d.ts +48 -0
- package/dist/core/model-resolver.d.ts.map +1 -0
- package/dist/core/model-resolver.js +244 -0
- package/dist/core/model-resolver.js.map +1 -0
- package/dist/core/oauth/anthropic.d.ts.map +1 -0
- package/dist/core/oauth/anthropic.js.map +1 -0
- package/dist/core/oauth/index.d.ts.map +1 -0
- package/dist/{oauth/index.d.ts.map → core/oauth/index.js.map} +1 -1
- package/dist/core/oauth/storage.d.ts.map +1 -0
- package/dist/{oauth → core/oauth}/storage.js +1 -1
- package/dist/core/oauth/storage.js.map +1 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/{session-manager.js → core/session-manager.js} +1 -1
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -0
- package/dist/{settings-manager.js → core/settings-manager.js} +1 -1
- package/dist/core/settings-manager.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -0
- package/dist/{slash-commands.js → core/slash-commands.js} +1 -1
- package/dist/core/slash-commands.js.map +1 -0
- package/dist/core/system-prompt.d.ts +17 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +203 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -0
- package/dist/{tools → core/tools}/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -0
- package/dist/core/tools/edit.js.map +1 -0
- package/dist/core/tools/find.d.ts.map +1 -0
- package/dist/{tools → core/tools}/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/{tools → core/tools}/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/ls.d.ts.map +1 -0
- package/dist/core/tools/ls.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -0
- package/dist/core/tools/read.js.map +1 -0
- package/dist/core/tools/truncate.d.ts.map +1 -0
- package/dist/core/tools/truncate.js.map +1 -0
- package/dist/core/tools/write.d.ts.map +1 -0
- package/dist/core/tools/write.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +176 -1082
- package/dist/main.js.map +1 -1
- package/dist/modes/index.d.ts +9 -0
- package/dist/modes/index.d.ts.map +1 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/index.js.map +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/bash-execution.d.ts +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/bash-execution.js +1 -1
- package/dist/modes/interactive/components/bash-execution.js.map +1 -0
- package/dist/modes/interactive/components/compaction.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-editor.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/model-selector.js +3 -3
- package/dist/modes/interactive/components/model-selector.js.map +1 -0
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/oauth-selector.js +2 -2
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -0
- package/dist/modes/interactive/components/queue-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/queue-mode-selector.js.map +1 -0
- package/dist/{tui → modes/interactive/components}/session-selector.d.ts +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -0
- package/dist/{tui → modes/interactive/components}/session-selector.js +1 -1
- package/dist/modes/interactive/components/session-selector.js.map +1 -0
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -0
- package/dist/{tui/theme-selector.d.ts.map → modes/interactive/components/theme-selector.js.map} +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -0
- package/dist/{tui/tui-renderer.d.ts → modes/interactive/interactive-mode.d.ts} +36 -38
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-mode.js +1217 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/{theme → modes/interactive/theme}/theme.js +1 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/modes/print-mode.d.ts +21 -0
- package/dist/modes/print-mode.d.ts.map +1 -0
- package/dist/modes/print-mode.js +53 -0
- package/dist/modes/print-mode.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +182 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-client.js +362 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -0
- package/dist/modes/rpc/rpc-mode.d.ts +19 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-mode.js +204 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -0
- package/dist/modes/rpc/rpc-types.d.ts +254 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/modes/rpc/rpc-types.js.map +1 -0
- package/dist/{changelog.d.ts → utils/changelog.d.ts} +1 -1
- package/dist/{changelog.js.map → utils/changelog.d.ts.map} +1 -1
- package/dist/{changelog.js → utils/changelog.js} +1 -1
- package/dist/utils/changelog.js.map +1 -0
- package/dist/utils/clipboard.d.ts.map +1 -0
- package/dist/utils/clipboard.js.map +1 -0
- package/dist/utils/fuzzy.d.ts.map +1 -0
- package/dist/utils/fuzzy.js.map +1 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/{shell.js → utils/shell.js} +1 -1
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -0
- package/dist/{tools-manager.js → utils/tools-manager.js} +1 -1
- package/dist/utils/tools-manager.js.map +1 -0
- package/package.json +6 -6
- package/dist/changelog.d.ts.map +0 -1
- package/dist/clipboard.d.ts.map +0 -1
- package/dist/clipboard.js.map +0 -1
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js.map +0 -1
- package/dist/export-html.js.map +0 -1
- package/dist/fuzzy.d.ts.map +0 -1
- package/dist/fuzzy.js.map +0 -1
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js.map +0 -1
- package/dist/model-config.d.ts.map +0 -1
- package/dist/model-config.js.map +0 -1
- package/dist/oauth/anthropic.d.ts.map +0 -1
- package/dist/oauth/anthropic.js.map +0 -1
- package/dist/oauth/index.js.map +0 -1
- package/dist/oauth/storage.d.ts.map +0 -1
- package/dist/oauth/storage.js.map +0 -1
- package/dist/session-manager.d.ts.map +0 -1
- package/dist/session-manager.js.map +0 -1
- package/dist/settings-manager.d.ts.map +0 -1
- package/dist/settings-manager.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/slash-commands.d.ts.map +0 -1
- package/dist/slash-commands.js.map +0 -1
- package/dist/theme/theme.d.ts.map +0 -1
- package/dist/theme/theme.js.map +0 -1
- package/dist/tools/bash.d.ts.map +0 -1
- package/dist/tools/bash.js.map +0 -1
- package/dist/tools/edit.d.ts.map +0 -1
- package/dist/tools/edit.js.map +0 -1
- package/dist/tools/find.d.ts.map +0 -1
- package/dist/tools/find.js.map +0 -1
- package/dist/tools/grep.d.ts.map +0 -1
- package/dist/tools/grep.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/ls.d.ts.map +0 -1
- package/dist/tools/ls.js.map +0 -1
- package/dist/tools/read.d.ts.map +0 -1
- package/dist/tools/read.js.map +0 -1
- package/dist/tools/truncate.d.ts.map +0 -1
- package/dist/tools/truncate.js.map +0 -1
- package/dist/tools/write.d.ts.map +0 -1
- package/dist/tools/write.js.map +0 -1
- package/dist/tools-manager.d.ts.map +0 -1
- package/dist/tools-manager.js.map +0 -1
- package/dist/tui/assistant-message.d.ts.map +0 -1
- package/dist/tui/assistant-message.js.map +0 -1
- package/dist/tui/bash-execution.d.ts.map +0 -1
- package/dist/tui/bash-execution.js.map +0 -1
- package/dist/tui/compaction.d.ts.map +0 -1
- package/dist/tui/compaction.js.map +0 -1
- package/dist/tui/custom-editor.d.ts.map +0 -1
- package/dist/tui/custom-editor.js.map +0 -1
- package/dist/tui/dynamic-border.d.ts.map +0 -1
- package/dist/tui/dynamic-border.js.map +0 -1
- package/dist/tui/footer.d.ts.map +0 -1
- package/dist/tui/footer.js.map +0 -1
- package/dist/tui/model-selector.d.ts.map +0 -1
- package/dist/tui/model-selector.js.map +0 -1
- package/dist/tui/oauth-selector.d.ts.map +0 -1
- package/dist/tui/oauth-selector.js.map +0 -1
- package/dist/tui/queue-mode-selector.d.ts.map +0 -1
- package/dist/tui/queue-mode-selector.js.map +0 -1
- package/dist/tui/session-selector.d.ts.map +0 -1
- package/dist/tui/session-selector.js.map +0 -1
- package/dist/tui/theme-selector.js.map +0 -1
- package/dist/tui/thinking-selector.d.ts.map +0 -1
- package/dist/tui/thinking-selector.js.map +0 -1
- package/dist/tui/tool-execution.d.ts.map +0 -1
- package/dist/tui/tool-execution.js.map +0 -1
- package/dist/tui/tui-renderer.d.ts.map +0 -1
- package/dist/tui/tui-renderer.js +0 -1937
- package/dist/tui/tui-renderer.js.map +0 -1
- package/dist/tui/user-message-selector.d.ts.map +0 -1
- package/dist/tui/user-message-selector.js.map +0 -1
- package/dist/tui/user-message.d.ts.map +0 -1
- package/dist/tui/user-message.js.map +0 -1
- /package/dist/{export-html.d.ts → core/export-html.d.ts} +0 -0
- /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
- /package/dist/{messages.js → core/messages.js} +0 -0
- /package/dist/{model-config.d.ts → core/model-config.d.ts} +0 -0
- /package/dist/{oauth → core/oauth}/anthropic.d.ts +0 -0
- /package/dist/{oauth → core/oauth}/anthropic.js +0 -0
- /package/dist/{oauth → core/oauth}/index.d.ts +0 -0
- /package/dist/{oauth → core/oauth}/index.js +0 -0
- /package/dist/{oauth → core/oauth}/storage.d.ts +0 -0
- /package/dist/{session-manager.d.ts → core/session-manager.d.ts} +0 -0
- /package/dist/{settings-manager.d.ts → core/settings-manager.d.ts} +0 -0
- /package/dist/{slash-commands.d.ts → core/slash-commands.d.ts} +0 -0
- /package/dist/{tools → core/tools}/bash.d.ts +0 -0
- /package/dist/{tools → core/tools}/edit.d.ts +0 -0
- /package/dist/{tools → core/tools}/edit.js +0 -0
- /package/dist/{tools → core/tools}/find.d.ts +0 -0
- /package/dist/{tools → core/tools}/grep.d.ts +0 -0
- /package/dist/{tools → core/tools}/index.d.ts +0 -0
- /package/dist/{tools → core/tools}/index.js +0 -0
- /package/dist/{tools → core/tools}/ls.d.ts +0 -0
- /package/dist/{tools → core/tools}/ls.js +0 -0
- /package/dist/{tools → core/tools}/read.d.ts +0 -0
- /package/dist/{tools → core/tools}/read.js +0 -0
- /package/dist/{tools → core/tools}/truncate.d.ts +0 -0
- /package/dist/{tools → core/tools}/truncate.js +0 -0
- /package/dist/{tools → core/tools}/write.d.ts +0 -0
- /package/dist/{tools → core/tools}/write.js +0 -0
- /package/dist/{tui → modes/interactive/components}/assistant-message.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/assistant-message.js +0 -0
- /package/dist/{tui → modes/interactive/components}/compaction.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/compaction.js +0 -0
- /package/dist/{tui → modes/interactive/components}/custom-editor.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/custom-editor.js +0 -0
- /package/dist/{tui → modes/interactive/components}/dynamic-border.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/dynamic-border.js +0 -0
- /package/dist/{tui → modes/interactive/components}/footer.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/oauth-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/queue-mode-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/queue-mode-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/theme-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/theme-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/thinking-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/thinking-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/tool-execution.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/tool-execution.js +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message-selector.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message-selector.js +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message.d.ts +0 -0
- /package/dist/{tui → modes/interactive/components}/user-message.js +0 -0
- /package/dist/{theme → modes/interactive/theme}/dark.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/light.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/theme-schema.json +0 -0
- /package/dist/{theme → modes/interactive/theme}/theme.d.ts +0 -0
- /package/dist/{clipboard.d.ts → utils/clipboard.d.ts} +0 -0
- /package/dist/{clipboard.js → utils/clipboard.js} +0 -0
- /package/dist/{fuzzy.d.ts → utils/fuzzy.d.ts} +0 -0
- /package/dist/{fuzzy.js → utils/fuzzy.js} +0 -0
- /package/dist/{shell.d.ts → utils/shell.d.ts} +0 -0
- /package/dist/{tools-manager.d.ts → utils/tools-manager.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-session.js","sourceRoot":"","sources":["../../src/core/agent-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * AgentSession - Core abstraction for agent lifecycle and session management.\n *\n * This class is shared between all run modes (interactive, print, rpc).\n * It encapsulates:\n * - Agent state access\n * - Event subscription with automatic session persistence\n * - Model and thinking level management\n * - Compaction (manual and auto)\n * - Bash execution\n * - Session switching and branching\n *\n * Modes use this class and add their own I/O layer on top.\n */\n\nimport type { Agent, AgentEvent, AgentState, AppMessage, Attachment, ThinkingLevel } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model } from \"@mariozechner/pi-ai\";\nimport { getModelsPath } from \"../config.js\";\nimport { type BashResult, executeBash as executeBashCommand } from \"./bash-executor.js\";\nimport { calculateContextTokens, compact, shouldCompact } from \"./compaction.js\";\nimport { exportSessionToHtml } from \"./export-html.js\";\nimport type { BashExecutionMessage } from \"./messages.js\";\nimport { getApiKeyForModel, getAvailableModels } from \"./model-config.js\";\nimport { loadSessionFromEntries, type SessionManager } from \"./session-manager.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\nimport { expandSlashCommand, type FileSlashCommand } from \"./slash-commands.js\";\n\n/** Session-specific events that extend the core AgentEvent */\nexport type AgentSessionEvent =\n\t| AgentEvent\n\t| { type: \"auto_compaction_start\" }\n\t| { type: \"auto_compaction_end\"; result: CompactionResult | null; aborted: boolean };\n\n/** Listener function for agent session events */\nexport type AgentSessionEventListener = (event: AgentSessionEvent) => void;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AgentSessionConfig {\n\tagent: Agent;\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\t/** Models to cycle through with Ctrl+P (from --models flag) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;\n\t/** File-based slash commands for expansion */\n\tfileCommands?: FileSlashCommand[];\n}\n\n/** Options for AgentSession.prompt() */\nexport interface PromptOptions {\n\t/** Whether to expand file-based slash commands (default: true) */\n\texpandSlashCommands?: boolean;\n\t/** Image/file attachments */\n\tattachments?: Attachment[];\n}\n\n/** Result from cycleModel() */\nexport interface ModelCycleResult {\n\tmodel: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\t/** Whether cycling through scoped models (--models flag) or all available */\n\tisScoped: boolean;\n}\n\n/** Result from compact() or checkAutoCompaction() */\nexport interface CompactionResult {\n\ttokensBefore: number;\n\tsummary: string;\n}\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n}\n\n// ============================================================================\n// AgentSession Class\n// ============================================================================\n\nexport class AgentSession {\n\treadonly agent: Agent;\n\treadonly sessionManager: SessionManager;\n\treadonly settingsManager: SettingsManager;\n\n\tprivate _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;\n\tprivate _fileCommands: FileSlashCommand[];\n\n\t// Event subscription state\n\tprivate _unsubscribeAgent?: () => void;\n\tprivate _eventListeners: AgentSessionEventListener[] = [];\n\n\t// Message queue state\n\tprivate _queuedMessages: string[] = [];\n\n\t// Compaction state\n\tprivate _compactionAbortController: AbortController | null = null;\n\tprivate _autoCompactionAbortController: AbortController | null = null;\n\n\t// Bash execution state\n\tprivate _bashAbortController: AbortController | null = null;\n\tprivate _pendingBashMessages: BashExecutionMessage[] = [];\n\n\tconstructor(config: AgentSessionConfig) {\n\t\tthis.agent = config.agent;\n\t\tthis.sessionManager = config.sessionManager;\n\t\tthis.settingsManager = config.settingsManager;\n\t\tthis._scopedModels = config.scopedModels ?? [];\n\t\tthis._fileCommands = config.fileCommands ?? [];\n\t}\n\n\t// =========================================================================\n\t// Event Subscription\n\t// =========================================================================\n\n\t/** Emit an event to all listeners */\n\tprivate _emit(event: AgentSessionEvent): void {\n\t\tfor (const l of this._eventListeners) {\n\t\t\tl(event);\n\t\t}\n\t}\n\n\t// Track last assistant message for auto-compaction check\n\tprivate _lastAssistantMessage: AssistantMessage | null = null;\n\n\t/** Internal handler for agent events - shared by subscribe and reconnect */\n\tprivate _handleAgentEvent = async (event: AgentEvent): Promise<void> => {\n\t\t// Notify all listeners\n\t\tthis._emit(event);\n\n\t\t// Handle session persistence\n\t\tif (event.type === \"message_end\") {\n\t\t\tthis.sessionManager.saveMessage(event.message);\n\n\t\t\t// Initialize session after first user+assistant exchange\n\t\t\tif (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {\n\t\t\t\tthis.sessionManager.startSession(this.agent.state);\n\t\t\t}\n\n\t\t\t// Track assistant message for auto-compaction (checked on agent_end)\n\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\tthis._lastAssistantMessage = event.message as AssistantMessage;\n\t\t\t}\n\t\t}\n\n\t\t// Check auto-compaction after agent completes (after agent_end clears UI)\n\t\tif (event.type === \"agent_end\" && this._lastAssistantMessage) {\n\t\t\tconst msg = this._lastAssistantMessage;\n\t\t\tthis._lastAssistantMessage = null;\n\t\t\tthis._runAutoCompaction(msg).catch(() => {});\n\t\t}\n\t};\n\n\t/**\n\t * Subscribe to agent events.\n\t * Session persistence is handled internally (saves messages on message_end).\n\t * Multiple listeners can be added. Returns unsubscribe function for this listener.\n\t */\n\tsubscribe(listener: AgentSessionEventListener): () => void {\n\t\tthis._eventListeners.push(listener);\n\n\t\t// Set up agent subscription if not already done\n\t\tif (!this._unsubscribeAgent) {\n\t\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t\t}\n\n\t\t// Return unsubscribe function for this specific listener\n\t\treturn () => {\n\t\t\tconst index = this._eventListeners.indexOf(listener);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis._eventListeners.splice(index, 1);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Temporarily disconnect from agent events.\n\t * User listeners are preserved and will receive events again after resubscribe().\n\t * Used internally during operations that need to pause event processing.\n\t */\n\tprivate _disconnectFromAgent(): void {\n\t\tif (this._unsubscribeAgent) {\n\t\t\tthis._unsubscribeAgent();\n\t\t\tthis._unsubscribeAgent = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Reconnect to agent events after _disconnectFromAgent().\n\t * Preserves all existing listeners.\n\t */\n\tprivate _reconnectToAgent(): void {\n\t\tif (this._unsubscribeAgent) return; // Already connected\n\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t}\n\n\t/**\n\t * Remove all listeners and disconnect from agent.\n\t * Call this when completely done with the session.\n\t */\n\tdispose(): void {\n\t\tthis._disconnectFromAgent();\n\t\tthis._eventListeners = [];\n\t}\n\n\t// =========================================================================\n\t// Read-only State Access\n\t// =========================================================================\n\n\t/** Full agent state */\n\tget state(): AgentState {\n\t\treturn this.agent.state;\n\t}\n\n\t/** Current model (may be null if not yet selected) */\n\tget model(): Model<any> | null {\n\t\treturn this.agent.state.model;\n\t}\n\n\t/** Current thinking level */\n\tget thinkingLevel(): ThinkingLevel {\n\t\treturn this.agent.state.thinkingLevel;\n\t}\n\n\t/** Whether agent is currently streaming a response */\n\tget isStreaming(): boolean {\n\t\treturn this.agent.state.isStreaming;\n\t}\n\n\t/** All messages including custom types like BashExecutionMessage */\n\tget messages(): AppMessage[] {\n\t\treturn this.agent.state.messages;\n\t}\n\n\t/** Current queue mode */\n\tget queueMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.agent.getQueueMode();\n\t}\n\n\t/** Current session file path */\n\tget sessionFile(): string {\n\t\treturn this.sessionManager.getSessionFile();\n\t}\n\n\t/** Current session ID */\n\tget sessionId(): string {\n\t\treturn this.sessionManager.getSessionId();\n\t}\n\n\t/** Scoped models for cycling (from --models flag) */\n\tget scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel: ThinkingLevel }> {\n\t\treturn this._scopedModels;\n\t}\n\n\t/** File-based slash commands */\n\tget fileCommands(): ReadonlyArray<FileSlashCommand> {\n\t\treturn this._fileCommands;\n\t}\n\n\t// =========================================================================\n\t// Prompting\n\t// =========================================================================\n\n\t/**\n\t * Send a prompt to the agent.\n\t * - Validates model and API key before sending\n\t * - Expands file-based slash commands by default\n\t * @throws Error if no model selected or no API key available\n\t */\n\tasync prompt(text: string, options?: PromptOptions): Promise<void> {\n\t\t// Flush any pending bash messages before the new prompt\n\t\tthis._flushPendingBashMessages();\n\n\t\tconst expandCommands = options?.expandSlashCommands ?? true;\n\n\t\t// Validate model\n\t\tif (!this.model) {\n\t\t\tthrow new Error(\n\t\t\t\t\"No model selected.\\n\\n\" +\n\t\t\t\t\t\"Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\\n\" +\n\t\t\t\t\t`or create ${getModelsPath()}\\n\\n` +\n\t\t\t\t\t\"Then use /model to select a model.\",\n\t\t\t);\n\t\t}\n\n\t\t// Validate API key\n\t\tconst apiKey = await getApiKeyForModel(this.model);\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\n\t\t\t\t`No API key found for ${this.model.provider}.\\n\\n` +\n\t\t\t\t\t`Set the appropriate environment variable or update ${getModelsPath()}`,\n\t\t\t);\n\t\t}\n\n\t\t// Expand slash commands if requested\n\t\tconst expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;\n\n\t\tawait this.agent.prompt(expandedText, options?.attachments);\n\t}\n\n\t/**\n\t * Queue a message to be sent after the current response completes.\n\t * Use when agent is currently streaming.\n\t */\n\tasync queueMessage(text: string): Promise<void> {\n\t\tthis._queuedMessages.push(text);\n\t\tawait this.agent.queueMessage({\n\t\t\trole: \"user\",\n\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t/**\n\t * Clear queued messages and return them.\n\t * Useful for restoring to editor when user aborts.\n\t */\n\tclearQueue(): string[] {\n\t\tconst queued = [...this._queuedMessages];\n\t\tthis._queuedMessages = [];\n\t\tthis.agent.clearMessageQueue();\n\t\treturn queued;\n\t}\n\n\t/** Number of messages currently queued */\n\tget queuedMessageCount(): number {\n\t\treturn this._queuedMessages.length;\n\t}\n\n\t/** Get queued messages (read-only) */\n\tgetQueuedMessages(): readonly string[] {\n\t\treturn this._queuedMessages;\n\t}\n\n\t/**\n\t * Abort current operation and wait for agent to become idle.\n\t */\n\tasync abort(): Promise<void> {\n\t\tthis.agent.abort();\n\t\tawait this.agent.waitForIdle();\n\t}\n\n\t/**\n\t * Reset agent and session to start fresh.\n\t * Clears all messages and starts a new session.\n\t * Listeners are preserved and will continue receiving events.\n\t */\n\tasync reset(): Promise<void> {\n\t\tthis._disconnectFromAgent();\n\t\tawait this.abort();\n\t\tthis.agent.reset();\n\t\tthis.sessionManager.reset();\n\t\tthis._queuedMessages = [];\n\t\tthis._reconnectToAgent();\n\t}\n\n\t// =========================================================================\n\t// Model Management\n\t// =========================================================================\n\n\t/**\n\t * Set model directly.\n\t * Validates API key, saves to session and settings.\n\t * @throws Error if no API key available for the model\n\t */\n\tasync setModel(model: Model<any>): Promise<void> {\n\t\tconst apiKey = await getApiKeyForModel(model);\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(`No API key for ${model.provider}/${model.id}`);\n\t\t}\n\n\t\tthis.agent.setModel(model);\n\t\tthis.sessionManager.saveModelChange(model.provider, model.id);\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t}\n\n\t/**\n\t * Cycle to next model.\n\t * Uses scoped models (from --models flag) if available, otherwise all available models.\n\t * @returns The new model info, or null if only one model available\n\t */\n\tasync cycleModel(): Promise<ModelCycleResult | null> {\n\t\tif (this._scopedModels.length > 0) {\n\t\t\treturn this._cycleScopedModel();\n\t\t}\n\t\treturn this._cycleAvailableModel();\n\t}\n\n\tprivate async _cycleScopedModel(): Promise<ModelCycleResult | null> {\n\t\tif (this._scopedModels.length <= 1) return null;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = this._scopedModels.findIndex(\n\t\t\t(sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider,\n\t\t);\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst nextIndex = (currentIndex + 1) % this._scopedModels.length;\n\t\tconst next = this._scopedModels[nextIndex];\n\n\t\t// Validate API key\n\t\tconst apiKey = await getApiKeyForModel(next.model);\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(`No API key for ${next.model.provider}/${next.model.id}`);\n\t\t}\n\n\t\t// Apply model\n\t\tthis.agent.setModel(next.model);\n\t\tthis.sessionManager.saveModelChange(next.model.provider, next.model.id);\n\t\tthis.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);\n\n\t\t// Apply thinking level (silently use \"off\" if not supported)\n\t\tconst effectiveThinking = next.model.reasoning ? next.thinkingLevel : \"off\";\n\t\tthis.agent.setThinkingLevel(effectiveThinking);\n\t\tthis.sessionManager.saveThinkingLevelChange(effectiveThinking);\n\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveThinking);\n\n\t\treturn { model: next.model, thinkingLevel: effectiveThinking, isScoped: true };\n\t}\n\n\tprivate async _cycleAvailableModel(): Promise<ModelCycleResult | null> {\n\t\tconst { models: availableModels, error } = await getAvailableModels();\n\t\tif (error) throw new Error(`Failed to load models: ${error}`);\n\t\tif (availableModels.length <= 1) return null;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = availableModels.findIndex(\n\t\t\t(m) => m.id === currentModel?.id && m.provider === currentModel?.provider,\n\t\t);\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst nextIndex = (currentIndex + 1) % availableModels.length;\n\t\tconst nextModel = availableModels[nextIndex];\n\n\t\tconst apiKey = await getApiKeyForModel(nextModel);\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);\n\t\t}\n\n\t\tthis.agent.setModel(nextModel);\n\t\tthis.sessionManager.saveModelChange(nextModel.provider, nextModel.id);\n\t\tthis.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);\n\n\t\treturn { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };\n\t}\n\n\t/**\n\t * Get all available models with valid API keys.\n\t */\n\tasync getAvailableModels(): Promise<Model<any>[]> {\n\t\tconst { models, error } = await getAvailableModels();\n\t\tif (error) throw new Error(error);\n\t\treturn models;\n\t}\n\n\t// =========================================================================\n\t// Thinking Level Management\n\t// =========================================================================\n\n\t/**\n\t * Set thinking level.\n\t * Silently uses \"off\" if model doesn't support thinking.\n\t * Saves to session and settings.\n\t */\n\tsetThinkingLevel(level: ThinkingLevel): void {\n\t\tconst effectiveLevel = this.supportsThinking() ? level : \"off\";\n\t\tthis.agent.setThinkingLevel(effectiveLevel);\n\t\tthis.sessionManager.saveThinkingLevelChange(effectiveLevel);\n\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveLevel);\n\t}\n\n\t/**\n\t * Cycle to next thinking level.\n\t * @returns New level, or null if model doesn't support thinking\n\t */\n\tcycleThinkingLevel(): ThinkingLevel | null {\n\t\tif (!this.supportsThinking()) return null;\n\n\t\tconst modelId = this.model?.id || \"\";\n\t\tconst supportsXhigh = modelId.includes(\"codex-max\");\n\t\tconst levels: ThinkingLevel[] = supportsXhigh\n\t\t\t? [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"]\n\t\t\t: [\"off\", \"minimal\", \"low\", \"medium\", \"high\"];\n\n\t\tconst currentIndex = levels.indexOf(this.thinkingLevel);\n\t\tconst nextIndex = (currentIndex + 1) % levels.length;\n\t\tconst nextLevel = levels[nextIndex];\n\n\t\tthis.setThinkingLevel(nextLevel);\n\t\treturn nextLevel;\n\t}\n\n\t/**\n\t * Check if current model supports thinking/reasoning.\n\t */\n\tsupportsThinking(): boolean {\n\t\treturn !!this.model?.reasoning;\n\t}\n\n\t// =========================================================================\n\t// Queue Mode Management\n\t// =========================================================================\n\n\t/**\n\t * Set message queue mode.\n\t * Saves to settings.\n\t */\n\tsetQueueMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.agent.setQueueMode(mode);\n\t\tthis.settingsManager.setQueueMode(mode);\n\t}\n\n\t// =========================================================================\n\t// Compaction\n\t// =========================================================================\n\n\t/**\n\t * Manually compact the session context.\n\t * Aborts current agent operation first.\n\t * @param customInstructions Optional instructions for the compaction summary\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\t// Abort any running operation\n\t\tthis._disconnectFromAgent();\n\t\tawait this.abort();\n\n\t\t// Create abort controller\n\t\tthis._compactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthrow new Error(\"No model selected\");\n\t\t\t}\n\n\t\t\tconst apiKey = await getApiKeyForModel(this.model);\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for ${this.model.provider}`);\n\t\t\t}\n\n\t\t\tconst entries = this.sessionManager.loadEntries();\n\t\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\t\tconst compactionEntry = await compact(\n\t\t\t\tentries,\n\t\t\t\tthis.model,\n\t\t\t\tsettings,\n\t\t\t\tapiKey,\n\t\t\t\tthis._compactionAbortController.signal,\n\t\t\t\tcustomInstructions,\n\t\t\t);\n\n\t\t\tif (this._compactionAbortController.signal.aborted) {\n\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t}\n\n\t\t\t// Save and reload\n\t\t\tthis.sessionManager.saveCompaction(compactionEntry);\n\t\t\tconst loaded = loadSessionFromEntries(this.sessionManager.loadEntries());\n\t\t\tthis.agent.replaceMessages(loaded.messages);\n\n\t\t\treturn {\n\t\t\t\ttokensBefore: compactionEntry.tokensBefore,\n\t\t\t\tsummary: compactionEntry.summary,\n\t\t\t};\n\t\t} finally {\n\t\t\tthis._compactionAbortController = null;\n\t\t\tthis._reconnectToAgent();\n\t\t}\n\t}\n\n\t/**\n\t * Cancel in-progress compaction (manual or auto).\n\t */\n\tabortCompaction(): void {\n\t\tthis._compactionAbortController?.abort();\n\t\tthis._autoCompactionAbortController?.abort();\n\t}\n\n\t/**\n\t * Internal: Run auto-compaction with events.\n\t * Called after assistant messages complete.\n\t */\n\tprivate async _runAutoCompaction(assistantMessage: AssistantMessage): Promise<void> {\n\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\tif (!settings.enabled) return;\n\n\t\t// Skip if message was aborted\n\t\tif (assistantMessage.stopReason === \"aborted\") return;\n\n\t\tconst contextTokens = calculateContextTokens(assistantMessage.usage);\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\n\t\tif (!shouldCompact(contextTokens, contextWindow, settings)) return;\n\n\t\t// Emit start event\n\t\tthis._emit({ type: \"auto_compaction_start\" });\n\t\tthis._autoCompactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthis._emit({ type: \"auto_compaction_end\", result: null, aborted: false });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst apiKey = await getApiKeyForModel(this.model);\n\t\t\tif (!apiKey) {\n\t\t\t\tthis._emit({ type: \"auto_compaction_end\", result: null, aborted: false });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Load entries (sync file read) then yield to let UI render\n\t\t\tconst entries = this.sessionManager.loadEntries();\n\t\t\tconst compactionEntry = await compact(\n\t\t\t\tentries,\n\t\t\t\tthis.model,\n\t\t\t\tsettings,\n\t\t\t\tapiKey,\n\t\t\t\tthis._autoCompactionAbortController.signal,\n\t\t\t);\n\n\t\t\tif (this._autoCompactionAbortController.signal.aborted) {\n\t\t\t\tthis._emit({ type: \"auto_compaction_end\", result: null, aborted: true });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.sessionManager.saveCompaction(compactionEntry);\n\t\t\tconst loaded = loadSessionFromEntries(this.sessionManager.loadEntries());\n\t\t\tthis.agent.replaceMessages(loaded.messages);\n\n\t\t\tconst result: CompactionResult = {\n\t\t\t\ttokensBefore: compactionEntry.tokensBefore,\n\t\t\t\tsummary: compactionEntry.summary,\n\t\t\t};\n\t\t\tthis._emit({ type: \"auto_compaction_end\", result, aborted: false });\n\t\t} catch {\n\t\t\t// Silently fail auto-compaction but emit end event\n\t\t\tthis._emit({ type: \"auto_compaction_end\", result: null, aborted: false });\n\t\t} finally {\n\t\t\tthis._autoCompactionAbortController = null;\n\t\t}\n\t}\n\n\t/**\n\t * Toggle auto-compaction setting.\n\t */\n\tsetAutoCompactionEnabled(enabled: boolean): void {\n\t\tthis.settingsManager.setCompactionEnabled(enabled);\n\t}\n\n\t/** Whether auto-compaction is enabled */\n\tget autoCompactionEnabled(): boolean {\n\t\treturn this.settingsManager.getCompactionEnabled();\n\t}\n\n\t// =========================================================================\n\t// Bash Execution\n\t// =========================================================================\n\n\t/**\n\t * Execute a bash command.\n\t * Adds result to agent context and session.\n\t * @param command The bash command to execute\n\t * @param onChunk Optional streaming callback for output\n\t */\n\tasync executeBash(command: string, onChunk?: (chunk: string) => void): Promise<BashResult> {\n\t\tthis._bashAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tconst result = await executeBashCommand(command, {\n\t\t\t\tonChunk,\n\t\t\t\tsignal: this._bashAbortController.signal,\n\t\t\t});\n\n\t\t\t// Create and save message\n\t\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\t\trole: \"bashExecution\",\n\t\t\t\tcommand,\n\t\t\t\toutput: result.output,\n\t\t\t\texitCode: result.exitCode,\n\t\t\t\tcancelled: result.cancelled,\n\t\t\t\ttruncated: result.truncated,\n\t\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\n\t\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\t\tif (this.isStreaming) {\n\t\t\t\t// Queue for later - will be flushed on agent_end\n\t\t\t\tthis._pendingBashMessages.push(bashMessage);\n\t\t\t} else {\n\t\t\t\t// Add to agent state immediately\n\t\t\t\tthis.agent.appendMessage(bashMessage);\n\n\t\t\t\t// Save to session\n\t\t\t\tthis.sessionManager.saveMessage(bashMessage);\n\n\t\t\t\t// Initialize session if needed\n\t\t\t\tif (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {\n\t\t\t\t\tthis.sessionManager.startSession(this.agent.state);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tthis._bashAbortController = null;\n\t\t}\n\t}\n\n\t/**\n\t * Cancel running bash command.\n\t */\n\tabortBash(): void {\n\t\tthis._bashAbortController?.abort();\n\t}\n\n\t/** Whether a bash command is currently running */\n\tget isBashRunning(): boolean {\n\t\treturn this._bashAbortController !== null;\n\t}\n\n\t/** Whether there are pending bash messages waiting to be flushed */\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this._pendingBashMessages.length > 0;\n\t}\n\n\t/**\n\t * Flush pending bash messages to agent state and session.\n\t * Called after agent turn completes to maintain proper message ordering.\n\t */\n\tprivate _flushPendingBashMessages(): void {\n\t\tif (this._pendingBashMessages.length === 0) return;\n\n\t\tfor (const bashMessage of this._pendingBashMessages) {\n\t\t\t// Add to agent state\n\t\t\tthis.agent.appendMessage(bashMessage);\n\n\t\t\t// Save to session\n\t\t\tthis.sessionManager.saveMessage(bashMessage);\n\t\t}\n\n\t\t// Initialize session if needed\n\t\tif (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {\n\t\t\tthis.sessionManager.startSession(this.agent.state);\n\t\t}\n\n\t\tthis._pendingBashMessages = [];\n\t}\n\n\t// =========================================================================\n\t// Session Management\n\t// =========================================================================\n\n\t/**\n\t * Switch to a different session file.\n\t * Aborts current operation, loads messages, restores model/thinking.\n\t * Listeners are preserved and will continue receiving events.\n\t */\n\tasync switchSession(sessionPath: string): Promise<void> {\n\t\tthis._disconnectFromAgent();\n\t\tawait this.abort();\n\t\tthis._queuedMessages = [];\n\n\t\t// Set new session\n\t\tthis.sessionManager.setSessionFile(sessionPath);\n\n\t\t// Reload messages\n\t\tconst loaded = loadSessionFromEntries(this.sessionManager.loadEntries());\n\t\tthis.agent.replaceMessages(loaded.messages);\n\n\t\t// Restore model if saved\n\t\tconst savedModel = this.sessionManager.loadModel();\n\t\tif (savedModel) {\n\t\t\tconst availableModels = (await getAvailableModels()).models;\n\t\t\tconst match = availableModels.find((m) => m.provider === savedModel.provider && m.id === savedModel.modelId);\n\t\t\tif (match) {\n\t\t\t\tthis.agent.setModel(match);\n\t\t\t}\n\t\t}\n\n\t\t// Restore thinking level if saved\n\t\tconst savedThinking = this.sessionManager.loadThinkingLevel();\n\t\tif (savedThinking) {\n\t\t\tthis.agent.setThinkingLevel(savedThinking as ThinkingLevel);\n\t\t}\n\n\t\tthis._reconnectToAgent();\n\t}\n\n\t/**\n\t * Create a branch from a specific entry index.\n\t * @param entryIndex Index into session entries to branch from\n\t * @returns The text of the selected user message (for editor pre-fill)\n\t */\n\tbranch(entryIndex: number): string {\n\t\tconst entries = this.sessionManager.loadEntries();\n\t\tconst selectedEntry = entries[entryIndex];\n\n\t\tif (!selectedEntry || selectedEntry.type !== \"message\" || selectedEntry.message.role !== \"user\") {\n\t\t\tthrow new Error(\"Invalid entry index for branching\");\n\t\t}\n\n\t\tconst selectedText = this._extractUserMessageText(selectedEntry.message.content);\n\n\t\t// Create branched session\n\t\tconst newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);\n\t\tthis.sessionManager.setSessionFile(newSessionFile);\n\n\t\t// Reload\n\t\tconst loaded = loadSessionFromEntries(this.sessionManager.loadEntries());\n\t\tthis.agent.replaceMessages(loaded.messages);\n\n\t\treturn selectedText;\n\t}\n\n\t/**\n\t * Get all user messages from session for branch selector.\n\t */\n\tgetUserMessagesForBranching(): Array<{ entryIndex: number; text: string }> {\n\t\tconst entries = this.sessionManager.loadEntries();\n\t\tconst result: Array<{ entryIndex: number; text: string }> = [];\n\n\t\tfor (let i = 0; i < entries.length; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type !== \"message\") continue;\n\t\t\tif (entry.message.role !== \"user\") continue;\n\n\t\t\tconst text = this._extractUserMessageText(entry.message.content);\n\t\t\tif (text) {\n\t\t\t\tresult.push({ entryIndex: i, text });\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\t\tif (typeof content === \"string\") return content;\n\t\tif (Array.isArray(content)) {\n\t\t\treturn content\n\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\");\n\t\t}\n\t\treturn \"\";\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.state;\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tsessionFile: this.sessionFile,\n\t\t\tsessionId: this.sessionId,\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\texportToHtml(outputPath?: string): string {\n\t\treturn exportSessionToHtml(this.sessionManager, this.state, outputPath);\n\t}\n\n\t// =========================================================================\n\t// Utilities\n\t// =========================================================================\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or null if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | null {\n\t\tconst lastAssistant = this.messages\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.find((m) => m.role === \"assistant\");\n\n\t\tif (!lastAssistant) return null;\n\n\t\tlet text = \"\";\n\t\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || null;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command execution with streaming support and cancellation.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a unified bash execution implementation used by:
|
|
5
|
+
* - AgentSession.executeBash() for interactive and RPC modes
|
|
6
|
+
* - Direct calls from modes that need bash execution
|
|
7
|
+
*/
|
|
8
|
+
export interface BashExecutorOptions {
|
|
9
|
+
/** Callback for streaming output chunks (already sanitized) */
|
|
10
|
+
onChunk?: (chunk: string) => void;
|
|
11
|
+
/** AbortSignal for cancellation */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface BashResult {
|
|
15
|
+
/** Combined stdout + stderr output (sanitized, possibly truncated) */
|
|
16
|
+
output: string;
|
|
17
|
+
/** Process exit code (null if killed/cancelled) */
|
|
18
|
+
exitCode: number | null;
|
|
19
|
+
/** Whether the command was cancelled via signal */
|
|
20
|
+
cancelled: boolean;
|
|
21
|
+
/** Whether the output was truncated */
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
/** Path to temp file containing full output (if output exceeded truncation threshold) */
|
|
24
|
+
fullOutputPath?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute a bash command with optional streaming and cancellation support.
|
|
28
|
+
*
|
|
29
|
+
* Features:
|
|
30
|
+
* - Streams sanitized output via onChunk callback
|
|
31
|
+
* - Writes large output to temp file for later retrieval
|
|
32
|
+
* - Supports cancellation via AbortSignal
|
|
33
|
+
* - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)
|
|
34
|
+
* - Truncates output if it exceeds the default max bytes
|
|
35
|
+
*
|
|
36
|
+
* @param command - The bash command to execute
|
|
37
|
+
* @param options - Optional streaming callback and abort signal
|
|
38
|
+
* @returns Promise resolving to execution result
|
|
39
|
+
*/
|
|
40
|
+
export declare function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult>;
|
|
41
|
+
//# sourceMappingURL=bash-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAqH/F","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (null if killed/cancelled) */\n\texitCode: number | null;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: null,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command execution with streaming support and cancellation.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a unified bash execution implementation used by:
|
|
5
|
+
* - AgentSession.executeBash() for interactive and RPC modes
|
|
6
|
+
* - Direct calls from modes that need bash execution
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { createWriteStream } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import stripAnsi from "strip-ansi";
|
|
14
|
+
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell.js";
|
|
15
|
+
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Implementation
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Execute a bash command with optional streaming and cancellation support.
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Streams sanitized output via onChunk callback
|
|
24
|
+
* - Writes large output to temp file for later retrieval
|
|
25
|
+
* - Supports cancellation via AbortSignal
|
|
26
|
+
* - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)
|
|
27
|
+
* - Truncates output if it exceeds the default max bytes
|
|
28
|
+
*
|
|
29
|
+
* @param command - The bash command to execute
|
|
30
|
+
* @param options - Optional streaming callback and abort signal
|
|
31
|
+
* @returns Promise resolving to execution result
|
|
32
|
+
*/
|
|
33
|
+
export function executeBash(command, options) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const { shell, args } = getShellConfig();
|
|
36
|
+
const child = spawn(shell, [...args, command], {
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
});
|
|
40
|
+
// Track sanitized output for truncation
|
|
41
|
+
const outputChunks = [];
|
|
42
|
+
let outputBytes = 0;
|
|
43
|
+
const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
|
|
44
|
+
// Temp file for large output
|
|
45
|
+
let tempFilePath;
|
|
46
|
+
let tempFileStream;
|
|
47
|
+
let totalBytes = 0;
|
|
48
|
+
// Handle abort signal
|
|
49
|
+
const abortHandler = () => {
|
|
50
|
+
if (child.pid) {
|
|
51
|
+
killProcessTree(child.pid);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (options?.signal) {
|
|
55
|
+
if (options.signal.aborted) {
|
|
56
|
+
// Already aborted, don't even start
|
|
57
|
+
child.kill();
|
|
58
|
+
resolve({
|
|
59
|
+
output: "",
|
|
60
|
+
exitCode: null,
|
|
61
|
+
cancelled: true,
|
|
62
|
+
truncated: false,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
67
|
+
}
|
|
68
|
+
const handleData = (data) => {
|
|
69
|
+
totalBytes += data.length;
|
|
70
|
+
// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
|
|
71
|
+
const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
|
|
72
|
+
// Start writing to temp file if exceeds threshold
|
|
73
|
+
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
74
|
+
const id = randomBytes(8).toString("hex");
|
|
75
|
+
tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
|
|
76
|
+
tempFileStream = createWriteStream(tempFilePath);
|
|
77
|
+
// Write already-buffered chunks to temp file
|
|
78
|
+
for (const chunk of outputChunks) {
|
|
79
|
+
tempFileStream.write(chunk);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (tempFileStream) {
|
|
83
|
+
tempFileStream.write(text);
|
|
84
|
+
}
|
|
85
|
+
// Keep rolling buffer of sanitized text
|
|
86
|
+
outputChunks.push(text);
|
|
87
|
+
outputBytes += text.length;
|
|
88
|
+
while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
|
|
89
|
+
const removed = outputChunks.shift();
|
|
90
|
+
outputBytes -= removed.length;
|
|
91
|
+
}
|
|
92
|
+
// Stream to callback if provided
|
|
93
|
+
if (options?.onChunk) {
|
|
94
|
+
options.onChunk(text);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
child.stdout?.on("data", handleData);
|
|
98
|
+
child.stderr?.on("data", handleData);
|
|
99
|
+
child.on("close", (code) => {
|
|
100
|
+
// Clean up abort listener
|
|
101
|
+
if (options?.signal) {
|
|
102
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
103
|
+
}
|
|
104
|
+
if (tempFileStream) {
|
|
105
|
+
tempFileStream.end();
|
|
106
|
+
}
|
|
107
|
+
// Combine buffered chunks for truncation (already sanitized)
|
|
108
|
+
const fullOutput = outputChunks.join("");
|
|
109
|
+
const truncationResult = truncateTail(fullOutput);
|
|
110
|
+
// code === null means killed (cancelled)
|
|
111
|
+
const cancelled = code === null;
|
|
112
|
+
resolve({
|
|
113
|
+
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
|
114
|
+
exitCode: code,
|
|
115
|
+
cancelled,
|
|
116
|
+
truncated: truncationResult.truncated,
|
|
117
|
+
fullOutputPath: tempFilePath,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
// Clean up abort listener
|
|
122
|
+
if (options?.signal) {
|
|
123
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
124
|
+
}
|
|
125
|
+
if (tempFileStream) {
|
|
126
|
+
tempFileStream.end();
|
|
127
|
+
}
|
|
128
|
+
reject(err);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=bash-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport stripAnsi from \"strip-ansi\";\nimport { getShellConfig, killProcessTree, sanitizeBinaryOutput } from \"../utils/shell.js\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (null if killed/cancelled) */\n\texitCode: number | null;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command with optional streaming and cancellation support.\n *\n * Features:\n * - Streams sanitized output via onChunk callback\n * - Writes large output to temp file for later retrieval\n * - Supports cancellation via AbortSignal\n * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines)\n * - Truncates output if it exceeds the default max bytes\n *\n * @param command - The bash command to execute\n * @param options - Optional streaming callback and abort signal\n * @returns Promise resolving to execution result\n */\nexport function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst { shell, args } = getShellConfig();\n\t\tconst child: ChildProcess = spawn(shell, [...args, command], {\n\t\t\tdetached: true,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\t// Track sanitized output for truncation\n\t\tconst outputChunks: string[] = [];\n\t\tlet outputBytes = 0;\n\t\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t// Temp file for large output\n\t\tlet tempFilePath: string | undefined;\n\t\tlet tempFileStream: WriteStream | undefined;\n\t\tlet totalBytes = 0;\n\n\t\t// Handle abort signal\n\t\tconst abortHandler = () => {\n\t\t\tif (child.pid) {\n\t\t\t\tkillProcessTree(child.pid);\n\t\t\t}\n\t\t};\n\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\t// Already aborted, don't even start\n\t\t\t\tchild.kill();\n\t\t\t\tresolve({\n\t\t\t\t\toutput: \"\",\n\t\t\t\t\texitCode: null,\n\t\t\t\t\tcancelled: true,\n\t\t\t\t\ttruncated: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\toptions.signal.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t}\n\n\t\tconst handleData = (data: Buffer) => {\n\t\t\ttotalBytes += data.length;\n\n\t\t\t// Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines\n\t\t\tconst text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\\r/g, \"\");\n\n\t\t\t// Start writing to temp file if exceeds threshold\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t// Write already-buffered chunks to temp file\n\t\t\t\tfor (const chunk of outputChunks) {\n\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.write(text);\n\t\t\t}\n\n\t\t\t// Keep rolling buffer of sanitized text\n\t\t\toutputChunks.push(text);\n\t\t\toutputBytes += text.length;\n\t\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\t\tconst removed = outputChunks.shift()!;\n\t\t\t\toutputBytes -= removed.length;\n\t\t\t}\n\n\t\t\t// Stream to callback if provided\n\t\t\tif (options?.onChunk) {\n\t\t\t\toptions.onChunk(text);\n\t\t\t}\n\t\t};\n\n\t\tchild.stdout?.on(\"data\", handleData);\n\t\tchild.stderr?.on(\"data\", handleData);\n\n\t\tchild.on(\"close\", (code) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Combine buffered chunks for truncation (already sanitized)\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\n\t\t\t// code === null means killed (cancelled)\n\t\t\tconst cancelled = code === null;\n\n\t\t\tresolve({\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: code,\n\t\t\t\tcancelled,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t});\n\t\t});\n\n\t\tchild.on(\"error\", (err) => {\n\t\t\t// Clean up abort listener\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", abortHandler);\n\t\t\t}\n\n\t\t\tif (tempFileStream) {\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\treject(err);\n\t\t});\n\t});\n}\n"]}
|
|
@@ -28,7 +28,11 @@ export declare function getLastAssistantUsage(entries: SessionEntry[]): Usage |
|
|
|
28
28
|
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
29
29
|
/**
|
|
30
30
|
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
|
31
|
-
* Returns the entry index of the first
|
|
31
|
+
* Returns the entry index of the first entry to keep.
|
|
32
|
+
*
|
|
33
|
+
* The cut point targets a user message (turn boundary), but then scans backwards
|
|
34
|
+
* to include any preceding non-turn entries (bash executions, settings changes, etc.)
|
|
35
|
+
* that should logically be part of the kept context.
|
|
32
36
|
*
|
|
33
37
|
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
34
38
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAoB,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAG1E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAM1E,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,2BAA2B,EAAE,kBAIzC,CAAC;AAMF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAeD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,KAAK,GAAG,IAAI,CAS3E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAGjH;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACtB,MAAM,CAmER;AAiBD;;GAEG;AACH,wBAAsB,eAAe,CACpC,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CA2BjB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAC5B,OAAO,EAAE,YAAY,EAAE,EACvB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,eAAe,CAAC,CA6D1B","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport { messageTransformer } from \"./messages.js\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Find indices of message entries that are user messages (turn boundaries).\n */\nfunction findTurnBoundaries(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst boundaries: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\" && entry.message.role === \"user\") {\n\t\t\tboundaries.push(i);\n\t\t}\n\t}\n\treturn boundaries;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n * Returns the entry index of the first entry to keep.\n *\n * The cut point targets a user message (turn boundary), but then scans backwards\n * to include any preceding non-turn entries (bash executions, settings changes, etc.)\n * that should logically be part of the kept context.\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): number {\n\tconst boundaries = findTurnBoundaries(entries, startIndex, endIndex);\n\n\tif (boundaries.length === 0) {\n\t\treturn startIndex; // No user messages, keep everything in range\n\t}\n\n\t// Collect assistant usages walking backwards from endIndex\n\tconst assistantUsages: Array<{ index: number; tokens: number }> = [];\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) {\n\t\t\t\tassistantUsages.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttokens: calculateContextTokens(usage),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (assistantUsages.length === 0) {\n\t\t// No usage info, keep last turn only\n\t\treturn boundaries[boundaries.length - 1];\n\t}\n\n\t// Walk through and find where cumulative token difference exceeds keepRecentTokens\n\tconst newestTokens = assistantUsages[0].tokens;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = 1; i < assistantUsages.length; i++) {\n\t\tconst tokenDiff = newestTokens - assistantUsages[i].tokens;\n\t\tif (tokenDiff >= keepRecentTokens) {\n\t\t\t// Find the turn boundary at or before the assistant we want to keep\n\t\t\tconst lastKeptAssistantIndex = assistantUsages[i - 1].index;\n\n\t\t\tfor (let b = boundaries.length - 1; b >= 0; b--) {\n\t\t\t\tif (boundaries[b] <= lastKeptAssistantIndex) {\n\t\t\t\t\tcutIndex = boundaries[b];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-turn entries (bash, settings, etc.)\n\t// that should logically be part of the kept context\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\tconst role = prevEntry.message.role;\n\t\t\t// Stop if we hit an assistant, user, or tool result (all part of previous turn)\n\t\t\tif (role === \"assistant\" || role === \"user\" || role === \"toolResult\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t// Include this non-turn entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\treturn cutIndex;\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: prompt }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages to summarize (before the cut point)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < firstKeptEntryIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToSummarize.push(entry.message);\n\t\t}\n\t}\n\n\t// Also include the previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\t// Prepend the previous summary as context\n\t\tmessagesToSummarize.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Generate summary from messages before the cut point\n\tconst summary = await generateSummary(\n\t\tmessagesToSummarize,\n\t\tmodel,\n\t\tsettings.reserveTokens,\n\t\tapiKey,\n\t\tsignal,\n\t\tcustomInstructions,\n\t);\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n"]}
|
|
@@ -73,7 +73,11 @@ function findTurnBoundaries(entries, startIndex, endIndex) {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
|
76
|
-
* Returns the entry index of the first
|
|
76
|
+
* Returns the entry index of the first entry to keep.
|
|
77
|
+
*
|
|
78
|
+
* The cut point targets a user message (turn boundary), but then scans backwards
|
|
79
|
+
* to include any preceding non-turn entries (bash executions, settings changes, etc.)
|
|
80
|
+
* that should logically be part of the kept context.
|
|
77
81
|
*
|
|
78
82
|
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
79
83
|
*/
|
|
@@ -117,6 +121,24 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
|
117
121
|
break;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
124
|
+
// Scan backwards from cutIndex to include any non-turn entries (bash, settings, etc.)
|
|
125
|
+
// that should logically be part of the kept context
|
|
126
|
+
while (cutIndex > startIndex) {
|
|
127
|
+
const prevEntry = entries[cutIndex - 1];
|
|
128
|
+
// Stop at compaction boundaries
|
|
129
|
+
if (prevEntry.type === "compaction") {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (prevEntry.type === "message") {
|
|
133
|
+
const role = prevEntry.message.role;
|
|
134
|
+
// Stop if we hit an assistant, user, or tool result (all part of previous turn)
|
|
135
|
+
if (role === "assistant" || role === "user" || role === "toolResult") {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Include this non-turn entry (bash, settings change, etc.)
|
|
140
|
+
cutIndex--;
|
|
141
|
+
}
|
|
120
142
|
return cutIndex;
|
|
121
143
|
}
|
|
122
144
|
// ============================================================================
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport { messageTransformer } from \"./messages.js\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Find indices of message entries that are user messages (turn boundaries).\n */\nfunction findTurnBoundaries(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst boundaries: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\" && entry.message.role === \"user\") {\n\t\t\tboundaries.push(i);\n\t\t}\n\t}\n\treturn boundaries;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n * Returns the entry index of the first entry to keep.\n *\n * The cut point targets a user message (turn boundary), but then scans backwards\n * to include any preceding non-turn entries (bash executions, settings changes, etc.)\n * that should logically be part of the kept context.\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): number {\n\tconst boundaries = findTurnBoundaries(entries, startIndex, endIndex);\n\n\tif (boundaries.length === 0) {\n\t\treturn startIndex; // No user messages, keep everything in range\n\t}\n\n\t// Collect assistant usages walking backwards from endIndex\n\tconst assistantUsages: Array<{ index: number; tokens: number }> = [];\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) {\n\t\t\t\tassistantUsages.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttokens: calculateContextTokens(usage),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (assistantUsages.length === 0) {\n\t\t// No usage info, keep last turn only\n\t\treturn boundaries[boundaries.length - 1];\n\t}\n\n\t// Walk through and find where cumulative token difference exceeds keepRecentTokens\n\tconst newestTokens = assistantUsages[0].tokens;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = 1; i < assistantUsages.length; i++) {\n\t\tconst tokenDiff = newestTokens - assistantUsages[i].tokens;\n\t\tif (tokenDiff >= keepRecentTokens) {\n\t\t\t// Find the turn boundary at or before the assistant we want to keep\n\t\t\tconst lastKeptAssistantIndex = assistantUsages[i - 1].index;\n\n\t\t\tfor (let b = boundaries.length - 1; b >= 0; b--) {\n\t\t\t\tif (boundaries[b] <= lastKeptAssistantIndex) {\n\t\t\t\t\tcutIndex = boundaries[b];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Scan backwards from cutIndex to include any non-turn entries (bash, settings, etc.)\n\t// that should logically be part of the kept context\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\tconst role = prevEntry.message.role;\n\t\t\t// Stop if we hit an assistant, user, or tool result (all part of previous turn)\n\t\t\tif (role === \"assistant\" || role === \"user\" || role === \"toolResult\") {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t// Include this non-turn entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\treturn cutIndex;\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: prompt }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages to summarize (before the cut point)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < firstKeptEntryIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToSummarize.push(entry.message);\n\t\t}\n\t}\n\n\t// Also include the previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\t// Prepend the previous summary as context\n\t\tmessagesToSummarize.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Generate summary from messages before the cut point\n\tconst summary = await generateSummary(\n\t\tmessagesToSummarize,\n\t\tmodel,\n\t\tsettings.reserveTokens,\n\t\tapiKey,\n\t\tsignal,\n\t\tcustomInstructions,\n\t);\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-html.d.ts","sourceRoot":"","sources":["../../src/core/export-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAO9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAmxB3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBlH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB7E","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Message, ToolResultMessage, UserMessage } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename } from \"path\";\nimport { APP_NAME, VERSION } from \"../config.js\";\nimport { type BashExecutionMessage, isBashExecutionMessage } from \"./messages.js\";\nimport type { SessionManager } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface MessageEvent {\n\ttype: \"message\";\n\tmessage: Message;\n\ttimestamp?: number;\n}\n\ninterface ModelChangeEvent {\n\ttype: \"model_change\";\n\tprovider: string;\n\tmodelId: string;\n\ttimestamp?: number;\n}\n\ninterface CompactionEvent {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\ttokensBefore: number;\n}\n\ntype SessionEvent = MessageEvent | ModelChangeEvent | CompactionEvent;\n\ninterface ParsedSessionData {\n\tsessionId: string;\n\ttimestamp: string;\n\tsystemPrompt?: string;\n\tmodelsUsed: Set<string>;\n\tmessages: Message[];\n\ttoolResultsMap: Map<string, ToolResultMessage>;\n\tsessionEvents: SessionEvent[];\n\ttokenStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\tcostStats: { input: number; output: number; cacheRead: number; cacheWrite: number };\n\ttools?: { name: string; description: string }[];\n\tcontextWindow?: number;\n\tisStreamingFormat?: boolean;\n}\n\n// ============================================================================\n// Color scheme (matching TUI)\n// ============================================================================\n\nconst COLORS = {\n\tuserMessageBg: \"rgb(52, 53, 65)\",\n\ttoolPendingBg: \"rgb(40, 40, 50)\",\n\ttoolSuccessBg: \"rgb(40, 50, 40)\",\n\ttoolErrorBg: \"rgb(60, 40, 40)\",\n\tuserBashBg: \"rgb(50, 48, 35)\", // Faint yellow/brown for user-executed bash\n\tuserBashErrorBg: \"rgb(60, 45, 35)\", // Slightly more orange for errors\n\tbodyBg: \"rgb(24, 24, 30)\",\n\tcontainerBg: \"rgb(30, 30, 36)\",\n\ttext: \"rgb(229, 229, 231)\",\n\ttextDim: \"rgb(161, 161, 170)\",\n\tcyan: \"rgb(103, 232, 249)\",\n\tgreen: \"rgb(34, 197, 94)\",\n\tred: \"rgb(239, 68, 68)\",\n\tyellow: \"rgb(234, 179, 8)\",\n};\n\n// ============================================================================\n// Utility functions\n// ============================================================================\n\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nfunction shortenPath(path: string): string {\n\tconst home = homedir();\n\treturn path.startsWith(home) ? \"~\" + path.slice(home.length) : path;\n}\n\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nfunction formatTimestamp(timestamp: number | string | undefined): string {\n\tif (!timestamp) return \"\";\n\tconst date = new Date(typeof timestamp === \"string\" ? timestamp : timestamp);\n\treturn date.toLocaleTimeString(undefined, { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" });\n}\n\nfunction formatExpandableOutput(lines: string[], maxLines: number): string {\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tif (remaining > 0) {\n\t\tlet out = '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\tout += '<div class=\"output-preview\">';\n\t\tfor (const line of displayLines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\tout += \"</div>\";\n\t\tout += '<div class=\"output-full\">';\n\t\tfor (const line of lines) {\n\t\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t}\n\t\tout += \"</div></div>\";\n\t\treturn out;\n\t}\n\n\tlet out = '<div class=\"tool-output\">';\n\tfor (const line of displayLines) {\n\t\tout += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t}\n\tout += \"</div>\";\n\treturn out;\n}\n\n// ============================================================================\n// Parsing functions\n// ============================================================================\n\nfunction parseSessionManagerFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t};\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; [key: string]: unknown };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; [key: string]: unknown };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tswitch (entry.type) {\n\t\t\tcase \"session\":\n\t\t\t\tdata.sessionId = (entry.id as string) || \"unknown\";\n\t\t\t\tdata.timestamp = (entry.timestamp as string) || data.timestamp;\n\t\t\t\tdata.systemPrompt = entry.systemPrompt as string | undefined;\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message\": {\n\t\t\t\tconst message = entry.message as Message;\n\t\t\t\tdata.messages.push(message);\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\tmessage,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\n\t\t\t\tif (message.role === \"toolResult\") {\n\t\t\t\t\tconst toolResult = message as ToolResultMessage;\n\t\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"model_change\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"model_change\",\n\t\t\t\t\tprovider: entry.provider as string,\n\t\t\t\t\tmodelId: entry.modelId as string,\n\t\t\t\t\ttimestamp: entry.timestamp as number | undefined,\n\t\t\t\t});\n\t\t\t\tif (entry.modelId) {\n\t\t\t\t\tconst modelInfo = entry.provider ? `${entry.provider}/${entry.modelId}` : (entry.modelId as string);\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"compaction\":\n\t\t\t\tdata.sessionEvents.push({\n\t\t\t\t\ttype: \"compaction\",\n\t\t\t\t\ttimestamp: entry.timestamp as string,\n\t\t\t\t\tsummary: entry.summary as string,\n\t\t\t\t\ttokensBefore: entry.tokensBefore as number,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn data;\n}\n\nfunction parseStreamingEventFormat(lines: string[]): ParsedSessionData {\n\tconst data: ParsedSessionData = {\n\t\tsessionId: \"unknown\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tmodelsUsed: new Set(),\n\t\tmessages: [],\n\t\ttoolResultsMap: new Map(),\n\t\tsessionEvents: [],\n\t\ttokenStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcostStats: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tisStreamingFormat: true,\n\t};\n\n\tlet timestampSet = false;\n\n\tfor (const line of lines) {\n\t\tlet entry: { type: string; message?: Message };\n\t\ttry {\n\t\t\tentry = JSON.parse(line) as { type: string; message?: Message };\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (entry.type === \"message_end\" && entry.message) {\n\t\t\tconst msg = entry.message;\n\t\t\tdata.messages.push(msg);\n\t\t\tdata.sessionEvents.push({\n\t\t\t\ttype: \"message\",\n\t\t\t\tmessage: msg,\n\t\t\t\ttimestamp: (msg as { timestamp?: number }).timestamp,\n\t\t\t});\n\n\t\t\tif (msg.role === \"toolResult\") {\n\t\t\t\tconst toolResult = msg as ToolResultMessage;\n\t\t\t\tdata.toolResultsMap.set(toolResult.toolCallId, toolResult);\n\t\t\t} else if (msg.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = msg as AssistantMessage;\n\t\t\t\tif (assistantMsg.model) {\n\t\t\t\t\tconst modelInfo = assistantMsg.provider\n\t\t\t\t\t\t? `${assistantMsg.provider}/${assistantMsg.model}`\n\t\t\t\t\t\t: assistantMsg.model;\n\t\t\t\t\tdata.modelsUsed.add(modelInfo);\n\t\t\t\t}\n\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\tdata.tokenStats.input += assistantMsg.usage.input || 0;\n\t\t\t\t\tdata.tokenStats.output += assistantMsg.usage.output || 0;\n\t\t\t\t\tdata.tokenStats.cacheRead += assistantMsg.usage.cacheRead || 0;\n\t\t\t\t\tdata.tokenStats.cacheWrite += assistantMsg.usage.cacheWrite || 0;\n\t\t\t\t\tif (assistantMsg.usage.cost) {\n\t\t\t\t\t\tdata.costStats.input += assistantMsg.usage.cost.input || 0;\n\t\t\t\t\t\tdata.costStats.output += assistantMsg.usage.cost.output || 0;\n\t\t\t\t\t\tdata.costStats.cacheRead += assistantMsg.usage.cost.cacheRead || 0;\n\t\t\t\t\t\tdata.costStats.cacheWrite += assistantMsg.usage.cost.cacheWrite || 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!timestampSet && (msg as { timestamp?: number }).timestamp) {\n\t\t\t\tdata.timestamp = new Date((msg as { timestamp: number }).timestamp).toISOString();\n\t\t\t\ttimestampSet = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tdata.sessionId = `stream-${data.timestamp.replace(/[:.]/g, \"-\")}`;\n\treturn data;\n}\n\nfunction detectFormat(lines: string[]): \"session-manager\" | \"streaming-events\" | \"unknown\" {\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as { type: string };\n\t\t\tif (entry.type === \"session\") return \"session-manager\";\n\t\t\tif (entry.type === \"agent_start\" || entry.type === \"message_start\" || entry.type === \"turn_start\") {\n\t\t\t\treturn \"streaming-events\";\n\t\t\t}\n\t\t} catch {}\n\t}\n\treturn \"unknown\";\n}\n\nfunction parseSessionFile(content: string): ParsedSessionData {\n\tconst lines = content\n\t\t.trim()\n\t\t.split(\"\\n\")\n\t\t.filter((l) => l.trim());\n\n\tif (lines.length === 0) {\n\t\tthrow new Error(\"Empty session file\");\n\t}\n\n\tconst format = detectFormat(lines);\n\tif (format === \"unknown\") {\n\t\tthrow new Error(\"Unknown session file format\");\n\t}\n\n\treturn format === \"session-manager\" ? parseSessionManagerFormat(lines) : parseStreamingEventFormat(lines);\n}\n\n// ============================================================================\n// HTML formatting functions\n// ============================================================================\n\nfunction formatToolExecution(\n\ttoolName: string,\n\targs: Record<string, unknown>,\n\tresult?: ToolResultMessage,\n): { html: string; bgColor: string } {\n\tlet html = \"\";\n\tconst isError = result?.isError || false;\n\tconst bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;\n\n\tconst getTextOutput = (): string => {\n\t\tif (!result) return \"\";\n\t\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\\n\");\n\t};\n\n\tswitch (toolName) {\n\t\tcase \"bash\": {\n\t\t\tconst command = (args?.command as string) || \"\";\n\t\t\thtml = `<div class=\"tool-command\">$ ${escapeHtml(command || \"...\")}</div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 5);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"read\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">read</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += formatExpandableOutput(output.split(\"\\n\"), 10);\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"write\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\tconst fileContent = (args?.content as string) || \"\";\n\t\t\tconst lines = fileContent ? fileContent.split(\"\\n\") : [];\n\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">write</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span>`;\n\t\t\tif (lines.length > 10) {\n\t\t\t\thtml += ` <span class=\"line-count\">(${lines.length} lines)</span>`;\n\t\t\t}\n\t\t\thtml += \"</div>\";\n\n\t\t\tif (fileContent) {\n\t\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"edit\": {\n\t\t\tconst path = shortenPath((args?.file_path as string) || (args?.path as string) || \"\");\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">edit</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\n\t\t\tif (result?.details?.diff) {\n\t\t\t\tconst diffLines = result.details.diff.split(\"\\n\");\n\t\t\t\thtml += '<div class=\"tool-diff\">';\n\t\t\t\tfor (const line of diffLines) {\n\t\t\t\t\tif (line.startsWith(\"+\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-new\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else if (line.startsWith(\"-\")) {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-old\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtml += `<div class=\"diff-line-context\">${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t}\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput().trim();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tdefault: {\n\t\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">${escapeHtml(toolName)}</span></div>`;\n\t\t\thtml += `<div class=\"tool-output\"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;\n\t\t\tif (result) {\n\t\t\t\tconst output = getTextOutput();\n\t\t\t\tif (output) {\n\t\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { html, bgColor };\n}\n\nfunction formatMessage(message: Message, toolResultsMap: Map<string, ToolResultMessage>): string {\n\tlet html = \"\";\n\tconst timestamp = (message as { timestamp?: number }).timestamp;\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${formatTimestamp(timestamp)}</div>` : \"\";\n\n\t// Handle bash execution messages (user-executed via ! command)\n\tif (isBashExecutionMessage(message)) {\n\t\tconst bashMsg = message as unknown as BashExecutionMessage;\n\t\tconst isError = bashMsg.cancelled || (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null);\n\t\tconst bgColor = isError ? COLORS.userBashErrorBg : COLORS.userBashBg;\n\n\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">`;\n\t\thtml += timestampHtml;\n\t\thtml += `<div class=\"tool-command\">$ ${escapeHtml(bashMsg.command)}</div>`;\n\n\t\tif (bashMsg.output) {\n\t\t\tconst lines = bashMsg.output.split(\"\\n\");\n\t\t\thtml += formatExpandableOutput(lines, 10);\n\t\t}\n\n\t\tif (bashMsg.cancelled) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.yellow}\">(cancelled)</div>`;\n\t\t} else if (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null) {\n\t\t\thtml += `<div class=\"bash-status\" style=\"color: ${COLORS.red}\">(exit ${bashMsg.exitCode})</div>`;\n\t\t}\n\n\t\tif (bashMsg.truncated && bashMsg.fullOutputPath) {\n\t\t\thtml += `<div class=\"bash-truncation\" style=\"color: ${COLORS.yellow}\">Output truncated. Full output: ${escapeHtml(bashMsg.fullOutputPath)}</div>`;\n\t\t}\n\n\t\thtml += `</div>`;\n\t\treturn html;\n\t}\n\n\tif (message.role === \"user\") {\n\t\tconst userMsg = message as UserMessage;\n\t\tlet textContent = \"\";\n\n\t\tif (typeof userMsg.content === \"string\") {\n\t\t\ttextContent = userMsg.content;\n\t\t} else {\n\t\t\tconst textBlocks = userMsg.content.filter((c) => c.type === \"text\");\n\t\t\ttextContent = textBlocks.map((c) => (c as { type: \"text\"; text: string }).text).join(\"\");\n\t\t}\n\n\t\tif (textContent.trim()) {\n\t\t\thtml += `<div class=\"user-message\">${timestampHtml}${escapeHtml(textContent).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t}\n\t} else if (message.role === \"assistant\") {\n\t\tconst assistantMsg = message as AssistantMessage;\n\t\thtml += timestampHtml ? `<div class=\"assistant-message\">${timestampHtml}` : \"\";\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\thtml += `<div class=\"assistant-text\">${escapeHtml(content.text.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\thtml += `<div class=\"thinking-text\">${escapeHtml(content.thinking.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\tconst toolResult = toolResultsMap.get(content.id);\n\t\t\t\tconst { html: toolHtml, bgColor } = formatToolExecution(\n\t\t\t\t\tcontent.name,\n\t\t\t\t\tcontent.arguments as Record<string, unknown>,\n\t\t\t\t\ttoolResult,\n\t\t\t\t);\n\t\t\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">${toolHtml}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tconst hasToolCalls = assistantMsg.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (assistantMsg.stopReason === \"aborted\") {\n\t\t\t\thtml += '<div class=\"error-text\">Aborted</div>';\n\t\t\t} else if (assistantMsg.stopReason === \"error\") {\n\t\t\t\thtml += `<div class=\"error-text\">Error: ${escapeHtml(assistantMsg.errorMessage || \"Unknown error\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\tif (timestampHtml) {\n\t\t\thtml += \"</div>\";\n\t\t}\n\t}\n\n\treturn html;\n}\n\nfunction formatModelChange(event: ModelChangeEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst modelInfo = `${event.provider}/${event.modelId}`;\n\treturn `<div class=\"model-change\">${timestampHtml}<div class=\"model-change-text\">Switched to model: <span class=\"model-name\">${escapeHtml(modelInfo)}</span></div></div>`;\n}\n\nfunction formatCompaction(event: CompactionEvent): string {\n\tconst timestamp = formatTimestamp(event.timestamp);\n\tconst timestampHtml = timestamp ? `<div class=\"message-timestamp\">${timestamp}</div>` : \"\";\n\tconst summaryHtml = escapeHtml(event.summary).replace(/\\n/g, \"<br>\");\n\n\treturn `<div class=\"compaction-container\">\n\t\t<div class=\"compaction-header\" onclick=\"this.parentElement.classList.toggle('expanded')\">\n\t\t\t${timestampHtml}\n\t\t\t<div class=\"compaction-header-row\">\n\t\t\t\t<span class=\"compaction-toggle\">▶</span>\n\t\t\t\t<span class=\"compaction-title\">Context compacted from ${event.tokensBefore.toLocaleString()} tokens</span>\n\t\t\t\t<span class=\"compaction-hint\">(click to expand summary)</span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"compaction-content\">\n\t\t\t<div class=\"compaction-summary\">\n\t\t\t\t<div class=\"compaction-summary-header\">Summary sent to model</div>\n\t\t\t\t<div class=\"compaction-summary-content\">${summaryHtml}</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>`;\n}\n\n// ============================================================================\n// HTML generation\n// ============================================================================\n\nfunction generateHtml(data: ParsedSessionData, filename: string): string {\n\tconst userMessages = data.messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = data.messages.filter((m) => m.role === \"assistant\").length;\n\n\tlet toolCallsCount = 0;\n\tfor (const message of data.messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\ttoolCallsCount += (message as AssistantMessage).content.filter((c) => c.type === \"toolCall\").length;\n\t\t}\n\t}\n\n\tconst lastAssistantMessage = data.messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => m.role === \"assistant\" && (m as AssistantMessage).stopReason !== \"aborted\") as\n\t\t| AssistantMessage\n\t\t| undefined;\n\n\tconst contextTokens = lastAssistantMessage\n\t\t? lastAssistantMessage.usage.input +\n\t\t\tlastAssistantMessage.usage.output +\n\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t: 0;\n\n\tconst lastModel = lastAssistantMessage?.model || \"unknown\";\n\tconst lastProvider = lastAssistantMessage?.provider || \"\";\n\tconst lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;\n\n\tconst contextWindow = data.contextWindow || 0;\n\tconst contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;\n\n\tlet messagesHtml = \"\";\n\tfor (const event of data.sessionEvents) {\n\t\tswitch (event.type) {\n\t\t\tcase \"message\":\n\t\t\t\tif (event.message.role !== \"toolResult\") {\n\t\t\t\t\tmessagesHtml += formatMessage(event.message, data.toolResultsMap);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"model_change\":\n\t\t\t\tmessagesHtml += formatModelChange(event);\n\t\t\t\tbreak;\n\t\t\tcase \"compaction\":\n\t\t\t\tmessagesHtml += formatCompaction(event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst systemPromptHtml = data.systemPrompt\n\t\t? `<div class=\"system-prompt\">\n <div class=\"system-prompt-header\">System Prompt</div>\n <div class=\"system-prompt-content\">${escapeHtml(data.systemPrompt)}</div>\n </div>`\n\t\t: \"\";\n\n\tconst toolsHtml = data.tools\n\t\t? `<div class=\"tools-list\">\n <div class=\"tools-header\">Available Tools</div>\n <div class=\"tools-content\">\n ${data.tools.map((tool) => `<div class=\"tool-item\"><span class=\"tool-item-name\">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`).join(\"\")}\n </div>\n </div>`\n\t\t: \"\";\n\n\tconst streamingNotice = data.isStreamingFormat\n\t\t? `<div class=\"streaming-notice\">\n <em>Note: This session was reconstructed from raw agent event logs, which do not contain system prompt or tool definitions.</em>\n </div>`\n\t\t: \"\";\n\n\tconst contextUsageText = contextPercent\n\t\t? `${contextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} tokens (${contextPercent}%) - ${escapeHtml(lastModelInfo)}`\n\t\t: `${contextTokens.toLocaleString()} tokens (last turn) - ${escapeHtml(lastModelInfo)}`;\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Session Export - ${escapeHtml(filename)}</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: ${COLORS.text};\n background: ${COLORS.bodyBg};\n padding: 24px;\n }\n .container { max-width: 700px; margin: 0 auto; }\n .header {\n margin-bottom: 24px;\n padding: 16px;\n background: ${COLORS.containerBg};\n border-radius: 4px;\n }\n .header h1 {\n font-size: 14px;\n font-weight: bold;\n margin-bottom: 12px;\n color: ${COLORS.cyan};\n }\n .header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }\n .info-item { color: ${COLORS.textDim}; display: flex; align-items: baseline; }\n .info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }\n .info-value { color: ${COLORS.text}; flex: 1; }\n .info-value.cost { font-family: 'SF Mono', monospace; }\n .messages { display: flex; flex-direction: column; gap: 16px; }\n .message-timestamp { font-size: 10px; color: ${COLORS.textDim}; margin-bottom: 4px; opacity: 0.8; }\n .user-message {\n background: ${COLORS.userMessageBg};\n padding: 12px 16px;\n border-radius: 4px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .assistant-message { padding: 0; }\n .assistant-text, .thinking-text {\n padding: 12px 16px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n }\n .thinking-text { color: ${COLORS.textDim}; font-style: italic; }\n .model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }\n .model-change-text { color: ${COLORS.textDim}; font-size: 11px; }\n .model-name { color: ${COLORS.cyan}; font-weight: bold; }\n .compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }\n .compaction-header { padding: 12px 16px; cursor: pointer; }\n .compaction-header:hover { background: rgba(255, 255, 255, 0.05); }\n .compaction-header-row { display: flex; align-items: center; gap: 8px; }\n .compaction-toggle { color: ${COLORS.cyan}; font-size: 10px; transition: transform 0.2s; }\n .compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }\n .compaction-title { color: ${COLORS.text}; font-weight: bold; }\n .compaction-hint { color: ${COLORS.textDim}; font-size: 11px; }\n .compaction-content { display: none; padding: 0 16px 16px 16px; }\n .compaction-container.expanded .compaction-content { display: block; }\n .compaction-summary { background: rgba(0, 0, 0, 0.2); border-radius: 4px; padding: 12px; }\n .compaction-summary-header { font-weight: bold; color: ${COLORS.cyan}; margin-bottom: 8px; font-size: 11px; }\n .compaction-summary-content { color: ${COLORS.text}; white-space: pre-wrap; word-wrap: break-word; }\n .tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }\n .tool-header, .tool-name { font-weight: bold; }\n .tool-path { color: ${COLORS.cyan}; word-break: break-all; }\n .line-count { color: ${COLORS.textDim}; }\n .tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }\n .tool-output {\n margin-top: 12px;\n color: ${COLORS.textDim};\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n word-break: break-word;\n font-family: inherit;\n overflow-x: auto;\n }\n .tool-output > div { line-height: 1.4; }\n .tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .tool-output.expandable { cursor: pointer; }\n .tool-output.expandable:hover { opacity: 0.9; }\n .tool-output.expandable .output-full { display: none; }\n .tool-output.expandable.expanded .output-preview { display: none; }\n .tool-output.expandable.expanded .output-full { display: block; }\n .expand-hint { color: ${COLORS.cyan}; font-style: italic; margin-top: 4px; }\n .system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }\n .system-prompt-header, .tools-header { font-weight: bold; color: ${COLORS.yellow}; margin-bottom: 8px; }\n .system-prompt-content, .tools-content { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }\n .tool-item { margin: 4px 0; }\n .tool-item-name { font-weight: bold; color: ${COLORS.text}; }\n .tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }\n .diff-line-old { color: ${COLORS.red}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-new { color: ${COLORS.green}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .diff-line-context { color: ${COLORS.textDim}; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }\n .error-text { color: ${COLORS.red}; padding: 12px 16px; }\n .footer { margin-top: 48px; padding: 20px; text-align: center; color: ${COLORS.textDim}; font-size: 10px; }\n .streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: ${COLORS.textDim}; font-size: 11px; }\n @media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>${APP_NAME} v${VERSION}</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Session:</span><span class=\"info-value\">${escapeHtml(data.sessionId)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Date:</span><span class=\"info-value\">${new Date(data.timestamp).toLocaleString()}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Models:</span><span class=\"info-value\">${\n\t\t\t\t\t\t\tArray.from(data.modelsUsed)\n\t\t\t\t\t\t\t\t.map((m) => escapeHtml(m))\n\t\t\t\t\t\t\t\t.join(\", \") || \"unknown\"\n\t\t\t\t\t\t}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Messages</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">User:</span><span class=\"info-value\">${userMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Assistant:</span><span class=\"info-value\">${assistantMessages}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Tool Calls:</span><span class=\"info-value\">${toolCallsCount}</span></div>\n </div>\n </div>\n\n <div class=\"header\">\n <h1>Tokens & Cost</h1>\n <div class=\"header-info\">\n <div class=\"info-item\"><span class=\"info-label\">Input:</span><span class=\"info-value\">${data.tokenStats.input.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output:</span><span class=\"info-value\">${data.tokenStats.output.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read:</span><span class=\"info-value\">${data.tokenStats.cacheRead.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write:</span><span class=\"info-value\">${data.tokenStats.cacheWrite.toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total:</span><span class=\"info-value\">${(data.tokenStats.input + data.tokenStats.output + data.tokenStats.cacheRead + data.tokenStats.cacheWrite).toLocaleString()} tokens</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Input Cost:</span><span class=\"info-value cost\">$${data.costStats.input.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Output Cost:</span><span class=\"info-value cost\">$${data.costStats.output.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Read Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheRead.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Cache Write Cost:</span><span class=\"info-value cost\">$${data.costStats.cacheWrite.toFixed(4)}</span></div>\n <div class=\"info-item\"><span class=\"info-label\">Total Cost:</span><span class=\"info-value cost\"><strong>$${(data.costStats.input + data.costStats.output + data.costStats.cacheRead + data.costStats.cacheWrite).toFixed(4)}</strong></span></div>\n <div class=\"info-item\"><span class=\"info-label\">Context Usage:</span><span class=\"info-value\">${contextUsageText}</span></div>\n </div>\n </div>\n\n ${systemPromptHtml}\n ${toolsHtml}\n ${streamingNotice}\n\n <div class=\"messages\">\n ${messagesHtml}\n </div>\n\n <div class=\"footer\">\n Generated by ${APP_NAME} coding-agent on ${new Date().toLocaleString()}\n </div>\n </div>\n</body>\n</html>`;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Export session to HTML using SessionManager and AgentState.\n * Used by TUI's /export command.\n */\nexport function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string {\n\tconst sessionFile = sessionManager.getSessionFile();\n\tconst content = readFileSync(sessionFile, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\t// Enrich with data from AgentState (tools, context window)\n\tdata.tools = state.tools.map((t) => ({ name: t.name, description: t.description }));\n\tdata.contextWindow = state.model?.contextWindow;\n\tif (!data.systemPrompt) {\n\t\tdata.systemPrompt = state.systemPrompt;\n\t}\n\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${sessionBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(sessionFile));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n\n/**\n * Export session file to HTML (standalone, without AgentState).\n * Auto-detects format: session manager format or streaming event format.\n * Used by CLI for exporting arbitrary session files.\n */\nexport function exportFromFile(inputPath: string, outputPath?: string): string {\n\tif (!existsSync(inputPath)) {\n\t\tthrow new Error(`File not found: ${inputPath}`);\n\t}\n\n\tconst content = readFileSync(inputPath, \"utf8\");\n\tconst data = parseSessionFile(content);\n\n\tif (!outputPath) {\n\t\tconst inputBasename = basename(inputPath, \".jsonl\");\n\t\toutputPath = `${APP_NAME}-session-${inputBasename}.html`;\n\t}\n\n\tconst html = generateHtml(data, basename(inputPath));\n\twriteFileSync(outputPath, html, \"utf8\");\n\treturn outputPath;\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { basename } from "path";
|
|
4
|
-
import { APP_NAME, VERSION } from "
|
|
4
|
+
import { APP_NAME, VERSION } from "../config.js";
|
|
5
5
|
import { isBashExecutionMessage } from "./messages.js";
|
|
6
6
|
// ============================================================================
|
|
7
7
|
// Color scheme (matching TUI)
|