@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.
Files changed (278) hide show
  1. package/README.md +93 -0
  2. package/package.json +56 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +223 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +312 -0
  136. package/src/commands/agent.mjs +221 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +202 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/model-catalog.mjs +57 -0
  143. package/src/config/settings-loader.mjs +45 -0
  144. package/src/config/settings.mjs +132 -0
  145. package/src/context/ast-parser.mjs +298 -0
  146. package/src/context/bm25.mjs +85 -0
  147. package/src/context/prose-chunker.mjs +255 -0
  148. package/src/context/retriever.mjs +425 -0
  149. package/src/context/skeleton.mjs +134 -0
  150. package/src/context/symbol-indexer.mjs +375 -0
  151. package/src/core/agent-history.mjs +111 -0
  152. package/src/core/agent-loop.mjs +486 -0
  153. package/src/core/approval-log.mjs +145 -0
  154. package/src/core/approval.mjs +700 -0
  155. package/src/core/attachments.mjs +666 -0
  156. package/src/core/backend-url.mjs +68 -0
  157. package/src/core/bundled-runtime.mjs +418 -0
  158. package/src/core/cache-control.mjs +92 -0
  159. package/src/core/cache.mjs +105 -0
  160. package/src/core/callback-client.mjs +180 -0
  161. package/src/core/checkpoints.mjs +142 -0
  162. package/src/core/compact-history.mjs +127 -0
  163. package/src/core/context-envelope.mjs +54 -0
  164. package/src/core/context-manager.mjs +198 -0
  165. package/src/core/error-guidance.mjs +331 -0
  166. package/src/core/file-diff.mjs +217 -0
  167. package/src/core/headless.mjs +460 -0
  168. package/src/core/hooks-manager.mjs +87 -0
  169. package/src/core/jsonl-writer.mjs +449 -0
  170. package/src/core/local-agent.mjs +538 -0
  171. package/src/core/local-store.mjs +836 -0
  172. package/src/core/mode-selector.mjs +51 -0
  173. package/src/core/output-filter.mjs +177 -0
  174. package/src/core/paths.mjs +190 -0
  175. package/src/core/policy-resolver.mjs +156 -0
  176. package/src/core/pricing.mjs +336 -0
  177. package/src/core/project-artifacts.mjs +39 -0
  178. package/src/core/project-context-loader.mjs +139 -0
  179. package/src/core/providers.mjs +219 -0
  180. package/src/core/rate-limit-display.mjs +121 -0
  181. package/src/core/rate-limiter.mjs +119 -0
  182. package/src/core/resume-mode.mjs +192 -0
  183. package/src/core/risk-tier.mjs +388 -0
  184. package/src/core/safety.mjs +260 -0
  185. package/src/core/scheduler.mjs +173 -0
  186. package/src/core/session-manager.mjs +360 -0
  187. package/src/core/session.mjs +143 -0
  188. package/src/core/settings-sync.mjs +85 -0
  189. package/src/core/stagnation.mjs +57 -0
  190. package/src/core/stream-client.mjs +957 -0
  191. package/src/core/streaming.mjs +182 -0
  192. package/src/core/system-prompt.mjs +140 -0
  193. package/src/core/tasks.mjs +196 -0
  194. package/src/core/tool-executor.mjs +2231 -0
  195. package/src/core/trust.mjs +160 -0
  196. package/src/core/work-scope.mjs +248 -0
  197. package/src/hooks/engine.mjs +162 -0
  198. package/src/mcp/client.mjs +253 -0
  199. package/src/mcp/transport-shttp.mjs +130 -0
  200. package/src/mcp/transport-sse.mjs +131 -0
  201. package/src/mcp/transport-ws.mjs +134 -0
  202. package/src/onboarding/preflight.mjs +374 -0
  203. package/src/permissions/checker.mjs +57 -0
  204. package/src/permissions/command-classifier.mjs +700 -0
  205. package/src/permissions/injection-check.mjs +60 -0
  206. package/src/permissions/path-check.mjs +102 -0
  207. package/src/permissions/prompt.mjs +73 -0
  208. package/src/permissions/sandbox.mjs +112 -0
  209. package/src/plugins/loader.mjs +138 -0
  210. package/src/skills/installer.mjs +188 -0
  211. package/src/skills/loader.mjs +252 -0
  212. package/src/skills/runner.mjs +55 -0
  213. package/src/state/orbit.mjs +263 -0
  214. package/src/state/verbosity.mjs +99 -0
  215. package/src/telemetry/index.mjs +122 -0
  216. package/src/terminal/agents.mjs +353 -0
  217. package/src/terminal/analytics.mjs +292 -0
  218. package/src/terminal/ansi.mjs +695 -0
  219. package/src/terminal/init.mjs +145 -0
  220. package/src/terminal/main.mjs +310 -0
  221. package/src/terminal/repl-ask-form.mjs +120 -0
  222. package/src/terminal/repl-explore.mjs +44 -0
  223. package/src/terminal/repl-format.mjs +317 -0
  224. package/src/terminal/repl-model-form.mjs +132 -0
  225. package/src/terminal/repl-render.mjs +833 -0
  226. package/src/terminal/repl-resume.mjs +640 -0
  227. package/src/terminal/repl-state.mjs +120 -0
  228. package/src/terminal/repl-utils.mjs +34 -0
  229. package/src/terminal/repl.mjs +5032 -0
  230. package/src/terminal/skills.mjs +54 -0
  231. package/src/terminal/tool-display.mjs +392 -0
  232. package/src/tools/agent.mjs +137 -0
  233. package/src/tools/ask-user.mjs +61 -0
  234. package/src/tools/bash.mjs +231 -0
  235. package/src/tools/cron-create.mjs +120 -0
  236. package/src/tools/cron-delete.mjs +49 -0
  237. package/src/tools/cron-list.mjs +37 -0
  238. package/src/tools/edit.mjs +82 -0
  239. package/src/tools/enter-worktree.mjs +69 -0
  240. package/src/tools/exit-worktree.mjs +57 -0
  241. package/src/tools/glob.mjs +117 -0
  242. package/src/tools/grep.mjs +129 -0
  243. package/src/tools/lint.mjs +71 -0
  244. package/src/tools/ls.mjs +58 -0
  245. package/src/tools/lsp.mjs +115 -0
  246. package/src/tools/multi-edit.mjs +94 -0
  247. package/src/tools/notebook-edit.mjs +96 -0
  248. package/src/tools/project-overview.mjs +703 -0
  249. package/src/tools/read-mcp-resource.mjs +57 -0
  250. package/src/tools/read.mjs +138 -0
  251. package/src/tools/registry.mjs +116 -0
  252. package/src/tools/remote-trigger.mjs +84 -0
  253. package/src/tools/send-message.mjs +64 -0
  254. package/src/tools/skill.mjs +52 -0
  255. package/src/tools/test-runner.mjs +49 -0
  256. package/src/tools/todo-write.mjs +68 -0
  257. package/src/tools/tool-search.mjs +77 -0
  258. package/src/tools/web-fetch.mjs +65 -0
  259. package/src/tools/web-search.mjs +89 -0
  260. package/src/tools/write.mjs +55 -0
  261. package/src/ui/approval.mjs +510 -0
  262. package/src/ui/banner.mjs +232 -0
  263. package/src/ui/commands.mjs +537 -0
  264. package/src/ui/formatter.mjs +409 -0
  265. package/src/ui/icons.mjs +170 -0
  266. package/src/ui/input-dock.mjs +772 -0
  267. package/src/ui/markdown.mjs +278 -0
  268. package/src/ui/mission-report.mjs +296 -0
  269. package/src/ui/palette.mjs +189 -0
  270. package/src/ui/render-queue.mjs +500 -0
  271. package/src/ui/slash-commands.mjs +257 -0
  272. package/src/ui/spinner.mjs +116 -0
  273. package/src/ui/sub-agent.mjs +167 -0
  274. package/src/ui/term.mjs +174 -0
  275. package/src/ui/text-layout.mjs +127 -0
  276. package/src/ui/tool-card.mjs +740 -0
  277. package/src/ui/tool-details.mjs +504 -0
  278. 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
+ }