@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.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- 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
|
+
}
|
package/src/ui/icons.mjs
ADDED
|
@@ -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
|
+
}
|