@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Formatter — Clean terminal output for Tarang events.
3
+ *
4
+ * Style reference: Claude Code screenshot
5
+ * ◐ Cyan spinner + cyan text for working/thinking
6
+ * ✓ Green checkmark + green path for file changes
7
+ * ✗ Red for errors
8
+ * White for content and summaries
9
+ */
10
+
11
+ import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
12
+ import { formatMessageWindow } from '../core/rate-limit-display.mjs';
13
+ import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
14
+
15
+ const RESET = '\x1b[0m';
16
+ const BOLD = '\x1b[1m';
17
+ const DIM = '\x1b[2m';
18
+ const RED = '\x1b[31m';
19
+ const GREEN = '\x1b[32m';
20
+ const YELLOW = '\x1b[33m';
21
+ const CYAN = '\x1b[36m';
22
+
23
+ // Spinner frames
24
+ const SPINNER = ['◐', '◓', '◑', '◒'];
25
+
26
+ export class EventFormatter {
27
+ constructor({ verbose = false } = {}) {
28
+ this.verbose = verbose;
29
+ this.toolCount = 0;
30
+ this.toolCalls = [];
31
+ this.changes = [];
32
+ this.phases = new Map();
33
+ this.sessionInfo = null;
34
+ this.tokenCount = { input: 0, output: 0 };
35
+ this._spinnerFrame = 0;
36
+ this._hasContent = false;
37
+ this._lastContent = '';
38
+ this._completed = false;
39
+ this._seenCallIds = new Set();
40
+ this._lastThinking = '';
41
+ this._lastBlock = null;
42
+ }
43
+
44
+ render(event) {
45
+ const { type, data } = event;
46
+ switch (type) {
47
+ case 'session_info':
48
+ this.sessionInfo = data;
49
+ if (this.verbose) {
50
+ process.stderr.write(`${DIM} [session] ${data.session_id || ''}${RESET}\n`);
51
+ }
52
+ return true;
53
+ case 'status':
54
+ this._status(data);
55
+ return true;
56
+ case 'thinking':
57
+ this._thinking(data);
58
+ return true;
59
+ case 'content':
60
+ case 'content_partial':
61
+ this._content(data);
62
+ return true;
63
+ case 'tool_call':
64
+ case 'tool_request':
65
+ this._toolCall(data);
66
+ return true;
67
+ case 'tool_done':
68
+ this._toolDone(data);
69
+ return true;
70
+ case 'complete':
71
+ if (data?.rate_limit) {
72
+ this.sessionInfo = { ...(this.sessionInfo || {}), rate_limit: data.rate_limit };
73
+ }
74
+ this._complete(data);
75
+ return true;
76
+ case 'error':
77
+ this._error(data);
78
+ return true;
79
+ case 'plan':
80
+ this._plan(data);
81
+ return true;
82
+ case 'phase_start':
83
+ this._phaseStart(data);
84
+ return true;
85
+ case 'phase_update':
86
+ this._phaseUpdate(data);
87
+ return true;
88
+ case 'phase_summary':
89
+ this._phaseSummary(data);
90
+ return true;
91
+ case 'change':
92
+ this._change(data);
93
+ return true;
94
+ case 'worker_update':
95
+ case 'worker_start':
96
+ case 'worker_done':
97
+ this._workerEvent(type, data);
98
+ return true;
99
+ case 'delegation':
100
+ this._delegation(data);
101
+ return true;
102
+ case 'cancelled':
103
+ this._boundary('status', { compactSame: true });
104
+ process.stderr.write(`${YELLOW} Cancelled${data?.reason ? ': ' + data.reason : ''}${RESET}\n`);
105
+ this._lastBlock = 'status';
106
+ return true;
107
+ case 'paused':
108
+ this._boundary('status', { compactSame: true });
109
+ process.stderr.write(`${CYAN} Paused${RESET}\n`);
110
+ this._lastBlock = 'status';
111
+ return true;
112
+ case 'resumed':
113
+ this._boundary('status', { compactSame: true });
114
+ process.stderr.write(`${GREEN} Resumed${RESET}\n`);
115
+ this._lastBlock = 'status';
116
+ return true;
117
+ case 'pause_instruction':
118
+ this._pauseInstruction(data);
119
+ return true;
120
+ default:
121
+ if (this.verbose) {
122
+ process.stderr.write(`${DIM} [${type}] ${JSON.stringify(data).slice(0, 100)}${RESET}\n`);
123
+ }
124
+ return false;
125
+ }
126
+ }
127
+
128
+ _spinner() {
129
+ const frame = SPINNER[this._spinnerFrame % SPINNER.length];
130
+ this._spinnerFrame++;
131
+ return `${CYAN}${frame}${RESET}`;
132
+ }
133
+
134
+ _boundary(nextBlock, { compactSame = false } = {}) {
135
+ if (!this._lastBlock) return;
136
+ if (compactSame && this._lastBlock === nextBlock) return;
137
+
138
+ const mode = String(process.env.KEPLER_BLOCK_SEPARATOR || 'space').toLowerCase();
139
+ if (mode === 'off' || mode === 'none') return;
140
+ if (mode === 'dotted' || mode === 'dots') {
141
+ const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
142
+ process.stderr.write(` ${DIM}${'·'.repeat(Math.min(44, cols - 4))}${RESET}\n`);
143
+ return;
144
+ }
145
+
146
+ process.stderr.write('\n');
147
+ }
148
+
149
+ _status(data) {
150
+ const msg = data?.message || '';
151
+ // Skip noisy per-turn statuses. Backend emits "Creating agent..." and
152
+ // "Task type: ..." on every SSE turn (v3_sse.py:566), not just the first —
153
+ // repeating them clutters the transcript.
154
+ if (!msg || msg === 'Agent started') return;
155
+ if (msg.startsWith('Creating agent') || msg.startsWith('Task type:')) return;
156
+ this._boundary('status', { compactSame: true });
157
+ process.stderr.write(` ${this._spinner()} ${CYAN}${msg}${RESET}\n`);
158
+ this._lastBlock = 'status';
159
+ }
160
+
161
+ _thinking(data) {
162
+ if (!this.verbose) return;
163
+
164
+ const text = data?.message || data?.text || '';
165
+ if (!text || text === this._lastThinking) return;
166
+ this._lastThinking = text;
167
+
168
+ // Skip generic "Processing (iteration N)..." — too noisy
169
+ if (text.startsWith('Processing')) return;
170
+
171
+ this._boundary('thinking');
172
+ const clipped = text.length > 200 ? `${text.slice(0, 198)} …` : text;
173
+ process.stderr.write(` ${this._spinner()} ${CYAN}Thinking · ${clipped}${RESET}\n`);
174
+ this._lastBlock = 'thinking';
175
+ }
176
+
177
+ _content(data) {
178
+ const text = data?.text || '';
179
+ if (!text) return;
180
+
181
+ // Deduplicate exact same content (CONTENT event may repeat)
182
+ if (text === this._lastContent) return;
183
+ this._lastContent = text;
184
+
185
+ this._boundary('content');
186
+ this._hasContent = true;
187
+
188
+ // Render content with 2-space indent
189
+ const lines = text.split('\n');
190
+ for (const line of lines) {
191
+ process.stdout.write(` ${line}\n`);
192
+ }
193
+ this._lastBlock = 'content';
194
+ }
195
+
196
+ _toolCall(data) {
197
+ const callId = data?.call_id;
198
+ const tool = data?.tool || 'unknown';
199
+ const args = data?.args || {};
200
+
201
+ // Deduplicate: agent event + bridge event both fire
202
+ if (callId) {
203
+ if (this._seenCallIds.has(callId)) return;
204
+ this._seenCallIds.add(callId);
205
+ } else {
206
+ // Agent event (no call_id) — skip if bridge event follows
207
+ // Use tool+args as dedup key
208
+ const key = `${tool}:${JSON.stringify(args)}`;
209
+ if (this._seenCallIds.has(key)) return;
210
+ this._seenCallIds.add(key);
211
+ }
212
+
213
+ this.toolCount++;
214
+ this._boundary('tool', { compactSame: true });
215
+
216
+ const label = toolDisplayLabel(tool);
217
+ const summary = toolDisplaySummary(tool, args);
218
+ if (tool === 'shell' && summary) {
219
+ process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${label}${RESET}\n`);
220
+ for (const line of wrapShellSummary(summary, 100)) {
221
+ process.stderr.write(` ${DIM} ${line}${RESET}\n`);
222
+ }
223
+ this.toolCalls.push({ name: tool, callId, startTime: Date.now() });
224
+ this._lastBlock = 'tool';
225
+ return;
226
+ }
227
+ const detail = summary ? `${DIM}${summary}${RESET}` : '';
228
+ process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${label}${RESET}${detail ? ` ${detail}` : ''}\n`);
229
+
230
+ this.toolCalls.push({ name: tool, callId, startTime: Date.now() });
231
+ this._lastBlock = 'tool';
232
+ }
233
+
234
+ _toolDone(data) {
235
+ const tool = data?.tool || '';
236
+ const success = data?.success !== false;
237
+ const durationMs = data?.duration_ms;
238
+
239
+ if (this.verbose) {
240
+ const dur = durationMs ? ` (${durationMs}ms)` : '';
241
+ process.stderr.write(` ${GREEN}✓${RESET} ${tool} done${dur}\n`);
242
+ }
243
+
244
+ // Show file modifications as green checkmarks
245
+ if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
246
+ const path = data?.result?.file_path || data?.args?.file_path || '';
247
+ if (path) {
248
+ const action = tool === 'edit_file' ? 'Modified' : tool === 'write_project' ? 'Created' : 'Written';
249
+ process.stderr.write(` ${GREEN}✓ ${action} ${path}${RESET}\n`);
250
+ this.changes.push({ path, action });
251
+ }
252
+ }
253
+
254
+ // Show validation results
255
+ if (tool === 'validate_build' || tool === 'lint_check' || tool === 'validate_file') {
256
+ if (success) {
257
+ const label = tool === 'validate_build' ? 'Build passed' :
258
+ tool === 'lint_check' ? 'Lint check passed' :
259
+ 'File validated';
260
+ process.stderr.write(` ${GREEN}✓ ${label}${RESET}\n`);
261
+ } else {
262
+ const msg = data?.result?.error || data?.result?.stderr || 'Failed';
263
+ process.stderr.write(` ${RED}✗ ${tool.replace('_', ' ')} failed: ${msg.slice(0, 100)}${RESET}\n`);
264
+ }
265
+ }
266
+
267
+ // Show shell command results (if verbose or if failed)
268
+ if (tool === 'shell' && !success) {
269
+ const stderr = data?.result?.stderr || '';
270
+ if (stderr) {
271
+ process.stderr.write(` ${RED}✗ Command failed: ${stderr.slice(0, 100)}${RESET}\n`);
272
+ }
273
+ }
274
+ }
275
+
276
+ _plan(data) {
277
+ const milestones = data?.milestones || [];
278
+ if (milestones.length === 0) return;
279
+ this._boundary('plan');
280
+ process.stderr.write(` ${BOLD}Plan${RESET}\n`);
281
+ for (const m of milestones) {
282
+ const icon = m.status === 'completed' ? `${GREEN}✓${RESET}` :
283
+ m.status === 'started' ? `${CYAN}◐${RESET}` :
284
+ `${DIM}○${RESET}`;
285
+ process.stderr.write(` ${icon} ${m.name}\n`);
286
+ }
287
+ this._lastBlock = 'plan';
288
+ }
289
+
290
+ _phaseStart(data) {
291
+ const phase = data?.phase || data?.stage_name || '';
292
+ if (phase && phase !== 'undefined') {
293
+ process.stderr.write(`\n ${this._spinner()} ${CYAN}${BOLD}${phase}${RESET}\n`);
294
+ }
295
+ }
296
+
297
+ _phaseUpdate(data) {
298
+ const phase = data?.phase || data?.stage_name || '';
299
+ const status = data?.status || '';
300
+ if (phase && phase !== 'undefined') {
301
+ this.phases.set(phase, status);
302
+ process.stderr.write(`\n ${this._spinner()} ${CYAN}${BOLD}${phase}${RESET}\n`);
303
+ }
304
+ }
305
+
306
+ _phaseSummary(data) {
307
+ if (data?.summary) {
308
+ process.stderr.write(` ${GREEN}✓${RESET} ${data.summary.slice(0, 200)}\n`);
309
+ }
310
+ }
311
+
312
+ _workerEvent(type, data) {
313
+ const worker = data?.worker || data?.name || '';
314
+ const status = data?.status || '';
315
+ if (type === 'worker_start') {
316
+ process.stderr.write(` ${this._spinner()} ${CYAN}${worker} starting${RESET}\n`);
317
+ } else if (type === 'worker_done') {
318
+ process.stderr.write(` ${GREEN}✓${RESET} ${worker} done\n`);
319
+ } else {
320
+ process.stderr.write(` ${this._spinner()} ${CYAN}${worker}: ${status}${RESET}\n`);
321
+ }
322
+ }
323
+
324
+ _delegation(data) {
325
+ const from = data?.from || '';
326
+ const to = data?.to || '';
327
+ const instruction = data?.instruction || '';
328
+ process.stderr.write(` ${CYAN}${from} → ${to}${RESET}${instruction ? ': ' + instruction : ''}\n`);
329
+ }
330
+
331
+ _pauseInstruction(data) {
332
+ const instruction = data?.instruction || '';
333
+ if (instruction) {
334
+ process.stderr.write(` ${YELLOW}Pause instruction: ${instruction}${RESET}\n`);
335
+ }
336
+ }
337
+
338
+ _change(data) {
339
+ const icon = data?.type === 'create' ? `${GREEN}+${RESET}` : `${GREEN}~${RESET}`;
340
+ process.stderr.write(` ${icon} ${GREEN}${data?.path || ''}${RESET}\n`);
341
+ this.changes.push(data);
342
+ }
343
+
344
+ _error(data) {
345
+ const guidance = formatAgentErrorGuidance(data || {});
346
+ process.stderr.write(`\n ${RED}✗ ${guidance.title}${RESET}\n`);
347
+ for (const line of guidance.lines) {
348
+ process.stderr.write(` ${DIM}${line}${RESET}\n`);
349
+ }
350
+ if (guidance.meta.length) {
351
+ process.stderr.write(` ${DIM}${guidance.meta.join(' · ')}${RESET}\n`);
352
+ }
353
+ }
354
+
355
+ _complete(data) {
356
+ // Only show once
357
+ if (this._completed) return;
358
+ this._completed = true;
359
+
360
+ const summary = data?.summary || '';
361
+ const duration = data?.duration_s ? `${Number(data.duration_s).toFixed(1)}s` : '';
362
+ const tools = this.toolCount || data?.tool_calls || 0;
363
+ const changeCount = data?.changes || this.changes.length || 0;
364
+
365
+ // Summary line
366
+ const parts = [];
367
+ if (duration) parts.push(duration);
368
+ if (tools > 0) parts.push(`${tools} tool calls`);
369
+ if (changeCount > 0) parts.push(`${changeCount} changes`);
370
+
371
+ const stats = parts.length > 0 ? ` (${parts.join(', ')})` : '';
372
+ if (summary) {
373
+ process.stderr.write(`\n ${GREEN}✓ ${summary}${stats}${RESET}\n`);
374
+ } else {
375
+ process.stderr.write(`\n ${GREEN}✓ Done${stats}${RESET}\n`);
376
+ }
377
+
378
+ const rateLimitLine = formatMessageWindow(data?.rate_limit || this.sessionInfo?.rate_limit);
379
+ if (rateLimitLine) {
380
+ process.stderr.write(` ${DIM}Messages: ${rateLimitLine}${RESET}\n`);
381
+ }
382
+
383
+ // Token usage if available
384
+ const usage = data?.usage;
385
+ if (usage && (usage.input_tokens || usage.total_tokens)) {
386
+ const inp = usage.input_tokens || 0;
387
+ const out = usage.output_tokens || 0;
388
+ process.stderr.write(` ${DIM}Tokens: ${inp.toLocaleString()} in / ${out.toLocaleString()} out${RESET}\n`);
389
+ }
390
+ }
391
+ }
392
+
393
+ function wrapShellSummary(command, width = 100) {
394
+ const max = Math.max(32, Number(width) || 100);
395
+ const text = String(command || '');
396
+ const lines = [];
397
+ let line = '';
398
+ for (const token of text.match(/\S+\s*/g) || [text]) {
399
+ const next = line + token;
400
+ if (line && next.trimEnd().length > max) {
401
+ lines.push(line.trimEnd());
402
+ line = token;
403
+ continue;
404
+ }
405
+ line = next;
406
+ }
407
+ if (line.trimEnd()) lines.push(line.trimEnd());
408
+ return lines.length ? lines : ['(empty command)'];
409
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Brand icon registry — Mission Control (PRD-055 §4.2).
3
+ *
4
+ * Every CLI surface that prints a tool, phase, or status uses these icons.
5
+ * Each icon has a Unicode form and an ASCII fallback used when the terminal
6
+ * advertises no Unicode support (older Windows shells, locked-down CI).
7
+ *
8
+ * import { icons, icon } from './icons.mjs';
9
+ * process.stdout.write(`${icons.subAgent} explore ${icons.pass} pass`);
10
+ *
11
+ * Or by token (for data-driven tool display):
12
+ *
13
+ * icon('shell') // ⚙️
14
+ * icon('edit_file') // 🛠️
15
+ */
16
+
17
+ import { term } from './term.mjs';
18
+
19
+ // ── Canonical icons ──────────────────────────────────────────────────────
20
+
21
+ // Note: cannot Object.freeze() this — it is the target of a Proxy whose
22
+ // `get` returns a rendered string instead of the raw record, which violates
23
+ // the Proxy invariant for frozen targets.
24
+ const ICONS = ({
25
+ // Brand
26
+ brand: { utf: '✦', ascii: '*' },
27
+ orbit: { utf: '◯', ascii: 'O' },
28
+
29
+ // Tool families
30
+ subAgent: { utf: '🛰️', ascii: '~>' }, // explore, plan, verify, debug, refactor
31
+ search: { utf: '🔭', ascii: '?' }, // search_code, grep, read_file, list_files
32
+ write: { utf: '🛠️', ascii: '+' }, // write_file, edit_file, write_project
33
+ shell: { utf: '⚙️', ascii: '$' }, // shell, run_tests, validators
34
+ network: { utf: '🌐', ascii: '@' }, // WebFetch, MCP network calls
35
+
36
+ // State
37
+ pass: { utf: '✅', ascii: 'OK' },
38
+ warn: { utf: '⚠️', ascii: '!' },
39
+ fail: { utf: '❌', ascii: 'X' },
40
+ pending: { utf: '◔', ascii: '.' },
41
+
42
+ // Workflow
43
+ approve: { utf: '✔', ascii: 'Y' },
44
+ reject: { utf: '✘', ascii: 'N' },
45
+ pause: { utf: '⏸', ascii: '||' },
46
+ resume: { utf: '▶', ascii: '>' },
47
+ });
48
+
49
+ // Internal table that the lookup functions consult directly. This stays
50
+ // frozen because we never expose it through a Proxy.
51
+ const ICON_RECORDS = Object.freeze({ ...ICONS });
52
+
53
+ // ── Tool → icon mapping ──────────────────────────────────────────────────
54
+ // One source of truth so tool display, status bar, and mission report
55
+ // agree. Unknown tools fall through to a generic "tool" icon.
56
+
57
+ const TOOL_ICON = Object.freeze({
58
+ // Sub-agents
59
+ explore: 'subAgent',
60
+ plan: 'subAgent',
61
+ verify: 'subAgent',
62
+ debug: 'subAgent',
63
+ refactor: 'subAgent',
64
+
65
+ // Read / search
66
+ read_file: 'search',
67
+ read_files: 'search',
68
+ search_code: 'search',
69
+ search_files: 'search',
70
+ grep: 'search',
71
+ list_files: 'search',
72
+ analyze_code: 'search',
73
+ get_file_info: 'search',
74
+ git_diff: 'search',
75
+ git_status: 'search',
76
+ get_project_overview: 'search',
77
+
78
+ // Write / edit
79
+ write_file: 'write',
80
+ write_project: 'write',
81
+ edit_file: 'write',
82
+ delete_file: 'write',
83
+
84
+ // Shell / validate
85
+ shell: 'shell',
86
+ run_tests: 'shell',
87
+ validate_build: 'shell',
88
+ validate_file: 'shell',
89
+ validate_structure:'shell',
90
+ lint_check: 'shell',
91
+
92
+ // Network
93
+ WebFetch: 'network',
94
+ fetch_url: 'network',
95
+ });
96
+
97
+ // ── Helpers ──────────────────────────────────────────────────────────────
98
+
99
+ function render(spec) {
100
+ if (!spec) return '';
101
+ return term().unicode ? spec.utf : spec.ascii;
102
+ }
103
+
104
+ /**
105
+ * Resolve an icon by name (e.g. `icons.subAgent`). Always safe — returns
106
+ * the ASCII fallback when the terminal cannot render Unicode.
107
+ *
108
+ * Implemented as a Proxy over a plain object (intentionally not frozen, see
109
+ * the comment above ICONS) so callers can write `icons.pass` directly.
110
+ */
111
+ export const icons = new Proxy({}, {
112
+ get(_target, prop) {
113
+ if (typeof prop !== 'string') return undefined;
114
+ return render(ICON_RECORDS[prop]);
115
+ },
116
+ has(_target, prop) {
117
+ return typeof prop === 'string' && prop in ICON_RECORDS;
118
+ },
119
+ ownKeys() {
120
+ return Object.keys(ICON_RECORDS);
121
+ },
122
+ getOwnPropertyDescriptor(_target, prop) {
123
+ if (typeof prop !== 'string' || !(prop in ICON_RECORDS)) return undefined;
124
+ return { configurable: true, enumerable: true, value: render(ICON_RECORDS[prop]) };
125
+ },
126
+ });
127
+
128
+ /**
129
+ * Resolve the icon for a tool name. Falls back to a generic tool icon
130
+ * (`◇`) when the tool is unknown to the registry — this is intentional so
131
+ * unmapped MCP tools and user-defined tools still render with the same
132
+ * visual rhythm.
133
+ */
134
+ export function icon(toolName) {
135
+ if (!toolName) return '';
136
+ const key = TOOL_ICON[toolName];
137
+ if (key) return render(ICON_RECORDS[key]);
138
+
139
+ // MCP tools often arrive as "mcp__server__tool" — strip the prefix and
140
+ // try again before falling back to the generic glyph.
141
+ if (toolName.startsWith('mcp')) {
142
+ const cleaned = toolName.replace(/^mcp[_-]+/, '').split(/[_-]+/)[0];
143
+ const fallback = TOOL_ICON[cleaned];
144
+ if (fallback) return render(ICON_RECORDS[fallback]);
145
+ }
146
+
147
+ return term().unicode ? '◇' : '*';
148
+ }
149
+
150
+ /**
151
+ * Tool family (one of: subAgent, search, write, shell, network, other).
152
+ * Used by tier classification and color choice in the tool card renderer.
153
+ */
154
+ export function toolFamily(toolName) {
155
+ return TOOL_ICON[toolName] || 'other';
156
+ }
157
+
158
+ /**
159
+ * Lower-level lookup for callers that want to know whether a name is in
160
+ * the registry (e.g. risk classification fall-throughs).
161
+ */
162
+ export function hasIcon(name) {
163
+ return name in ICON_RECORDS;
164
+ }