whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,509 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ChatInput — Custom input with bracketed paste + image attachments
4
+ *
5
+ * Replaces ink-text-input with raw stdin handling to fix:
6
+ * - Paste losing content on enter (newlines in paste triggered submit)
7
+ * - Multi-line paste mangled/unformatted
8
+ * - No drag-drop image support
9
+ *
10
+ * Features:
11
+ * - Bracketed paste mode — clean multi-line paste
12
+ * - Drag-drop images — detects image file paths, attaches as chips
13
+ * - Image chips above input like Claude Code: [image1.png] [image2.jpg]
14
+ * - Backspace on empty input removes last image
15
+ * - Slash command menu preserved
16
+ * - Multi-line input with ⎸ continuation markers
17
+ */
18
+ import { useState, useEffect, useRef } from "react";
19
+ import { Box, Text, useInput, useStdin } from "ink";
20
+ import SelectInput from "ink-select-input";
21
+ import { readFileSync, existsSync, statSync } from "fs";
22
+ import { basename, extname } from "path";
23
+ import { colors } from "../shared/Theme.js";
24
+ import { loadKeybindings, bindingToControlChar } from "../services/keybinding-manager.js";
25
+ export const SLASH_COMMANDS = [
26
+ { name: "/help", description: "Show available commands" },
27
+ { name: "/tools", description: "List all tools" },
28
+ { name: "/model", description: "Switch model (Anthropic/Bedrock/Gemini)" },
29
+ { name: "/compact", description: "Compress conversation context" },
30
+ { name: "/save", description: "Save session to disk" },
31
+ { name: "/sessions", description: "List saved sessions" },
32
+ { name: "/resume", description: "Resume a saved session" },
33
+ { name: "/mcp", description: "Server connection status" },
34
+ { name: "/store", description: "Switch active store" },
35
+ { name: "/status", description: "Show session info" },
36
+ { name: "/agents", description: "List available agent types" },
37
+ { name: "/remember", description: "Remember a fact across sessions" },
38
+ { name: "/forget", description: "Forget a remembered fact" },
39
+ { name: "/memory", description: "List all remembered facts" },
40
+ { name: "/mode", description: "Permission mode (default/plan/yolo)" },
41
+ { name: "/thinking", description: "Toggle extended thinking" },
42
+ { name: "/rewind", description: "Rewind conversation to earlier point" },
43
+ { name: "/init", description: "Generate project config (.whale/CLAUDE.md)" },
44
+ { name: "/update", description: "Check for updates & install" },
45
+ { name: "/clear", description: "Clear conversation" },
46
+ { name: "/exit", description: "Exit" },
47
+ ];
48
+ // ── Constants ──
49
+ const IMAGE_EXTENSIONS = {
50
+ ".png": "image/png",
51
+ ".jpg": "image/jpeg",
52
+ ".jpeg": "image/jpeg",
53
+ ".gif": "image/gif",
54
+ ".webp": "image/webp",
55
+ };
56
+ const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
57
+ const MAX_DISPLAY_LINES = 8;
58
+ // ── Helpers ──
59
+ function dividerLine() {
60
+ const w = (process.stdout.columns || 80) - 2;
61
+ return "─".repeat(Math.max(20, w));
62
+ }
63
+ function isImagePath(text) {
64
+ const p = normalizePath(text);
65
+ if (!p || p.includes("\n"))
66
+ return false;
67
+ const ext = extname(p).toLowerCase();
68
+ return ext in IMAGE_EXTENSIONS && existsSync(p);
69
+ }
70
+ /** Normalize a terminal-pasted path (handles escapes, file:// URLs, quotes) */
71
+ function normalizePath(raw) {
72
+ let p = raw.trim();
73
+ // Strip file:// URL prefix + percent-decode
74
+ if (p.startsWith("file://")) {
75
+ p = decodeURIComponent(p.replace(/^file:\/\//, ""));
76
+ }
77
+ // Strip surrounding quotes
78
+ p = p.replace(/^['"]|['"]$/g, "");
79
+ // Unescape backslash-escaped characters (spaces, parens, etc.)
80
+ p = p.replace(/\\(.)/g, "$1");
81
+ return p;
82
+ }
83
+ function isFilePath(text) {
84
+ const p = normalizePath(text);
85
+ if (!p || p.includes("\n"))
86
+ return false;
87
+ // Skip image paths — handled separately
88
+ const ext = extname(p).toLowerCase();
89
+ if (ext in IMAGE_EXTENSIONS)
90
+ return false;
91
+ try {
92
+ return existsSync(p) && statSync(p).isFile();
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function loadImage(filePath) {
99
+ try {
100
+ const p = normalizePath(filePath);
101
+ const ext = extname(p).toLowerCase();
102
+ const mediaType = IMAGE_EXTENSIONS[ext];
103
+ if (!mediaType)
104
+ return null;
105
+ const stat = statSync(p);
106
+ if (stat.size > MAX_IMAGE_SIZE)
107
+ return null;
108
+ const data = readFileSync(p);
109
+ return { path: p, name: basename(p), base64: data.toString("base64"), mediaType };
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ function getCursorLineCol(text, cursor) {
116
+ let pos = 0;
117
+ const lines = text.split("\n");
118
+ for (let i = 0; i < lines.length; i++) {
119
+ if (cursor <= pos + lines[i].length) {
120
+ return { line: i, col: cursor - pos };
121
+ }
122
+ pos += lines[i].length + 1;
123
+ }
124
+ return { line: lines.length - 1, col: lines[lines.length - 1].length };
125
+ }
126
+ // ── Component ──
127
+ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
128
+ const { stdin } = useStdin();
129
+ // Input state — ref for synchronous handler access, state for render
130
+ const inputRef = useRef({ value: "", cursor: 0 });
131
+ const [displayValue, setDisplayValue] = useState("");
132
+ const [displayCursor, setDisplayCursor] = useState(0);
133
+ // Mode & attachments
134
+ const [menuMode, setMenuMode] = useState(false);
135
+ const [menuFilter, setMenuFilter] = useState("");
136
+ const [images, setImages] = useState([]);
137
+ const imagesRef = useRef([]);
138
+ const [files, setFiles] = useState([]);
139
+ const filesRef = useRef([]);
140
+ // Input history (up/down arrow recall)
141
+ const historyRef = useRef([]);
142
+ const historyIndexRef = useRef(-1);
143
+ const savedInputRef = useRef("");
144
+ const MAX_HISTORY = 50;
145
+ // Paste tracking
146
+ const isPasting = useRef(false);
147
+ const pasteBuffer = useRef("");
148
+ // Multi-line paste — stored separately, displayed as chip
149
+ const [pastedText, setPastedText] = useState(null);
150
+ const pastedTextRef = useRef(null);
151
+ // Sync refs
152
+ useEffect(() => { imagesRef.current = images; }, [images]);
153
+ useEffect(() => { filesRef.current = files; }, [files]);
154
+ useEffect(() => { pastedTextRef.current = pastedText; }, [pastedText]);
155
+ // ── Enable bracketed paste mode ──
156
+ useEffect(() => {
157
+ process.stdout.write("\x1b[?2004h");
158
+ return () => { process.stdout.write("\x1b[?2004l"); };
159
+ }, []);
160
+ // ── Update helper ──
161
+ function update(value, cursor) {
162
+ inputRef.current = { value, cursor };
163
+ setDisplayValue(value);
164
+ setDisplayCursor(cursor);
165
+ }
166
+ // ── Process paste content ──
167
+ function processPaste(text) {
168
+ const clean = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
169
+ // Check if ALL non-empty lines are image paths → attach them
170
+ const lines = clean.split("\n").map(l => l.trim()).filter(Boolean);
171
+ if (lines.length > 0 && lines.every(l => isImagePath(l))) {
172
+ const newImages = [];
173
+ for (const line of lines) {
174
+ const img = loadImage(line);
175
+ if (img)
176
+ newImages.push(img);
177
+ }
178
+ if (newImages.length > 0) {
179
+ setImages(prev => [...prev, ...newImages]);
180
+ return;
181
+ }
182
+ }
183
+ // Check for non-image file paths → attach as file chips
184
+ if (lines.length > 0 && lines.length <= 4 && lines.every(l => isFilePath(l))) {
185
+ const newFiles = lines.map(l => {
186
+ const p = normalizePath(l);
187
+ return { path: p, name: basename(p) };
188
+ });
189
+ setFiles(prev => [...prev, ...newFiles]);
190
+ return;
191
+ }
192
+ // Slash command pasted directly (must look like /word, not an absolute path)
193
+ if (clean.startsWith("/") && !clean.includes("\n") && !clean.includes("/", 1) && inputRef.current.value === "") {
194
+ onCommand(clean.trim());
195
+ update("", 0);
196
+ return;
197
+ }
198
+ // Multi-line paste → store as chip, don't spam input
199
+ const pasteLines = clean.split("\n");
200
+ if (pasteLines.length >= 4) {
201
+ setPastedText(clean);
202
+ return;
203
+ }
204
+ // Short paste — insert inline as before
205
+ const { value: val, cursor: cur } = inputRef.current;
206
+ const newValue = val.slice(0, cur) + clean + val.slice(cur);
207
+ update(newValue, cur + clean.length);
208
+ }
209
+ // ── Handle submit ──
210
+ function handleSubmit() {
211
+ const typed = inputRef.current.value.trim();
212
+ const paste = pastedTextRef.current;
213
+ const imgs = imagesRef.current;
214
+ const fls = filesRef.current;
215
+ if (!typed && !paste && imgs.length === 0 && fls.length === 0)
216
+ return;
217
+ if (typed.startsWith("/") && !paste && imgs.length === 0 && fls.length === 0) {
218
+ update("", 0);
219
+ onCommand(typed);
220
+ return;
221
+ }
222
+ // Build file context prefix
223
+ const filePrefix = fls.length > 0
224
+ ? fls.map(f => f.path).join("\n") + "\n\n"
225
+ : "";
226
+ // Combine file prefix + typed text + pasted content
227
+ const body = paste
228
+ ? (typed ? `${typed}\n\n${paste}` : paste)
229
+ : typed;
230
+ const fullText = filePrefix + body;
231
+ // Record in history
232
+ if (fullText.trim()) {
233
+ historyRef.current.push(fullText.trim());
234
+ if (historyRef.current.length > MAX_HISTORY)
235
+ historyRef.current.shift();
236
+ }
237
+ historyIndexRef.current = -1;
238
+ onSubmit(fullText, imgs.length > 0 ? imgs : undefined);
239
+ update("", 0);
240
+ setImages([]);
241
+ setFiles([]);
242
+ setPastedText(null);
243
+ }
244
+ // ── Raw stdin handler ──
245
+ useEffect(() => {
246
+ if (!stdin || disabled || menuMode)
247
+ return;
248
+ const onData = (data) => {
249
+ const str = data.toString("utf-8");
250
+ // ── Bracketed paste detection ──
251
+ if (str.includes("\x1b[200~")) {
252
+ isPasting.current = true;
253
+ let text = str.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
254
+ pasteBuffer.current += text;
255
+ if (str.includes("\x1b[201~")) {
256
+ isPasting.current = false;
257
+ const paste = pasteBuffer.current;
258
+ pasteBuffer.current = "";
259
+ processPaste(paste);
260
+ }
261
+ return;
262
+ }
263
+ if (isPasting.current) {
264
+ if (str.includes("\x1b[201~")) {
265
+ isPasting.current = false;
266
+ pasteBuffer.current += str.replace(/\x1b\[201~/g, "");
267
+ const paste = pasteBuffer.current;
268
+ pasteBuffer.current = "";
269
+ processPaste(paste);
270
+ }
271
+ else {
272
+ pasteBuffer.current += str;
273
+ }
274
+ return;
275
+ }
276
+ // ── Control chars handled by ChatApp (keybinding-aware) ──
277
+ const _kb = loadKeybindings();
278
+ const exitChar = bindingToControlChar(_kb.exit);
279
+ const expandChar = bindingToControlChar(_kb.toggle_expand);
280
+ const thinkingChar = bindingToControlChar(_kb.toggle_thinking);
281
+ if (str === exitChar || str === expandChar || str === thinkingChar)
282
+ return;
283
+ // ── Enter ──
284
+ if (str === "\r" || str === "\n") {
285
+ handleSubmit();
286
+ return;
287
+ }
288
+ // ── Tab ── (no-op in input)
289
+ if (str === "\t")
290
+ return;
291
+ // ── Backspace ──
292
+ if (str === "\x7f" || str === "\b") {
293
+ const { value: val, cursor: cur } = inputRef.current;
294
+ if (cur > 0) {
295
+ update(val.slice(0, cur - 1) + val.slice(cur), cur - 1);
296
+ }
297
+ else if (val === "") {
298
+ // Empty input + backspace → remove paste chip, then files, then images
299
+ if (pastedTextRef.current) {
300
+ setPastedText(null);
301
+ }
302
+ else if (filesRef.current.length > 0) {
303
+ setFiles(prev => prev.slice(0, -1));
304
+ }
305
+ else if (imagesRef.current.length > 0) {
306
+ setImages(prev => prev.slice(0, -1));
307
+ }
308
+ }
309
+ return;
310
+ }
311
+ // ── Clear line (default: Ctrl+U) ──
312
+ if (str === bindingToControlChar(_kb.clear_line)) {
313
+ update("", 0);
314
+ setImages([]);
315
+ setFiles([]);
316
+ setPastedText(null);
317
+ return;
318
+ }
319
+ // ── Delete word (default: Ctrl+W) ──
320
+ if (str === bindingToControlChar(_kb.delete_word)) {
321
+ const { value: val, cursor: cur } = inputRef.current;
322
+ const before = val.slice(0, cur);
323
+ const match = before.match(/\S+\s*$/);
324
+ if (match) {
325
+ const len = match[0].length;
326
+ update(val.slice(0, cur - len) + val.slice(cur), cur - len);
327
+ }
328
+ return;
329
+ }
330
+ // ── Home (default: Ctrl+A) ──
331
+ if (str === bindingToControlChar(_kb.home)) {
332
+ update(inputRef.current.value, 0);
333
+ return;
334
+ }
335
+ // ── Escape sequences ──
336
+ if (str.startsWith("\x1b[")) {
337
+ const { value: val, cursor: cur } = inputRef.current;
338
+ if (str === "\x1b[C") { // Right
339
+ update(val, Math.min(cur + 1, val.length));
340
+ }
341
+ else if (str === "\x1b[D") { // Left
342
+ update(val, Math.max(cur - 1, 0));
343
+ }
344
+ else if (str === "\x1b[H" || str === "\x1b[1~") { // Home
345
+ update(val, 0);
346
+ }
347
+ else if (str === "\x1b[F" || str === "\x1b[4~") { // End
348
+ update(val, val.length);
349
+ }
350
+ else if (str === "\x1b[3~") { // Delete
351
+ if (cur < val.length) {
352
+ update(val.slice(0, cur) + val.slice(cur + 1), cur);
353
+ }
354
+ }
355
+ else if (str === "\x1b[A" || str === "\x1b[B") { // Up/Down
356
+ const lines = val.split("\n");
357
+ if (lines.length > 1) {
358
+ // Multi-line: navigate lines
359
+ const { line: curLine, col: curCol } = getCursorLineCol(val, cur);
360
+ const targetLine = str === "\x1b[A"
361
+ ? Math.max(0, curLine - 1)
362
+ : Math.min(lines.length - 1, curLine + 1);
363
+ if (targetLine !== curLine) {
364
+ const targetCol = Math.min(curCol, lines[targetLine].length);
365
+ let newCursor = 0;
366
+ for (let i = 0; i < targetLine; i++)
367
+ newCursor += lines[i].length + 1;
368
+ newCursor += targetCol;
369
+ update(val, newCursor);
370
+ }
371
+ }
372
+ else if (str === "\x1b[A") {
373
+ // Up arrow — history recall
374
+ if (historyRef.current.length === 0) { /* no history */ }
375
+ else {
376
+ if (historyIndexRef.current === -1)
377
+ savedInputRef.current = val;
378
+ const newIdx = Math.min(historyIndexRef.current + 1, historyRef.current.length - 1);
379
+ historyIndexRef.current = newIdx;
380
+ const hist = historyRef.current[historyRef.current.length - 1 - newIdx];
381
+ update(hist, hist.length);
382
+ }
383
+ }
384
+ else {
385
+ // Down arrow — forward in history
386
+ if (historyIndexRef.current <= -1) { /* already at current */ }
387
+ else {
388
+ const newIdx = historyIndexRef.current - 1;
389
+ historyIndexRef.current = newIdx;
390
+ if (newIdx < 0) {
391
+ update(savedInputRef.current, savedInputRef.current.length);
392
+ }
393
+ else {
394
+ const hist = historyRef.current[historyRef.current.length - 1 - newIdx];
395
+ update(hist, hist.length);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ // Ignore unrecognized escape sequences
401
+ return;
402
+ }
403
+ // ── Standalone escape — clear input if non-empty ──
404
+ if (str === "\x1b") {
405
+ const { value } = inputRef.current;
406
+ if (value || filesRef.current.length > 0) {
407
+ update("", 0);
408
+ setImages([]);
409
+ setFiles([]);
410
+ setPastedText(null);
411
+ }
412
+ return;
413
+ }
414
+ // ── Multi-character non-escape = paste without brackets ──
415
+ if (str.length > 1 && !str.startsWith("\x1b")) {
416
+ const codePoints = [...str];
417
+ if (codePoints.length === 1) {
418
+ // Single code point (emoji etc.)
419
+ const { value: val, cursor: cur } = inputRef.current;
420
+ update(val.slice(0, cur) + str + val.slice(cur), cur + str.length);
421
+ }
422
+ else {
423
+ processPaste(str);
424
+ }
425
+ return;
426
+ }
427
+ // ── Single printable character ──
428
+ if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
429
+ const { value: val, cursor: cur } = inputRef.current;
430
+ // Slash menu trigger
431
+ if (str === "/" && val === "") {
432
+ setMenuMode(true);
433
+ setMenuFilter("");
434
+ return;
435
+ }
436
+ update(val.slice(0, cur) + str + val.slice(cur), cur + 1);
437
+ }
438
+ };
439
+ stdin.on("data", onData);
440
+ return () => { stdin.off("data", onData); };
441
+ }, [stdin, disabled, menuMode]); // eslint-disable-line react-hooks/exhaustive-deps
442
+ // ── Menu input — filter + dismiss (uses Ink's useInput for SelectInput compatibility) ──
443
+ useInput((input, key) => {
444
+ if (!menuMode || disabled)
445
+ return;
446
+ if (key.escape) {
447
+ setMenuMode(false);
448
+ setMenuFilter("");
449
+ return;
450
+ }
451
+ if (key.backspace || key.delete) {
452
+ setMenuFilter(prev => {
453
+ if (prev.length > 0)
454
+ return prev.slice(0, -1);
455
+ setMenuMode(false);
456
+ return "";
457
+ });
458
+ return;
459
+ }
460
+ // Printable character → append to filter
461
+ if (input && !key.ctrl && !key.meta && !key.return && !key.tab) {
462
+ setMenuFilter(prev => prev + input);
463
+ }
464
+ }, { isActive: menuMode });
465
+ const handleMenuSelect = (item) => {
466
+ setMenuMode(false);
467
+ setMenuFilter("");
468
+ update("", 0);
469
+ onCommand(item.value);
470
+ };
471
+ // ── Render ──
472
+ const divider = dividerLine();
473
+ // Disabled — minimal divider
474
+ if (disabled) {
475
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: divider })] }));
476
+ }
477
+ // Slash command menu
478
+ if (menuMode) {
479
+ const q = menuFilter.toLowerCase();
480
+ const filtered = q
481
+ ? SLASH_COMMANDS.filter(c => c.name.includes(q) || c.description.toLowerCase().includes(q))
482
+ : SLASH_COMMANDS;
483
+ const items = filtered.map((c) => ({
484
+ label: c.name,
485
+ value: c.name,
486
+ }));
487
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: divider }), _jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "/ " }), menuFilter ? (_jsxs(Text, { children: [menuFilter, _jsx(Text, { inverse: true, children: " " })] })) : (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: " type to filter" })] }))] }), items.length > 0 ? (_jsx(SelectInput, { items: items, onSelect: handleMenuSelect, indicatorComponent: ({ isSelected = false }) => (_jsxs(Text, { color: isSelected ? colors.brand : colors.quaternary, children: [isSelected ? "›" : " ", " "] })), itemComponent: ({ isSelected = false, label = "" }) => {
488
+ const cmd = SLASH_COMMANDS.find((c) => c.name === label);
489
+ return (_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? colors.brand : colors.secondary, bold: isSelected, children: label }), _jsxs(Text, { color: colors.tertiary, children: [" ", cmd?.description] })] }));
490
+ } })) : (_jsx(Text, { color: colors.tertiary, children: " no matching commands" })), _jsx(Text, { color: colors.quaternary, children: " esc to dismiss" })] }));
491
+ }
492
+ // ── Normal input rendering ──
493
+ const lines = displayValue.split("\n");
494
+ const { line: cursorLine, col: cursorCol } = displayValue
495
+ ? getCursorLineCol(displayValue, displayCursor)
496
+ : { line: 0, col: 0 };
497
+ // Truncate display for very long pastes
498
+ const isTruncated = lines.length > MAX_DISPLAY_LINES;
499
+ const visibleLines = isTruncated
500
+ ? [...lines.slice(0, MAX_DISPLAY_LINES - 1), `… ${lines.length - MAX_DISPLAY_LINES + 1} more lines`]
501
+ : lines;
502
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), (images.length > 0 || files.length > 0 || pastedText) && (_jsxs(Box, { children: [_jsx(Text, { children: " " }), images.map((img, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: img.name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, `img-${i}`))), files.map((f, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.purple, children: "[" }), _jsx(Text, { color: colors.secondary, children: f.name }), _jsx(Text, { color: colors.purple, children: "]" }), _jsx(Text, { children: " " })] }, `file-${i}`))), pastedText && (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsxs(Text, { color: colors.secondary, children: ["pasted ", pastedText.split("\n").length, " lines"] }), _jsx(Text, { color: colors.indigo, children: "]" })] }))] })), _jsx(Text, { color: colors.separator, children: divider }), _jsx(Text, { children: " " }), lines.length <= 1 ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), !displayValue ? (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: `Message ${agentName || "whale"}, or type / for commands` })] })) : (_jsxs(Text, { children: [displayValue.slice(0, displayCursor), _jsx(Text, { inverse: true, children: displayCursor < displayValue.length ? displayValue[displayCursor] : " " }), displayCursor < displayValue.length ? displayValue.slice(displayCursor + 1) : ""] }))] })) : (
503
+ /* Multi-line input */
504
+ _jsx(Box, { flexDirection: "column", children: visibleLines.map((line, i) => {
505
+ const isRealLine = !isTruncated || i < MAX_DISPLAY_LINES - 1;
506
+ const isCursorOnLine = isRealLine && i === cursorLine;
507
+ return (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: i === 0 ? "❯ " : "⎸ " }), isCursorOnLine ? (_jsxs(Text, { children: [line.slice(0, cursorCol), _jsx(Text, { inverse: true, children: cursorCol < line.length ? line[cursorCol] : " " }), cursorCol < line.length ? line.slice(cursorCol + 1) : ""] })) : (_jsx(Text, { color: isRealLine ? undefined : colors.tertiary, children: line }))] }, i));
508
+ }) }))] }));
509
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * MarkdownText — renders markdown as ANSI-styled terminal output
3
+ */
4
+ interface MarkdownTextProps {
5
+ text: string;
6
+ /** When true, closes incomplete fences for safe mid-stream display. Default: false. */
7
+ streaming?: boolean;
8
+ }
9
+ export declare function MarkdownText({ text, streaming }: MarkdownTextProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * MarkdownText — renders markdown as ANSI-styled terminal output
4
+ */
5
+ import { useMemo } from "react";
6
+ import { Text } from "ink";
7
+ import { renderMarkdown } from "../shared/markdown.js";
8
+ export function MarkdownText({ text, streaming = false }) {
9
+ const rendered = useMemo(() => {
10
+ if (!text)
11
+ return "";
12
+ try {
13
+ return renderMarkdown(text, streaming);
14
+ }
15
+ catch {
16
+ return text;
17
+ }
18
+ }, [text, streaming]);
19
+ return _jsx(Text, { children: rendered });
20
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * MessageList — types and CompletedMessage component
3
+ *
4
+ * CompletedMessage is React.memo'd to prevent re-renders during streaming.
5
+ * Telemetry footer: tokens, cost estimate, tool count.
6
+ * Minimal Box usage — Text-first for reliable rendering.
7
+ */
8
+ import React from "react";
9
+ import { type CompletedSubagentInfo } from "./SubagentPanel.js";
10
+ export interface ToolCall {
11
+ name: string;
12
+ status: "running" | "success" | "error";
13
+ result?: string;
14
+ input?: Record<string, unknown>;
15
+ durationMs?: number;
16
+ }
17
+ export interface ChatMessage {
18
+ role: "user" | "assistant";
19
+ text: string;
20
+ images?: string[];
21
+ toolCalls?: ToolCall[];
22
+ completedSubagents?: CompletedSubagentInfo[];
23
+ usage?: {
24
+ input_tokens: number;
25
+ output_tokens: number;
26
+ thinking_tokens?: number;
27
+ model?: string;
28
+ costUsd?: number;
29
+ cache_read_tokens?: number;
30
+ cache_creation_tokens?: number;
31
+ };
32
+ }
33
+ export declare const CompletedMessage: React.NamedExoticComponent<{
34
+ msg: ChatMessage;
35
+ index: number;
36
+ toolsExpanded: boolean;
37
+ }>;
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * MessageList — types and CompletedMessage component
4
+ *
5
+ * CompletedMessage is React.memo'd to prevent re-renders during streaming.
6
+ * Telemetry footer: tokens, cost estimate, tool count.
7
+ * Minimal Box usage — Text-first for reliable rendering.
8
+ */
9
+ import React, { useMemo } from "react";
10
+ import { Box, Text } from "ink";
11
+ import { ToolIndicator } from "./ToolIndicator.js";
12
+ import { MarkdownText } from "./MarkdownText.js";
13
+ import { CompletedSubagentTree } from "./SubagentPanel.js";
14
+ import { colors } from "../shared/Theme.js";
15
+ import { contentWidth } from "../shared/markdown.js";
16
+ import { MODEL_PRICING } from "../../shared/agent-core.js";
17
+ // ============================================================================
18
+ // HELPERS
19
+ // ============================================================================
20
+ function estimateCost(input, output, model, precomputedCost) {
21
+ // Use precomputed cost if available (accurate for all providers)
22
+ let cost = precomputedCost;
23
+ if (cost == null) {
24
+ // Fall back to model-specific pricing, then Sonnet as default
25
+ const pricing = (model && MODEL_PRICING[model])
26
+ || MODEL_PRICING[Object.keys(MODEL_PRICING).find(k => model?.startsWith(k)) ?? ""]
27
+ || MODEL_PRICING["claude-sonnet-4-6"];
28
+ cost = (input * pricing.inputPer1M + output * pricing.outputPer1M) / 1_000_000;
29
+ }
30
+ if (cost < 0.001)
31
+ return "<$0.001";
32
+ if (cost < 0.01)
33
+ return `$${cost.toFixed(4)}`;
34
+ return `$${cost.toFixed(3)}`;
35
+ }
36
+ function formatTokens(n) {
37
+ if (n < 1000)
38
+ return String(n);
39
+ if (n < 10000)
40
+ return `${(n / 1000).toFixed(1)}k`;
41
+ return `${Math.round(n / 1000)}k`;
42
+ }
43
+ function totalToolDuration(toolCalls) {
44
+ return toolCalls.reduce((sum, tc) => sum + (tc.durationMs || 0), 0);
45
+ }
46
+ function formatMs(ms) {
47
+ if (ms < 1000)
48
+ return `${ms}ms`;
49
+ if (ms < 10000)
50
+ return `${(ms / 1000).toFixed(1)}s`;
51
+ return `${Math.round(ms / 1000)}s`;
52
+ }
53
+ /**
54
+ * Group consecutive tool calls with the same name and input into groups.
55
+ * Identical consecutive calls collapse into one entry with count > 1.
56
+ * Caches previous key to avoid redundant JSON.stringify calls.
57
+ */
58
+ function groupConsecutiveTools(toolCalls) {
59
+ const groups = [];
60
+ let prevKey = "";
61
+ for (const tc of toolCalls) {
62
+ const key = tc.name + "|" + tc.status + "|" + JSON.stringify(tc.input || {});
63
+ if (key === prevKey && groups.length > 0) {
64
+ groups[groups.length - 1].count++;
65
+ }
66
+ else {
67
+ groups.push({ tool: tc, count: 1 });
68
+ prevKey = key;
69
+ }
70
+ }
71
+ return groups;
72
+ }
73
+ // ============================================================================
74
+ // COMPLETED MESSAGE — memoized, never re-renders during streaming
75
+ // ============================================================================
76
+ export const CompletedMessage = React.memo(function CompletedMessage({ msg, index, toolsExpanded }) {
77
+ const cw = Math.max(20, contentWidth());
78
+ const toolGroups = useMemo(() => msg.toolCalls ? groupConsecutiveTools(msg.toolCalls) : [], [msg.toolCalls]);
79
+ return (_jsxs(Box, { flexDirection: "column", children: [msg.role === "user" && index > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: "─".repeat(cw) })] })), msg.role === "user" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), msg.images && msg.images.length > 0 && (_jsx(Box, { marginLeft: 2, children: msg.images.map((name, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, i))) })), _jsxs(Text, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), _jsx(Text, { color: colors.user, children: msg.text })] })] })) : (_jsxs(Box, { flexDirection: "column", children: [toolGroups.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: toolGroups.map((group, j) => (_jsx(ToolIndicator, { id: `done-${index}-${j}`, name: group.tool.name, status: group.tool.status, result: group.tool.result, input: group.tool.input, durationMs: group.tool.durationMs, expanded: toolsExpanded, count: group.count }, j))) })), msg.completedSubagents && msg.completedSubagents.length > 0 && (_jsx(CompletedSubagentTree, { agents: msg.completedSubagents })), msg.text && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: " " }), _jsx(MarkdownText, { text: msg.text })] })), msg.usage && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsxs(Text, { color: colors.quaternary, children: [formatTokens(msg.usage.input_tokens), _jsx(Text, { color: colors.indigo, children: "\u2191" }), " ", formatTokens(msg.usage.output_tokens), _jsx(Text, { color: colors.purple, children: "\u2193" }), msg.usage.thinking_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.thinking_tokens), _jsx(Text, { color: colors.warning, children: "T" })] })) : null, msg.usage.cache_read_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.cache_read_tokens), _jsx(Text, { color: colors.success, children: "C" })] })) : null] }), _jsxs(Text, { color: colors.quaternary, children: [" ", estimateCost(msg.usage.input_tokens, msg.usage.output_tokens, msg.usage.model, msg.usage.costUsd)] }), msg.toolCalls && msg.toolCalls.length > 0 ? (_jsxs(Text, { color: colors.quaternary, children: [" ", msg.toolCalls.length, " tool", msg.toolCalls.length !== 1 ? "s" : "", " ", formatMs(totalToolDuration(msg.toolCalls))] })) : null] })] }))] }))] }));
80
+ });