@mariozechner/pi-coding-agent 0.6.2

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.
Files changed (78) hide show
  1. package/README.md +485 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +21 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/export-html.d.ts +7 -0
  7. package/dist/export-html.d.ts.map +1 -0
  8. package/dist/export-html.js +650 -0
  9. package/dist/export-html.js.map +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/main.d.ts +2 -0
  15. package/dist/main.d.ts.map +1 -0
  16. package/dist/main.js +514 -0
  17. package/dist/main.js.map +1 -0
  18. package/dist/session-manager.d.ts +70 -0
  19. package/dist/session-manager.d.ts.map +1 -0
  20. package/dist/session-manager.js +323 -0
  21. package/dist/session-manager.js.map +1 -0
  22. package/dist/tools/bash.d.ts +7 -0
  23. package/dist/tools/bash.d.ts.map +1 -0
  24. package/dist/tools/bash.js +130 -0
  25. package/dist/tools/bash.js.map +1 -0
  26. package/dist/tools/edit.d.ts +9 -0
  27. package/dist/tools/edit.d.ts.map +1 -0
  28. package/dist/tools/edit.js +207 -0
  29. package/dist/tools/edit.js.map +1 -0
  30. package/dist/tools/index.d.ts +19 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +10 -0
  33. package/dist/tools/index.js.map +1 -0
  34. package/dist/tools/read.d.ts +9 -0
  35. package/dist/tools/read.d.ts.map +1 -0
  36. package/dist/tools/read.js +165 -0
  37. package/dist/tools/read.js.map +1 -0
  38. package/dist/tools/write.d.ts +8 -0
  39. package/dist/tools/write.d.ts.map +1 -0
  40. package/dist/tools/write.js +81 -0
  41. package/dist/tools/write.js.map +1 -0
  42. package/dist/tui/assistant-message.d.ts +11 -0
  43. package/dist/tui/assistant-message.d.ts.map +1 -0
  44. package/dist/tui/assistant-message.js +53 -0
  45. package/dist/tui/assistant-message.js.map +1 -0
  46. package/dist/tui/custom-editor.d.ts +10 -0
  47. package/dist/tui/custom-editor.d.ts.map +1 -0
  48. package/dist/tui/custom-editor.js +24 -0
  49. package/dist/tui/custom-editor.js.map +1 -0
  50. package/dist/tui/footer.d.ts +11 -0
  51. package/dist/tui/footer.d.ts.map +1 -0
  52. package/dist/tui/footer.js +101 -0
  53. package/dist/tui/footer.js.map +1 -0
  54. package/dist/tui/model-selector.d.ts +23 -0
  55. package/dist/tui/model-selector.d.ts.map +1 -0
  56. package/dist/tui/model-selector.js +157 -0
  57. package/dist/tui/model-selector.js.map +1 -0
  58. package/dist/tui/session-selector.d.ts +37 -0
  59. package/dist/tui/session-selector.d.ts.map +1 -0
  60. package/dist/tui/session-selector.js +176 -0
  61. package/dist/tui/session-selector.js.map +1 -0
  62. package/dist/tui/thinking-selector.d.ts +11 -0
  63. package/dist/tui/thinking-selector.d.ts.map +1 -0
  64. package/dist/tui/thinking-selector.js +48 -0
  65. package/dist/tui/thinking-selector.js.map +1 -0
  66. package/dist/tui/tool-execution.d.ts +26 -0
  67. package/dist/tui/tool-execution.d.ts.map +1 -0
  68. package/dist/tui/tool-execution.js +246 -0
  69. package/dist/tui/tool-execution.js.map +1 -0
  70. package/dist/tui/tui-renderer.d.ts +44 -0
  71. package/dist/tui/tui-renderer.d.ts.map +1 -0
  72. package/dist/tui/tui-renderer.js +539 -0
  73. package/dist/tui/tui-renderer.js.map +1 -0
  74. package/dist/tui/user-message.d.ts +9 -0
  75. package/dist/tui/user-message.d.ts.map +1 -0
  76. package/dist/tui/user-message.js +18 -0
  77. package/dist/tui/user-message.js.map +1 -0
  78. package/package.json +53 -0
