my-pi-agent 0.1.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.
Files changed (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. package/tui/package.json +25 -0
@@ -0,0 +1,1625 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as process from "node:process";
4
+ import { spawn } from "node:child_process";
5
+ import { CombinedAutocompleteProvider, Container, matchesKey, Spacer, Text, } from "@earendil-works/pi-tui";
6
+ import { AssistantMessageComponent } from "../components/assistant-message.js";
7
+ import { CompactionSummaryMessageComponent } from "../components/compaction-summary-message.js";
8
+ import { CustomEditor } from "../components/custom-editor.js";
9
+ import { DynamicBorder } from "../components/dynamic-border.js";
10
+ import { FooterComponent, formatTokens } from "../components/footer.js";
11
+ import { HeaderComponent } from "../components/header.js";
12
+ import { LoginSelectorComponent } from "../components/login-selector.js";
13
+ import { LogoutSelectorComponent, } from "../components/logout-selector.js";
14
+ import { ModelSelectorComponent, } from "../components/model-selector.js";
15
+ import { SessionSelectorComponent, } from "../components/session-selector.js";
16
+ import { SettingsSelectorComponent } from "../components/settings-selector.js";
17
+ import { CompactionStatusIndicator, WorkingStatusIndicator, } from "../components/status-indicator.js";
18
+ import { ThemeSelectorComponent } from "../components/theme-selector.js";
19
+ import { ThinkingSelectorComponent } from "../components/thinking-selector.js";
20
+ import { ToolExecutionComponent } from "../components/tool-execution.js";
21
+ import { TreeSelectorComponent, } from "../components/tree-selector.js";
22
+ import { UserMessageComponent } from "../components/user-message.js";
23
+ import { UserMessageSelectorComponent, } from "../components/user-message-selector.js";
24
+ import { theme } from "../theme/theme.js";
25
+ import { createChatViewport } from "./chat-viewport.js";
26
+ import { createInteractiveTui, } from "./tui-renderer.js";
27
+ export const BUILTIN_SLASH_COMMANDS = [
28
+ { name: "help", description: "查看所有可用命令与快捷键说明" },
29
+ { name: "clear", description: "清空当前终端屏幕会话" },
30
+ { name: "new", description: "结束当前会话,开启全新的空白会话" },
31
+ {
32
+ name: "resume",
33
+ description: "列出、搜索或恢复指定历史会话",
34
+ argumentHint: "[session_id]",
35
+ },
36
+ {
37
+ name: "session",
38
+ description: "列出、搜索或恢复指定历史会话",
39
+ argumentHint: "[session_id]",
40
+ },
41
+ {
42
+ name: "name",
43
+ description: "查看或设置当前会话的显示名称",
44
+ argumentHint: "[title]",
45
+ },
46
+ {
47
+ name: "compact",
48
+ description: "立即对当前上下文执行压缩,释放 Token 空间",
49
+ argumentHint: "[instructions]",
50
+ },
51
+ { name: "tree", description: "以可视化 DAG 树状图展现会话分支拓扑" },
52
+ {
53
+ name: "fork",
54
+ description: "基于当前节点创建全新分支",
55
+ argumentHint: "[node_id]",
56
+ },
57
+ { name: "clone", description: "深度克隆当前分支,开辟全新探索副本" },
58
+ {
59
+ name: "model",
60
+ description: "交互式查看与切换当前使用的语言模型",
61
+ argumentHint: "[model_id]",
62
+ },
63
+ {
64
+ name: "thinking",
65
+ description: "调整模型思考预算深度等级 (off/minimal/low/medium/high/xhigh/max)",
66
+ argumentHint: "[level]",
67
+ },
68
+ { name: "login", description: "两阶段交互式绑定 Provider API Key" },
69
+ {
70
+ name: "logout",
71
+ description: "清除指定 Provider 的已存 API 密钥凭据",
72
+ argumentHint: "[provider]",
73
+ },
74
+ { name: "theme", description: "实时预览并切换终端 TrueColor 主题方案" },
75
+ { name: "settings", description: "交互式管理模型与运行时核心参数" },
76
+ {
77
+ name: "steer",
78
+ description: "向运行中的智能体插话或修正方向",
79
+ argumentHint: "<instruction>",
80
+ },
81
+ {
82
+ name: "followup",
83
+ description: "添加后续任务指令,在当前任务结束后执行",
84
+ argumentHint: "<instruction>",
85
+ },
86
+ { name: "reload", description: "重新载入所有动态 Skills 与 Prompt 模板" },
87
+ {
88
+ name: "trust",
89
+ description: "查看或更新当前工作区的代码执行信任安全策略",
90
+ argumentHint: "[true|false]",
91
+ },
92
+ { name: "copy", description: "复制最后一条智能体消息到剪贴板" },
93
+ { name: "hotkeys", description: "查看所有键盘快捷键说明清单" },
94
+ { name: "quit", description: "优雅退出当前智能体终端" },
95
+ ];
96
+ function findFdPath() {
97
+ const envPath = process.env.PATH || "";
98
+ const paths = envPath.split(path.delimiter);
99
+ const isWindows = process.platform === "win32";
100
+ const names = isWindows ? ["fd.exe", "fdfind.exe"] : ["fd", "fdfind"];
101
+ for (const dir of paths) {
102
+ for (const name of names) {
103
+ const fullPath = path.join(dir, name);
104
+ try {
105
+ if (fs.existsSync(fullPath)) {
106
+ return fullPath;
107
+ }
108
+ }
109
+ catch {
110
+ // ignore
111
+ }
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+ export class InteractiveMode {
117
+ bridge;
118
+ options;
119
+ ui;
120
+ chatContainer;
121
+ documentContainer;
122
+ pendingMessagesContainer;
123
+ statusContainer;
124
+ editorContainer;
125
+ defaultEditor;
126
+ footer;
127
+ header;
128
+ dynamicBorder;
129
+ activeSelectorToken;
130
+ activeSelectorDispose;
131
+ activeSelectorComponent;
132
+ currentStreamingAssistant;
133
+ latestAssistantMessage;
134
+ activeToolCalls = new Map();
135
+ toolStartTimes = new Map();
136
+ transcriptScrollView;
137
+ isStreaming = false;
138
+ isWorking = false;
139
+ currentThinkingLevel = "off";
140
+ currentModelName = "default";
141
+ workspace;
142
+ onExit;
143
+ unsubscribeBridge;
144
+ constructor(bridge, options = {}) {
145
+ this.bridge = bridge;
146
+ this.options = options;
147
+ this.workspace = options.workspace || process.cwd();
148
+ this.currentModelName = options.model || "default";
149
+ this.currentThinkingLevel = options.thinking || "off";
150
+ // 1. 初始化终端 UI 宿主
151
+ this.ui = createInteractiveTui({
152
+ tuiMode: options.tuiMode || "regular",
153
+ showHardwareCursor: options.showHardwareCursor ?? false,
154
+ logDirectory: options.logDirectory || "",
155
+ });
156
+ // 2. 初始化核心布局容器
157
+ this.documentContainer = new Container();
158
+ this.chatContainer = new Container();
159
+ this.header = new HeaderComponent("0.1.0");
160
+ this.dynamicBorder = new DynamicBorder();
161
+ this.documentContainer.addChild(this.header);
162
+ this.documentContainer.addChild(new Spacer(1));
163
+ this.documentContainer.addChild(this.chatContainer);
164
+ const welcome = new Text(theme.fg("muted", "欢迎使用 my-pi-agent!输入需求或按 / 开启命令菜单。"), 1, 0);
165
+ this.chatContainer.addChild(welcome);
166
+ this.pendingMessagesContainer = new Container();
167
+ this.statusContainer = new Container();
168
+ this.editorContainer = new Container();
169
+ // 3. 初始化 Footer
170
+ this.footer = new FooterComponent({
171
+ workspace: this.workspace,
172
+ modelName: this.currentModelName,
173
+ thinkingLevel: this.currentThinkingLevel,
174
+ sessionName: options.sessionName,
175
+ }, () => this.ui.requestRender());
176
+ // 4. 初始化 Editor 与 Autocomplete
177
+ this.defaultEditor = this.createEditor();
178
+ this.editorContainer.addChild(this.defaultEditor);
179
+ // 5. 挂载视口与组件 (对齐 Pi 原厂 mountInteractiveTui 架构规范)
180
+ if (options.tuiMode === "fullscreen") {
181
+ const viewport = createChatViewport({
182
+ document: this.documentContainer,
183
+ pendingMessages: this.pendingMessagesContainer,
184
+ status: this.statusContainer,
185
+ editor: this.editorContainer,
186
+ footer: this.footer,
187
+ });
188
+ this.transcriptScrollView = viewport.transcript;
189
+ if (typeof this.ui.setLayoutRoot === "function") {
190
+ this.ui.setLayoutRoot(viewport.root);
191
+ }
192
+ else {
193
+ this.ui.addChild(viewport.root);
194
+ }
195
+ }
196
+ else {
197
+ this.ui.addChild(this.documentContainer);
198
+ this.ui.addChild(this.pendingMessagesContainer);
199
+ this.ui.addChild(this.statusContainer);
200
+ this.ui.addChild(this.editorContainer);
201
+ this.ui.addChild(this.footer);
202
+ }
203
+ this.ui.setFocus(this.defaultEditor);
204
+ }
205
+ async init() {
206
+ // 1. 订阅 KernelBridge 事件
207
+ this.subscribeToBridge();
208
+ // 2. 注册终端按键拦截
209
+ this.setupKeybindings();
210
+ // 3. 首次启动刷新
211
+ this.ui.requestRender();
212
+ }
213
+ start() {
214
+ this.ui.start();
215
+ }
216
+ stop() {
217
+ this.clearStatusDisplay();
218
+ for (const tool of this.activeToolCalls.values()) {
219
+ tool.dispose();
220
+ }
221
+ this.footer.dispose();
222
+ if (this.unsubscribeBridge) {
223
+ this.unsubscribeBridge();
224
+ this.unsubscribeBridge = undefined;
225
+ }
226
+ this.ui.stop();
227
+ }
228
+ dispose() {
229
+ this.stop();
230
+ }
231
+ // --------------------------------------------------------------------------
232
+ // 事件处理与流式分发
233
+ // --------------------------------------------------------------------------
234
+ subscribeToBridge() {
235
+ this.unsubscribeBridge = this.bridge.subscribe((event) => {
236
+ this.handleAgentEvent(event);
237
+ });
238
+ }
239
+ handleAgentEvent(event) {
240
+ if (!event || !event.type)
241
+ return;
242
+ switch (event.type) {
243
+ case "agent_start": {
244
+ this.isStreaming = true;
245
+ this.isWorking = true;
246
+ this.activeToolCalls.clear();
247
+ this.currentStreamingAssistant = undefined;
248
+ this.updateStatusDisplay("Working");
249
+ break;
250
+ }
251
+ case "turn_start": {
252
+ this.isWorking = true;
253
+ this.updateStatusDisplay("Working");
254
+ break;
255
+ }
256
+ case "message_start": {
257
+ if (event.message?.role === "assistant") {
258
+ this.currentStreamingAssistant = new AssistantMessageComponent();
259
+ this.chatContainer.addChild(this.currentStreamingAssistant);
260
+ this.chatContainer.addChild(new Spacer(1));
261
+ }
262
+ break;
263
+ }
264
+ case "message_update": {
265
+ if (!this.currentStreamingAssistant) {
266
+ this.currentStreamingAssistant = new AssistantMessageComponent();
267
+ this.latestAssistantMessage = this.currentStreamingAssistant;
268
+ this.chatContainer.addChild(this.currentStreamingAssistant);
269
+ this.chatContainer.addChild(new Spacer(1));
270
+ }
271
+ // 解析 message.content 中的 thinking 与 text 块 (使用全量快照,杜绝二次方爆炸)
272
+ if (Array.isArray(event.message?.content)) {
273
+ let thinkingText = "";
274
+ let contentText = "";
275
+ for (const block of event.message.content) {
276
+ if (block.type === "thinking" && block.thinking) {
277
+ thinkingText += block.thinking;
278
+ }
279
+ else if (block.type === "text" && block.text) {
280
+ contentText += block.text;
281
+ }
282
+ }
283
+ if (thinkingText) {
284
+ this.currentStreamingAssistant.setReasoning(thinkingText);
285
+ }
286
+ if (contentText) {
287
+ this.currentStreamingAssistant.setContent(contentText);
288
+ }
289
+ }
290
+ else if (typeof event.message?.content === "string" &&
291
+ event.message.content) {
292
+ this.currentStreamingAssistant.setContent(event.message.content);
293
+ }
294
+ break;
295
+ }
296
+ case "message_end": {
297
+ if (this.currentStreamingAssistant) {
298
+ this.currentStreamingAssistant.finalize();
299
+ this.latestAssistantMessage = this.currentStreamingAssistant;
300
+ this.currentStreamingAssistant = undefined;
301
+ }
302
+ if (event.usage) {
303
+ this.updateFooterUsage(event.usage, event.contextWindow);
304
+ }
305
+ break;
306
+ }
307
+ case "tool_execution_start": {
308
+ const id = event.toolCallId || `tc-${Date.now()}`;
309
+ const name = event.toolName || "tool";
310
+ const args = event.args || {};
311
+ const toolComponent = new ToolExecutionComponent(name, id, args, () => this.ui.requestRender());
312
+ this.activeToolCalls.set(id, toolComponent);
313
+ this.toolStartTimes.set(id, Date.now());
314
+ this.chatContainer.addChild(toolComponent);
315
+ this.chatContainer.addChild(new Spacer(1));
316
+ this.updateStatusDisplay(`正在执行工具: ${name}...`);
317
+ break;
318
+ }
319
+ case "tool_execution_update": {
320
+ const id = event.toolCallId;
321
+ const toolComponent = this.activeToolCalls.get(id);
322
+ if (toolComponent && event.partialResult) {
323
+ toolComponent.updatePartialResult(event.partialResult);
324
+ }
325
+ break;
326
+ }
327
+ case "tool_execution_end": {
328
+ const id = event.toolCallId || event.tool_call_id;
329
+ const toolComponent = id ? this.activeToolCalls.get(id) : undefined;
330
+ if (toolComponent) {
331
+ toolComponent.updateResult(event.result, Boolean(event.isError));
332
+ this.toolStartTimes.delete(id);
333
+ }
334
+ if (this.isWorking) {
335
+ this.updateStatusDisplay("Working");
336
+ }
337
+ break;
338
+ }
339
+ case "turn_end": {
340
+ this.isWorking = false;
341
+ this.clearStatusDisplay();
342
+ if (event.usage) {
343
+ this.updateFooterUsage(event.usage, event.contextWindow);
344
+ }
345
+ break;
346
+ }
347
+ case "agent_end": {
348
+ this.isStreaming = false;
349
+ this.isWorking = false;
350
+ if (this.currentStreamingAssistant) {
351
+ this.currentStreamingAssistant.finalize();
352
+ this.currentStreamingAssistant = undefined;
353
+ }
354
+ // 自动闭合所有未正常结束的工具调用
355
+ for (const tool of this.activeToolCalls.values()) {
356
+ if (!tool.finished) {
357
+ tool.updateResult("执行中断", true);
358
+ }
359
+ }
360
+ this.activeToolCalls.clear();
361
+ this.toolStartTimes.clear();
362
+ this.clearStatusDisplay();
363
+ if (event.usage) {
364
+ this.updateFooterUsage(event.usage, event.contextWindow);
365
+ }
366
+ this.footer.update({ isBusy: false });
367
+ break;
368
+ }
369
+ case "context_compacted": {
370
+ if (event.tokensAfter !== undefined) {
371
+ this.footer.update({ contextTokens: event.tokensAfter });
372
+ }
373
+ this.appendSystemNotice(`✓ 上下文已压缩: ${(event.tokensBefore ?? 0).toLocaleString()} -> ${(event.tokensAfter ?? 0).toLocaleString()} tokens`);
374
+ break;
375
+ }
376
+ }
377
+ this.ui.requestRender();
378
+ }
379
+ updateFooterUsage(usage, contextWindow) {
380
+ if (!usage)
381
+ return;
382
+ const u = usage;
383
+ this.footer.update({
384
+ inputTokens: u.input ?? u.prompt_tokens,
385
+ outputTokens: u.output ?? u.completion_tokens,
386
+ cacheReadTokens: u.cacheRead ?? u.cache_read,
387
+ cacheWriteTokens: u.cacheWrite ?? u.cache_write,
388
+ cacheHitRate: u.cacheHitRate ?? u.latestCacheHitRate,
389
+ costUsd: u.cost ?? u.cost_usd,
390
+ totalTokens: u.total ?? u.total_tokens,
391
+ contextTokens: u.contextTokens ?? u.total ?? u.total_tokens,
392
+ contextWindow: contextWindow ?? this.footer.getContextWindow(),
393
+ });
394
+ }
395
+ // --------------------------------------------------------------------------
396
+ // 编辑器与输入提交
397
+ // --------------------------------------------------------------------------
398
+ createEditor() {
399
+ const editorTheme = {
400
+ borderColor: (str) => theme.fg("borderMuted", str),
401
+ selectList: {
402
+ selectedPrefix: (s) => theme.fg("accent", s),
403
+ selectedText: (s) => theme.bold(theme.fg("accent", s)),
404
+ description: (s) => theme.fg("muted", s),
405
+ scrollInfo: (s) => theme.dim(s),
406
+ noMatch: (_text) => theme.dim("无匹配项"),
407
+ },
408
+ };
409
+ const editor = new CustomEditor(this.ui, editorTheme, {
410
+ embedWorkingStatus: true,
411
+ });
412
+ editor.onChange = (text) => {
413
+ const isBash = text.startsWith("!");
414
+ if (isBash) {
415
+ editor.borderColor = (str) => theme.fg("warning", str);
416
+ }
417
+ else {
418
+ this.updateEditorBorderColor();
419
+ }
420
+ };
421
+ const fdPath = findFdPath();
422
+ const autocompleteProvider = new CombinedAutocompleteProvider(BUILTIN_SLASH_COMMANDS, this.workspace, fdPath);
423
+ editor.setAutocompleteProvider(autocompleteProvider);
424
+ editor.onSubmit = async (text) => {
425
+ const trimmed = text.trim();
426
+ if (!trimmed)
427
+ return;
428
+ editor.addToHistory?.(trimmed);
429
+ editor.setText("");
430
+ await this.handleUserInput(trimmed);
431
+ };
432
+ return editor;
433
+ }
434
+ async handleUserInput(input) {
435
+ if (this.isStreaming) {
436
+ this.appendErrorMessage("当前智能体正在执行中,请等待完成或按 Esc 中断后再提交。");
437
+ return;
438
+ }
439
+ // 1. 处理技能或提示词模板宏扩展
440
+ if (input.startsWith("/") &&
441
+ (input.startsWith("/skill:") ||
442
+ !BUILTIN_SLASH_COMMANDS.some((c) => input.toLowerCase().startsWith(`/${c.name}`)))) {
443
+ try {
444
+ const client = this.bridge.client;
445
+ const macroRes = (await client?.request?.("macro_expand", { text: input })) ||
446
+ (await client?.sendRequest?.("macro_expand", { text: input }));
447
+ if (macroRes?.expanded && macroRes.text) {
448
+ input = macroRes.text;
449
+ }
450
+ else if (!BUILTIN_SLASH_COMMANDS.some((c) => input.toLowerCase().startsWith(`/${c.name}`))) {
451
+ this.appendErrorMessage(`命令 ${input.split(" ")[0]} 暂未在当前内核模式下启用,输入 /help 查看所有可用命令。`);
452
+ return;
453
+ }
454
+ }
455
+ catch {
456
+ // ignore
457
+ }
458
+ }
459
+ // 2. 处理斜杠命令
460
+ if (input.startsWith("/")) {
461
+ await this.handleSlashCommand(input);
462
+ return;
463
+ }
464
+ // 3. 处理 Shell 快捷命令 !cmd 或 !!cmd
465
+ if (input.startsWith("!")) {
466
+ await this.handleShellMacro(input);
467
+ return;
468
+ }
469
+ // 4. 普通文本输入:渲染用户气泡并提交给 Python
470
+ const userMsg = new UserMessageComponent(input);
471
+ this.chatContainer.addChild(userMsg);
472
+ this.chatContainer.addChild(new Spacer(1));
473
+ this.ui.requestRender();
474
+ try {
475
+ this.footer.update({ isBusy: true });
476
+ await this.bridge.prompt(input);
477
+ }
478
+ catch (err) {
479
+ this.appendErrorMessage(`请求失败: ${err.message || String(err)}`);
480
+ this.isStreaming = false;
481
+ this.isWorking = false;
482
+ this.clearStatusDisplay();
483
+ this.footer.update({ isBusy: false });
484
+ }
485
+ }
486
+ async handleExit() {
487
+ try {
488
+ if (this.onExit) {
489
+ await this.onExit();
490
+ }
491
+ }
492
+ catch {
493
+ // 忽略退出清理异常,确保正常终止进程
494
+ }
495
+ finally {
496
+ this.stop();
497
+ process.exit(0);
498
+ }
499
+ }
500
+ // --------------------------------------------------------------------------
501
+ // 快捷键拦截与生命周期
502
+ // --------------------------------------------------------------------------
503
+ setupKeybindings() {
504
+ this.ui.addInputListener((data) => {
505
+ // 若当前挂载了活动的 Selector,直接委托给 Selector 处理键盘事件并消费
506
+ if (this.activeSelectorComponent) {
507
+ this.activeSelectorComponent.handleInput?.(data);
508
+ this.ui.requestRender();
509
+ return { consume: true };
510
+ }
511
+ if (matchesKey(data, "ctrl+c")) {
512
+ if (this.isStreaming) {
513
+ void this.bridge.abort();
514
+ this.isStreaming = false;
515
+ this.isWorking = false;
516
+ this.clearStatusDisplay();
517
+ this.footer.update({ isBusy: false });
518
+ this.appendSystemNotice("执行已中断。");
519
+ this.ui.requestRender();
520
+ return { consume: true };
521
+ }
522
+ if (this.defaultEditor.getText().length > 0) {
523
+ this.defaultEditor.setText("");
524
+ this.ui.requestRender();
525
+ return { consume: true };
526
+ }
527
+ void this.handleExit();
528
+ return { consume: true };
529
+ }
530
+ else if (matchesKey(data, "ctrl+d")) {
531
+ if (this.defaultEditor.getText().length === 0 && !this.isStreaming) {
532
+ void this.handleExit();
533
+ return { consume: true };
534
+ }
535
+ }
536
+ else if (matchesKey(data, "escape")) {
537
+ if (this.isStreaming) {
538
+ void this.bridge.abort();
539
+ this.isStreaming = false;
540
+ this.isWorking = false;
541
+ this.clearStatusDisplay();
542
+ this.footer.update({ isBusy: false });
543
+ this.appendSystemNotice("执行已中断。");
544
+ this.ui.requestRender();
545
+ return { consume: true };
546
+ }
547
+ }
548
+ else if (matchesKey(data, "ctrl+o")) {
549
+ const targetAssistant = this.currentStreamingAssistant || this.latestAssistantMessage;
550
+ if (targetAssistant) {
551
+ targetAssistant.toggleThinking();
552
+ }
553
+ for (const tool of this.activeToolCalls.values()) {
554
+ tool.toggleExpanded();
555
+ }
556
+ for (const child of this.chatContainer.children) {
557
+ if (child && typeof child.toggleExpanded === "function") {
558
+ child.toggleExpanded();
559
+ }
560
+ }
561
+ this.ui.requestRender();
562
+ return { consume: true };
563
+ }
564
+ else if (matchesKey(data, "ctrl+l")) {
565
+ this.showModelSelector();
566
+ return { consume: true };
567
+ }
568
+ else if (matchesKey(data, "shift+tab") ||
569
+ data === "\x1b[Z" ||
570
+ matchesKey(data, "ctrl+t")) {
571
+ void this.cycleThinkingLevel();
572
+ return { consume: true };
573
+ }
574
+ return undefined;
575
+ });
576
+ }
577
+ // --------------------------------------------------------------------------
578
+ // 动态选择器与视口抽换 (showSelector)
579
+ // --------------------------------------------------------------------------
580
+ showSelector(create) {
581
+ const token = {};
582
+ let dispose;
583
+ const done = () => {
584
+ dispose?.();
585
+ if (this.activeSelectorToken !== token)
586
+ return;
587
+ this.activeSelectorToken = undefined;
588
+ this.activeSelectorDispose = undefined;
589
+ this.activeSelectorComponent = undefined;
590
+ this.editorContainer.clear();
591
+ this.editorContainer.addChild(this.defaultEditor);
592
+ this.ui.setFocus(this.defaultEditor);
593
+ this.ui.requestRender();
594
+ };
595
+ const created = create(done);
596
+ dispose = created.dispose;
597
+ this.disposeActiveSelector();
598
+ this.activeSelectorToken = token;
599
+ this.activeSelectorDispose = dispose;
600
+ this.activeSelectorComponent = created.component;
601
+ this.editorContainer.clear();
602
+ this.editorContainer.addChild(created.component);
603
+ this.ui.setFocus(created.focus);
604
+ this.ui.requestRender();
605
+ }
606
+ disposeActiveSelector() {
607
+ if (this.activeSelectorDispose) {
608
+ this.activeSelectorDispose();
609
+ this.activeSelectorDispose = undefined;
610
+ this.activeSelectorToken = undefined;
611
+ this.activeSelectorComponent = undefined;
612
+ }
613
+ }
614
+ // --------------------------------------------------------------------------
615
+ // 常用选择器封装
616
+ // --------------------------------------------------------------------------
617
+ showModelSelector(initialSearch) {
618
+ this.showSelector((done) => {
619
+ const selector = new ModelSelectorComponent(this.currentModelName, async () => {
620
+ const res = await this.bridge.listModels({ scope: "configured" });
621
+ return (res?.models || []).map((m) => ({
622
+ id: m.id || m.name,
623
+ name: m.name || m.id,
624
+ provider: m.provider || "default",
625
+ contextWindow: m.context_window || 128000,
626
+ is_configured: m.is_configured ?? true,
627
+ }));
628
+ }, async (selected) => {
629
+ done();
630
+ if (selected) {
631
+ try {
632
+ this.currentModelName = selected.id;
633
+ const supported = this.getSupportedThinkingLevels();
634
+ if (!supported.includes(this.currentThinkingLevel.toLowerCase())) {
635
+ this.currentThinkingLevel = supported[0] || "off";
636
+ void this.bridge.setThinking(this.currentThinkingLevel);
637
+ }
638
+ this.footer.update({
639
+ modelName: selected.id,
640
+ providerName: selected.provider,
641
+ thinkingLevel: this.currentThinkingLevel,
642
+ contextWindow: selected.contextWindow ||
643
+ (selected.id.startsWith("gemini-") ? 1048576 : 128000),
644
+ });
645
+ this.updateEditorBorderColor();
646
+ await this.bridge.switchModel(selected.id, selected.provider);
647
+ this.appendSystemNotice(`✓ 已成功切换至模型: ${selected.id} (${selected.provider})`);
648
+ }
649
+ catch (err) {
650
+ this.appendErrorMessage(`切换模型失败: ${err.message || String(err)}`);
651
+ }
652
+ }
653
+ }, () => done(), initialSearch, async (defaultModel) => {
654
+ done();
655
+ if (defaultModel) {
656
+ try {
657
+ this.currentModelName = defaultModel.id;
658
+ const supported = this.getSupportedThinkingLevels();
659
+ if (!supported.includes(this.currentThinkingLevel.toLowerCase())) {
660
+ this.currentThinkingLevel = supported[0] || "off";
661
+ void this.bridge.setThinking(this.currentThinkingLevel);
662
+ }
663
+ this.footer.update({
664
+ modelName: defaultModel.id,
665
+ providerName: defaultModel.provider,
666
+ thinkingLevel: this.currentThinkingLevel,
667
+ contextWindow: defaultModel.contextWindow ||
668
+ (defaultModel.id.startsWith("gemini-") ? 1048576 : 128000),
669
+ });
670
+ this.updateEditorBorderColor();
671
+ await this.bridge.switchModel(defaultModel.id, defaultModel.provider);
672
+ await this.bridge.setSetting("defaultModel", defaultModel.id);
673
+ await this.bridge.setSetting("defaultProvider", defaultModel.provider);
674
+ this.appendSystemNotice(`✓ 已成功切换并保存为默认模型: ${defaultModel.id} (${defaultModel.provider})`);
675
+ }
676
+ catch (err) {
677
+ this.appendErrorMessage(`设置默认模型失败: ${err.message || String(err)}`);
678
+ }
679
+ }
680
+ }, undefined, () => this.ui.requestRender());
681
+ return { component: selector, focus: selector };
682
+ });
683
+ }
684
+ showSessionSelector() {
685
+ this.showSelector((done) => {
686
+ const selector = new SessionSelectorComponent(async (allProjects) => {
687
+ const res = await this.bridge.listSessions(allProjects ? { all_projects: true } : {});
688
+ return (res?.sessions || []).map((s) => ({
689
+ id: s.id || s.session_id,
690
+ name: s.name || s.title || s.first_message || s.session_id,
691
+ path: s.path,
692
+ modified: s.modified ??
693
+ (s.updated_at
694
+ ? Math.floor(s.updated_at / 1000)
695
+ : Math.floor(Date.now() / 1000)),
696
+ cwd: s.cwd || s.workspace || this.workspace,
697
+ message_count: s.message_count || 0,
698
+ parent_session: s.parent_session || s.parent_session_path,
699
+ parent_session_path: s.parent_session_path || s.parent_session,
700
+ }));
701
+ }, async (session) => {
702
+ done();
703
+ if (session) {
704
+ try {
705
+ const target = session.path || session.id;
706
+ const res = await this.bridge.resumeSession(target);
707
+ if (res?.session_name || res?.session_id) {
708
+ this.footer.update({
709
+ sessionName: res.session_name || res.session_id,
710
+ });
711
+ }
712
+ if (res?.usage) {
713
+ this.updateFooterUsage(res.usage, res.context_window);
714
+ }
715
+ this.renderSessionHistory(res?.messages || [], `✓ 已成功恢复会话: \`${res?.session_name || res?.session_id || session.id}\``);
716
+ }
717
+ catch (err) {
718
+ this.appendErrorMessage(`恢复会话失败: ${err.message || String(err)}`);
719
+ }
720
+ }
721
+ }, () => done(), () => this.ui.requestRender(), this.footer.getSessionName(), async (sessionToDelete) => {
722
+ try {
723
+ await this.bridge.deleteSession(sessionToDelete.path || sessionToDelete.id);
724
+ this.appendSystemNotice(`✓ 已成功删除历史会话: \`${sessionToDelete.name || sessionToDelete.id}\``);
725
+ }
726
+ catch (err) {
727
+ const msg = err instanceof Error ? err.message : String(err);
728
+ this.appendErrorMessage(`删除会话失败: ${msg}`);
729
+ }
730
+ });
731
+ return { component: selector, focus: selector };
732
+ });
733
+ }
734
+ getSupportedThinkingLevels() {
735
+ const m = (this.currentModelName || "").toLowerCase();
736
+ // 1. 完全不支持思考的模型(如 gpt-4o, gpt-3.5, claude-haiku, claude-opus, deepseek-chat 等)
737
+ if (m.includes("gpt-4o") ||
738
+ m.includes("gpt-4.1") ||
739
+ m.includes("gpt-3.5") ||
740
+ m.includes("claude-3-5-haiku") ||
741
+ m.includes("claude-3-opus") ||
742
+ m === "deepseek-chat" ||
743
+ m === "deepseek-v3") {
744
+ return ["off"];
745
+ }
746
+ // 2. OpenAI o1/o3/o4 系列:API 仅支持 low, medium, high
747
+ if (m.includes("o1") || m.includes("o3") || m.includes("o4")) {
748
+ return ["low", "medium", "high"];
749
+ }
750
+ // 3. DeepSeek R1 / Reasoner:支持开启或关闭
751
+ if (m.includes("reasoner") || m.includes("r1")) {
752
+ return ["low", "medium", "high"];
753
+ }
754
+ // 4. Google Gemini 思考模型(对标 pi-antigravity,支持 off/minimal/low/medium/high,无 xhigh/max)
755
+ if (m.includes("gemini")) {
756
+ return ["off", "minimal", "low", "medium", "high"];
757
+ }
758
+ // 5. Claude 3.7 Sonnet 系列:支持连续 Token 预算,完整映射至 max
759
+ if (m.includes("claude") &&
760
+ (m.includes("sonnet") || m.includes("3-7") || m.includes("4"))) {
761
+ return ["off", "minimal", "low", "medium", "high", "max"];
762
+ }
763
+ return ["off", "low", "medium", "high"];
764
+ }
765
+ updateEditorBorderColor() {
766
+ if (this.defaultEditor) {
767
+ if (this.defaultEditor.getText().startsWith("!")) {
768
+ this.defaultEditor.borderColor = (str) => theme.fg("warning", str);
769
+ }
770
+ else {
771
+ this.defaultEditor.borderColor = theme.getThinkingBorderColor(this.currentThinkingLevel);
772
+ }
773
+ this.ui.requestRender();
774
+ }
775
+ }
776
+ async cycleThinkingLevel() {
777
+ const levels = this.getSupportedThinkingLevels();
778
+ if (levels.length === 1 && levels[0] === "off") {
779
+ this.currentThinkingLevel = "off";
780
+ this.footer.update({ thinkingLevel: "off" });
781
+ this.updateEditorBorderColor();
782
+ this.appendSystemNotice("当前模型不支持思考模式 (thinking: off)");
783
+ this.ui.requestRender();
784
+ return undefined;
785
+ }
786
+ const curIdx = levels.indexOf(this.currentThinkingLevel.toLowerCase());
787
+ const nextIdx = (curIdx + 1) % levels.length;
788
+ const nextLevel = levels[nextIdx];
789
+ this.currentThinkingLevel = nextLevel;
790
+ this.footer.update({ thinkingLevel: nextLevel });
791
+ this.updateEditorBorderColor();
792
+ this.appendSystemNotice(`✓ 思考预算已更新为: ${nextLevel}`);
793
+ this.ui.requestRender();
794
+ try {
795
+ await this.bridge.setThinking(nextLevel);
796
+ }
797
+ catch {
798
+ // ignore
799
+ }
800
+ return nextLevel;
801
+ }
802
+ showThinkingSelector() {
803
+ const levels = this.getSupportedThinkingLevels();
804
+ if (levels.length === 1 && levels[0] === "off") {
805
+ this.appendSystemNotice("当前模型不支持思考模式 (thinking: off)。");
806
+ return;
807
+ }
808
+ this.showSelector((done) => {
809
+ const selector = new ThinkingSelectorComponent(this.currentThinkingLevel, levels, async (level) => {
810
+ done();
811
+ if (level) {
812
+ try {
813
+ this.currentThinkingLevel = level;
814
+ this.footer.update({ thinkingLevel: level });
815
+ this.updateEditorBorderColor();
816
+ await this.bridge.setThinking(level);
817
+ this.appendSystemNotice(`✓ 思考预算等级已调整为: ${level}`);
818
+ }
819
+ catch (err) {
820
+ this.appendErrorMessage(`设置思考预算失败: ${err.message || String(err)}`);
821
+ }
822
+ }
823
+ }, () => done());
824
+ return { component: selector, focus: selector };
825
+ });
826
+ }
827
+ showLoginSelector() {
828
+ this.showSelector((done) => {
829
+ const selector = new LoginSelectorComponent(async (provider, key) => {
830
+ done();
831
+ try {
832
+ await this.bridge.login(provider, key);
833
+ this.appendSystemNotice(`✓ 已成功为 ${provider} 绑定 API 密钥。`);
834
+ }
835
+ catch (err) {
836
+ this.appendErrorMessage(`绑定 API 密钥失败: ${err.message || String(err)}`);
837
+ }
838
+ }, () => done());
839
+ return { component: selector, focus: selector };
840
+ });
841
+ }
842
+ showLogoutSelector() {
843
+ this.showSelector((done) => {
844
+ const defaultProviders = [
845
+ { id: "deepseek", label: "DeepSeek", description: "deepseek API key" },
846
+ { id: "openai", label: "OpenAI", description: "openai API key" },
847
+ {
848
+ id: "anthropic",
849
+ label: "Anthropic",
850
+ description: "anthropic API key",
851
+ },
852
+ {
853
+ id: "antigravity",
854
+ label: "Antigravity",
855
+ description: "oauth credential",
856
+ },
857
+ ];
858
+ const selector = new LogoutSelectorComponent(defaultProviders, async (providerId) => {
859
+ done();
860
+ try {
861
+ await this.bridge.logout(providerId);
862
+ this.appendSystemNotice(`✓ 已成功注销 ${providerId} 的凭据。`);
863
+ }
864
+ catch (err) {
865
+ this.appendErrorMessage(`注销凭据失败: ${err.message || String(err)}`);
866
+ }
867
+ }, () => done());
868
+ return { component: selector, focus: selector };
869
+ });
870
+ }
871
+ showThemeSelector() {
872
+ const curTheme = theme.currentThemeName;
873
+ this.showSelector((done) => {
874
+ const selector = new ThemeSelectorComponent(curTheme, ["dark", "light"], async (themeName) => {
875
+ done();
876
+ theme.setTheme(themeName);
877
+ try {
878
+ await this.bridge.setSetting("theme", themeName);
879
+ }
880
+ catch {
881
+ // ignore
882
+ }
883
+ this.appendSystemNotice(`✓ 主题已切换至: ${themeName}`);
884
+ this.ui.requestRender();
885
+ }, () => {
886
+ theme.setTheme(curTheme);
887
+ done();
888
+ this.ui.requestRender();
889
+ }, (previewTheme) => {
890
+ theme.setTheme(previewTheme);
891
+ this.ui.requestRender();
892
+ });
893
+ return { component: selector, focus: selector };
894
+ });
895
+ }
896
+ showTreeSelector() {
897
+ this.showSelector((done) => {
898
+ const selector = new TreeSelectorComponent(async () => {
899
+ const res = await this.bridge.getTree();
900
+ return (res?.nodes ||
901
+ res?.tree ||
902
+ []);
903
+ }, async (node) => {
904
+ done();
905
+ try {
906
+ const branchRes = await this.bridge.branchSession(node.id);
907
+ if (branchRes?.messages && Array.isArray(branchRes.messages)) {
908
+ this.renderSessionHistory(branchRes.messages);
909
+ }
910
+ if (branchRes?.editor_text) {
911
+ this.defaultEditor.setText(String(branchRes.editor_text));
912
+ }
913
+ this.appendSystemNotice(`✓ 已切换至分支节点: ${node.id}`);
914
+ }
915
+ catch (err) {
916
+ this.appendErrorMessage(`切换分支失败: ${err.message || String(err)}`);
917
+ }
918
+ }, () => done(), undefined, () => this.ui.requestRender());
919
+ return { component: selector, focus: selector };
920
+ });
921
+ }
922
+ async showForkSelector() {
923
+ const userMessages = [];
924
+ try {
925
+ const res = await this.bridge.getTree();
926
+ const tree = (res?.tree || res?.nodes || []);
927
+ for (const n of tree) {
928
+ if (n.role === "user") {
929
+ userMessages.push({ id: n.id, text: n.preview || n.id });
930
+ }
931
+ }
932
+ }
933
+ catch {
934
+ // ignore
935
+ }
936
+ if (userMessages.length === 0) {
937
+ this.appendSystemNotice("当前会话暂无历史用户消息可供分叉。");
938
+ return;
939
+ }
940
+ this.showSelector((done) => {
941
+ const selector = new UserMessageSelectorComponent(userMessages, async (msg) => {
942
+ done();
943
+ try {
944
+ const client = this.bridge.client;
945
+ const res = (await client?.sendRequest?.("session_fork", {
946
+ entry_id: msg.id,
947
+ })) || (await this.bridge.forkSession?.(msg.id));
948
+ if (res?.messages) {
949
+ if (res?.new_session_id) {
950
+ this.footer.update({ sessionName: res.new_session_id });
951
+ }
952
+ this.renderSessionHistory(res.messages, `✓ 已从用户提问分叉开辟新会话: ${res.new_session_id || msg.id}`);
953
+ }
954
+ else {
955
+ this.appendSystemNotice(`✓ 已成功从节点 ${msg.id} 分叉开辟新会话: ${res?.new_session_id || ""}`);
956
+ }
957
+ }
958
+ catch (err) {
959
+ this.appendErrorMessage(`分叉会话失败: ${err.message || String(err)}`);
960
+ }
961
+ }, () => done());
962
+ return { component: selector, focus: selector };
963
+ });
964
+ }
965
+ async showSettingsSelector() {
966
+ let currentSettings = {};
967
+ try {
968
+ const res = await this.bridge.getSettings();
969
+ if (res?.settings) {
970
+ currentSettings = res.settings;
971
+ }
972
+ }
973
+ catch {
974
+ // ignore
975
+ }
976
+ this.showSelector((done) => {
977
+ const selector = new SettingsSelectorComponent(currentSettings, async (key, value) => {
978
+ try {
979
+ await this.bridge.setSetting(key, value);
980
+ }
981
+ catch (err) {
982
+ this.appendErrorMessage(`修改配置失败: ${err.message || String(err)}`);
983
+ }
984
+ }, () => done());
985
+ return { component: selector, focus: selector };
986
+ });
987
+ }
988
+ // --------------------------------------------------------------------------
989
+ // 斜杠命令分发
990
+ // --------------------------------------------------------------------------
991
+ async handleSlashCommand(input) {
992
+ const parts = input.slice(1).split(" ");
993
+ const cmd = (parts[0] || "").toLowerCase();
994
+ const args = parts.slice(1).join(" ").trim();
995
+ if (this.isStreaming &&
996
+ [
997
+ "clear",
998
+ "new",
999
+ "resume",
1000
+ "session",
1001
+ "compact",
1002
+ "clone",
1003
+ "fork",
1004
+ ].includes(cmd)) {
1005
+ this.appendErrorMessage(`当前智能体正在执行中,无法执行 /${cmd} 操作。`);
1006
+ return;
1007
+ }
1008
+ try {
1009
+ switch (cmd) {
1010
+ case "clear": {
1011
+ if (this.isStreaming) {
1012
+ this.appendErrorMessage("当前智能体正在执行中,无法清空屏幕会话。");
1013
+ return;
1014
+ }
1015
+ this.chatContainer.clear();
1016
+ this.ui.requestRender();
1017
+ break;
1018
+ }
1019
+ case "help": {
1020
+ this.appendSystemNotice("可用命令列表:\n" +
1021
+ BUILTIN_SLASH_COMMANDS.map((c) => ` /${c.name.padEnd(12)} - ${c.description}`).join("\n"));
1022
+ break;
1023
+ }
1024
+ case "model": {
1025
+ if (args) {
1026
+ // 检查是否为已知模型的完全匹配 (对齐 Pi 原厂 handleModelCommand)
1027
+ const allModelsRes = await this.bridge.listModels({
1028
+ scope: "all",
1029
+ });
1030
+ const models = (allModelsRes?.models || []).map((m) => ({
1031
+ id: m.id || m.name,
1032
+ provider: m.provider || "default",
1033
+ }));
1034
+ const matched = models.find((m) => m.id.toLowerCase() === args.toLowerCase() ||
1035
+ `${m.provider}/${m.id}`.toLowerCase() === args.toLowerCase());
1036
+ if (matched) {
1037
+ this.currentModelName = matched.id;
1038
+ this.footer.update({
1039
+ modelName: matched.id,
1040
+ providerName: matched.provider,
1041
+ });
1042
+ await this.bridge.switchModel(matched.id, matched.provider);
1043
+ this.appendSystemNotice(`✓ 已成功切换至模型: ${matched.id} (${matched.provider})`);
1044
+ }
1045
+ else {
1046
+ // 未精确匹配时,将参数作为初始搜索词呼出模型选择器 (对齐 Pi 原厂行为)
1047
+ this.showModelSelector(args);
1048
+ }
1049
+ }
1050
+ else {
1051
+ this.showModelSelector();
1052
+ }
1053
+ break;
1054
+ }
1055
+ case "session": {
1056
+ if (args) {
1057
+ const res = await this.bridge.resumeSession(args);
1058
+ if (res?.session_name || res?.session_id) {
1059
+ this.footer.update({
1060
+ sessionName: res.session_name || res.session_id,
1061
+ });
1062
+ }
1063
+ this.renderSessionHistory(res?.messages || [], `✓ 已成功恢复历史会话 [${args}]`);
1064
+ }
1065
+ else {
1066
+ try {
1067
+ const res = await this.bridge.getSessionStats();
1068
+ if (res?.stats) {
1069
+ this.renderPiSessionStats(res.stats);
1070
+ }
1071
+ else {
1072
+ this.renderFallbackSessionStats();
1073
+ }
1074
+ }
1075
+ catch {
1076
+ this.renderFallbackSessionStats();
1077
+ }
1078
+ }
1079
+ break;
1080
+ }
1081
+ case "resume": {
1082
+ if (args) {
1083
+ try {
1084
+ const res = await this.bridge.resumeSession(args);
1085
+ if (res?.session_name || res?.session_id) {
1086
+ this.footer.update({
1087
+ sessionName: res.session_name || res.session_id,
1088
+ });
1089
+ }
1090
+ if (res?.usage) {
1091
+ this.updateFooterUsage(res.usage, res.context_window);
1092
+ }
1093
+ this.renderSessionHistory(res?.messages || [], `✓ 已成功恢复历史会话 [${args}]`);
1094
+ }
1095
+ catch (err) {
1096
+ this.appendErrorMessage(`恢复会话失败: ${err.message || String(err)}`);
1097
+ }
1098
+ }
1099
+ else {
1100
+ this.showSessionSelector();
1101
+ }
1102
+ break;
1103
+ }
1104
+ case "thinking": {
1105
+ const validLevels = [
1106
+ "off",
1107
+ "minimal",
1108
+ "low",
1109
+ "medium",
1110
+ "high",
1111
+ "xhigh",
1112
+ "max",
1113
+ ];
1114
+ if (args) {
1115
+ const normalized = args.trim().toLowerCase();
1116
+ if (validLevels.includes(normalized)) {
1117
+ this.currentThinkingLevel = normalized;
1118
+ this.footer.update({ thinkingLevel: normalized });
1119
+ this.updateEditorBorderColor();
1120
+ await this.bridge.setThinking(normalized);
1121
+ this.appendSystemNotice(`✓ 思考预算已更新为: ${normalized}`);
1122
+ }
1123
+ else {
1124
+ this.appendErrorMessage(`未知思考等级 "${args}"。可用等级: ${validLevels.join(", ")}。`);
1125
+ }
1126
+ }
1127
+ else {
1128
+ this.showThinkingSelector();
1129
+ }
1130
+ break;
1131
+ }
1132
+ case "login": {
1133
+ if (args) {
1134
+ const parts = args.split(" ");
1135
+ const provider = parts[0] || "";
1136
+ const key = parts.slice(1).join(" ");
1137
+ const client = this.bridge.client;
1138
+ const res = (await client?.sendRequest?.("login", { provider, key })) ||
1139
+ (await client?.request?.("login", { provider, key })) ||
1140
+ (await this.bridge.login(provider, key));
1141
+ this.appendSystemNotice(res?.message || `✓ 成功保存 ${provider.toUpperCase()}_API_KEY`);
1142
+ }
1143
+ else {
1144
+ this.showLoginSelector();
1145
+ }
1146
+ break;
1147
+ }
1148
+ case "logout": {
1149
+ if (args) {
1150
+ const client = this.bridge.client;
1151
+ const res = (await client?.sendRequest?.("auth_logout", {
1152
+ provider: args,
1153
+ })) || (await this.bridge.logout(args));
1154
+ this.appendSystemNotice(`✓ 已成功清除 ${res?.provider || args} 的认证凭据。`);
1155
+ }
1156
+ else {
1157
+ this.showLogoutSelector();
1158
+ }
1159
+ break;
1160
+ }
1161
+ case "theme": {
1162
+ this.showThemeSelector();
1163
+ break;
1164
+ }
1165
+ case "tree": {
1166
+ this.showTreeSelector();
1167
+ break;
1168
+ }
1169
+ case "settings": {
1170
+ await this.showSettingsSelector();
1171
+ break;
1172
+ }
1173
+ case "new": {
1174
+ const client = this.bridge.client;
1175
+ const res = (await client?.sendRequest?.("session_new", {})) ||
1176
+ (await client?.request?.("session_new", {})) ||
1177
+ (await this.bridge.newSession());
1178
+ this.chatContainer.clear();
1179
+ const sid = res?.session_id || res?.new_session_id || res?.id || "";
1180
+ this.appendSystemNotice(`✓ 已成功结束旧会话并开启新会话: ${sid}`);
1181
+ break;
1182
+ }
1183
+ case "name": {
1184
+ if (!args) {
1185
+ const currentName = this.footer?.data?.sessionName || "未命名会话 (default)";
1186
+ this.appendSystemNotice(`当前会话名称: ${currentName}\n修改名称用法: /name <新名称>`);
1187
+ break;
1188
+ }
1189
+ const res = (await this.bridge.sessionName?.(args)) ??
1190
+ (await this.bridge.client.sendRequest?.("session_name", {
1191
+ name: args,
1192
+ }));
1193
+ const newName = res?.name || args;
1194
+ this.footer.update({ sessionName: newName });
1195
+ this.appendSystemNotice(`✓ 会话名称已更新: ${newName}`);
1196
+ break;
1197
+ }
1198
+ case "compact": {
1199
+ this.isStreaming = true;
1200
+ this.clearStatusDisplay();
1201
+ const compIndicator = new CompactionStatusIndicator(this.ui, "manual");
1202
+ compIndicator.start();
1203
+ this.activeStatusIndicator = compIndicator;
1204
+ this.defaultEditor.setWorkingStatusIndicator(compIndicator);
1205
+ this.footer.update({ isBusy: true });
1206
+ this.ui.requestRender();
1207
+ try {
1208
+ const client = this.bridge.client;
1209
+ const res = (await client?.sendRequest?.("session_compact", {
1210
+ instructions: args,
1211
+ })) ||
1212
+ (await client?.request?.("session_compact", {
1213
+ instructions: args,
1214
+ })) ||
1215
+ (await this.bridge.compact?.(args));
1216
+ if (res?.tokens_after !== undefined) {
1217
+ this.footer.update({ contextTokens: res.tokens_after });
1218
+ }
1219
+ if (res?.summary) {
1220
+ const compComponent = new CompactionSummaryMessageComponent({
1221
+ summary: res.summary,
1222
+ tokensBefore: res.tokens_before ?? 0,
1223
+ });
1224
+ this.chatContainer.addChild(new Spacer(1));
1225
+ this.chatContainer.addChild(compComponent);
1226
+ }
1227
+ }
1228
+ catch (err) {
1229
+ this.appendErrorMessage(`压缩失败: ${err.message || String(err)}`);
1230
+ }
1231
+ finally {
1232
+ this.isStreaming = false;
1233
+ this.clearStatusDisplay();
1234
+ this.footer.update({ isBusy: false });
1235
+ this.ui.requestRender();
1236
+ }
1237
+ break;
1238
+ }
1239
+ case "clone": {
1240
+ const res = await (this.bridge.cloneSession?.() ??
1241
+ this.bridge.client.sendRequest?.("session_clone"));
1242
+ const newId = res?.new_session_id || "";
1243
+ if (newId) {
1244
+ this.footer.update({ sessionName: newId });
1245
+ }
1246
+ this.appendSystemNotice(`✓ 已克隆当前会话开辟全新探索副本: ${newId}`);
1247
+ break;
1248
+ }
1249
+ case "fork": {
1250
+ if (args) {
1251
+ const client = this.bridge.client;
1252
+ const res = (await client?.sendRequest?.("session_fork", {
1253
+ node_id: args,
1254
+ })) || (await this.bridge.forkSession?.(args));
1255
+ this.appendSystemNotice(`✓ 已成功从节点 ${args} 分叉开辟新会话: ${res?.new_session_id || ""}`);
1256
+ }
1257
+ else {
1258
+ await this.showForkSelector();
1259
+ }
1260
+ break;
1261
+ }
1262
+ case "reload": {
1263
+ const res = await (this.bridge.reloadResources?.() ??
1264
+ this.bridge.client.sendRequest?.("resource_reload"));
1265
+ this.appendSystemNotice(`✓ 资源重载完成: ${res?.summary || ""}`);
1266
+ break;
1267
+ }
1268
+ case "trust": {
1269
+ const res = (await this.bridge.setTrust?.(args === "true")) ??
1270
+ (await this.bridge.client.sendRequest?.("trust_set", {
1271
+ trusted: args === "true",
1272
+ }));
1273
+ this.appendSystemNotice(`✓ 项目信任状态已设置为: ${res?.decision || (args === "true" ? "trusted" : "untrusted")} (${res?.path || this.workspace})`);
1274
+ break;
1275
+ }
1276
+ case "quota": {
1277
+ this.appendSystemNotice("当前配额状态:正常");
1278
+ break;
1279
+ }
1280
+ case "steer": {
1281
+ if (args) {
1282
+ await this.bridge.steer(args);
1283
+ this.appendSystemNotice(`[Steer 提示已注入]: ${args}`);
1284
+ }
1285
+ break;
1286
+ }
1287
+ case "followup": {
1288
+ if (args) {
1289
+ await this.bridge.followUp(args);
1290
+ this.appendSystemNotice(`[Followup 任务已排队]: ${args}`);
1291
+ }
1292
+ break;
1293
+ }
1294
+ case "copy": {
1295
+ const target = this.currentStreamingAssistant || this.latestAssistantMessage;
1296
+ const text = target?.getContentText();
1297
+ if (!text) {
1298
+ this.appendErrorMessage("当前暂无智能体消息可供复制。");
1299
+ break;
1300
+ }
1301
+ const isWindows = process.platform === "win32";
1302
+ const isMac = process.platform === "darwin";
1303
+ try {
1304
+ let proc;
1305
+ if (isWindows) {
1306
+ proc = spawn("clip");
1307
+ }
1308
+ else if (isMac) {
1309
+ proc = spawn("pbcopy");
1310
+ }
1311
+ else {
1312
+ proc = spawn("xclip", ["-selection", "clipboard"]);
1313
+ }
1314
+ proc.on("error", () => { });
1315
+ proc.stdin?.write(text);
1316
+ proc.stdin?.end();
1317
+ }
1318
+ catch {
1319
+ // ignore clipboard error
1320
+ }
1321
+ this.appendSystemNotice("✓ 已将最后一条智能体回答内容复制到系统剪贴板。");
1322
+ break;
1323
+ }
1324
+ case "hotkeys": {
1325
+ const list = [
1326
+ theme.bold("常用键盘快捷键说明清单 (Hotkeys):"),
1327
+ "",
1328
+ ` ${theme.bold("导航与视口 (Navigation):")}`,
1329
+ ` ↑ / ↓ 在选择器列表中上下选择条目`,
1330
+ ` Tab 切换选择器范围 (all vs scoped)`,
1331
+ ` Ctrl+O 展开 / 折叠思考过程 (Thinking) 与工具执行卡片`,
1332
+ "",
1333
+ ` ${theme.bold("编辑与会话 (Editing):")}`,
1334
+ ` Enter 提交提问 (在输入框) 或确认当前所选条目 (在选择器)`,
1335
+ ` Ctrl+C 清空当前输入文字 (输入框有文字时) / 关闭弹窗 (选择器中)`,
1336
+ ` Ctrl+D 快速退出终端 (仅当输入框为空时生效)`,
1337
+ ` Ctrl+L 快速唤起模型选择器 (Model Catalog)`,
1338
+ "",
1339
+ ` ${theme.bold("流程与控制 (Control):")}`,
1340
+ ` Esc 中断当前正在执行的流式回答 (Abort) / 取消并关闭弹窗`,
1341
+ ` Shift+Tab 轮转切换思考预算深度 (off -> low -> high -> max)`,
1342
+ ` / 呼出全部斜杠命令菜单与自动补全`,
1343
+ ` !cmd 执行本地 Shell 命令并将输出加入上下文`,
1344
+ ` !!cmd 静默执行本地 Shell 命令 (不加入对话上下文)`,
1345
+ ].join("\n");
1346
+ this.appendSystemNotice(list);
1347
+ break;
1348
+ }
1349
+ case "quit":
1350
+ case "exit": {
1351
+ void this.handleExit();
1352
+ break;
1353
+ }
1354
+ default: {
1355
+ this.appendErrorMessage(`未知命令 /${cmd},输入 /help 查看可用命令。`);
1356
+ }
1357
+ }
1358
+ }
1359
+ catch (err) {
1360
+ this.appendErrorMessage(`执行命令 /${cmd} 失败: ${err.message || String(err)}`);
1361
+ }
1362
+ }
1363
+ async handleShellMacro(input) {
1364
+ const isSilent = input.startsWith("!!");
1365
+ const rawCmd = input.replace(/^!!?/, "").trim();
1366
+ if (!rawCmd) {
1367
+ this.appendErrorMessage("请输入要执行的本地 Shell 命令,例如:!git status 或 !!ls -la");
1368
+ return;
1369
+ }
1370
+ try {
1371
+ const client = this.bridge.client;
1372
+ let output = "";
1373
+ let exitCode = 0;
1374
+ if (client?.sendRequest) {
1375
+ const res = await client.sendRequest("shell_exec", {
1376
+ command: rawCmd,
1377
+ exclude_from_context: isSilent,
1378
+ });
1379
+ if (res?.output !== undefined)
1380
+ output = res.output;
1381
+ if (res?.exit_code !== undefined)
1382
+ exitCode = res.exit_code;
1383
+ }
1384
+ this.appendSystemNotice(`$ ${rawCmd} (${isSilent ? "静默执行,未加入上下文" : "已加入上下文"}) (Exit: ${exitCode})\n\n\`\`\`text\n${output}\n\`\`\``);
1385
+ }
1386
+ catch (err) {
1387
+ this.appendErrorMessage(`执行失败: ${err.message || String(err)}`);
1388
+ }
1389
+ }
1390
+ renderSessionHistory(messages, banner) {
1391
+ this.chatContainer.clear();
1392
+ this.activeToolCalls.clear();
1393
+ this.toolStartTimes.clear();
1394
+ this.currentStreamingAssistant = undefined;
1395
+ if (banner) {
1396
+ const bannerComp = new AssistantMessageComponent();
1397
+ bannerComp.appendTextDelta(banner);
1398
+ bannerComp.finalize();
1399
+ this.chatContainer.addChild(bannerComp);
1400
+ }
1401
+ else if (!messages || messages.length === 0) {
1402
+ const welcome = new Text(theme.fg("muted", "欢迎使用 my-pi-agent!输入需求或按 / 开启命令菜单。"), 1, 0);
1403
+ this.chatContainer.addChild(welcome);
1404
+ }
1405
+ if (!messages || messages.length === 0) {
1406
+ this.ui.requestRender();
1407
+ return;
1408
+ }
1409
+ const pendingTools = new Map();
1410
+ for (const msg of messages) {
1411
+ if (msg.role === "compaction" ||
1412
+ msg.type === "compaction" ||
1413
+ msg.role === "compactionSummary") {
1414
+ const compComponent = new CompactionSummaryMessageComponent({
1415
+ summary: String(msg.summary || msg.content || ""),
1416
+ tokensBefore: Number(msg.tokens_before || msg.tokensBefore || 0),
1417
+ });
1418
+ this.chatContainer.addChild(new Spacer(1));
1419
+ this.chatContainer.addChild(compComponent);
1420
+ continue;
1421
+ }
1422
+ if (msg.role === "user") {
1423
+ const userText = typeof msg.content === "string"
1424
+ ? msg.content
1425
+ : Array.isArray(msg.content)
1426
+ ? msg.content
1427
+ .map((b) => typeof b === "string" ? b : b?.text || b?.content || "")
1428
+ .join("")
1429
+ : String(msg.content ?? "");
1430
+ this.chatContainer.addChild(new UserMessageComponent(userText));
1431
+ }
1432
+ else if (msg.role === "assistant") {
1433
+ const assistantComp = new AssistantMessageComponent();
1434
+ const thinking = msg.metadata?.thinking || msg.metadata?.reasoning_content;
1435
+ if (thinking) {
1436
+ assistantComp.setReasoning(String(thinking));
1437
+ }
1438
+ const assistantText = typeof msg.content === "string"
1439
+ ? msg.content
1440
+ : Array.isArray(msg.content)
1441
+ ? msg.content
1442
+ .map((b) => typeof b === "string" ? b : b?.text || b?.content || "")
1443
+ .join("")
1444
+ : msg.content == null
1445
+ ? ""
1446
+ : String(msg.content);
1447
+ if (assistantText) {
1448
+ assistantComp.setContent(assistantText);
1449
+ }
1450
+ assistantComp.finalize();
1451
+ this.chatContainer.addChild(assistantComp);
1452
+ const toolCalls = msg.metadata?.tool_calls;
1453
+ if (Array.isArray(toolCalls)) {
1454
+ for (const tc of toolCalls) {
1455
+ const rawTc = tc;
1456
+ const toolName = String(rawTc.name ||
1457
+ rawTc.function?.name ||
1458
+ "tool");
1459
+ const callId = String(rawTc.id || "");
1460
+ let parsedArgs = {};
1461
+ if (rawTc.args && typeof rawTc.args === "object") {
1462
+ parsedArgs = rawTc.args;
1463
+ }
1464
+ else if (rawTc.function?.arguments) {
1465
+ const fnArgs = rawTc.function
1466
+ .arguments;
1467
+ if (typeof fnArgs === "string") {
1468
+ try {
1469
+ parsedArgs = JSON.parse(fnArgs);
1470
+ }
1471
+ catch {
1472
+ parsedArgs = { raw: fnArgs };
1473
+ }
1474
+ }
1475
+ else if (typeof fnArgs === "object" && fnArgs !== null) {
1476
+ parsedArgs = fnArgs;
1477
+ }
1478
+ }
1479
+ else if (rawTc.arguments && typeof rawTc.arguments === "object") {
1480
+ parsedArgs = rawTc.arguments;
1481
+ }
1482
+ const toolComp = new ToolExecutionComponent(toolName, callId, parsedArgs);
1483
+ this.chatContainer.addChild(toolComp);
1484
+ if (callId) {
1485
+ pendingTools.set(callId, toolComp);
1486
+ }
1487
+ }
1488
+ }
1489
+ }
1490
+ else if (msg.role === "tool") {
1491
+ const callId = String(msg.metadata?.tool_call_id || "");
1492
+ const toolComp = callId ? pendingTools.get(callId) : undefined;
1493
+ const isError = Boolean(msg.metadata?.is_error);
1494
+ if (toolComp) {
1495
+ toolComp.updateResult(msg.content, isError);
1496
+ pendingTools.delete(callId);
1497
+ }
1498
+ else {
1499
+ const toolName = String(msg.metadata?.tool_name || "tool");
1500
+ const standalone = new ToolExecutionComponent(toolName, callId, {});
1501
+ standalone.updateResult(msg.content, isError);
1502
+ this.chatContainer.addChild(standalone);
1503
+ }
1504
+ }
1505
+ }
1506
+ for (const toolComp of pendingTools.values()) {
1507
+ if (!toolComp.finished) {
1508
+ toolComp.updateResult("(已完成)", false);
1509
+ }
1510
+ }
1511
+ this.transcriptScrollView?.scrollToEnd?.();
1512
+ this.ui.requestRender();
1513
+ }
1514
+ renderPiSessionStats(stats) {
1515
+ let info = `${theme.bold("Session Info")}\n\n`;
1516
+ const sessionName = stats.sessionName || this.footer?.data?.sessionName;
1517
+ if (sessionName) {
1518
+ info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
1519
+ }
1520
+ info += `${theme.fg("dim", "File:")}\n${stats.sessionFile ?? "In-memory"}\n`;
1521
+ info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
1522
+ info += `${theme.bold("Messages")}\n`;
1523
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages ?? 0}\n`;
1524
+ info += `${theme.fg("dim", "User:")} ${stats.userMessages ?? 0}\n`;
1525
+ info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages ?? 0}\n`;
1526
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls ?? 0} calls, ${stats.toolResults ?? 0} results\n\n`;
1527
+ info += `${theme.bold("Tokens")}\n`;
1528
+ const tokens = stats.tokens || {};
1529
+ const input = Number(tokens.input ?? 0);
1530
+ const cacheRead = Number(tokens.cacheRead ?? 0);
1531
+ const cacheWrite = Number(tokens.cacheWrite ?? 0);
1532
+ const output = Number(tokens.output ?? 0);
1533
+ const promptTokens = input + cacheRead + cacheWrite;
1534
+ const total = Number(tokens.total ?? promptTokens + output);
1535
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
1536
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
1537
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
1538
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
1539
+ const written = cacheWrite > 0
1540
+ ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}`
1541
+ : "";
1542
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
1543
+ }
1544
+ info += `${theme.fg("dim", "Output:")} ${output.toLocaleString()}\n`;
1545
+ info += `${theme.fg("dim", "Total:")} ${total.toLocaleString()}\n`;
1546
+ const cost = Number(stats.cost ?? 0);
1547
+ const breakdown = stats.usageBreakdown || [];
1548
+ const cacheWaste = stats.cacheWaste || {};
1549
+ if (cost > 0 || (cacheWaste.missedTokens && cacheWaste.missedTokens > 0)) {
1550
+ info += `\n${theme.bold("Cost")}\n`;
1551
+ info += `${theme.fg("dim", "Total:")} $${cost.toFixed(3)}`;
1552
+ if (breakdown.length > 0) {
1553
+ for (const entry of breakdown) {
1554
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${Number(entry.cost ?? 0).toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens ?? 0)} tokens)`)}`;
1555
+ }
1556
+ }
1557
+ if (cacheWaste.missedTokens > 0) {
1558
+ const missLabel = cacheWaste.missCount === 1
1559
+ ? "1 miss"
1560
+ : `${cacheWaste.missCount} misses`;
1561
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
1562
+ info +=
1563
+ cacheWaste.missedCost >= 0.0001
1564
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
1565
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
1566
+ }
1567
+ }
1568
+ this.chatContainer.addChild(new Spacer(1));
1569
+ this.chatContainer.addChild(new Text(info, 1, 0));
1570
+ this.ui.requestRender();
1571
+ }
1572
+ renderFallbackSessionStats() {
1573
+ const sessionName = this.footer?.data?.sessionName || "default";
1574
+ const model = this.currentModelName;
1575
+ const thinking = this.currentThinkingLevel;
1576
+ const messageCount = this.chatContainer.children.length;
1577
+ const info = [
1578
+ theme.bold("Session Info"),
1579
+ "",
1580
+ ` ${theme.fg("dim", "Name:")} ${sessionName}`,
1581
+ ` ${theme.fg("dim", "Workspace:")} ${this.workspace}`,
1582
+ ` ${theme.fg("dim", "Model:")} ${model} (thinking: ${thinking})`,
1583
+ ` ${theme.fg("dim", "Messages:")} ${messageCount}`,
1584
+ ].join("\n");
1585
+ this.chatContainer.addChild(new Spacer(1));
1586
+ this.chatContainer.addChild(new Text(info, 1, 0));
1587
+ this.ui.requestRender();
1588
+ }
1589
+ // --------------------------------------------------------------------------
1590
+ // 辅助渲染方法
1591
+ // --------------------------------------------------------------------------
1592
+ appendSystemNotice(text) {
1593
+ const notice = new Text(theme.fg("accent", text), 1, 0);
1594
+ this.chatContainer.addChild(notice);
1595
+ this.ui.requestRender();
1596
+ }
1597
+ appendErrorMessage(text) {
1598
+ const errorNotice = new Text(theme.fg("error", `⚠ ${text}`), 1, 0);
1599
+ this.chatContainer.addChild(errorNotice);
1600
+ this.ui.requestRender();
1601
+ }
1602
+ activeStatusIndicator = null;
1603
+ showWorkingStatusIndicator(message = "Working") {
1604
+ this.clearStatusDisplay();
1605
+ const colorFn = (str) => theme.getThinkingBorderColor(this.currentThinkingLevel)(str);
1606
+ const indicator = new WorkingStatusIndicator(this.ui, message, undefined, colorFn);
1607
+ indicator.start();
1608
+ this.activeStatusIndicator = indicator;
1609
+ this.defaultEditor.setWorkingStatusIndicator(indicator);
1610
+ this.footer.update({ isBusy: true });
1611
+ this.ui.requestRender();
1612
+ }
1613
+ updateStatusDisplay(text = "Working") {
1614
+ this.showWorkingStatusIndicator(text);
1615
+ }
1616
+ clearStatusDisplay() {
1617
+ if (this.activeStatusIndicator) {
1618
+ this.activeStatusIndicator.dispose();
1619
+ this.activeStatusIndicator = null;
1620
+ }
1621
+ this.defaultEditor.setWorkingStatusIndicator(undefined);
1622
+ this.statusContainer.clear();
1623
+ this.ui.requestRender();
1624
+ }
1625
+ }