@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,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure display formatters used by repl.mjs.
|
|
3
|
+
*
|
|
4
|
+
* Extracted so the giant repl.mjs shrinks and the pieces become unit-
|
|
5
|
+
* testable. Everything here is a pure function — no runtime state, no
|
|
6
|
+
* mutable side effects beyond the color helpers from ansi.mjs (which
|
|
7
|
+
* themselves just wrap strings in ANSI codes).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { c, stripAnsi, formatElapsed, inPlace } from './ansi.mjs';
|
|
12
|
+
import * as rqueue from '../ui/render-queue.mjs';
|
|
13
|
+
|
|
14
|
+
// Transient one-line status writer that is safe under the render queue.
|
|
15
|
+
// Raw inPlace() writes get REDIRECTED into transcript content when the
|
|
16
|
+
// queue is active (cursor codes stripped) — that leaked one line per
|
|
17
|
+
// spinner frame during resume summarization. queue.status() coalesces
|
|
18
|
+
// and overwrites in place; inPlace stays as the no-queue fallback.
|
|
19
|
+
function transientLine(text) {
|
|
20
|
+
if (rqueue.isActive()) {
|
|
21
|
+
if (text) rqueue.status(text);
|
|
22
|
+
else rqueue.clearStatus();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
inPlace(text);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Atomic repaint writer for raw-stdin overlays (resume picker, /model form).
|
|
30
|
+
*
|
|
31
|
+
* While the render queue is active, plain process.stderr.write is REDIRECTED
|
|
32
|
+
* into transcript content with cursor codes stripped — an overlay's
|
|
33
|
+
* "cursor-up N + erase" repaint becomes "append another copy" (the form-
|
|
34
|
+
* replication bug). Each repaint therefore goes through rqueue.raw() as one
|
|
35
|
+
* frame. On the first paint the frame's rows are reserved with newlines so
|
|
36
|
+
* painting near the bottom (input dock) scrolls once up-front and the
|
|
37
|
+
* cursor-relative repaint math stays stable afterwards.
|
|
38
|
+
*/
|
|
39
|
+
export function writeOverlayFrame(erasePrev, lines) {
|
|
40
|
+
const body = lines.join('\n') + '\n';
|
|
41
|
+
if (erasePrev > 0) {
|
|
42
|
+
rqueue.raw(`\x1b[${erasePrev}F\r\x1b[J` + body);
|
|
43
|
+
} else {
|
|
44
|
+
rqueue.raw('\n'.repeat(lines.length) + `\x1b[${lines.length}A` + body);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Erase the previously drawn overlay frame (rows lines tall) so the next
|
|
50
|
+
* prompt / transcript output starts on a clean line. Call from every
|
|
51
|
+
* overlay's cleanup path — without this, picker frames stack on screen
|
|
52
|
+
* when a follow-up prompt (cwd confirm, next overlay) writes below them.
|
|
53
|
+
*/
|
|
54
|
+
export function eraseOverlayFrame(rows, summaryLine = '') {
|
|
55
|
+
if (!rows || rows <= 0) {
|
|
56
|
+
if (summaryLine) rqueue.raw(summaryLine + '\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Collapse the frame to a compact one-line summary (or nothing). A pure
|
|
60
|
+
// erase leaves a blank void mid-screen because the input dock parks the
|
|
61
|
+
// cursor at the bottom before the next plain write lands.
|
|
62
|
+
rqueue.raw(`\x1b[${rows}F\r\x1b[J` + (summaryLine ? summaryLine + '\n' : ''));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── One-liners ───────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function messageCountLabel(count) {
|
|
68
|
+
return `${count} ${count === 1 ? 'message' : 'messages'}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sessionListTimestamp(s) {
|
|
72
|
+
const value = s.updatedAt || s.startedAt;
|
|
73
|
+
return value ? new Date(value).toLocaleString() : '?';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function oneLineInstruction(text, max = 72) {
|
|
77
|
+
const compact = String(text || '(no instruction)').replace(/\s+/g, ' ').trim();
|
|
78
|
+
return compact.length > max ? compact.slice(0, max - 3) + '...' : compact;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Truncate `text` to `maxColumns` visible columns, preserving ANSI
|
|
83
|
+
* escape sequences and appending a dim ellipsis when truncation happens.
|
|
84
|
+
*/
|
|
85
|
+
export function fitAnsiLine(text, maxColumns) {
|
|
86
|
+
const max = Math.max(1, Number(maxColumns) || 80);
|
|
87
|
+
const value = String(text || '');
|
|
88
|
+
if (stripAnsi(value).length <= max) return value;
|
|
89
|
+
|
|
90
|
+
let visible = 0;
|
|
91
|
+
let out = '';
|
|
92
|
+
for (let i = 0; i < value.length; i++) {
|
|
93
|
+
if (value[i] === '\x1b') {
|
|
94
|
+
const match = value.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
95
|
+
if (match) {
|
|
96
|
+
out += match[0];
|
|
97
|
+
i += match[0].length - 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (visible >= max - 1) break;
|
|
102
|
+
out += value[i];
|
|
103
|
+
visible++;
|
|
104
|
+
}
|
|
105
|
+
return `${out}${c.dim('…')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── PRD-068 §5.14 helpers ────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function endStatusMarker(status) {
|
|
111
|
+
switch (status) {
|
|
112
|
+
case 'completed': return c.green('✓');
|
|
113
|
+
case 'interrupted': return c.yellow('⚠');
|
|
114
|
+
case 'errored': return c.red('✗');
|
|
115
|
+
default: return c.dim('·');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatSessionCost(usd) {
|
|
120
|
+
const n = Number(usd);
|
|
121
|
+
if (!Number.isFinite(n) || n <= 0) return c.dim(' ');
|
|
122
|
+
if (n < 0.01) return c.dim('<$0.01');
|
|
123
|
+
return c.dim(`$${n.toFixed(2)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatRelativeTime(iso) {
|
|
127
|
+
if (!iso) return '';
|
|
128
|
+
const t = Date.parse(iso);
|
|
129
|
+
if (!Number.isFinite(t)) return '';
|
|
130
|
+
const ago = Date.now() - t;
|
|
131
|
+
const m = Math.floor(ago / 60_000);
|
|
132
|
+
if (m < 1) return 'just now';
|
|
133
|
+
if (m < 60) return `${m}m ago`;
|
|
134
|
+
const h = Math.floor(m / 60);
|
|
135
|
+
if (h < 24) return `${h}h ago`;
|
|
136
|
+
const d = Math.floor(h / 24);
|
|
137
|
+
if (d < 30) return `${d}d ago`;
|
|
138
|
+
const mo = Math.floor(d / 30);
|
|
139
|
+
return `${mo}mo ago`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Resume-mode helpers ──────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export function resumeTailTurnCount(mode = '') {
|
|
145
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
146
|
+
if (!match) return null;
|
|
147
|
+
return Math.max(1, Number(match[1]) || 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resumeModeLabel(mode = 'full') {
|
|
151
|
+
if (mode === 'checkpoint-full') return 'checkpointed transcript';
|
|
152
|
+
if (mode === 'summary') return 'summary only';
|
|
153
|
+
const tailTurns = resumeTailTurnCount(mode);
|
|
154
|
+
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
155
|
+
return mode || 'full';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function replayStartOrderForMode(history = [], mode = '') {
|
|
159
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
160
|
+
if (!match) return null;
|
|
161
|
+
const wanted = Math.max(1, Number(match[1]) || 1);
|
|
162
|
+
let seen = 0;
|
|
163
|
+
const userTurns = history.filter(m => m.role === 'user' && typeof m.content === 'string');
|
|
164
|
+
for (let i = userTurns.length - 1; i >= 0; i--) {
|
|
165
|
+
seen++;
|
|
166
|
+
if (seen >= wanted) {
|
|
167
|
+
const order = Number(userTurns[i].order);
|
|
168
|
+
return Number.isFinite(order) ? order : null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function filterResumeReplayEvents(events = []) {
|
|
175
|
+
return events.filter(item => {
|
|
176
|
+
const type = item?.event?.type;
|
|
177
|
+
return !['status', 'session_info', 'complete', 'resumed', 'paused'].includes(type);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mergeResumeReplayItems(userTurns = [], replayEvents = []) {
|
|
182
|
+
const items = [];
|
|
183
|
+
let order = 0;
|
|
184
|
+
for (const message of userTurns) {
|
|
185
|
+
items.push({
|
|
186
|
+
kind: 'user',
|
|
187
|
+
message,
|
|
188
|
+
fileOrder: Number.isFinite(Number(message.order)) ? Number(message.order) : null,
|
|
189
|
+
order: order++,
|
|
190
|
+
time: Date.parse(message.timestamp || '') || 0,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
for (const event of replayEvents) {
|
|
194
|
+
items.push({
|
|
195
|
+
kind: 'event',
|
|
196
|
+
event,
|
|
197
|
+
fileOrder: Number.isFinite(Number(event.order)) ? Number(event.order) : null,
|
|
198
|
+
order: order++,
|
|
199
|
+
time: Date.parse(event.timestamp || '') || 0,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return items.sort((a, b) => {
|
|
203
|
+
if (a.fileOrder !== null && b.fileOrder !== null && a.fileOrder !== b.fileOrder) {
|
|
204
|
+
return a.fileOrder - b.fileOrder;
|
|
205
|
+
}
|
|
206
|
+
if (a.fileOrder !== null && b.fileOrder === null) return -1;
|
|
207
|
+
if (a.fileOrder === null && b.fileOrder !== null) return 1;
|
|
208
|
+
const at = a.time || Number.MAX_SAFE_INTEGER;
|
|
209
|
+
const bt = b.time || Number.MAX_SAFE_INTEGER;
|
|
210
|
+
return at - bt || a.order - b.order;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function resumeProgressBar(percent, width = 12) {
|
|
215
|
+
const p = Math.max(0, Math.min(100, Math.round(percent)));
|
|
216
|
+
const filled = Math.round((p / 100) * width);
|
|
217
|
+
return `${c.brand('█'.repeat(filled))}${c.gray('░'.repeat(width - filled))} ${String(p).padStart(3)}%`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Start an animated "resuming…" progress line. Returns { update, stop }.
|
|
222
|
+
* The caller updates the label + percent as the resume flow advances and
|
|
223
|
+
* calls stop() when done. inPlace-based render, so it lives on one line.
|
|
224
|
+
*/
|
|
225
|
+
export function startResumeProgress(mode = 'full') {
|
|
226
|
+
let percent = 8;
|
|
227
|
+
let label = `resuming as ${resumeModeLabel(mode)}`;
|
|
228
|
+
let active = true;
|
|
229
|
+
const started = Date.now();
|
|
230
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
231
|
+
let frame = 0;
|
|
232
|
+
|
|
233
|
+
const render = () => {
|
|
234
|
+
if (!active) return;
|
|
235
|
+
const glyph = frames[frame % frames.length];
|
|
236
|
+
frame++;
|
|
237
|
+
transientLine(` ${c.brand(glyph)} ${c.dim(label)} ${resumeProgressBar(percent)} ${c.dim(formatElapsed(started))}`);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
render();
|
|
241
|
+
let timer = setInterval(render, 100);
|
|
242
|
+
return {
|
|
243
|
+
// Self-healing: the resume flow stops the progress line to show the
|
|
244
|
+
// cwd-confirm overlay, then keeps reporting phases ('rebuilding local
|
|
245
|
+
// session state', …). A dead update() silently dropped every cue after
|
|
246
|
+
// that prompt — revive the ticker instead (same pattern as
|
|
247
|
+
// updateSpinner in repl-render.mjs).
|
|
248
|
+
update(nextLabel, nextPercent) {
|
|
249
|
+
if (nextLabel) label = nextLabel;
|
|
250
|
+
if (Number.isFinite(nextPercent)) percent = Math.max(percent, Math.min(98, nextPercent));
|
|
251
|
+
if (!active) {
|
|
252
|
+
active = true;
|
|
253
|
+
timer = setInterval(render, 100);
|
|
254
|
+
}
|
|
255
|
+
render();
|
|
256
|
+
},
|
|
257
|
+
stop() {
|
|
258
|
+
if (!active) return;
|
|
259
|
+
active = false;
|
|
260
|
+
clearInterval(timer);
|
|
261
|
+
transientLine('');
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Normalize a raw session record (from getRecentSessions or persistence)
|
|
268
|
+
* into the shape the resume picker expects. Pure data transform.
|
|
269
|
+
*/
|
|
270
|
+
export function normalizeResumableSession(s) {
|
|
271
|
+
return {
|
|
272
|
+
sessionId: s.sessionId,
|
|
273
|
+
instruction: s.firstPrompt || s.instruction || '(no instruction)',
|
|
274
|
+
startedAt: s.startTime || s.startedAt || '',
|
|
275
|
+
updatedAt: s.endTime || s.updatedAt || (s.mtime ? new Date(s.mtime).toISOString() : ''),
|
|
276
|
+
project: s.project ? path.basename(s.project) : s.projectName || s.project || '',
|
|
277
|
+
projectPath: s.project || s.projectPath || '',
|
|
278
|
+
transcriptPath: s.filePath || s.transcriptPath || '',
|
|
279
|
+
messageCount: (s.userMessages || 0) + (s.assistantMessages || 0),
|
|
280
|
+
// PRD-068 §5.14.11 derived fields for the picker
|
|
281
|
+
endStatus: s.endStatus || 'unknown', // 'completed' | 'interrupted' | 'errored' | 'unknown'
|
|
282
|
+
contextTokens: s.contextTokens || 0, // projected transcript token count
|
|
283
|
+
contextTokenSource: s.contextTokenSource || 'jsonl_bytes',
|
|
284
|
+
resumeSummary: s.resumeSummary || null, // latest resume_summary checkpoint metadata
|
|
285
|
+
models: Array.isArray(s.models) ? s.models : [],
|
|
286
|
+
modelLimits: s.modelLimits && typeof s.modelLimits === 'object' ? s.modelLimits : {},
|
|
287
|
+
costUsd: typeof s.costUsd === 'number' ? s.costUsd : 0,
|
|
288
|
+
partial: !!s.partial, // true if the transcript file was partially malformed
|
|
289
|
+
source: 'transcript',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Speaker prefix label for a transcript entry.
|
|
295
|
+
*/
|
|
296
|
+
export function historyRoleLabel(role) {
|
|
297
|
+
return role === 'user'
|
|
298
|
+
? c.white('You')
|
|
299
|
+
: role === 'tool'
|
|
300
|
+
? c.dim('Tool')
|
|
301
|
+
: c.brand('bahulam');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Render the tail of a conversation transcript to stderr. Used by
|
|
306
|
+
* /history and /resume preview flows.
|
|
307
|
+
*/
|
|
308
|
+
export function renderHistoryEntries(entries, { limit = 20, maxChars = 120, title = 'Conversation' } = {}) {
|
|
309
|
+
const shown = limit === Infinity ? entries : entries.slice(-limit);
|
|
310
|
+
process.stderr.write(`\n ${c.bold(title)} (${shown.length}${shown.length === entries.length ? '' : ` of ${entries.length}`} entries)\n`);
|
|
311
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
312
|
+
for (const msg of shown) {
|
|
313
|
+
const content = String(msg.content || '').replace(/\s+/g, ' ').trim();
|
|
314
|
+
process.stderr.write(` ${historyRoleLabel(msg.role)}: ${content.slice(0, maxChars)}${content.length > maxChars ? '...' : ''}\n`);
|
|
315
|
+
}
|
|
316
|
+
process.stderr.write('\n');
|
|
317
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive per-role model form for /model (PRD-076 W7).
|
|
3
|
+
*
|
|
4
|
+
* ↑↓ picks a role row, ←→ cycles through [backend default] + the curated
|
|
5
|
+
* platform catalog for that role, Enter applies to session overrides,
|
|
6
|
+
* c resets every row to default, Esc cancels. Same raw-stdin overlay
|
|
7
|
+
* pattern as the resume picker (repl-resume.mjs): pause readline, raw
|
|
8
|
+
* mode on, redraw in place with cursor-up + erase-down, restore on exit.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { c } from './ansi.mjs';
|
|
12
|
+
import { fitAnsiLine, writeOverlayFrame, eraseOverlayFrame } from './repl-format.mjs';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SENTINEL = '__default__';
|
|
15
|
+
|
|
16
|
+
function creditBadge(row) {
|
|
17
|
+
const usd = Number(row?.input_cost_usd_per_m);
|
|
18
|
+
if (!Number.isFinite(usd) || usd <= 0) return '';
|
|
19
|
+
const credits = usd * 200; // credits = provider cost × 2 × 100/USD
|
|
20
|
+
return `~${credits < 10 ? credits.toFixed(1) : String(Math.round(credits))} cr/M`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {object|null} opts.rl readline instance to pause/resume
|
|
26
|
+
* @param {Array} opts.roles [{ role, label, current, defaultLabel }]
|
|
27
|
+
* @param {Array} opts.catalog raw /api/models rows (may be empty)
|
|
28
|
+
* @param {Array} [opts.fallbackIds] model ids to cycle when no curated catalog
|
|
29
|
+
* @param {string} [opts.unavailableNote] why the catalog is missing (shown in header)
|
|
30
|
+
* @returns {Promise<{overrides: Record<string,string>}|null>} null = cancelled
|
|
31
|
+
*/
|
|
32
|
+
export async function pickModelOverridesForm({ rl, roles, catalog, fallbackIds, unavailableNote }) {
|
|
33
|
+
if (!process.stdin.isTTY) return null;
|
|
34
|
+
if (rl) rl.pause();
|
|
35
|
+
|
|
36
|
+
const curated = (catalog || []).filter(m => m?.harness_validated && m?.id);
|
|
37
|
+
const usingFallback = curated.length === 0;
|
|
38
|
+
const optionIds = usingFallback
|
|
39
|
+
? [...new Set((fallbackIds || []).filter(Boolean))]
|
|
40
|
+
: curated.map(m => m.id);
|
|
41
|
+
const baseOptions = [DEFAULT_SENTINEL, ...optionIds];
|
|
42
|
+
const byId = new Map(curated.map(m => [m.id, m]));
|
|
43
|
+
|
|
44
|
+
// Per-row option list; a current override that isn't in the curated list
|
|
45
|
+
// is appended so it stays visible and selectable.
|
|
46
|
+
const rows = roles.map(r => {
|
|
47
|
+
let opts = baseOptions;
|
|
48
|
+
let idx = 0;
|
|
49
|
+
if (r.current) {
|
|
50
|
+
const found = baseOptions.indexOf(r.current);
|
|
51
|
+
if (found >= 0) {
|
|
52
|
+
idx = found;
|
|
53
|
+
} else {
|
|
54
|
+
opts = [...baseOptions, r.current];
|
|
55
|
+
idx = opts.length - 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { ...r, opts, idx };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return await new Promise((resolve) => {
|
|
62
|
+
const wasRaw = process.stdin.isRaw;
|
|
63
|
+
let cursor = 0;
|
|
64
|
+
let renderedLines = 0;
|
|
65
|
+
|
|
66
|
+
const valueLabel = (row) => {
|
|
67
|
+
const value = row.opts[row.idx];
|
|
68
|
+
if (value === DEFAULT_SENTINEL) {
|
|
69
|
+
return c.dim(row.defaultLabel ? `default · ${row.defaultLabel}` : 'backend default');
|
|
70
|
+
}
|
|
71
|
+
const meta = byId.get(value);
|
|
72
|
+
const badge = meta ? creditBadge(meta) : '';
|
|
73
|
+
// Only flag uncurated picks when a curated catalog actually loaded —
|
|
74
|
+
// in fallback mode every option is a known backend model, not a stray.
|
|
75
|
+
const flag = meta || usingFallback ? '' : c.yellow(' (uncurated)');
|
|
76
|
+
return `${c.brand(value)}${badge ? ` ${c.dim(badge)}` : ''}${flag}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const render = () => {
|
|
80
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(` ${c.bold('Models')} ${c.dim('· session overrides · curated platform catalog')}`);
|
|
83
|
+
if (usingFallback) {
|
|
84
|
+
const why = unavailableNote ? ` — ${unavailableNote}` : '';
|
|
85
|
+
lines.push(` ${c.yellow('!')} ${c.dim(`catalog unavailable${why}; showing this session's backend models`)}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
rows.forEach((row, i) => {
|
|
89
|
+
const marker = i === cursor ? c.brand('▸') : ' ';
|
|
90
|
+
const rawLabel = String(row.label || row.role).padEnd(14, ' ');
|
|
91
|
+
const label = i === cursor ? c.brand(rawLabel) : rawLabel;
|
|
92
|
+
lines.push(fitAnsiLine(` ${marker} ${label} ${c.dim('‹')} ${valueLabel(row)} ${c.dim('›')}`, cols - 1));
|
|
93
|
+
});
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(fitAnsiLine(` ${c.dim('↑↓ role · ←→ model · Enter apply · c defaults · Esc cancel')}`, cols - 1));
|
|
96
|
+
writeOverlayFrame(renderedLines, lines);
|
|
97
|
+
renderedLines = lines.length;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const cleanup = (value) => {
|
|
101
|
+
process.stdin.removeListener('data', onData);
|
|
102
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
103
|
+
eraseOverlayFrame(renderedLines);
|
|
104
|
+
if (rl) rl.resume();
|
|
105
|
+
resolve(value);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onData = (data) => {
|
|
109
|
+
const key = data.toString('utf8');
|
|
110
|
+
if (key === '\x1b' || key === '\x03' || key === 'q') { cleanup(null); return; }
|
|
111
|
+
if (key === '\r' || key === '\n') {
|
|
112
|
+
const overrides = {};
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
const value = row.opts[row.idx];
|
|
115
|
+
if (value && value !== DEFAULT_SENTINEL) overrides[row.role] = value;
|
|
116
|
+
}
|
|
117
|
+
cleanup({ overrides });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key === 'c' || key === 'C') { rows.forEach(r => { r.idx = 0; }); render(); return; }
|
|
121
|
+
if (key === '\x1b[A') { cursor = Math.max(0, cursor - 1); render(); return; }
|
|
122
|
+
if (key === '\x1b[B') { cursor = Math.min(rows.length - 1, cursor + 1); render(); return; }
|
|
123
|
+
if (key === '\x1b[D') { const r = rows[cursor]; r.idx = (r.idx - 1 + r.opts.length) % r.opts.length; render(); return; }
|
|
124
|
+
if (key === '\x1b[C') { const r = rows[cursor]; r.idx = (r.idx + 1) % r.opts.length; render(); return; }
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
process.stdin.setRawMode(true);
|
|
128
|
+
process.stdin.resume();
|
|
129
|
+
process.stdin.on('data', onData);
|
|
130
|
+
render();
|
|
131
|
+
});
|
|
132
|
+
}
|