@@ -0,0 +1,44 @@
1
+ import type { Agent, AgentEvent, AgentState } from "@mariozechner/pi-agent";
2
+ import type { SessionManager } from "../session-manager.js";
3
+ /**
4
+ * TUI renderer for the coding agent
5
+ */
6
+ export declare class TuiRenderer {
7
+ private ui;
8
+ private chatContainer;
9
+ private statusContainer;
10
+ private editor;
11
+ private editorContainer;
12
+ private footer;
13
+ private agent;
14
+ private sessionManager;
15
+ private version;
16
+ private isInitialized;
17
+ private onInputCallback?;
18
+ private loadingAnimation;
19
+ private onInterruptCallback?;
20
+ private lastSigintTime;
21
+ private streamingComponent;
22
+ private pendingTools;
23
+ private thinkingSelector;
24
+ private modelSelector;
25
+ private isFirstUserMessage;
26
+ constructor(agent: Agent, sessionManager: SessionManager, version: string);
27
+ init(): Promise<void>;
28
+ handleEvent(event: AgentEvent, state: AgentState): Promise<void>;
29
+ private addMessageToChat;
30
+ renderInitialMessages(state: AgentState): void;
31
+ getUserInput(): Promise<string>;
32
+ setInterruptCallback(callback: () => void): void;
33
+ private handleCtrlC;
34
+ clearEditor(): void;
35
+ showError(errorMessage: string): void;
36
+ private showThinkingSelector;
37
+ private hideThinkingSelector;
38
+ private showModelSelector;
39
+ private hideModelSelector;
40
+ private handleExportCommand;
41
+ private handleSessionCommand;
42
+ stop(): void;
43
+ }
44
+ //# sourceMappingURL=tui-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-renderer.d.ts","sourceRoot":"","sources":["../../src/tui/tui-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAc5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAS5D;;GAEG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAC,CAAyB;IACjD,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,mBAAmB,CAAC,CAAa;IACzC,OAAO,CAAC,cAAc,CAAK;IAG3B,OAAO,CAAC,kBAAkB,CAA0C;IAGpE,OAAO,CAAC,YAAY,CAA6C;IAGjE,OAAO,CAAC,gBAAgB,CAA0C;IAGlE,OAAO,CAAC,aAAa,CAAuC;IAG5D,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YAAY,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAuCxE;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA2F1B;IAEK,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsIrE;IAED,OAAO,CAAC,gBAAgB;IAqBxB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CA8D7C;IAEK,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAOpC;IAED,oBAAoB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAE/C;IAED,OAAO,CAAC,WAAW;IAgBnB,WAAW,IAAI,IAAI,CAGlB;IAED,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAKpC;IAED,OAAO,CAAC,oBAAoB;IAkC5B,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,oBAAoB;IA4D5B,IAAI,IAAI,IAAI,CASX;CACD","sourcesContent":["import type { Agent, AgentEvent, AgentState } from \"@mariozechner/pi-agent\";\nimport type { AssistantMessage, Message } from \"@mariozechner/pi-ai\";\nimport type { SlashCommand } from \"@mariozechner/pi-tui\";\nimport {\n\tCombinedAutocompleteProvider,\n\tContainer,\n\tLoader,\n\tProcessTerminal,\n\tSpacer,\n\tText,\n\tTUI,\n} from \"@mariozechner/pi-tui\";\nimport chalk from \"chalk\";\nimport { exportSessionToHtml } from \"../export-html.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport { AssistantMessageComponent } from \"./assistant-message.js\";\nimport { CustomEditor } from \"./custom-editor.js\";\nimport { FooterComponent } from \"./footer.js\";\nimport { ModelSelectorComponent } from \"./model-selector.js\";\nimport { ThinkingSelectorComponent } from \"./thinking-selector.js\";\nimport { ToolExecutionComponent } from \"./tool-execution.js\";\nimport { UserMessageComponent } from \"./user-message.js\";\n\n/**\n * TUI renderer for the coding agent\n */\nexport class TuiRenderer {\n\tprivate ui: TUI;\n\tprivate chatContainer: Container;\n\tprivate statusContainer: Container;\n\tprivate editor: CustomEditor;\n\tprivate editorContainer: Container; // Container to swap between editor and selector\n\tprivate footer: FooterComponent;\n\tprivate agent: Agent;\n\tprivate sessionManager: SessionManager;\n\tprivate version: string;\n\tprivate isInitialized = false;\n\tprivate onInputCallback?: (text: string) => void;\n\tprivate loadingAnimation: Loader | null = null;\n\tprivate onInterruptCallback?: () => void;\n\tprivate lastSigintTime = 0;\n\n\t// Streaming message tracking\n\tprivate streamingComponent: AssistantMessageComponent | null = null;\n\n\t// Tool execution tracking: toolCallId -> component\n\tprivate pendingTools = new Map<string, ToolExecutionComponent>();\n\n\t// Thinking level selector\n\tprivate thinkingSelector: ThinkingSelectorComponent | null = null;\n\n\t// Model selector\n\tprivate modelSelector: ModelSelectorComponent | null = null;\n\n\t// Track if this is the first user message (to skip spacer)\n\tprivate isFirstUserMessage = true;\n\n\tconstructor(agent: Agent, sessionManager: SessionManager, version: string) {\n\t\tthis.agent = agent;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.version = version;\n\t\tthis.ui = new TUI(new ProcessTerminal());\n\t\tthis.chatContainer = new Container();\n\t\tthis.statusContainer = new Container();\n\t\tthis.editor = new CustomEditor();\n\t\tthis.editorContainer = new Container(); // Container to hold editor or selector\n\t\tthis.editorContainer.addChild(this.editor); // Start with editor\n\t\tthis.footer = new FooterComponent(agent.state);\n\n\t\t// Define slash commands\n\t\tconst thinkingCommand: SlashCommand = {\n\t\t\tname: \"thinking\",\n\t\t\tdescription: \"Select reasoning level (opens selector UI)\",\n\t\t};\n\n\t\tconst modelCommand: SlashCommand = {\n\t\t\tname: \"model\",\n\t\t\tdescription: \"Select model (opens selector UI)\",\n\t\t};\n\n\t\tconst exportCommand: SlashCommand = {\n\t\t\tname: \"export\",\n\t\t\tdescription: \"Export session to HTML file\",\n\t\t};\n\n\t\tconst sessionCommand: SlashCommand = {\n\t\t\tname: \"session\",\n\t\t\tdescription: \"Show session info and stats\",\n\t\t};\n\n\t\t// Setup autocomplete for file paths and slash commands\n\t\tconst autocompleteProvider = new CombinedAutocompleteProvider(\n\t\t\t[thinkingCommand, modelCommand, exportCommand, sessionCommand],\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tthis.editor.setAutocompleteProvider(autocompleteProvider);\n\t}\n\n\tasync init(): Promise<void> {\n\t\tif (this.isInitialized) return;\n\n\t\t// Add header with logo and instructions\n\t\tconst logo = chalk.bold.cyan(\"pi\") + chalk.dim(` v${this.version}`);\n\t\tconst instructions =\n\t\t\tchalk.dim(\"esc\") +\n\t\t\tchalk.gray(\" to interrupt\") +\n\t\t\t\"\\n\" +\n\t\t\tchalk.dim(\"ctrl+c\") +\n\t\t\tchalk.gray(\" to clear\") +\n\t\t\t\"\\n\" +\n\t\t\tchalk.dim(\"ctrl+c twice\") +\n\t\t\tchalk.gray(\" to exit\") +\n\t\t\t\"\\n\" +\n\t\t\tchalk.dim(\"ctrl+k\") +\n\t\t\tchalk.gray(\" to delete line\") +\n\t\t\t\"\\n\" +\n\t\t\tchalk.dim(\"/\") +\n\t\t\tchalk.gray(\" for commands\") +\n\t\t\t\"\\n\" +\n\t\t\tchalk.dim(\"drop files\") +\n\t\t\tchalk.gray(\" to attach\");\n\t\tconst header = new Text(logo + \"\\n\" + instructions, 1, 0);\n\n\t\t// Setup UI layout\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(header);\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(this.chatContainer);\n\t\tthis.ui.addChild(this.statusContainer);\n\t\tthis.ui.addChild(new Spacer(1));\n\t\tthis.ui.addChild(this.editorContainer); // Use container that can hold editor or selector\n\t\tthis.ui.addChild(this.footer);\n\t\tthis.ui.setFocus(this.editor);\n\n\t\t// Set up custom key handlers on the editor\n\t\tthis.editor.onEscape = () => {\n\t\t\t// Intercept Escape key when processing\n\t\t\tif (this.loadingAnimation && this.onInterruptCallback) {\n\t\t\t\tthis.onInterruptCallback();\n\t\t\t}\n\t\t};\n\n\t\tthis.editor.onCtrlC = () => {\n\t\t\tthis.handleCtrlC();\n\t\t};\n\n\t\t// Handle editor submission\n\t\tthis.editor.onSubmit = (text: string) => {\n\t\t\ttext = text.trim();\n\t\t\tif (!text) return;\n\n\t\t\t// Check for /thinking command\n\t\t\tif (text === \"/thinking\") {\n\t\t\t\t// Show thinking level selector\n\t\t\t\tthis.showThinkingSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /model command\n\t\t\tif (text === \"/model\") {\n\t\t\t\t// Show model selector\n\t\t\t\tthis.showModelSelector();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /export command\n\t\t\tif (text.startsWith(\"/export\")) {\n\t\t\t\tthis.handleExportCommand(text);\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for /session command\n\t\t\tif (text === \"/session\") {\n\t\t\t\tthis.handleSessionCommand();\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (this.onInputCallback) {\n\t\t\t\tthis.onInputCallback(text);\n\t\t\t}\n\t\t};\n\n\t\t// Start the UI\n\t\tthis.ui.start();\n\t\tthis.isInitialized = true;\n\t}\n\n\tasync handleEvent(event: AgentEvent, state: AgentState): Promise<void> {\n\t\tif (!this.isInitialized) {\n\t\t\tawait this.init();\n\t\t}\n\n\t\t// Update footer with current stats\n\t\tthis.footer.updateState(state);\n\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\t// Show loading animation\n\t\t\t\tthis.editor.disableSubmit = true;\n\t\t\t\t// Stop old loader before clearing\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t}\n\t\t\t\tthis.statusContainer.clear();\n\t\t\t\tthis.loadingAnimation = new Loader(this.ui, \"Working... (esc to interrupt)\");\n\t\t\t\tthis.statusContainer.addChild(this.loadingAnimation);\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_start\":\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\t// Show user message immediately and clear editor\n\t\t\t\t\tthis.addMessageToChat(event.message);\n\t\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t} else if (event.message.role === \"assistant\") {\n\t\t\t\t\t// Create assistant component for streaming\n\t\t\t\t\tthis.streamingComponent = new AssistantMessageComponent();\n\t\t\t\t\tthis.chatContainer.addChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent.updateContent(event.message as AssistantMessage);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_update\":\n\t\t\t\t// Update streaming component\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// Create tool execution components as soon as we see tool calls\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\t\t// Only create if we haven't created it yet\n\t\t\t\t\t\t\tif (!this.pendingTools.has(content.id)) {\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(new Text(\"\", 0, 0));\n\t\t\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Update existing component with latest arguments as they stream\n\t\t\t\t\t\t\t\tconst component = this.pendingTools.get(content.id);\n\t\t\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\t\t\tcomponent.updateArgs(content.arguments);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"message_end\":\n\t\t\t\t// Skip user messages (already shown in message_start)\n\t\t\t\tif (event.message.role === \"user\") {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent && event.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\n\t\t\t\t\t// Update streaming component with final message (includes stopReason)\n\t\t\t\t\tthis.streamingComponent.updateContent(assistantMsg);\n\n\t\t\t\t\t// If message was aborted or errored, mark all pending tool components as failed\n\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\" ? \"Operation aborted\" : assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\tfor (const [toolCallId, component] of this.pendingTools.entries()) {\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.pendingTools.clear();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Keep the streaming component - it's now the final assistant message\n\t\t\t\t\tthis.streamingComponent = null;\n\t\t\t\t}\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t// Component should already exist from message_update, but create if missing\n\t\t\t\tif (!this.pendingTools.has(event.toolCallId)) {\n\t\t\t\t\tconst component = new ToolExecutionComponent(event.toolName, event.args);\n\t\t\t\t\tthis.chatContainer.addChild(component);\n\t\t\t\t\tthis.pendingTools.set(event.toolCallId, component);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Update the existing tool component with the result\n\t\t\t\tconst component = this.pendingTools.get(event.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult(event.result);\n\t\t\t\t\tthis.pendingTools.delete(event.toolCallId);\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\":\n\t\t\t\t// Stop loading animation\n\t\t\t\tif (this.loadingAnimation) {\n\t\t\t\t\tthis.loadingAnimation.stop();\n\t\t\t\t\tthis.loadingAnimation = null;\n\t\t\t\t\tthis.statusContainer.clear();\n\t\t\t\t}\n\t\t\t\tif (this.streamingComponent) {\n\t\t\t\t\tthis.chatContainer.removeChild(this.streamingComponent);\n\t\t\t\t\tthis.streamingComponent = null;\n\t\t\t\t}\n\t\t\t\tthis.pendingTools.clear();\n\t\t\t\tthis.editor.disableSubmit = false;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate addMessageToChat(message: Message): void {\n\t\tif (message.role === \"user\") {\n\t\t\tconst userMsg = message as any;\n\t\t\t// Extract text content from content blocks\n\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\tif (textContent) {\n\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t}\n\t\t} else if (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\n\t\t\t// Add assistant message component\n\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\tthis.chatContainer.addChild(assistantComponent);\n\t\t}\n\t\t// Note: tool calls and results are now handled via tool_execution_start/end events\n\t}\n\n\trenderInitialMessages(state: AgentState): void {\n\t\t// Render all existing messages (for --continue mode)\n\t\t// Reset first user message flag for initial render\n\t\tthis.isFirstUserMessage = true;\n\n\t\t// Render messages\n\t\tfor (let i = 0; i < state.messages.length; i++) {\n\t\t\tconst message = state.messages[i];\n\n\t\t\tif (message.role === \"user\") {\n\t\t\t\tconst userMsg = message as any;\n\t\t\t\tconst textBlocks = userMsg.content.filter((c: any) => c.type === \"text\");\n\t\t\t\tconst textContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t\t\tif (textContent) {\n\t\t\t\t\tconst userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);\n\t\t\t\t\tthis.chatContainer.addChild(userComponent);\n\t\t\t\t\tthis.isFirstUserMessage = false;\n\t\t\t\t}\n\t\t\t} else if (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\tconst assistantComponent = new AssistantMessageComponent(assistantMsg);\n\t\t\t\tthis.chatContainer.addChild(assistantComponent);\n\n\t\t\t\t// Create tool execution components for any tool calls\n\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\t\t\tconst component = new ToolExecutionComponent(content.name, content.arguments);\n\t\t\t\t\t\tthis.chatContainer.addChild(component);\n\n\t\t\t\t\t\t// If message was aborted/errored, immediately mark tool as failed\n\t\t\t\t\t\tif (assistantMsg.stopReason === \"aborted\" || assistantMsg.stopReason === \"error\") {\n\t\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\t\tassistantMsg.stopReason === \"aborted\"\n\t\t\t\t\t\t\t\t\t? \"Operation aborted\"\n\t\t\t\t\t\t\t\t\t: assistantMsg.errorMessage || \"Error\";\n\t\t\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: errorMessage }],\n\t\t\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Store in map so we can update with results later\n\t\t\t\t\t\t\tthis.pendingTools.set(content.id, component);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.role === \"toolResult\") {\n\t\t\t\t// Update existing tool execution component with results\t\t\t\t;\n\t\t\t\tconst component = this.pendingTools.get(message.toolCallId);\n\t\t\t\tif (component) {\n\t\t\t\t\tcomponent.updateResult({\n\t\t\t\t\t\tcontent: message.content,\n\t\t\t\t\t\tdetails: message.details,\n\t\t\t\t\t\tisError: message.isError,\n\t\t\t\t\t});\n\t\t\t\t\t// Remove from pending map since it's complete\n\t\t\t\t\tthis.pendingTools.delete(message.toolCallId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Clear pending tools after rendering initial messages\n\t\tthis.pendingTools.clear();\n\t\tthis.ui.requestRender();\n\t}\n\n\tasync getUserInput(): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.onInputCallback = (text: string) => {\n\t\t\t\tthis.onInputCallback = undefined;\n\t\t\t\tresolve(text);\n\t\t\t};\n\t\t});\n\t}\n\n\tsetInterruptCallback(callback: () => void): void {\n\t\tthis.onInterruptCallback = callback;\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\t// Handle Ctrl+C double-press logic\n\t\tconst now = Date.now();\n\t\tconst timeSinceLastCtrlC = now - this.lastSigintTime;\n\n\t\tif (timeSinceLastCtrlC < 500) {\n\t\t\t// Second Ctrl+C within 500ms - exit\n\t\t\tthis.stop();\n\t\t\tprocess.exit(0);\n\t\t} else {\n\t\t\t// First Ctrl+C - clear the editor\n\t\t\tthis.clearEditor();\n\t\t\tthis.lastSigintTime = now;\n\t\t}\n\t}\n\n\tclearEditor(): void {\n\t\tthis.editor.setText(\"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tshowError(errorMessage: string): void {\n\t\t// Show error message in the chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(chalk.red(`Error: ${errorMessage}`), 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate showThinkingSelector(): void {\n\t\t// Create thinking selector with current level\n\t\tthis.thinkingSelector = new ThinkingSelectorComponent(\n\t\t\tthis.agent.state.thinkingLevel,\n\t\t\t(level) => {\n\t\t\t\t// Apply the selected thinking level\n\t\t\t\tthis.agent.setThinkingLevel(level);\n\n\t\t\t\t// Save thinking level change to session\n\t\t\t\tthis.sessionManager.saveThinkingLevelChange(level);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideThinkingSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.thinkingSelector);\n\t\tthis.ui.setFocus(this.thinkingSelector.getSelectList());\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideThinkingSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.thinkingSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate showModelSelector(): void {\n\t\t// Create model selector with current model\n\t\tthis.modelSelector = new ModelSelectorComponent(\n\t\t\tthis.agent.state.model,\n\t\t\t(model) => {\n\t\t\t\t// Apply the selected model\n\t\t\t\tthis.agent.setModel(model);\n\n\t\t\t\t// Save model change to session\n\t\t\t\tthis.sessionManager.saveModelChange(`${model.provider}/${model.id}`);\n\n\t\t\t\t// Show confirmation message with proper spacing\n\t\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\t\tconst confirmText = new Text(chalk.dim(`Model: ${model.id}`), 1, 0);\n\t\t\t\tthis.chatContainer.addChild(confirmText);\n\n\t\t\t\t// Hide selector and show editor again\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Just hide the selector\n\t\t\t\tthis.hideModelSelector();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t);\n\n\t\t// Replace editor with selector\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.modelSelector);\n\t\tthis.ui.setFocus(this.modelSelector);\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate hideModelSelector(): void {\n\t\t// Replace selector with editor in the container\n\t\tthis.editorContainer.clear();\n\t\tthis.editorContainer.addChild(this.editor);\n\t\tthis.modelSelector = null;\n\t\tthis.ui.setFocus(this.editor);\n\t}\n\n\tprivate handleExportCommand(text: string): void {\n\t\t// Parse optional filename from command: /export [filename]\n\t\tconst parts = text.split(/\\s+/);\n\t\tconst outputPath = parts.length > 1 ? parts[1] : undefined;\n\n\t\ttry {\n\t\t\t// Export session to HTML\n\t\t\tconst filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);\n\n\t\t\t// Show success message in chat - matching thinking level style\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(new Text(chalk.dim(`Session exported to: ${filePath}`), 1, 0));\n\t\t\tthis.ui.requestRender();\n\t\t} catch (error: any) {\n\t\t\t// Show error message in chat\n\t\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.chatContainer.addChild(\n\t\t\t\tnew Text(chalk.red(`Failed to export session: ${error.message || \"Unknown error\"}`), 1, 0),\n\t\t\t);\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate handleSessionCommand(): void {\n\t\t// Get session info\n\t\tconst sessionFile = this.sessionManager.getSessionFile();\n\t\tconst state = this.agent.state;\n\n\t\t// Count messages\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 totalMessages = state.messages.length;\n\n\t\t// Calculate cumulative usage from all assistant messages (same as footer)\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\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\tconst totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;\n\n\t\t// Build info text\n\t\tlet info = `${chalk.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${chalk.dim(\"File:\")} ${sessionFile}\\n`;\n\t\tinfo += `${chalk.dim(\"ID:\")} ${this.sessionManager.getSessionId()}\\n\\n`;\n\t\tinfo += `${chalk.bold(\"Messages\")}\\n`;\n\t\tinfo += `${chalk.dim(\"User:\")} ${userMessages}\\n`;\n\t\tinfo += `${chalk.dim(\"Assistant:\")} ${assistantMessages}\\n`;\n\t\tinfo += `${chalk.dim(\"Total:\")} ${totalMessages}\\n\\n`;\n\t\tinfo += `${chalk.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${chalk.dim(\"Input:\")} ${totalInput.toLocaleString()}\\n`;\n\t\tinfo += `${chalk.dim(\"Output:\")} ${totalOutput.toLocaleString()}\\n`;\n\t\tif (totalCacheRead > 0) {\n\t\t\tinfo += `${chalk.dim(\"Cache Read:\")} ${totalCacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (totalCacheWrite > 0) {\n\t\t\tinfo += `${chalk.dim(\"Cache Write:\")} ${totalCacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${chalk.dim(\"Total:\")} ${totalTokens.toLocaleString()}\\n`;\n\n\t\tif (totalCost > 0) {\n\t\t\tinfo += `\\n${chalk.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${chalk.dim(\"Total:\")} ${totalCost.toFixed(4)}`;\n\t\t}\n\n\t\t// Show info in chat\n\t\tthis.chatContainer.addChild(new Spacer(1));\n\t\tthis.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ui.requestRender();\n\t}\n\n\tstop(): void {\n\t\tif (this.loadingAnimation) {\n\t\t\tthis.loadingAnimation.stop();\n\t\t\tthis.loadingAnimation = null;\n\t\t}\n\t\tif (this.isInitialized) {\n\t\t\tthis.ui.stop();\n\t\t\tthis.isInitialized = false;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,539 @@
1
+ import { CombinedAutocompleteProvider, Container, Loader, ProcessTerminal, Spacer, Text, TUI, } from "@mariozechner/pi-tui";
2
+ import chalk from "chalk";
3
+ import { exportSessionToHtml } from "../export-html.js";
4
+ import { AssistantMessageComponent } from "./assistant-message.js";
5
+ import { CustomEditor } from "./custom-editor.js";
6
+ import { FooterComponent } from "./footer.js";
7
+ import { ModelSelectorComponent } from "./model-selector.js";
8
+ import { ThinkingSelectorComponent } from "./thinking-selector.js";
9
+ import { ToolExecutionComponent } from "./tool-execution.js";
10
+ import { UserMessageComponent } from "./user-message.js";
11
+ /**
12
+ * TUI renderer for the coding agent
13
+ */
14
+ export class TuiRenderer {
15
+ ui;
16
+ chatContainer;
17
+ statusContainer;
18
+ editor;
19
+ editorContainer; // Container to swap between editor and selector
20
+ footer;
21
+ agent;
22
+ sessionManager;
23
+ version;
24
+ isInitialized = false;
25
+ onInputCallback;
26
+ loadingAnimation = null;
27
+ onInterruptCallback;
28
+ lastSigintTime = 0;
29
+ // Streaming message tracking
30
+ streamingComponent = null;
31
+ // Tool execution tracking: toolCallId -> component
32
+ pendingTools = new Map();
33
+ // Thinking level selector
34
+ thinkingSelector = null;
35
+ // Model selector
36
+ modelSelector = null;
37
+ // Track if this is the first user message (to skip spacer)
38
+ isFirstUserMessage = true;
39
+ constructor(agent, sessionManager, version) {
40
+ this.agent = agent;
41
+ this.sessionManager = sessionManager;
42
+ this.version = version;
43
+ this.ui = new TUI(new ProcessTerminal());
44
+ this.chatContainer = new Container();
45
+ this.statusContainer = new Container();
46
+ this.editor = new CustomEditor();
47
+ this.editorContainer = new Container(); // Container to hold editor or selector
48
+ this.editorContainer.addChild(this.editor); // Start with editor
49
+ this.footer = new FooterComponent(agent.state);
50
+ // Define slash commands
51
+ const thinkingCommand = {
52
+ name: "thinking",
53
+ description: "Select reasoning level (opens selector UI)",
54
+ };
55
+ const modelCommand = {
56
+ name: "model",
57
+ description: "Select model (opens selector UI)",
58
+ };
59
+ const exportCommand = {
60
+ name: "export",
61
+ description: "Export session to HTML file",
62
+ };
63
+ const sessionCommand = {
64
+ name: "session",
65
+ description: "Show session info and stats",
66
+ };
67
+ // Setup autocomplete for file paths and slash commands
68
+ const autocompleteProvider = new CombinedAutocompleteProvider([thinkingCommand, modelCommand, exportCommand, sessionCommand], process.cwd());
69
+ this.editor.setAutocompleteProvider(autocompleteProvider);
70
+ }
71
+ async init() {
72
+ if (this.isInitialized)
73
+ return;
74
+ // Add header with logo and instructions
75
+ const logo = chalk.bold.cyan("pi") + chalk.dim(` v${this.version}`);
76
+ const instructions = chalk.dim("esc") +
77
+ chalk.gray(" to interrupt") +
78
+ "\n" +
79
+ chalk.dim("ctrl+c") +
80
+ chalk.gray(" to clear") +
81
+ "\n" +
82
+ chalk.dim("ctrl+c twice") +
83
+ chalk.gray(" to exit") +
84
+ "\n" +
85
+ chalk.dim("ctrl+k") +
86
+ chalk.gray(" to delete line") +
87
+ "\n" +
88
+ chalk.dim("/") +
89
+ chalk.gray(" for commands") +
90
+ "\n" +
91
+ chalk.dim("drop files") +
92
+ chalk.gray(" to attach");
93
+ const header = new Text(logo + "\n" + instructions, 1, 0);
94
+ // Setup UI layout
95
+ this.ui.addChild(new Spacer(1));
96
+ this.ui.addChild(header);
97
+ this.ui.addChild(new Spacer(1));
98
+ this.ui.addChild(this.chatContainer);
99
+ this.ui.addChild(this.statusContainer);
100
+ this.ui.addChild(new Spacer(1));
101
+ this.ui.addChild(this.editorContainer); // Use container that can hold editor or selector
102
+ this.ui.addChild(this.footer);
103
+ this.ui.setFocus(this.editor);
104
+ // Set up custom key handlers on the editor
105
+ this.editor.onEscape = () => {
106
+ // Intercept Escape key when processing
107
+ if (this.loadingAnimation && this.onInterruptCallback) {
108
+ this.onInterruptCallback();
109
+ }
110
+ };
111
+ this.editor.onCtrlC = () => {
112
+ this.handleCtrlC();
113
+ };
114
+ // Handle editor submission
115
+ this.editor.onSubmit = (text) => {
116
+ text = text.trim();
117
+ if (!text)
118
+ return;
119
+ // Check for /thinking command
120
+ if (text === "/thinking") {
121
+ // Show thinking level selector
122
+ this.showThinkingSelector();
123
+ this.editor.setText("");
124
+ return;
125
+ }
126
+ // Check for /model command
127
+ if (text === "/model") {
128
+ // Show model selector
129
+ this.showModelSelector();
130
+ this.editor.setText("");
131
+ return;
132
+ }
133
+ // Check for /export command
134
+ if (text.startsWith("/export")) {
135
+ this.handleExportCommand(text);
136
+ this.editor.setText("");
137
+ return;
138
+ }
139
+ // Check for /session command
140
+ if (text === "/session") {
141
+ this.handleSessionCommand();
142
+ this.editor.setText("");
143
+ return;
144
+ }
145
+ if (this.onInputCallback) {
146
+ this.onInputCallback(text);
147
+ }
148
+ };
149
+ // Start the UI
150
+ this.ui.start();
151
+ this.isInitialized = true;
152
+ }
153
+ async handleEvent(event, state) {
154
+ if (!this.isInitialized) {
155
+ await this.init();
156
+ }
157
+ // Update footer with current stats
158
+ this.footer.updateState(state);
159
+ switch (event.type) {
160
+ case "agent_start":
161
+ // Show loading animation
162
+ this.editor.disableSubmit = true;
163
+ // Stop old loader before clearing
164
+ if (this.loadingAnimation) {
165
+ this.loadingAnimation.stop();
166
+ }
167
+ this.statusContainer.clear();
168
+ this.loadingAnimation = new Loader(this.ui, "Working... (esc to interrupt)");
169
+ this.statusContainer.addChild(this.loadingAnimation);
170
+ this.ui.requestRender();
171
+ break;
172
+ case "message_start":
173
+ if (event.message.role === "user") {
174
+ // Show user message immediately and clear editor
175
+ this.addMessageToChat(event.message);
176
+ this.editor.setText("");
177
+ this.ui.requestRender();
178
+ }
179
+ else if (event.message.role === "assistant") {
180
+ // Create assistant component for streaming
181
+ this.streamingComponent = new AssistantMessageComponent();
182
+ this.chatContainer.addChild(this.streamingComponent);
183
+ this.streamingComponent.updateContent(event.message);
184
+ this.ui.requestRender();
185
+ }
186
+ break;
187
+ case "message_update":
188
+ // Update streaming component
189
+ if (this.streamingComponent && event.message.role === "assistant") {
190
+ const assistantMsg = event.message;
191
+ this.streamingComponent.updateContent(assistantMsg);
192
+ // Create tool execution components as soon as we see tool calls
193
+ for (const content of assistantMsg.content) {
194
+ if (content.type === "toolCall") {
195
+ // Only create if we haven't created it yet
196
+ if (!this.pendingTools.has(content.id)) {
197
+ this.chatContainer.addChild(new Text("", 0, 0));
198
+ const component = new ToolExecutionComponent(content.name, content.arguments);
199
+ this.chatContainer.addChild(component);
200
+ this.pendingTools.set(content.id, component);
201
+ }
202
+ else {
203
+ // Update existing component with latest arguments as they stream
204
+ const component = this.pendingTools.get(content.id);
205
+ if (component) {
206
+ component.updateArgs(content.arguments);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ this.ui.requestRender();
212
+ }
213
+ break;
214
+ case "message_end":
215
+ // Skip user messages (already shown in message_start)
216
+ if (event.message.role === "user") {
217
+ break;
218
+ }
219
+ if (this.streamingComponent && event.message.role === "assistant") {
220
+ const assistantMsg = event.message;
221
+ // Update streaming component with final message (includes stopReason)
222
+ this.streamingComponent.updateContent(assistantMsg);
223
+ // If message was aborted or errored, mark all pending tool components as failed
224
+ if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
225
+ const errorMessage = assistantMsg.stopReason === "aborted" ? "Operation aborted" : assistantMsg.errorMessage || "Error";
226
+ for (const [toolCallId, component] of this.pendingTools.entries()) {
227
+ component.updateResult({
228
+ content: [{ type: "text", text: errorMessage }],
229
+ isError: true,
230
+ });
231
+ }
232
+ this.pendingTools.clear();
233
+ }
234
+ // Keep the streaming component - it's now the final assistant message
235
+ this.streamingComponent = null;
236
+ }
237
+ this.ui.requestRender();
238
+ break;
239
+ case "tool_execution_start": {
240
+ // Component should already exist from message_update, but create if missing
241
+ if (!this.pendingTools.has(event.toolCallId)) {
242
+ const component = new ToolExecutionComponent(event.toolName, event.args);
243
+ this.chatContainer.addChild(component);
244
+ this.pendingTools.set(event.toolCallId, component);
245
+ this.ui.requestRender();
246
+ }
247
+ break;
248
+ }
249
+ case "tool_execution_end": {
250
+ // Update the existing tool component with the result
251
+ const component = this.pendingTools.get(event.toolCallId);
252
+ if (component) {
253
+ component.updateResult(event.result);
254
+ this.pendingTools.delete(event.toolCallId);
255
+ this.ui.requestRender();
256
+ }
257
+ break;
258
+ }
259
+ case "agent_end":
260
+ // Stop loading animation
261
+ if (this.loadingAnimation) {
262
+ this.loadingAnimation.stop();
263
+ this.loadingAnimation = null;
264
+ this.statusContainer.clear();
265
+ }
266
+ if (this.streamingComponent) {
267
+ this.chatContainer.removeChild(this.streamingComponent);
268
+ this.streamingComponent = null;
269
+ }
270
+ this.pendingTools.clear();
271
+ this.editor.disableSubmit = false;
272
+ this.ui.requestRender();
273
+ break;
274
+ }
275
+ }
276
+ addMessageToChat(message) {
277
+ if (message.role === "user") {
278
+ const userMsg = message;
279
+ // Extract text content from content blocks
280
+ const textBlocks = userMsg.content.filter((c) => c.type === "text");
281
+ const textContent = textBlocks.map((c) => c.text).join("");
282
+ if (textContent) {
283
+ const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
284
+ this.chatContainer.addChild(userComponent);
285
+ this.isFirstUserMessage = false;
286
+ }
287
+ }
288
+ else if (message.role === "assistant") {
289
+ const assistantMsg = message;
290
+ // Add assistant message component
291
+ const assistantComponent = new AssistantMessageComponent(assistantMsg);
292
+ this.chatContainer.addChild(assistantComponent);
293
+ }
294
+ // Note: tool calls and results are now handled via tool_execution_start/end events
295
+ }
296
+ renderInitialMessages(state) {
297
+ // Render all existing messages (for --continue mode)
298
+ // Reset first user message flag for initial render
299
+ this.isFirstUserMessage = true;
300
+ // Render messages
301
+ for (let i = 0; i < state.messages.length; i++) {
302
+ const message = state.messages[i];
303
+ if (message.role === "user") {
304
+ const userMsg = message;
305
+ const textBlocks = userMsg.content.filter((c) => c.type === "text");
306
+ const textContent = textBlocks.map((c) => c.text).join("");
307
+ if (textContent) {
308
+ const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
309
+ this.chatContainer.addChild(userComponent);
310
+ this.isFirstUserMessage = false;
311
+ }
312
+ }
313
+ else if (message.role === "assistant") {
314
+ const assistantMsg = message;
315
+ const assistantComponent = new AssistantMessageComponent(assistantMsg);
316
+ this.chatContainer.addChild(assistantComponent);
317
+ // Create tool execution components for any tool calls
318
+ for (const content of assistantMsg.content) {
319
+ if (content.type === "toolCall") {
320
+ const component = new ToolExecutionComponent(content.name, content.arguments);
321
+ this.chatContainer.addChild(component);
322
+ // If message was aborted/errored, immediately mark tool as failed
323
+ if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
324
+ const errorMessage = assistantMsg.stopReason === "aborted"
325
+ ? "Operation aborted"
326
+ : assistantMsg.errorMessage || "Error";
327
+ component.updateResult({
328
+ content: [{ type: "text", text: errorMessage }],
329
+ isError: true,
330
+ });
331
+ }
332
+ else {
333
+ // Store in map so we can update with results later
334
+ this.pendingTools.set(content.id, component);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ else if (message.role === "toolResult") {
340
+ // Update existing tool execution component with results ;
341
+ const component = this.pendingTools.get(message.toolCallId);
342
+ if (component) {
343
+ component.updateResult({
344
+ content: message.content,
345
+ details: message.details,
346
+ isError: message.isError,
347
+ });
348
+ // Remove from pending map since it's complete
349
+ this.pendingTools.delete(message.toolCallId);
350
+ }
351
+ }
352
+ }
353
+ // Clear pending tools after rendering initial messages
354
+ this.pendingTools.clear();
355
+ this.ui.requestRender();
356
+ }
357
+ async getUserInput() {
358
+ return new Promise((resolve) => {
359
+ this.onInputCallback = (text) => {
360
+ this.onInputCallback = undefined;
361
+ resolve(text);
362
+ };
363
+ });
364
+ }
365
+ setInterruptCallback(callback) {
366
+ this.onInterruptCallback = callback;
367
+ }
368
+ handleCtrlC() {
369
+ // Handle Ctrl+C double-press logic
370
+ const now = Date.now();
371
+ const timeSinceLastCtrlC = now - this.lastSigintTime;
372
+ if (timeSinceLastCtrlC < 500) {
373
+ // Second Ctrl+C within 500ms - exit
374
+ this.stop();
375
+ process.exit(0);
376
+ }
377
+ else {
378
+ // First Ctrl+C - clear the editor
379
+ this.clearEditor();
380
+ this.lastSigintTime = now;
381
+ }
382
+ }
383
+ clearEditor() {
384
+ this.editor.setText("");
385
+ this.ui.requestRender();
386
+ }
387
+ showError(errorMessage) {
388
+ // Show error message in the chat
389
+ this.chatContainer.addChild(new Spacer(1));
390
+ this.chatContainer.addChild(new Text(chalk.red(`Error: ${errorMessage}`), 1, 0));
391
+ this.ui.requestRender();
392
+ }
393
+ showThinkingSelector() {
394
+ // Create thinking selector with current level
395
+ this.thinkingSelector = new ThinkingSelectorComponent(this.agent.state.thinkingLevel, (level) => {
396
+ // Apply the selected thinking level
397
+ this.agent.setThinkingLevel(level);
398
+ // Save thinking level change to session
399
+ this.sessionManager.saveThinkingLevelChange(level);
400
+ // Show confirmation message with proper spacing
401
+ this.chatContainer.addChild(new Spacer(1));
402
+ const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);
403
+ this.chatContainer.addChild(confirmText);
404
+ // Hide selector and show editor again
405
+ this.hideThinkingSelector();
406
+ this.ui.requestRender();
407
+ }, () => {
408
+ // Just hide the selector
409
+ this.hideThinkingSelector();
410
+ this.ui.requestRender();
411
+ });
412
+ // Replace editor with selector
413
+ this.editorContainer.clear();
414
+ this.editorContainer.addChild(this.thinkingSelector);
415
+ this.ui.setFocus(this.thinkingSelector.getSelectList());
416
+ this.ui.requestRender();
417
+ }
418
+ hideThinkingSelector() {
419
+ // Replace selector with editor in the container
420
+ this.editorContainer.clear();
421
+ this.editorContainer.addChild(this.editor);
422
+ this.thinkingSelector = null;
423
+ this.ui.setFocus(this.editor);
424
+ }
425
+ showModelSelector() {
426
+ // Create model selector with current model
427
+ this.modelSelector = new ModelSelectorComponent(this.agent.state.model, (model) => {
428
+ // Apply the selected model
429
+ this.agent.setModel(model);
430
+ // Save model change to session
431
+ this.sessionManager.saveModelChange(`${model.provider}/${model.id}`);
432
+ // Show confirmation message with proper spacing
433
+ this.chatContainer.addChild(new Spacer(1));
434
+ const confirmText = new Text(chalk.dim(`Model: ${model.id}`), 1, 0);
435
+ this.chatContainer.addChild(confirmText);
436
+ // Hide selector and show editor again
437
+ this.hideModelSelector();
438
+ this.ui.requestRender();
439
+ }, () => {
440
+ // Just hide the selector
441
+ this.hideModelSelector();
442
+ this.ui.requestRender();
443
+ });
444
+ // Replace editor with selector
445
+ this.editorContainer.clear();
446
+ this.editorContainer.addChild(this.modelSelector);
447
+ this.ui.setFocus(this.modelSelector);
448
+ this.ui.requestRender();
449
+ }
450
+ hideModelSelector() {
451
+ // Replace selector with editor in the container
452
+ this.editorContainer.clear();
453
+ this.editorContainer.addChild(this.editor);
454
+ this.modelSelector = null;
455
+ this.ui.setFocus(this.editor);
456
+ }
457
+ handleExportCommand(text) {
458
+ // Parse optional filename from command: /export [filename]
459
+ const parts = text.split(/\s+/);
460
+ const outputPath = parts.length > 1 ? parts[1] : undefined;
461
+ try {
462
+ // Export session to HTML
463
+ const filePath = exportSessionToHtml(this.sessionManager, this.agent.state, outputPath);
464
+ // Show success message in chat - matching thinking level style
465
+ this.chatContainer.addChild(new Spacer(1));
466
+ this.chatContainer.addChild(new Text(chalk.dim(`Session exported to: ${filePath}`), 1, 0));
467
+ this.ui.requestRender();
468
+ }
469
+ catch (error) {
470
+ // Show error message in chat
471
+ this.chatContainer.addChild(new Spacer(1));
472
+ this.chatContainer.addChild(new Text(chalk.red(`Failed to export session: ${error.message || "Unknown error"}`), 1, 0));
473
+ this.ui.requestRender();
474
+ }
475
+ }
476
+ handleSessionCommand() {
477
+ // Get session info
478
+ const sessionFile = this.sessionManager.getSessionFile();
479
+ const state = this.agent.state;
480
+ // Count messages
481
+ const userMessages = state.messages.filter((m) => m.role === "user").length;
482
+ const assistantMessages = state.messages.filter((m) => m.role === "assistant").length;
483
+ const totalMessages = state.messages.length;
484
+ // Calculate cumulative usage from all assistant messages (same as footer)
485
+ let totalInput = 0;
486
+ let totalOutput = 0;
487
+ let totalCacheRead = 0;
488
+ let totalCacheWrite = 0;
489
+ let totalCost = 0;
490
+ for (const message of state.messages) {
491
+ if (message.role === "assistant") {
492
+ const assistantMsg = message;
493
+ totalInput += assistantMsg.usage.input;
494
+ totalOutput += assistantMsg.usage.output;
495
+ totalCacheRead += assistantMsg.usage.cacheRead;
496
+ totalCacheWrite += assistantMsg.usage.cacheWrite;
497
+ totalCost += assistantMsg.usage.cost.total;
498
+ }
499
+ }
500
+ const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
501
+ // Build info text
502
+ let info = `${chalk.bold("Session Info")}\n\n`;
503
+ info += `${chalk.dim("File:")} ${sessionFile}\n`;
504
+ info += `${chalk.dim("ID:")} ${this.sessionManager.getSessionId()}\n\n`;
505
+ info += `${chalk.bold("Messages")}\n`;
506
+ info += `${chalk.dim("User:")} ${userMessages}\n`;
507
+ info += `${chalk.dim("Assistant:")} ${assistantMessages}\n`;
508
+ info += `${chalk.dim("Total:")} ${totalMessages}\n\n`;
509
+ info += `${chalk.bold("Tokens")}\n`;
510
+ info += `${chalk.dim("Input:")} ${totalInput.toLocaleString()}\n`;
511
+ info += `${chalk.dim("Output:")} ${totalOutput.toLocaleString()}\n`;
512
+ if (totalCacheRead > 0) {
513
+ info += `${chalk.dim("Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
514
+ }
515
+ if (totalCacheWrite > 0) {
516
+ info += `${chalk.dim("Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
517
+ }
518
+ info += `${chalk.dim("Total:")} ${totalTokens.toLocaleString()}\n`;
519
+ if (totalCost > 0) {
520
+ info += `\n${chalk.bold("Cost")}\n`;
521
+ info += `${chalk.dim("Total:")} ${totalCost.toFixed(4)}`;
522
+ }
523
+ // Show info in chat
524
+ this.chatContainer.addChild(new Spacer(1));
525
+ this.chatContainer.addChild(new Text(info, 1, 0));
526
+ this.ui.requestRender();
527
+ }
528
+ stop() {
529
+ if (this.loadingAnimation) {
530
+ this.loadingAnimation.stop();
531
+ this.loadingAnimation = null;
532
+ }
533
+ if (this.isInitialized) {
534
+ this.ui.stop();
535
+ this.isInitialized = false;
536
+ }
537
+ }
538
+ }
539
+ //# sourceMappingURL=tui-renderer.js.map