@bahulam/code 0.1.1
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 +93 -0
- package/package.json +56 -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 +223 -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 +312 -0
- package/src/commands/agent.mjs +221 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +202 -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/model-catalog.mjs +57 -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/prose-chunker.mjs +255 -0
- package/src/context/retriever.mjs +425 -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 +145 -0
- package/src/core/approval.mjs +700 -0
- package/src/core/attachments.mjs +666 -0
- package/src/core/backend-url.mjs +68 -0
- package/src/core/bundled-runtime.mjs +418 -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 +331 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +460 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +538 -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 +388 -0
- package/src/core/safety.mjs +260 -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 +957 -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 +2231 -0
- package/src/core/trust.mjs +160 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -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 +374 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +700 -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 +122 -0
- package/src/terminal/agents.mjs +353 -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 +310 -0
- package/src/terminal/repl-ask-form.mjs +120 -0
- package/src/terminal/repl-explore.mjs +44 -0
- package/src/terminal/repl-format.mjs +317 -0
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +833 -0
- package/src/terminal/repl-resume.mjs +640 -0
- package/src/terminal/repl-state.mjs +120 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +5032 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +392 -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 +703 -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 +510 -0
- package/src/ui/banner.mjs +232 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +170 -0
- package/src/ui/input-dock.mjs +772 -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/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +257 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +167 -0
- package/src/ui/term.mjs +174 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +740 -0
- package/src/ui/tool-details.mjs +504 -0
- package/src/ui/transcript-block.mjs +20 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL rendering pipeline — event-facing side.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from repl.mjs. Contains the pieces that write to the terminal
|
|
5
|
+
* once an event has been dispatched:
|
|
6
|
+
* - Block boundaries between different display sections
|
|
7
|
+
* - Tool head + result rendering (single-line vs two-line card shape)
|
|
8
|
+
* - Explore-run collapse (list/read/search/index burst → one animated line)
|
|
9
|
+
* - Content streaming (SSE partials → debounced markdown flush)
|
|
10
|
+
* - Spinner (single animated line, shared across concerns)
|
|
11
|
+
* - Stagnation banner
|
|
12
|
+
* - Detail expansion (/last, /expand N, F2)
|
|
13
|
+
*
|
|
14
|
+
* All mutable state lives in repl-state.mjs (`runtime`). Consumers of this
|
|
15
|
+
* module (renderEvent, handleCommand, keypress handlers) import the specific
|
|
16
|
+
* functions they need.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { c, stripAnsi, renderMarkdown, inPlace } from './ansi.mjs';
|
|
20
|
+
import { paint } from '../ui/palette.mjs';
|
|
21
|
+
import { runtime, session } from './repl-state.mjs';
|
|
22
|
+
import { fitAnsiLine } from './repl-format.mjs';
|
|
23
|
+
import { exploreCategory, isExploreTool } from './repl-explore.mjs';
|
|
24
|
+
import {
|
|
25
|
+
clearPinnedStatus,
|
|
26
|
+
drawPinnedStatus,
|
|
27
|
+
isInputDockMounted,
|
|
28
|
+
moveToContent,
|
|
29
|
+
} from '../ui/input-dock.mjs';
|
|
30
|
+
import * as queue from '../ui/render-queue.mjs';
|
|
31
|
+
|
|
32
|
+
// Single seam for the transient spinner/status line. Rich mode (render
|
|
33
|
+
// queue active) → coalesced last-wins status that can never interleave
|
|
34
|
+
// with content. Legacy dock path and bare-TTY inPlace stay as fallbacks
|
|
35
|
+
// until their write-sites migrate onto the queue too.
|
|
36
|
+
// Max inner-tool lines shown under the spinner during a sub-agent run.
|
|
37
|
+
const SUB_AGENT_WINDOW_ROWS = 7;
|
|
38
|
+
|
|
39
|
+
function presentStatus(rendered) {
|
|
40
|
+
if (queue.isActive()) {
|
|
41
|
+
const win = runtime.subAgentWindow;
|
|
42
|
+
if (win?.active && win.lines.length) {
|
|
43
|
+
queue.statusBlock([
|
|
44
|
+
rendered,
|
|
45
|
+
...win.lines.slice(-SUB_AGENT_WINDOW_ROWS).map(l => ` ${c.dim(l)}`),
|
|
46
|
+
]);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
queue.status(rendered);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isInputDockMounted()) { drawPinnedStatus(rendered); return; }
|
|
53
|
+
inPlace(rendered);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Push a line into the live sub-agent tool window (dedup consecutive). */
|
|
57
|
+
export function pushSubAgentWindowLine(line) {
|
|
58
|
+
const win = runtime.subAgentWindow;
|
|
59
|
+
if (!win?.active) return;
|
|
60
|
+
const text = String(line || '').trim();
|
|
61
|
+
if (!text || win.lines[win.lines.length - 1] === text) return;
|
|
62
|
+
win.lines.push(text);
|
|
63
|
+
if (win.lines.length > 24) win.lines.splice(0, win.lines.length - 24);
|
|
64
|
+
repaintSpinnerStatus();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function setSubAgentWindowActive(active) {
|
|
68
|
+
runtime.subAgentWindow = { active: Boolean(active), lines: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the entire sub-agent live window with these lines. Called by
|
|
73
|
+
* repl.mjs's fold-* functions on every sub-agent tool_call and tool_result
|
|
74
|
+
* so the window shows RICH per-tool progress (same '• tool — outcome'
|
|
75
|
+
* format as the final summary) live during the sub-agent run instead of
|
|
76
|
+
* a bare '→ tool' spinner text or nothing at all.
|
|
77
|
+
*/
|
|
78
|
+
export function rebuildSubAgentWindow(lines) {
|
|
79
|
+
const win = runtime.subAgentWindow;
|
|
80
|
+
if (!win?.active) return;
|
|
81
|
+
win.lines = Array.isArray(lines) ? lines.slice() : [];
|
|
82
|
+
repaintSpinnerStatus();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function erasePresentedStatus() {
|
|
86
|
+
if (queue.isActive()) { queue.clearStatus(); return; }
|
|
87
|
+
if (isInputDockMounted()) { clearPinnedStatus(); moveToContent(); return; }
|
|
88
|
+
inPlace('');
|
|
89
|
+
}
|
|
90
|
+
import {
|
|
91
|
+
formatCardHead,
|
|
92
|
+
formatCompactFileDiff,
|
|
93
|
+
summarizeResult,
|
|
94
|
+
recordCard,
|
|
95
|
+
lastCard,
|
|
96
|
+
getCard,
|
|
97
|
+
allCards,
|
|
98
|
+
clearCards,
|
|
99
|
+
} from '../ui/tool-card.mjs';
|
|
100
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
101
|
+
import { subAgentIndent, inSubAgent as inSubAgentBlock } from '../ui/sub-agent.mjs';
|
|
102
|
+
import { safeCwd } from './repl-utils.mjs';
|
|
103
|
+
import { transcriptHeader, transcriptLine } from '../ui/transcript-block.mjs';
|
|
104
|
+
|
|
105
|
+
export function blockSeparatorMode() {
|
|
106
|
+
return String(process.env.KEPLER_BLOCK_SEPARATOR || 'space').toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function renderBlockBoundary(nextBlock, { compactSame = false } = {}) {
|
|
110
|
+
if (!runtime.lastRenderedBlock) return;
|
|
111
|
+
if (compactSame && runtime.lastRenderedBlock === nextBlock) return;
|
|
112
|
+
|
|
113
|
+
const mode = blockSeparatorMode();
|
|
114
|
+
if (mode === 'off' || mode === 'none') return;
|
|
115
|
+
if (mode === 'dotted' || mode === 'dots') {
|
|
116
|
+
const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
|
|
117
|
+
process.stderr.write(` ${c.dim('·'.repeat(Math.min(44, cols - 4)))}\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.stderr.write('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function flushPendingHead() {
|
|
125
|
+
if (!runtime.pendingHead) return;
|
|
126
|
+
process.stderr.write(`${runtime.pendingHead.head}\n`);
|
|
127
|
+
runtime.lastRenderedBlock = 'tool';
|
|
128
|
+
runtime.pendingHead = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function clearPendingHead() {
|
|
132
|
+
// Called by interleaving handlers — flush as 2-line shape (because we are
|
|
133
|
+
// about to print something else) and continue.
|
|
134
|
+
flushPendingHead();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isInlineOutcomeTool(tool) {
|
|
138
|
+
return [
|
|
139
|
+
'read_file', 'read_files', 'read_batch', 'get_file_info',
|
|
140
|
+
'search_code', 'search_files', 'grep', 'list_files',
|
|
141
|
+
].includes(String(tool || '').toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function compactHeadForOutcome(head, outcome, cols) {
|
|
145
|
+
const reserve = stripAnsi(outcome).length + 4;
|
|
146
|
+
const maxHead = Math.max(28, cols - reserve);
|
|
147
|
+
return fitAnsiLine(head, maxHead);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function readToolLabel(tool, data = {}) {
|
|
151
|
+
const args = data.args || {};
|
|
152
|
+
const filePath = args.file_path || args.path || data.file_path || data.path
|
|
153
|
+
|| args.pattern || args.query || '';
|
|
154
|
+
if (filePath) return shortPath(String(filePath));
|
|
155
|
+
const output = String(data.output_preview || data.output || '').split('\n').find(Boolean) || '';
|
|
156
|
+
const match = output.match(/^([^:\s][^:\n]*):/);
|
|
157
|
+
return match ? shortPath(match[1]) : String(tool || 'file');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function rememberExplore(label) {
|
|
161
|
+
const value = String(label || '').trim();
|
|
162
|
+
if (!value) return;
|
|
163
|
+
runtime.exploreRun.recent.push(value);
|
|
164
|
+
if (runtime.exploreRun.recent.length > 3) runtime.exploreRun.recent.shift();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function exploreSummary() {
|
|
168
|
+
const { counts, recent } = runtime.exploreRun;
|
|
169
|
+
const bits = [];
|
|
170
|
+
if (counts.list) bits.push(`${counts.list} listed`);
|
|
171
|
+
if (counts.read) bits.push(`${counts.read} read`);
|
|
172
|
+
if (counts.search) bits.push(`${counts.search} searched`);
|
|
173
|
+
if (counts.index) bits.push(`${counts.index} indexed`);
|
|
174
|
+
const stats = bits.length ? bits.join(' · ') : 'starting…';
|
|
175
|
+
const latest = recent.length ? ` · ${recent[recent.length - 1]}` : '';
|
|
176
|
+
return `exploring · ${stats}${latest}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function exploreRunTotal() {
|
|
180
|
+
return Object.values(runtime.exploreRun.counts).reduce((a, b) => a + b, 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function exploreSnapshotEvery() {
|
|
184
|
+
const n = Number.parseInt(process.env.KEPLER_EXPLORE_SNAPSHOT_EVERY || '8', 10);
|
|
185
|
+
return Number.isFinite(n) ? Math.max(1, n) : 8;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function exploreSnapshotMs() {
|
|
189
|
+
const n = Number.parseInt(process.env.KEPLER_EXPLORE_SNAPSHOT_MS || '900', 10);
|
|
190
|
+
return Number.isFinite(n) ? Math.max(100, n) : 900;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function writeExploreSnapshot(summary = exploreSummary()) {
|
|
194
|
+
const cols = process.stderr.columns || 120;
|
|
195
|
+
const line = ` ${paint.text.dim(fitAnsiLine(summary, Math.max(32, cols - 2)))}`;
|
|
196
|
+
process.stderr.write(`${line}\n`);
|
|
197
|
+
runtime.exploreRun.lastPrintedSummary = summary;
|
|
198
|
+
runtime.exploreRun.lastPrintedTotal = exploreRunTotal();
|
|
199
|
+
runtime.exploreRun.lastPrintedAt = Date.now();
|
|
200
|
+
runtime.lastRenderedBlock = 'tool';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function shouldPrintExploreSnapshot() {
|
|
204
|
+
const summary = exploreSummary();
|
|
205
|
+
const total = exploreRunTotal();
|
|
206
|
+
if (!summary || total <= 0) return false;
|
|
207
|
+
if (!runtime.exploreRun.lastPrintedSummary) return true;
|
|
208
|
+
if (summary === runtime.exploreRun.lastPrintedSummary) return false;
|
|
209
|
+
|
|
210
|
+
const sinceTotal = total - (runtime.exploreRun.lastPrintedTotal || 0);
|
|
211
|
+
const sinceMs = Date.now() - (runtime.exploreRun.lastPrintedAt || 0);
|
|
212
|
+
return sinceTotal >= exploreSnapshotEvery() || sinceMs >= exploreSnapshotMs();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function renderExploreRun() {
|
|
216
|
+
// Set the lock BEFORE touching the spinner so the interval's next tick
|
|
217
|
+
// picks up exploreSummary() text instead of any stale label.
|
|
218
|
+
runtime.exploreRun.lineActive = true;
|
|
219
|
+
|
|
220
|
+
if (!queue.isActive() && isInputDockMounted()) {
|
|
221
|
+
// Legacy docked path (queue not engaged): an animated bottom overlay
|
|
222
|
+
// created visible gaps, so emit bounded snapshots into the transcript
|
|
223
|
+
// instead. With the render queue active this branch is skipped — the
|
|
224
|
+
// coalesced status line can animate safely in dock mode.
|
|
225
|
+
if (runtime.spinInterval) {
|
|
226
|
+
clearInterval(runtime.spinInterval);
|
|
227
|
+
runtime.spinInterval = null;
|
|
228
|
+
inPlace('');
|
|
229
|
+
}
|
|
230
|
+
runtime.spinText = '';
|
|
231
|
+
if (shouldPrintExploreSnapshot()) writeExploreSnapshot();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!runtime.spinInterval) {
|
|
236
|
+
// Bypass the lockout in startSpinner by seeding runtime.spinText directly.
|
|
237
|
+
runtime.spinText = exploreSummary();
|
|
238
|
+
runtime.spinFrame = 0;
|
|
239
|
+
runtime.spinInterval = setInterval(() => {
|
|
240
|
+
const isExploreActive = runtime.exploreRun && runtime.exploreRun.lineActive;
|
|
241
|
+
const label = isExploreActive ? exploreSummary() : runtime.spinText;
|
|
242
|
+
if (!label) return;
|
|
243
|
+
const frame = SPIN_FRAMES[runtime.spinFrame % SPIN_FRAMES.length];
|
|
244
|
+
runtime.spinFrame++;
|
|
245
|
+
const rendered = ` ${c.brand(frame)} ${c.dim(label)}`;
|
|
246
|
+
presentStatus(rendered);
|
|
247
|
+
}, 80);
|
|
248
|
+
}
|
|
249
|
+
runtime.lastRenderedBlock = 'tool';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function flushExploreRun() {
|
|
253
|
+
const total = exploreRunTotal();
|
|
254
|
+
const summary = exploreSummary();
|
|
255
|
+
// Release the lock first so the real spinner teardown can run.
|
|
256
|
+
const wasActive = runtime.exploreRun.lineActive;
|
|
257
|
+
runtime.exploreRun.lineActive = false;
|
|
258
|
+
if (total > 0) {
|
|
259
|
+
if (wasActive) {
|
|
260
|
+
if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
|
|
261
|
+
runtime.spinText = '';
|
|
262
|
+
if (queue.isActive()) queue.clearStatus();
|
|
263
|
+
else if (!isInputDockMounted()) inPlace('');
|
|
264
|
+
}
|
|
265
|
+
if (queue.isActive() || !isInputDockMounted() || summary !== runtime.exploreRun.lastPrintedSummary) {
|
|
266
|
+
writeExploreSnapshot(summary);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Legacy alias — several sites (resetContentStream, older event handlers)
|
|
273
|
+
// call this. Keep pointing at the new flush so nothing has to change.
|
|
274
|
+
const flushCompactReadRun = flushExploreRun;
|
|
275
|
+
|
|
276
|
+
export function renderToolCall(data) {
|
|
277
|
+
const tool = data?.tool || 'unknown';
|
|
278
|
+
const args = data?.args || {};
|
|
279
|
+
const indent = subAgentIndent();
|
|
280
|
+
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
281
|
+
|
|
282
|
+
// If a previous head is still pending (no result yet), flush it as a
|
|
283
|
+
// regular two-line shape before starting the next one.
|
|
284
|
+
flushPendingHead();
|
|
285
|
+
|
|
286
|
+
// ── Explore-run collapse ────────────────────────────────────────────────
|
|
287
|
+
// For list/read/search/index tools, skip the per-call head entirely and
|
|
288
|
+
// update a single animated summary spinner. The transcript stays clean;
|
|
289
|
+
// the user still sees live progress (12 read · 3 listed · latest: foo.py).
|
|
290
|
+
if (isExploreTool(tool)) {
|
|
291
|
+
runtime.exploreRun.counts[exploreCategory(tool)] =
|
|
292
|
+
(runtime.exploreRun.counts[exploreCategory(tool)] || 0) + 1;
|
|
293
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
294
|
+
const label = readToolLabel(tool, { args });
|
|
295
|
+
if (label) rememberExplore(label);
|
|
296
|
+
recordCard({ id: callId, tool, args, startedAt: Date.now() });
|
|
297
|
+
renderExploreRun();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Sub-agent live window (queue mode): inner tool calls stream into the
|
|
302
|
+
// fixed-height status block instead of appending transcript lines. The
|
|
303
|
+
// card is still recorded so /expand, /last, and `d` show full detail.
|
|
304
|
+
if (queue.isActive() && runtime.subAgentWindow?.active && inSubAgentBlock()) {
|
|
305
|
+
recordCard({ id: callId, tool, args, startedAt: Date.now() });
|
|
306
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
307
|
+
const label = readToolLabel(tool, { args });
|
|
308
|
+
pushSubAgentWindowLine(label ? `→ ${tool} · ${label}` : `→ ${tool}`);
|
|
309
|
+
return; // the spinner tick paints the window block
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
flushExploreRun();
|
|
313
|
+
renderBlockBoundary('tool', { compactSame: tool !== 'shell' });
|
|
314
|
+
|
|
315
|
+
const head = formatCardHead(tool, args, {
|
|
316
|
+
cwd: safeCwd(),
|
|
317
|
+
columns: process.stderr.columns || 120,
|
|
318
|
+
indent,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
322
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
323
|
+
runtime.pendingHead = { callId, head, indent };
|
|
324
|
+
runtime.lastRenderedBlock = 'tool';
|
|
325
|
+
// Spinner shows what's running until the result arrives. Per-call phase
|
|
326
|
+
// gives each tool its own elapsed clock — long shell runs count up live.
|
|
327
|
+
const spinLabel = tool === 'shell' && args.command
|
|
328
|
+
? `shell: ${String(args.command).split('\n')[0].slice(0, 48)}`
|
|
329
|
+
: `${tool}…`;
|
|
330
|
+
startSpinner(spinLabel, { phase: `tool:${callId}` });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Render a tool result (success/failure, output snippet).
|
|
335
|
+
*/
|
|
336
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
337
|
+
|
|
338
|
+
export function formatToolDuration(data) {
|
|
339
|
+
const ms = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
340
|
+
if (ms == null) return '';
|
|
341
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function renderToolResult(data, eventType = 'tool_result') {
|
|
345
|
+
if (!data) return;
|
|
346
|
+
const indent = subAgentIndent();
|
|
347
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
348
|
+
const callId = data.call_id || data._callId;
|
|
349
|
+
// Either tool_result or tool_done is allowed to render — whichever wins
|
|
350
|
+
// the race. Subsequent events for the same callId are duplicates.
|
|
351
|
+
if (callId && runtime.renderedToolResults.has(callId)) return;
|
|
352
|
+
if (callId) runtime.renderedToolResults.add(callId);
|
|
353
|
+
|
|
354
|
+
const tool = data.tool || data._tool || '';
|
|
355
|
+
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
356
|
+
recordReadActivity(tool, data.args || {});
|
|
357
|
+
recordWriteActivity(tool, data.args || {}, data);
|
|
358
|
+
|
|
359
|
+
// Update the card buffer so /last and `d` can find it.
|
|
360
|
+
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
361
|
+
|
|
362
|
+
if (data._blocked) session.blockedOps++;
|
|
363
|
+
|
|
364
|
+
const { text, tone: t } = summarizeResult(tool, data);
|
|
365
|
+
// Em dash reads more like prose than a system arrow.
|
|
366
|
+
const arrow = shellResultTool(tool)
|
|
367
|
+
? `${paint.text.dim('result')} ${paint.text.dim('—')}`
|
|
368
|
+
: paint.text.dim('—');
|
|
369
|
+
const painter = t === 'success' ? paint.state.success
|
|
370
|
+
: t === 'warn' ? paint.state.warn
|
|
371
|
+
: t === 'danger' ? paint.state.danger
|
|
372
|
+
: paint.text.dim;
|
|
373
|
+
// Skip the duration tail when the tool was effectively instant (<200ms) —
|
|
374
|
+
// "1ms" / "0ms" was noise that hurt the prose feel.
|
|
375
|
+
const duration = (durationMs != null && durationMs < 200) ? '' : formatToolDuration(data);
|
|
376
|
+
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
377
|
+
const outcome = `${arrow} ${painter(text || 'done')}${tail}`;
|
|
378
|
+
const hasLint = (tool === 'write_file' || tool === 'edit_file') && data.lint;
|
|
379
|
+
const diffPreview = formatCompactFileDiff(data, {
|
|
380
|
+
indent: gutter,
|
|
381
|
+
columns: process.stderr.columns || 120,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Explore tools: the call already updated the summary spinner. Refresh
|
|
385
|
+
// the "latest" hint with the result's file if we have one, and skip the
|
|
386
|
+
// per-result render entirely.
|
|
387
|
+
if (isExploreTool(tool)) {
|
|
388
|
+
const label = readToolLabel(tool, data);
|
|
389
|
+
if (label) rememberExplore(label);
|
|
390
|
+
renderExploreRun();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Sub-agent live window: the call line is already streaming in the
|
|
395
|
+
// status block; the result stays card-only (close card summarizes).
|
|
396
|
+
if (queue.isActive() && runtime.subAgentWindow?.active && inSubAgentBlock()) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Single-line combined emit ──
|
|
401
|
+
// If the head for this call is still buffered (no interleaving content
|
|
402
|
+
// landed), and the combined line fits the terminal width, emit ONE line
|
|
403
|
+
// and skip the gutter entirely. Multi-line result text (shell preview
|
|
404
|
+
// with rows + "+ N more" tail) skips this path — a wrapped multi-line
|
|
405
|
+
// block needs its own real estate.
|
|
406
|
+
const outcomeIsMultiLine = outcome.includes('\n');
|
|
407
|
+
if (runtime.pendingHead && runtime.pendingHead.callId === callId && !hasLint && !runtime.pendingHead.head.includes('\n') && !outcomeIsMultiLine) {
|
|
408
|
+
const cols = process.stderr.columns || 120;
|
|
409
|
+
const combined = `${runtime.pendingHead.head} ${outcome}`;
|
|
410
|
+
if (stripAnsi(combined).length <= cols) {
|
|
411
|
+
process.stderr.write(`${combined}\n`);
|
|
412
|
+
if (diffPreview) {
|
|
413
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
414
|
+
rememberFileDiffPreview(data);
|
|
415
|
+
}
|
|
416
|
+
renderPlanBody(tool, data);
|
|
417
|
+
runtime.lastRenderedBlock = 'tool';
|
|
418
|
+
runtime.pendingHead = null;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (isInlineOutcomeTool(tool)) {
|
|
422
|
+
const compactHead = compactHeadForOutcome(runtime.pendingHead.head, outcome, cols);
|
|
423
|
+
process.stderr.write(`${compactHead} ${outcome}\n`);
|
|
424
|
+
if (diffPreview) {
|
|
425
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
426
|
+
rememberFileDiffPreview(data);
|
|
427
|
+
}
|
|
428
|
+
renderPlanBody(tool, data);
|
|
429
|
+
runtime.lastRenderedBlock = 'tool';
|
|
430
|
+
runtime.pendingHead = null;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Combined too wide — flush the head as 2-line and fall through.
|
|
434
|
+
flushPendingHead();
|
|
435
|
+
} else if (runtime.pendingHead) {
|
|
436
|
+
// Stale pending head (different callId) — flush it before printing this
|
|
437
|
+
// result's gutter line below.
|
|
438
|
+
flushPendingHead();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
442
|
+
// Multi-line outcome (shell preview with rows + "+ N more" tail): prepend
|
|
443
|
+
// the gutter to each line so the block stays aligned instead of ragged.
|
|
444
|
+
if (outcomeIsMultiLine) {
|
|
445
|
+
for (const line of outcome.split('\n')) {
|
|
446
|
+
process.stderr.write(`${gutter}${line}\n`);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
process.stderr.write(`${gutter}${outcome}\n`);
|
|
450
|
+
}
|
|
451
|
+
if (diffPreview) {
|
|
452
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
453
|
+
rememberFileDiffPreview(data);
|
|
454
|
+
}
|
|
455
|
+
renderPlanBody(tool, data);
|
|
456
|
+
runtime.lastRenderedBlock = 'tool';
|
|
457
|
+
|
|
458
|
+
// Lint warnings stay visible alongside writes.
|
|
459
|
+
if (hasLint) {
|
|
460
|
+
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// The plan sub-agent's output is the one peer result the USER needs to
|
|
465
|
+
// see, not just the model — it's the execution contract for the turn.
|
|
466
|
+
// Render the body as a bordered block (capped; full text stays on the
|
|
467
|
+
// card via /last). All other peer verbs keep the one-line summary.
|
|
468
|
+
const PLAN_BODY_MAX_LINES = 30;
|
|
469
|
+
|
|
470
|
+
function renderPlanBody(tool, data) {
|
|
471
|
+
if (String(tool || '').toLowerCase() !== 'plan') return;
|
|
472
|
+
const text = String(data?.output ?? data?.result ?? '').trim();
|
|
473
|
+
if (!text) return;
|
|
474
|
+
const indent = subAgentIndent();
|
|
475
|
+
const lines = transcriptRenderableLines(renderMarkdown(text));
|
|
476
|
+
if (!lines.length) return;
|
|
477
|
+
const shown = lines.slice(0, PLAN_BODY_MAX_LINES);
|
|
478
|
+
process.stderr.write(`${indent}${paint.text.dim('┌ plan')}\n`);
|
|
479
|
+
for (const line of shown) {
|
|
480
|
+
process.stderr.write(`${indent}${paint.text.dim('│')} ${line}\n`);
|
|
481
|
+
}
|
|
482
|
+
process.stderr.write(lines.length > shown.length
|
|
483
|
+
? `${indent}${paint.text.dim(`└ … ${lines.length - shown.length} more lines · /last to expand`)}\n`
|
|
484
|
+
: `${indent}${paint.text.dim('└')}\n`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function fileDiffKey(data = {}) {
|
|
488
|
+
return fileDiffKeys(data)[0] || '';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function fileDiffKeys(data = {}) {
|
|
492
|
+
const keys = [];
|
|
493
|
+
const callId = data.call_id || data._callId || data.request_id || data.id;
|
|
494
|
+
if (callId) keys.push(`call:${callId}`);
|
|
495
|
+
const diff = Array.isArray(data.file_diffs) ? data.file_diffs[0]
|
|
496
|
+
: data.file_diff ? data.file_diff
|
|
497
|
+
: data.type === 'file_diff' ? data
|
|
498
|
+
: null;
|
|
499
|
+
const file = diff?.relative_path || diff?.path || data.relative_path || data.path || '';
|
|
500
|
+
if (file) {
|
|
501
|
+
const added = diff?.lines_added ?? data.lines_added ?? '';
|
|
502
|
+
const removed = diff?.lines_removed ?? data.lines_removed ?? '';
|
|
503
|
+
keys.push(`file:${file}:${added}:${removed}`);
|
|
504
|
+
}
|
|
505
|
+
return keys;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function rememberFileDiffPreview(data = {}) {
|
|
509
|
+
for (const key of fileDiffKeys(data)) {
|
|
510
|
+
runtime.renderedFileDiffPreviews.add(key);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function renderFileDiffEvent(data = {}) {
|
|
515
|
+
const keys = fileDiffKeys(data);
|
|
516
|
+
if (keys.some(key => runtime.renderedFileDiffPreviews.has(key))) return false;
|
|
517
|
+
|
|
518
|
+
const indent = subAgentIndent();
|
|
519
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
520
|
+
const diffPreview = formatCompactFileDiff({
|
|
521
|
+
file_diff: data,
|
|
522
|
+
lines_added: data.lines_added,
|
|
523
|
+
lines_removed: data.lines_removed,
|
|
524
|
+
}, {
|
|
525
|
+
indent: gutter,
|
|
526
|
+
columns: process.stderr.columns || 120,
|
|
527
|
+
showFileHeader: true,
|
|
528
|
+
});
|
|
529
|
+
if (!diffPreview) return false;
|
|
530
|
+
|
|
531
|
+
renderBlockBoundary('tool', { compactSame: true });
|
|
532
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
533
|
+
for (const key of keys) runtime.renderedFileDiffPreviews.add(key);
|
|
534
|
+
rememberChangedFile(data.relative_path || data.path);
|
|
535
|
+
runtime.lastRenderedBlock = 'tool';
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function shellResultTool(tool) {
|
|
540
|
+
return [
|
|
541
|
+
'shell', 'run_tests', 'validate_build', 'lint_check',
|
|
542
|
+
'validate_file', 'validate_structure',
|
|
543
|
+
].includes(String(tool || '').toLowerCase());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
547
|
+
//
|
|
548
|
+
// All three call into the same renderer so output is consistent across
|
|
549
|
+
// keypress and slash-command paths. `expandLast` and `expandIndex` write
|
|
550
|
+
// directly to stderr.
|
|
551
|
+
|
|
552
|
+
export function expandLast() {
|
|
553
|
+
const card = lastCard();
|
|
554
|
+
if (!card) {
|
|
555
|
+
process.stderr.write(` ${paint.text.dim('(no tool to expand yet)')}\n`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function expandIndex(idxOrAll) {
|
|
562
|
+
if (idxOrAll === 'all') {
|
|
563
|
+
const cards = allCards();
|
|
564
|
+
if (!cards.length) {
|
|
565
|
+
process.stderr.write(` ${paint.text.dim('(no tools to expand yet)')}\n`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
process.stderr.write('\n');
|
|
569
|
+
for (const c of cards) process.stderr.write(detailFor(c) + '\n');
|
|
570
|
+
process.stderr.write('\n');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const card = getCard(idxOrAll);
|
|
574
|
+
if (!card) {
|
|
575
|
+
process.stderr.write(` ${paint.text.dim('(no card at index ' + idxOrAll + ')')}\n`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Shorten a file path for display: /Users/sree/Sites/project/src/foo.mjs → src/foo.mjs
|
|
583
|
+
*/
|
|
584
|
+
export function shortPath(p) {
|
|
585
|
+
if (!p) return '';
|
|
586
|
+
const cwd = safeCwd();
|
|
587
|
+
if (p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
588
|
+
// Show last 2 segments
|
|
589
|
+
const parts = p.split('/');
|
|
590
|
+
return parts.length > 2 ? parts.slice(-2).join('/') : p;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function rememberReadFile(filePath) {
|
|
594
|
+
const file = shortPath(String(filePath || '').trim());
|
|
595
|
+
if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function rememberChangedFile(filePath) {
|
|
599
|
+
const file = shortPath(String(filePath || '').trim());
|
|
600
|
+
if (file && !session.filesChanged.includes(file)) session.filesChanged.push(file);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function recordReadActivity(tool, args = {}) {
|
|
604
|
+
const normalized = String(tool || '').toLowerCase();
|
|
605
|
+
if (normalized === 'read_file' || normalized === 'read') {
|
|
606
|
+
rememberReadFile(args.file_path || args.path);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (normalized === 'read_files') {
|
|
610
|
+
const files = args.file_paths || args.paths || args.files || [];
|
|
611
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
612
|
+
rememberReadFile(typeof file === 'string' ? file : file?.file_path || file?.path);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function recordWriteActivity(tool, args = {}, result = {}) {
|
|
618
|
+
const normalized = String(tool || '').toLowerCase();
|
|
619
|
+
if (!['write_file', 'edit_file', 'delete_file', 'write_project'].includes(normalized)) return;
|
|
620
|
+
|
|
621
|
+
if (normalized === 'write_project') {
|
|
622
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
623
|
+
for (const file of files) {
|
|
624
|
+
rememberChangedFile(file?.file_path || file?.path);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
rememberChangedFile(args.file_path || args.path || result.file_path || result.path);
|
|
629
|
+
const diffs = Array.isArray(result.file_diffs)
|
|
630
|
+
? result.file_diffs
|
|
631
|
+
: result.file_diff ? [result.file_diff] : [];
|
|
632
|
+
for (const diff of diffs) {
|
|
633
|
+
rememberChangedFile(diff.relative_path || diff.path);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function thinkingKind(text) {
|
|
638
|
+
return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
|
|
639
|
+
? 'Reading'
|
|
640
|
+
: 'Thinking';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function thinkingPrefix(text) {
|
|
644
|
+
const kind = thinkingKind(text);
|
|
645
|
+
return kind === 'Thinking' ? 'Thinking' : `Thinking · ${kind}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export function clippedThinking(text, limit = 200) {
|
|
649
|
+
const value = String(text || '');
|
|
650
|
+
return value.length > limit ? `${value.slice(0, limit - 2)} …` : value;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── Live Spinner ──
|
|
654
|
+
// A real animated spinner that ticks on an interval, not just per-call.
|
|
655
|
+
// Shows what's happening right now — thinking, tool executing, etc.
|
|
656
|
+
|
|
657
|
+
export const SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
658
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
659
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
660
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
661
|
+
|
|
662
|
+
// Compose the status label with live phase telemetry: elapsed seconds
|
|
663
|
+
// (shown once a phase runs ≥3s — quick tools stay clean) and the
|
|
664
|
+
// per-phase tool-call counter (sub-agent progress). The 80ms tick calls
|
|
665
|
+
// this every frame, so elapsed counts up without any extra timer.
|
|
666
|
+
function composeStatusLabel(label) {
|
|
667
|
+
const parts = [label];
|
|
668
|
+
if (runtime.spinStartedAt) {
|
|
669
|
+
const elapsedS = Math.floor((Date.now() - runtime.spinStartedAt) / 1000);
|
|
670
|
+
if (runtime.spinToolCalls > 0) {
|
|
671
|
+
parts.push(`${runtime.spinToolCalls} call${runtime.spinToolCalls === 1 ? '' : 's'}`);
|
|
672
|
+
}
|
|
673
|
+
if (elapsedS >= 3) parts.push(`${elapsedS}s`);
|
|
674
|
+
}
|
|
675
|
+
return parts.join(' · ');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function repaintSpinnerStatus(text = runtime.spinText, { advance = false } = {}) {
|
|
679
|
+
const isExploreActive = runtime.exploreRun && runtime.exploreRun.lineActive;
|
|
680
|
+
const label = isExploreActive ? exploreSummary() : (text || runtime.spinText);
|
|
681
|
+
if (!label) return;
|
|
682
|
+
const frame = SPIN_FRAMES[runtime.spinFrame % SPIN_FRAMES.length];
|
|
683
|
+
if (advance) runtime.spinFrame++;
|
|
684
|
+
const rendered = ` ${c.brand(frame)} ${c.dim(composeStatusLabel(label))}`;
|
|
685
|
+
if (!queue.isActive() && isExploreActive && isInputDockMounted()) return;
|
|
686
|
+
presentStatus(rendered);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Start (or re-label) the spinner. `phase` scopes the elapsed clock:
|
|
691
|
+
* a phase change resets it, same-phase updates keep it counting. Callers
|
|
692
|
+
* that don't pass a phase get a generic per-call reset (old behavior).
|
|
693
|
+
*/
|
|
694
|
+
export function startSpinner(text, { phase = null } = {}) {
|
|
695
|
+
const nextPhase = phase || `generic:${text}`;
|
|
696
|
+
if (runtime.spinPhase !== nextPhase) {
|
|
697
|
+
runtime.spinPhase = nextPhase;
|
|
698
|
+
runtime.spinStartedAt = Date.now();
|
|
699
|
+
runtime.spinToolCalls = 0;
|
|
700
|
+
}
|
|
701
|
+
runtime.spinText = text;
|
|
702
|
+
runtime.spinFrame = 0;
|
|
703
|
+
if (!queue.isActive() && runtime.exploreRun && runtime.exploreRun.lineActive && isInputDockMounted()) return;
|
|
704
|
+
if (runtime.spinInterval) {
|
|
705
|
+
repaintSpinnerStatus(runtime.spinText);
|
|
706
|
+
return; // already running
|
|
707
|
+
}
|
|
708
|
+
runtime.spinInterval = setInterval(() => {
|
|
709
|
+
repaintSpinnerStatus(runtime.spinText, { advance: true });
|
|
710
|
+
}, 80);
|
|
711
|
+
repaintSpinnerStatus(runtime.spinText);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function updateSpinner(text) {
|
|
715
|
+
runtime.spinText = text;
|
|
716
|
+
// Self-healing: a content flush stops the spinner (clears the
|
|
717
|
+
// interval), but long-running work — sub-agent runs especially —
|
|
718
|
+
// keeps sending updates afterwards. Without reviving the interval
|
|
719
|
+
// those updates write into a dead timer and the user sees a frozen
|
|
720
|
+
// "▸ running" with no progress at all. Same phase → clock continues.
|
|
721
|
+
if (!runtime.spinInterval && text) {
|
|
722
|
+
startSpinner(text, { phase: runtime.spinPhase || undefined });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
repaintSpinnerStatus(runtime.spinText);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Bump the per-phase progress counter (sub-agent tool calls). */
|
|
729
|
+
export function bumpSpinnerProgress() {
|
|
730
|
+
runtime.spinToolCalls++;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function stopSpinner() {
|
|
734
|
+
// Explore owns the line while active — a stray stopSpinner from a
|
|
735
|
+
// transient handler must not blank the progress feedback.
|
|
736
|
+
// flushExploreRun() releases the lock and does the real teardown.
|
|
737
|
+
if (runtime.exploreRun && runtime.exploreRun.lineActive) return;
|
|
738
|
+
if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
|
|
739
|
+
runtime.spinText = '';
|
|
740
|
+
runtime.spinPhase = null;
|
|
741
|
+
runtime.spinStartedAt = 0;
|
|
742
|
+
runtime.spinToolCalls = 0;
|
|
743
|
+
erasePresentedStatus();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ── Content Streaming Display ──
|
|
747
|
+
|
|
748
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
749
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
750
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
751
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
752
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
753
|
+
|
|
754
|
+
export function startContentStream() {
|
|
755
|
+
runtime.streamBuffer = '';
|
|
756
|
+
runtime.streamedPartialText = '';
|
|
757
|
+
runtime.renderedToolResults.clear();
|
|
758
|
+
runtime.renderedFileDiffPreviews.clear();
|
|
759
|
+
runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
|
|
760
|
+
runtime.renderedContentThisTurn = false;
|
|
761
|
+
runtime.contentHeaderPrinted = false;
|
|
762
|
+
runtime.lastRenderedBlock = null;
|
|
763
|
+
stopSpinner();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function appendContent(text) {
|
|
767
|
+
if (!text) return;
|
|
768
|
+
// Any streamed content between renderToolCall and renderToolResult would
|
|
769
|
+
// scroll the head off "the line above", breaking the in-place collapse.
|
|
770
|
+
clearPendingHead();
|
|
771
|
+
runtime.streamBuffer += text;
|
|
772
|
+
runtime.streamedPartialText += text;
|
|
773
|
+
|
|
774
|
+
// Debounce rendering to avoid flicker on rapid partial updates
|
|
775
|
+
if (runtime.streamTimer) clearTimeout(runtime.streamTimer);
|
|
776
|
+
runtime.streamTimer = setTimeout(() => flushContent(), 50);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export function flushContent() {
|
|
780
|
+
if (runtime.streamTimer) { clearTimeout(runtime.streamTimer); runtime.streamTimer = null; }
|
|
781
|
+
if (!runtime.streamBuffer) return;
|
|
782
|
+
|
|
783
|
+
const rendered = renderMarkdown(runtime.streamBuffer);
|
|
784
|
+
const lines = transcriptRenderableLines(rendered);
|
|
785
|
+
runtime.streamBuffer = '';
|
|
786
|
+
if (!lines.length) return;
|
|
787
|
+
|
|
788
|
+
if (isInputDockMounted()) moveToContent();
|
|
789
|
+
stopSpinner();
|
|
790
|
+
// Any buffered tool head needs to land BEFORE this content so the order
|
|
791
|
+
// is preserved on screen.
|
|
792
|
+
flushPendingHead();
|
|
793
|
+
flushCompactReadRun();
|
|
794
|
+
renderBlockBoundary('content', { compactSame: true });
|
|
795
|
+
if (!runtime.contentHeaderPrinted) {
|
|
796
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
797
|
+
runtime.contentHeaderPrinted = true;
|
|
798
|
+
}
|
|
799
|
+
for (const line of lines) {
|
|
800
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
801
|
+
}
|
|
802
|
+
runtime.renderedContentThisTurn = true;
|
|
803
|
+
runtime.lastRenderedBlock = 'content';
|
|
804
|
+
if (typeof runtime.afterContentFlush === 'function') runtime.afterContentFlush();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function transcriptRenderableLines(rendered) {
|
|
808
|
+
const lines = String(rendered ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
809
|
+
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
810
|
+
return lines;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function renderStagnation(data = {}) {
|
|
814
|
+
const rawMessage = data?.message || '';
|
|
815
|
+
const reason = data?.reason || rawMessage.replace(/^Stagnation:\s*/i, '').trim();
|
|
816
|
+
const tool = data?.tool || data?.tool_name || '';
|
|
817
|
+
const suggestion = data?.suggestion || data?.recovery_strategy || data?.strategy || '';
|
|
818
|
+
const message = reason
|
|
819
|
+
? `Stagnation${tool ? ` (${tool})` : ''}: ${reason}`
|
|
820
|
+
: `Stagnation${tool ? ` (${tool})` : ''} detected`;
|
|
821
|
+
const key = `${message}\n${suggestion}`;
|
|
822
|
+
|
|
823
|
+
if (session._lastStagnationWarning === key) return;
|
|
824
|
+
session._lastStagnationWarning = key;
|
|
825
|
+
|
|
826
|
+
stopSpinner();
|
|
827
|
+
flushContent();
|
|
828
|
+
flushPendingHead();
|
|
829
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
830
|
+
process.stderr.write(` ${c.yellow('!')} ${c.yellow(message)}\n`);
|
|
831
|
+
if (suggestion) process.stderr.write(` ${c.dim(suggestion)}\n`);
|
|
832
|
+
runtime.lastRenderedBlock = 'status';
|
|
833
|
+
}
|