@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,561 @@
|
|
|
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, Ctrl+D)
|
|
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
|
+
isInputDockMounted,
|
|
26
|
+
moveToContent,
|
|
27
|
+
} from '../ui/input-dock.mjs';
|
|
28
|
+
import {
|
|
29
|
+
formatCardHead,
|
|
30
|
+
formatCompactFileDiff,
|
|
31
|
+
summarizeResult,
|
|
32
|
+
recordCard,
|
|
33
|
+
lastCard,
|
|
34
|
+
getCard,
|
|
35
|
+
allCards,
|
|
36
|
+
clearCards,
|
|
37
|
+
} from '../ui/tool-card.mjs';
|
|
38
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
39
|
+
import { subAgentIndent, inSubAgent as inSubAgentBlock } from '../ui/sub-agent.mjs';
|
|
40
|
+
import { safeCwd } from './repl-utils.mjs';
|
|
41
|
+
import { transcriptHeader, transcriptLine } from '../ui/transcript-block.mjs';
|
|
42
|
+
|
|
43
|
+
export function blockSeparatorMode() {
|
|
44
|
+
return String(process.env.KEPLER_BLOCK_SEPARATOR || 'space').toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderBlockBoundary(nextBlock, { compactSame = false } = {}) {
|
|
48
|
+
if (!runtime.lastRenderedBlock) return;
|
|
49
|
+
if (compactSame && runtime.lastRenderedBlock === nextBlock) return;
|
|
50
|
+
|
|
51
|
+
const mode = blockSeparatorMode();
|
|
52
|
+
if (mode === 'off' || mode === 'none') return;
|
|
53
|
+
if (mode === 'dotted' || mode === 'dots') {
|
|
54
|
+
const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
|
|
55
|
+
process.stderr.write(` ${c.dim('·'.repeat(Math.min(44, cols - 4)))}\n`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stderr.write('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function flushPendingHead() {
|
|
63
|
+
if (!runtime.pendingHead) return;
|
|
64
|
+
process.stderr.write(`${runtime.pendingHead.head}\n`);
|
|
65
|
+
runtime.lastRenderedBlock = 'tool';
|
|
66
|
+
runtime.pendingHead = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function clearPendingHead() {
|
|
70
|
+
// Called by interleaving handlers — flush as 2-line shape (because we are
|
|
71
|
+
// about to print something else) and continue.
|
|
72
|
+
flushPendingHead();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isInlineOutcomeTool(tool) {
|
|
76
|
+
return [
|
|
77
|
+
'read_file', 'read_files', 'read_batch', 'get_file_info',
|
|
78
|
+
'search_code', 'search_files', 'grep', 'list_files',
|
|
79
|
+
].includes(String(tool || '').toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function compactHeadForOutcome(head, outcome, cols) {
|
|
83
|
+
const reserve = stripAnsi(outcome).length + 4;
|
|
84
|
+
const maxHead = Math.max(28, cols - reserve);
|
|
85
|
+
return fitAnsiLine(head, maxHead);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readToolLabel(tool, data = {}) {
|
|
89
|
+
const args = data.args || {};
|
|
90
|
+
const filePath = args.file_path || args.path || data.file_path || data.path
|
|
91
|
+
|| args.pattern || args.query || '';
|
|
92
|
+
if (filePath) return shortPath(String(filePath));
|
|
93
|
+
const output = String(data.output_preview || data.output || '').split('\n').find(Boolean) || '';
|
|
94
|
+
const match = output.match(/^([^:\s][^:\n]*):/);
|
|
95
|
+
return match ? shortPath(match[1]) : String(tool || 'file');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function rememberExplore(label) {
|
|
99
|
+
const value = String(label || '').trim();
|
|
100
|
+
if (!value) return;
|
|
101
|
+
runtime.exploreRun.recent.push(value);
|
|
102
|
+
if (runtime.exploreRun.recent.length > 3) runtime.exploreRun.recent.shift();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function exploreSummary() {
|
|
106
|
+
const { counts, recent } = runtime.exploreRun;
|
|
107
|
+
const bits = [];
|
|
108
|
+
if (counts.list) bits.push(`${counts.list} listed`);
|
|
109
|
+
if (counts.read) bits.push(`${counts.read} read`);
|
|
110
|
+
if (counts.search) bits.push(`${counts.search} searched`);
|
|
111
|
+
if (counts.index) bits.push(`${counts.index} indexed`);
|
|
112
|
+
const stats = bits.length ? bits.join(' · ') : 'starting…';
|
|
113
|
+
const latest = recent.length ? ` · ${recent[recent.length - 1]}` : '';
|
|
114
|
+
return `exploring · ${stats}${latest}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function exploreRunTotal() {
|
|
118
|
+
return Object.values(runtime.exploreRun.counts).reduce((a, b) => a + b, 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function exploreSnapshotEvery() {
|
|
122
|
+
const n = Number.parseInt(process.env.KEPLER_EXPLORE_SNAPSHOT_EVERY || '8', 10);
|
|
123
|
+
return Number.isFinite(n) ? Math.max(1, n) : 8;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function exploreSnapshotMs() {
|
|
127
|
+
const n = Number.parseInt(process.env.KEPLER_EXPLORE_SNAPSHOT_MS || '900', 10);
|
|
128
|
+
return Number.isFinite(n) ? Math.max(100, n) : 900;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeExploreSnapshot(summary = exploreSummary()) {
|
|
132
|
+
const cols = process.stderr.columns || 120;
|
|
133
|
+
const line = ` ${paint.text.dim(fitAnsiLine(summary, Math.max(32, cols - 2)))}`;
|
|
134
|
+
process.stderr.write(`${line}\n`);
|
|
135
|
+
runtime.exploreRun.lastPrintedSummary = summary;
|
|
136
|
+
runtime.exploreRun.lastPrintedTotal = exploreRunTotal();
|
|
137
|
+
runtime.exploreRun.lastPrintedAt = Date.now();
|
|
138
|
+
runtime.lastRenderedBlock = 'tool';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function shouldPrintExploreSnapshot() {
|
|
142
|
+
const summary = exploreSummary();
|
|
143
|
+
const total = exploreRunTotal();
|
|
144
|
+
if (!summary || total <= 0) return false;
|
|
145
|
+
if (!runtime.exploreRun.lastPrintedSummary) return true;
|
|
146
|
+
if (summary === runtime.exploreRun.lastPrintedSummary) return false;
|
|
147
|
+
|
|
148
|
+
const sinceTotal = total - (runtime.exploreRun.lastPrintedTotal || 0);
|
|
149
|
+
const sinceMs = Date.now() - (runtime.exploreRun.lastPrintedAt || 0);
|
|
150
|
+
return sinceTotal >= exploreSnapshotEvery() || sinceMs >= exploreSnapshotMs();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function renderExploreRun() {
|
|
154
|
+
// Set the lock BEFORE touching the spinner so the interval's next tick
|
|
155
|
+
// picks up exploreSummary() text instead of any stale label.
|
|
156
|
+
runtime.exploreRun.lineActive = true;
|
|
157
|
+
|
|
158
|
+
if (isInputDockMounted()) {
|
|
159
|
+
// Docked terminals reserve a bottom input area. Keeping explore progress
|
|
160
|
+
// as an animated bottom overlay creates visible gaps between the sub-agent
|
|
161
|
+
// transcript and the current tool. Emit bounded snapshots into the
|
|
162
|
+
// transcript instead; every hidden call remains available through /expand.
|
|
163
|
+
if (runtime.spinInterval) {
|
|
164
|
+
clearInterval(runtime.spinInterval);
|
|
165
|
+
runtime.spinInterval = null;
|
|
166
|
+
inPlace('');
|
|
167
|
+
}
|
|
168
|
+
runtime.spinText = '';
|
|
169
|
+
if (shouldPrintExploreSnapshot()) writeExploreSnapshot();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!runtime.spinInterval) {
|
|
174
|
+
// Bypass the lockout in startSpinner by seeding runtime.spinText directly.
|
|
175
|
+
runtime.spinText = exploreSummary();
|
|
176
|
+
runtime.spinFrame = 0;
|
|
177
|
+
runtime.spinInterval = setInterval(() => {
|
|
178
|
+
const isExploreActive = runtime.exploreRun && runtime.exploreRun.lineActive;
|
|
179
|
+
const label = isExploreActive ? exploreSummary() : runtime.spinText;
|
|
180
|
+
if (!label) return;
|
|
181
|
+
const frame = SPIN_FRAMES[runtime.spinFrame % SPIN_FRAMES.length];
|
|
182
|
+
runtime.spinFrame++;
|
|
183
|
+
const rendered = ` ${c.brand(frame)} ${c.dim(label)}`;
|
|
184
|
+
if (isInputDockMounted()) moveToContent();
|
|
185
|
+
inPlace(rendered);
|
|
186
|
+
}, 80);
|
|
187
|
+
}
|
|
188
|
+
runtime.lastRenderedBlock = 'tool';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function flushExploreRun() {
|
|
192
|
+
const total = exploreRunTotal();
|
|
193
|
+
const summary = exploreSummary();
|
|
194
|
+
// Release the lock first so the real spinner teardown can run.
|
|
195
|
+
const wasActive = runtime.exploreRun.lineActive;
|
|
196
|
+
runtime.exploreRun.lineActive = false;
|
|
197
|
+
if (total > 0) {
|
|
198
|
+
if (wasActive) {
|
|
199
|
+
if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
|
|
200
|
+
runtime.spinText = '';
|
|
201
|
+
if (!isInputDockMounted()) inPlace('');
|
|
202
|
+
}
|
|
203
|
+
if (!isInputDockMounted() || summary !== runtime.exploreRun.lastPrintedSummary) {
|
|
204
|
+
writeExploreSnapshot(summary);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Legacy alias — several sites (resetContentStream, older event handlers)
|
|
211
|
+
// call this. Keep pointing at the new flush so nothing has to change.
|
|
212
|
+
const flushCompactReadRun = flushExploreRun;
|
|
213
|
+
|
|
214
|
+
export function renderToolCall(data) {
|
|
215
|
+
const tool = data?.tool || 'unknown';
|
|
216
|
+
const args = data?.args || {};
|
|
217
|
+
const indent = subAgentIndent();
|
|
218
|
+
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
219
|
+
|
|
220
|
+
// If a previous head is still pending (no result yet), flush it as a
|
|
221
|
+
// regular two-line shape before starting the next one.
|
|
222
|
+
flushPendingHead();
|
|
223
|
+
|
|
224
|
+
// ── Explore-run collapse ────────────────────────────────────────────────
|
|
225
|
+
// For list/read/search/index tools, skip the per-call head entirely and
|
|
226
|
+
// update a single animated summary spinner. The transcript stays clean;
|
|
227
|
+
// the user still sees live progress (12 read · 3 listed · latest: foo.py).
|
|
228
|
+
if (isExploreTool(tool)) {
|
|
229
|
+
runtime.exploreRun.counts[exploreCategory(tool)] =
|
|
230
|
+
(runtime.exploreRun.counts[exploreCategory(tool)] || 0) + 1;
|
|
231
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
232
|
+
const label = readToolLabel(tool, { args });
|
|
233
|
+
if (label) rememberExplore(label);
|
|
234
|
+
recordCard({ id: callId, tool, args, startedAt: Date.now() });
|
|
235
|
+
renderExploreRun();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
flushExploreRun();
|
|
240
|
+
renderBlockBoundary('tool', { compactSame: tool !== 'shell' });
|
|
241
|
+
|
|
242
|
+
const head = formatCardHead(tool, args, {
|
|
243
|
+
cwd: safeCwd(),
|
|
244
|
+
columns: process.stderr.columns || 120,
|
|
245
|
+
indent,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
249
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
250
|
+
runtime.pendingHead = { callId, head, indent };
|
|
251
|
+
runtime.lastRenderedBlock = 'tool';
|
|
252
|
+
// Spinner shows what's running until the result arrives.
|
|
253
|
+
startSpinner(`${tool}…`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render a tool result (success/failure, output snippet).
|
|
258
|
+
*/
|
|
259
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
260
|
+
|
|
261
|
+
export function formatToolDuration(data) {
|
|
262
|
+
const ms = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
263
|
+
if (ms == null) return '';
|
|
264
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function renderToolResult(data, eventType = 'tool_result') {
|
|
268
|
+
if (!data) return;
|
|
269
|
+
const indent = subAgentIndent();
|
|
270
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
271
|
+
const callId = data.call_id || data._callId;
|
|
272
|
+
// Either tool_result or tool_done is allowed to render — whichever wins
|
|
273
|
+
// the race. Subsequent events for the same callId are duplicates.
|
|
274
|
+
if (callId && runtime.renderedToolResults.has(callId)) return;
|
|
275
|
+
if (callId) runtime.renderedToolResults.add(callId);
|
|
276
|
+
|
|
277
|
+
const tool = data.tool || data._tool || '';
|
|
278
|
+
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
279
|
+
recordReadActivity(tool, data.args || {});
|
|
280
|
+
|
|
281
|
+
// Update the card buffer so /last and `d` can find it.
|
|
282
|
+
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
283
|
+
|
|
284
|
+
if (data._blocked) session.blockedOps++;
|
|
285
|
+
|
|
286
|
+
const { text, tone: t } = summarizeResult(tool, data);
|
|
287
|
+
// Em dash reads more like prose than a system arrow.
|
|
288
|
+
const arrow = paint.text.dim('—');
|
|
289
|
+
const painter = t === 'success' ? paint.state.success
|
|
290
|
+
: t === 'warn' ? paint.state.warn
|
|
291
|
+
: t === 'danger' ? paint.state.danger
|
|
292
|
+
: paint.text.dim;
|
|
293
|
+
// Skip the duration tail when the tool was effectively instant (<200ms) —
|
|
294
|
+
// "1ms" / "0ms" was noise that hurt the prose feel.
|
|
295
|
+
const duration = (durationMs != null && durationMs < 200) ? '' : formatToolDuration(data);
|
|
296
|
+
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
297
|
+
const outcome = `${arrow} ${painter(text || 'done')}${tail}`;
|
|
298
|
+
const hasLint = (tool === 'write_file' || tool === 'edit_file') && data.lint;
|
|
299
|
+
const diffPreview = formatCompactFileDiff(data, {
|
|
300
|
+
indent: gutter,
|
|
301
|
+
columns: process.stderr.columns || 120,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Explore tools: the call already updated the summary spinner. Refresh
|
|
305
|
+
// the "latest" hint with the result's file if we have one, and skip the
|
|
306
|
+
// per-result render entirely.
|
|
307
|
+
if (isExploreTool(tool)) {
|
|
308
|
+
const label = readToolLabel(tool, data);
|
|
309
|
+
if (label) rememberExplore(label);
|
|
310
|
+
renderExploreRun();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Single-line combined emit ──
|
|
315
|
+
// If the head for this call is still buffered (no interleaving content
|
|
316
|
+
// landed), and the combined line fits the terminal width, emit ONE line
|
|
317
|
+
// and skip the gutter entirely.
|
|
318
|
+
if (runtime.pendingHead && runtime.pendingHead.callId === callId && !hasLint && !runtime.pendingHead.head.includes('\n')) {
|
|
319
|
+
const cols = process.stderr.columns || 120;
|
|
320
|
+
const combined = `${runtime.pendingHead.head} ${outcome}`;
|
|
321
|
+
if (stripAnsi(combined).length <= cols) {
|
|
322
|
+
process.stderr.write(`${combined}\n`);
|
|
323
|
+
if (diffPreview) process.stderr.write(`${diffPreview}\n`);
|
|
324
|
+
runtime.lastRenderedBlock = 'tool';
|
|
325
|
+
runtime.pendingHead = null;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (isInlineOutcomeTool(tool)) {
|
|
329
|
+
const compactHead = compactHeadForOutcome(runtime.pendingHead.head, outcome, cols);
|
|
330
|
+
process.stderr.write(`${compactHead} ${outcome}\n`);
|
|
331
|
+
if (diffPreview) process.stderr.write(`${diffPreview}\n`);
|
|
332
|
+
runtime.lastRenderedBlock = 'tool';
|
|
333
|
+
runtime.pendingHead = null;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// Combined too wide — flush the head as 2-line and fall through.
|
|
337
|
+
flushPendingHead();
|
|
338
|
+
} else if (runtime.pendingHead) {
|
|
339
|
+
// Stale pending head (different callId) — flush it before printing this
|
|
340
|
+
// result's gutter line below.
|
|
341
|
+
flushPendingHead();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
345
|
+
process.stderr.write(`${gutter}${outcome}\n`);
|
|
346
|
+
if (diffPreview) process.stderr.write(`${diffPreview}\n`);
|
|
347
|
+
runtime.lastRenderedBlock = 'tool';
|
|
348
|
+
|
|
349
|
+
// Lint warnings stay visible alongside writes.
|
|
350
|
+
if (hasLint) {
|
|
351
|
+
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
356
|
+
//
|
|
357
|
+
// All three call into the same renderer so output is consistent across
|
|
358
|
+
// keypress and slash-command paths. `expandLast` and `expandIndex` write
|
|
359
|
+
// directly to stderr.
|
|
360
|
+
|
|
361
|
+
export function expandLast() {
|
|
362
|
+
const card = lastCard();
|
|
363
|
+
if (!card) {
|
|
364
|
+
process.stderr.write(` ${paint.text.dim('(no tool to expand yet)')}\n`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function expandIndex(idxOrAll) {
|
|
371
|
+
if (idxOrAll === 'all') {
|
|
372
|
+
const cards = allCards();
|
|
373
|
+
if (!cards.length) {
|
|
374
|
+
process.stderr.write(` ${paint.text.dim('(no tools to expand yet)')}\n`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
process.stderr.write('\n');
|
|
378
|
+
for (const c of cards) process.stderr.write(detailFor(c) + '\n');
|
|
379
|
+
process.stderr.write('\n');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const card = getCard(idxOrAll);
|
|
383
|
+
if (!card) {
|
|
384
|
+
process.stderr.write(` ${paint.text.dim('(no card at index ' + idxOrAll + ')')}\n`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Shorten a file path for display: /Users/sree/Sites/project/src/foo.mjs → src/foo.mjs
|
|
392
|
+
*/
|
|
393
|
+
export function shortPath(p) {
|
|
394
|
+
if (!p) return '';
|
|
395
|
+
const cwd = safeCwd();
|
|
396
|
+
if (p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
397
|
+
// Show last 2 segments
|
|
398
|
+
const parts = p.split('/');
|
|
399
|
+
return parts.length > 2 ? parts.slice(-2).join('/') : p;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function rememberReadFile(filePath) {
|
|
403
|
+
const file = shortPath(String(filePath || '').trim());
|
|
404
|
+
if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function recordReadActivity(tool, args = {}) {
|
|
408
|
+
const normalized = String(tool || '').toLowerCase();
|
|
409
|
+
if (normalized === 'read_file' || normalized === 'read') {
|
|
410
|
+
rememberReadFile(args.file_path || args.path);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (normalized === 'read_files') {
|
|
414
|
+
const files = args.file_paths || args.paths || args.files || [];
|
|
415
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
416
|
+
rememberReadFile(typeof file === 'string' ? file : file?.file_path || file?.path);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function thinkingKind(text) {
|
|
422
|
+
return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
|
|
423
|
+
? 'Reading'
|
|
424
|
+
: 'Thinking';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function thinkingPrefix(text) {
|
|
428
|
+
const kind = thinkingKind(text);
|
|
429
|
+
return kind === 'Thinking' ? 'Thinking' : `Thinking · ${kind}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function clippedThinking(text, limit = 200) {
|
|
433
|
+
const value = String(text || '');
|
|
434
|
+
return value.length > limit ? `${value.slice(0, limit - 2)} …` : value;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Live Spinner ──
|
|
438
|
+
// A real animated spinner that ticks on an interval, not just per-call.
|
|
439
|
+
// Shows what's happening right now — thinking, tool executing, etc.
|
|
440
|
+
|
|
441
|
+
export const SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
442
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
443
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
444
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
445
|
+
|
|
446
|
+
export function startSpinner(text) {
|
|
447
|
+
runtime.spinText = text;
|
|
448
|
+
runtime.spinFrame = 0;
|
|
449
|
+
if (runtime.exploreRun && runtime.exploreRun.lineActive && isInputDockMounted()) return;
|
|
450
|
+
if (runtime.spinInterval) return; // already running
|
|
451
|
+
runtime.spinInterval = setInterval(() => {
|
|
452
|
+
// While an explore run is active, its live counts own the spinner
|
|
453
|
+
// line — re-fetch on every tick so the text stays current no matter
|
|
454
|
+
// which handler happened to call startSpinner/updateSpinner last.
|
|
455
|
+
const isExploreActive = runtime.exploreRun && runtime.exploreRun.lineActive;
|
|
456
|
+
const label = isExploreActive ? exploreSummary() : runtime.spinText;
|
|
457
|
+
if (!label) return;
|
|
458
|
+
const frame = SPIN_FRAMES[runtime.spinFrame % SPIN_FRAMES.length];
|
|
459
|
+
runtime.spinFrame++;
|
|
460
|
+
const rendered = ` ${c.brand(frame)} ${c.dim(label)}`;
|
|
461
|
+
if (isExploreActive && isInputDockMounted()) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (isInputDockMounted()) moveToContent();
|
|
465
|
+
inPlace(rendered);
|
|
466
|
+
}, 80);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function updateSpinner(text) {
|
|
470
|
+
runtime.spinText = text;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function stopSpinner() {
|
|
474
|
+
// Explore owns the line while active — a stray stopSpinner from a
|
|
475
|
+
// transient handler must not blank the progress feedback.
|
|
476
|
+
// flushExploreRun() releases the lock and does the real teardown.
|
|
477
|
+
if (runtime.exploreRun && runtime.exploreRun.lineActive) return;
|
|
478
|
+
if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
|
|
479
|
+
runtime.spinText = '';
|
|
480
|
+
if (isInputDockMounted()) moveToContent();
|
|
481
|
+
inPlace('');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Content Streaming Display ──
|
|
485
|
+
|
|
486
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
487
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
488
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
489
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
490
|
+
// (declaration moved to repl-state.mjs runtime.*)
|
|
491
|
+
|
|
492
|
+
export function startContentStream() {
|
|
493
|
+
runtime.streamBuffer = '';
|
|
494
|
+
runtime.streamedPartialText = '';
|
|
495
|
+
runtime.renderedToolResults.clear();
|
|
496
|
+
runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
|
|
497
|
+
runtime.renderedContentThisTurn = false;
|
|
498
|
+
runtime.contentHeaderPrinted = false;
|
|
499
|
+
runtime.lastRenderedBlock = null;
|
|
500
|
+
stopSpinner();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function appendContent(text) {
|
|
504
|
+
if (!text) return;
|
|
505
|
+
// Any streamed content between renderToolCall and renderToolResult would
|
|
506
|
+
// scroll the head off "the line above", breaking the in-place collapse.
|
|
507
|
+
clearPendingHead();
|
|
508
|
+
runtime.streamBuffer += text;
|
|
509
|
+
runtime.streamedPartialText += text;
|
|
510
|
+
|
|
511
|
+
// Debounce rendering to avoid flicker on rapid partial updates
|
|
512
|
+
if (runtime.streamTimer) clearTimeout(runtime.streamTimer);
|
|
513
|
+
runtime.streamTimer = setTimeout(() => flushContent(), 50);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function flushContent() {
|
|
517
|
+
if (runtime.streamTimer) { clearTimeout(runtime.streamTimer); runtime.streamTimer = null; }
|
|
518
|
+
if (!runtime.streamBuffer) return;
|
|
519
|
+
|
|
520
|
+
if (isInputDockMounted()) moveToContent();
|
|
521
|
+
stopSpinner();
|
|
522
|
+
// Any buffered tool head needs to land BEFORE this content so the order
|
|
523
|
+
// is preserved on screen.
|
|
524
|
+
flushPendingHead();
|
|
525
|
+
flushCompactReadRun();
|
|
526
|
+
renderBlockBoundary('content');
|
|
527
|
+
if (!runtime.contentHeaderPrinted) {
|
|
528
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
529
|
+
runtime.contentHeaderPrinted = true;
|
|
530
|
+
}
|
|
531
|
+
const rendered = renderMarkdown(runtime.streamBuffer);
|
|
532
|
+
for (const line of rendered.split('\n')) {
|
|
533
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
534
|
+
}
|
|
535
|
+
runtime.streamBuffer = '';
|
|
536
|
+
runtime.renderedContentThisTurn = true;
|
|
537
|
+
runtime.lastRenderedBlock = 'content';
|
|
538
|
+
if (typeof runtime.afterContentFlush === 'function') runtime.afterContentFlush();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function renderStagnation(data = {}) {
|
|
542
|
+
const rawMessage = data?.message || '';
|
|
543
|
+
const reason = data?.reason || rawMessage.replace(/^Stagnation:\s*/i, '').trim();
|
|
544
|
+
const tool = data?.tool || data?.tool_name || '';
|
|
545
|
+
const suggestion = data?.suggestion || data?.recovery_strategy || data?.strategy || '';
|
|
546
|
+
const message = reason
|
|
547
|
+
? `Stagnation${tool ? ` (${tool})` : ''}: ${reason}`
|
|
548
|
+
: `Stagnation${tool ? ` (${tool})` : ''} detected`;
|
|
549
|
+
const key = `${message}\n${suggestion}`;
|
|
550
|
+
|
|
551
|
+
if (session._lastStagnationWarning === key) return;
|
|
552
|
+
session._lastStagnationWarning = key;
|
|
553
|
+
|
|
554
|
+
stopSpinner();
|
|
555
|
+
flushContent();
|
|
556
|
+
flushPendingHead();
|
|
557
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
558
|
+
process.stderr.write(` ${c.yellow('!')} ${c.yellow(message)}\n`);
|
|
559
|
+
if (suggestion) process.stderr.write(` ${c.dim(suggestion)}\n`);
|
|
560
|
+
runtime.lastRenderedBlock = 'status';
|
|
561
|
+
}
|