@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.
Files changed (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -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 +222 -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 +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -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/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,625 @@
1
+ /**
2
+ * Resume / session-picker flow.
3
+ *
4
+ * Extracted from repl.mjs. Handles listing previous sessions, previewing
5
+ * one before resuming, mode selection (full / summary / tail-N /
6
+ * checkpoint-full), cwd confirmation, and the /compact runtime.
7
+ *
8
+ * Every function takes an explicit `ctx` for the pieces that only the REPL
9
+ * loop owns (`ctx._rl` readline, `ctx.jsonlWriter`, `ctx.auth`,
10
+ * `ctx.toolExecutor`, `ctx.sessionMgrRef.current`). Shared state (session, orbitRef,
11
+ * safeCwd) is imported directly.
12
+ */
13
+
14
+ import { c } from './ansi.mjs';
15
+ import { session, orbitRef, sessionMgrRef } from './repl-state.mjs';
16
+ import { safeCwd } from './repl-utils.mjs';
17
+ import { startContentStream, flushContent, stopSpinner } from './repl-render.mjs';
18
+ import {
19
+ endStatusMarker,
20
+ filterResumeReplayEvents,
21
+ fitAnsiLine,
22
+ formatRelativeTime,
23
+ formatSessionCost,
24
+ historyRoleLabel,
25
+ mergeResumeReplayItems,
26
+ normalizeResumableSession,
27
+ oneLineInstruction,
28
+ renderHistoryEntries,
29
+ replayStartOrderForMode,
30
+ resumeModeLabel,
31
+ resumeTailTurnCount,
32
+ startResumeProgress,
33
+ } from './repl-format.mjs';
34
+ import { TarangStreamClient } from '../core/stream-client.mjs';
35
+ import { getRecentSessions, getSessionDetail, buildResumeHistory, combineResumeSummaries } from '../core/local-store.mjs';
36
+ import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
37
+ import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
38
+
39
+ export async function listResumableSessions() {
40
+ // PRD-068 §5.14.6: JSONL is the single source of truth. The legacy
41
+ // per-project state-only entries never had a transcript, so they can't be
42
+ // replayed — silently dropping them removes a source of "picked a session
43
+ // and got a flat history" surprises.
44
+ const rich = (await getRecentSessions(Infinity)).map(normalizeResumableSession);
45
+ return rich.sort((a, b) => {
46
+ const at = Date.parse(a.updatedAt || a.startedAt || 0) || 0;
47
+ const bt = Date.parse(b.updatedAt || b.startedAt || 0) || 0;
48
+ return bt - at;
49
+ });
50
+ }
51
+
52
+ // ── PRD-068 §5.14 helpers ────────────────────────────────────────────
53
+
54
+ export function formatResumeCheckpointStatus(session) {
55
+ const marker = session?.resumeSummary;
56
+ if (!marker || !Number(marker.sourceMessageCount)) return '';
57
+ const full = Number(marker.fullMessageCount) || 0;
58
+ const covered = Number(marker.sourceMessageCount) || 0;
59
+ const pct = full > 0 ? ` ${Math.min(100, Math.round((covered / full) * 100))}%` : '';
60
+ return c.dim(` · summarized${pct}`);
61
+ }
62
+
63
+ export function formatResumeContextStatus(session) {
64
+ const full = formatCtxTokens(session?.contextTokens || 0);
65
+ const marker = session?.resumeSummary;
66
+ if (!marker || !Number(marker.sourceMessageCount)) {
67
+ return c.dim(`${full.padStart(5, ' ')} ctx`);
68
+ }
69
+ const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
70
+ resumeSummary: marker,
71
+ }));
72
+ return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
73
+ }
74
+
75
+ export async function pickResumableSession(resumable, ctx) {
76
+ const rl = ctx._rl || null;
77
+ if (rl) rl.pause();
78
+
79
+ return await new Promise((resolve) => {
80
+ if (!process.stdin.isTTY) { resolve(null); return; }
81
+ const wasRaw = process.stdin.isRaw;
82
+ const pageSize = Math.min(10, resumable.length);
83
+ const numWidth = String(resumable.length).length;
84
+ let selected = 0;
85
+ let offset = 0;
86
+ let renderedLines = 0;
87
+
88
+ const renderMenu = () => {
89
+ if (renderedLines > 0) {
90
+ process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
91
+ }
92
+ if (selected < offset) offset = selected;
93
+ if (selected >= offset + pageSize) offset = selected - pageSize + 1;
94
+
95
+ const cols = Math.max(60, process.stderr.columns || 120);
96
+ const lines = [];
97
+ lines.push(` ${c.bold('Resume a session')}`);
98
+ lines.push('');
99
+ const end = Math.min(offset + pageSize, resumable.length);
100
+ for (let i = offset; i < end; i++) {
101
+ const s = resumable[i];
102
+ const marker = i === selected ? c.brand('▸') : ' ';
103
+ const num = c.dim(`[${String(i + 1).padStart(numWidth, ' ')}]`);
104
+ const project = (s.project || '(unknown)').padEnd(18, ' ').slice(0, 18);
105
+ const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
106
+ const status = endStatusMarker(s.endStatus);
107
+ const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
108
+ const ctx = formatResumeContextStatus(s);
109
+ const cost = formatSessionCost(s.costUsd);
110
+ const partial = s.partial ? c.yellow(' ⚠partial') : '';
111
+ const instr = oneLineInstruction(s.instruction, 48);
112
+ lines.push(fitAnsiLine(
113
+ ` ${marker} ${num} ${c.brand(project)} ${c.dim(ago)} ${status} ${c.dim(msgs)} ${ctx} ${cost}${partial} ${c.dim(instr)}`,
114
+ cols - 1
115
+ ));
116
+ }
117
+ lines.push('');
118
+ lines.push(fitAnsiLine(
119
+ ` ${c.dim(`↑↓ move · Enter resume · P preview · Esc cancel · ${selected + 1}/${resumable.length}`)}`,
120
+ cols - 1
121
+ ));
122
+ process.stderr.write(lines.join('\n') + '\n');
123
+ renderedLines = lines.length;
124
+ };
125
+
126
+ const cleanup = (value) => {
127
+ process.stdin.removeListener('data', onData);
128
+ process.stdin.setRawMode(wasRaw || false);
129
+ if (rl) rl.resume();
130
+ resolve(value);
131
+ };
132
+ const onData = (data) => {
133
+ const key = data.toString('utf8');
134
+ if (key === '' || key === '') { cleanup(null); return; }
135
+ if (key === '\r' || key === '\n') { cleanup({ action: 'resume', session: resumable[selected] }); return; }
136
+ if (key === 'p' || key === 'P') { cleanup({ action: 'preview', session: resumable[selected] }); return; }
137
+ if (key === '') { selected = Math.max(0, selected - 1); renderMenu(); return; }
138
+ if (key === '') { selected = Math.min(resumable.length - 1, selected + 1); renderMenu(); return; }
139
+ if (key === '[5~') { selected = Math.max(0, selected - pageSize); renderMenu(); return; }
140
+ if (key === '[6~') { selected = Math.min(resumable.length - 1, selected + pageSize); renderMenu(); return; }
141
+ if (key === '' || key === '[1~') { selected = 0; renderMenu(); return; }
142
+ if (key === '' || key === '[4~') { selected = resumable.length - 1; renderMenu(); return; }
143
+ if (/^[1-9]$/.test(key)) {
144
+ const index = Number(key) - 1;
145
+ if (index < resumable.length) cleanup({ action: 'resume', session: resumable[index] });
146
+ }
147
+ };
148
+
149
+ process.stdin.setRawMode(true);
150
+ process.stdin.resume();
151
+ process.stdin.on('data', onData);
152
+ renderMenu();
153
+ });
154
+ }
155
+
156
+ /**
157
+ * PRD-068 §5.14.4 — tri-choice overlay shown only when projected ctx > highWatermark.
158
+ * Returns 'full' | 'summary' | 'tail-10' | 'tail-20' | null (cancel).
159
+ */
160
+ export async function chooseThresholdMode(ctx, decision) {
161
+ if (!process.stdin.isTTY) return decision.defaultChoice;
162
+ const rl = ctx._rl || null;
163
+ if (rl) rl.pause();
164
+
165
+ const canFull = decision.mode !== 'no-full-allowed';
166
+ const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
167
+ const firstOption = canFull
168
+ ? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
169
+ : hasCheckpoint
170
+ ? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
171
+ : { key: 'f', value: 'full', label: 'full transcript', enabled: false };
172
+ const options = [
173
+ firstOption,
174
+ { key: 's', value: 'summary', label: 'summary only', enabled: true },
175
+ { key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
176
+ { key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
177
+ ];
178
+ let selected = options.findIndex(o => o.value === decision.defaultChoice && o.enabled);
179
+ if (selected < 0) selected = options.findIndex(o => o.enabled);
180
+
181
+ return await new Promise((resolve) => {
182
+ const wasRaw = process.stdin.isRaw;
183
+ let renderedLines = 0;
184
+
185
+ const render = () => {
186
+ if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
187
+ const cols = Math.max(60, process.stderr.columns || 120);
188
+ const pct = Math.round(decision.usageRatio * 100);
189
+ const projected = formatCtxTokens(decision.projected);
190
+ const win = `${formatCtxTokens(decision.windowSize)}${decision.windowKnown ? '' : ' est'}`;
191
+ const lines = [];
192
+ const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
193
+ lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
194
+ if (!decision.windowKnown) {
195
+ lines.push(` ${c.dim('Model context window is a CLI fallback estimate; backend/provider limits may differ.')}`);
196
+ }
197
+ if (decision.resumeSummary?.sourceMessageCount) {
198
+ const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
199
+ const full = Number(decision.resumeSummary.fullMessageCount) || 0;
200
+ const suffix = full > 0 ? ` (${covered}/${full} resume messages)` : '';
201
+ lines.push(` ${c.green('✓')} ${c.dim(`summary checkpoint available${suffix}; summary/tail modes reuse it`)}`);
202
+ }
203
+ lines.push(canFull
204
+ ? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
205
+ : hasCheckpoint
206
+ ? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
207
+ : ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
208
+ lines.push('');
209
+ for (let i = 0; i < options.length; i++) {
210
+ const o = options[i];
211
+ const disabled = !o.enabled;
212
+ const marker = i === selected && !disabled ? c.brand('▸') : ' ';
213
+ const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
214
+ const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
215
+ resumeSummary: decision.resumeSummary,
216
+ }));
217
+ const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
218
+ const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
219
+ const suffix = disabled ? c.dim(' (over hardCap)') : '';
220
+ lines.push(fitAnsiLine(` ${marker} ${keyTag} ${label.padEnd(30, ' ')} ${projCol}${suffix}`, cols - 1));
221
+ }
222
+ lines.push('');
223
+ lines.push(fitAnsiLine(` ${c.dim('↑↓ move · Enter pick · f/s/1/2 shortcut · Esc cancel')}`, cols - 1));
224
+ process.stderr.write(lines.join('\n') + '\n');
225
+ renderedLines = lines.length;
226
+ };
227
+
228
+ const cleanup = (value) => {
229
+ process.stdin.removeListener('data', onData);
230
+ process.stdin.setRawMode(wasRaw || false);
231
+ if (rl) rl.resume();
232
+ resolve(value);
233
+ };
234
+ const onData = (data) => {
235
+ const key = data.toString('utf8');
236
+ const low = key.toLowerCase();
237
+ if (key === '' || key === '') { cleanup(null); return; }
238
+ if (key === '\r' || key === '\n') { cleanup(options[selected]?.value || null); return; }
239
+ if (key === '') {
240
+ // step to previous enabled option
241
+ for (let i = selected - 1; i >= 0; i--) if (options[i].enabled) { selected = i; render(); return; }
242
+ return;
243
+ }
244
+ if (key === '') {
245
+ for (let i = selected + 1; i < options.length; i++) if (options[i].enabled) { selected = i; render(); return; }
246
+ return;
247
+ }
248
+ for (let i = 0; i < options.length; i++) {
249
+ if (options[i].key === low && options[i].enabled) { cleanup(options[i].value); return; }
250
+ }
251
+ };
252
+
253
+ process.stdin.setRawMode(true);
254
+ process.stdin.resume();
255
+ process.stdin.on('data', onData);
256
+ render();
257
+ });
258
+ }
259
+
260
+ // resumeModeLabel moved to ./repl-format.mjs.
261
+
262
+ /**
263
+ * PRD-068 §5.14.5 — preview overlay for a session/mode. Read-only, `q` to return.
264
+ * If user hits Enter, resolve to the currently-previewed mode so caller can activate.
265
+ */
266
+ export async function previewResumeSession(session, ctx) {
267
+ if (!process.stdin.isTTY) return null;
268
+ const detail = await getSessionDetail(session.sessionId, { filePath: session.transcriptPath });
269
+ if (!detail) return null;
270
+
271
+ const rl = ctx._rl || null;
272
+ if (rl) rl.pause();
273
+
274
+ let mode = 'summary';
275
+ const rich = () => buildResumeHistory({ ...detail, recapTailTurns: 8 }, mode);
276
+ let history = rich();
277
+
278
+ return await new Promise((resolve) => {
279
+ const wasRaw = process.stdin.isRaw;
280
+ let renderedLines = 0;
281
+ let scrollOffset = 0;
282
+
283
+ const render = () => {
284
+ if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
285
+ const cols = Math.max(60, process.stderr.columns || 120);
286
+ const rows = Math.max(10, Math.min((process.stderr.rows || 30) - 6, 20));
287
+ const contentLines = (history.summary || '').split('\n');
288
+ // For tail/full modes, also append serialized tail so preview reflects
289
+ // what the agent will actually receive.
290
+ if (mode !== 'summary') {
291
+ contentLines.push('', c.dim('── conversation tail ──'));
292
+ for (const msg of history.agentHistory.slice(1)) {
293
+ contentLines.push(`${msg.role === 'user' ? c.dim('You:') : c.brand('bahulam:')} ${String(msg.content).slice(0, 300)}`);
294
+ }
295
+ }
296
+ const totalLines = contentLines.length;
297
+ const maxOffset = Math.max(0, totalLines - rows);
298
+ if (scrollOffset > maxOffset) scrollOffset = maxOffset;
299
+
300
+ const lines = [];
301
+ lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens, { resumeSummary: session.resumeSummary })) + ' ctx')}`);
302
+ lines.push(` ${c.dim('─'.repeat(60))}`);
303
+ for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
304
+ lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
305
+ }
306
+ lines.push('');
307
+ lines.push(fitAnsiLine(` ${c.dim(`↑↓/PgUp/PgDn scroll · f/s/1/2 switch mode · Enter resume this · q back · ${scrollOffset + 1}-${Math.min(scrollOffset + rows, totalLines)}/${totalLines}`)}`, cols - 1));
308
+ process.stderr.write(lines.join('\n') + '\n');
309
+ renderedLines = lines.length;
310
+ };
311
+
312
+ const cleanup = (value) => {
313
+ process.stdin.removeListener('data', onData);
314
+ process.stdin.setRawMode(wasRaw || false);
315
+ if (rl) rl.resume();
316
+ resolve(value);
317
+ };
318
+ const onData = (data) => {
319
+ const key = data.toString('utf8');
320
+ const low = key.toLowerCase();
321
+ if (key === '' || key === '' || low === 'q') { cleanup({ action: 'back' }); return; }
322
+ if (key === '\r' || key === '\n') { cleanup({ action: 'resume', mode }); return; }
323
+ if (key === '') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; }
324
+ if (key === '') { scrollOffset += 1; render(); return; }
325
+ if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
326
+ if (key === '[6~') { scrollOffset += 10; render(); return; }
327
+ if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
328
+ if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
329
+ if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
330
+ if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
331
+ };
332
+
333
+ process.stdin.setRawMode(true);
334
+ process.stdin.resume();
335
+ process.stdin.on('data', onData);
336
+ render();
337
+ });
338
+ }
339
+
340
+ /**
341
+ * PRD-068 §5.14.7 — explicit cwd confirmation when the picked session lives
342
+ * elsewhere. Returns 'switch' | 'stay' | 'cancel'.
343
+ */
344
+ export async function confirmCwdSwitch(ctx, savedPath, currentPath) {
345
+ if (!process.stdin.isTTY) return 'switch';
346
+ process.stderr.write(`\n ${c.dim('This session lives in another repo:')}\n`);
347
+ process.stderr.write(` ${c.dim('→')} ${c.brand(savedPath)} ${c.dim(`(current cwd: ${currentPath})`)}\n`);
348
+ process.stderr.write(` ${c.dim('[Enter]')} switch cwd and resume · ${c.dim('[s]')} stay here and resume anyway · ${c.dim('[n]')} cancel `);
349
+ const rl = ctx._rl || null;
350
+ if (rl) rl.pause();
351
+ return await new Promise((resolve) => {
352
+ const wasRaw = process.stdin.isRaw;
353
+ const cleanup = (value) => {
354
+ process.stdin.removeListener('data', onData);
355
+ process.stdin.setRawMode(wasRaw || false);
356
+ if (rl) rl.resume();
357
+ process.stderr.write('\n');
358
+ resolve(value);
359
+ };
360
+ const onData = (data) => {
361
+ const key = data.toString('utf8').toLowerCase();
362
+ if (key === '' || key === '' || key === 'n') { cleanup('cancel'); return; }
363
+ if (key === '\r' || key === '\n') { cleanup('switch'); return; }
364
+ if (key === 's') { cleanup('stay'); return; }
365
+ };
366
+ process.stdin.setRawMode(true);
367
+ process.stdin.resume();
368
+ process.stdin.on('data', onData);
369
+ });
370
+ }
371
+
372
+ // Legacy prompt (kept as a fallback for callers that force compact/full explicitly).
373
+ export async function chooseResumeHistoryMode(ctx, { defaultMode = 'compact' } = {}) {
374
+ if (!process.stdin.isTTY) return defaultMode;
375
+ process.stderr.write(`\n ${c.dim('Load history for agent:')} ${c.brand('[c]')} ${c.dim('compact summary')} ${c.brand('[f]')} ${c.dim('full transcript')} ${c.dim('(Enter = compact, Esc = cancel):')} `);
376
+ const rl = ctx._rl || null;
377
+ if (rl) rl.pause();
378
+ return await new Promise((resolve) => {
379
+ const wasRaw = process.stdin.isRaw;
380
+ const cleanup = (value) => {
381
+ process.stdin.removeListener('data', onData);
382
+ process.stdin.setRawMode(wasRaw || false);
383
+ if (rl) rl.resume();
384
+ process.stderr.write('\n');
385
+ resolve(value);
386
+ };
387
+ const onData = (data) => {
388
+ const key = data.toString('utf8').toLowerCase();
389
+ if (key === '\u0003' || key === '\u001b') { cleanup(null); return; }
390
+ if (key === '\r' || key === '\n' || key === 'c') { cleanup('compact'); return; }
391
+ if (key === 'f') { cleanup('full'); }
392
+ };
393
+ process.stdin.setRawMode(true);
394
+ process.stdin.resume();
395
+ process.stdin.on('data', onData);
396
+ });
397
+ }
398
+
399
+ // historyRoleLabel, renderHistoryEntries moved to ./repl-format.mjs.
400
+
401
+ // `renderResumePreview` needs to replay each transcript event through the
402
+ // live event renderer. `renderEvent` still lives in repl.mjs (its extraction
403
+ // is a later slice); passing it via `ctx` here avoids a circular import.
404
+ export function renderResumePreview(resumed, ctx = {}) {
405
+ const renderEvent = ctx.renderEvent;
406
+ const tailTurns = resumeTailTurnCount(resumed.historyMode);
407
+ if (resumed.historyMode === 'compact' || resumed.historyMode === 'summary') {
408
+ if (!resumed.summary) return;
409
+ process.stderr.write(`\n ${c.bold('Continuity Summary Sent To Agent')}\n`);
410
+ process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
411
+ for (const line of resumed.summary.split('\n')) {
412
+ process.stderr.write(` ${c.dim(line)}\n`);
413
+ }
414
+ process.stderr.write('\n');
415
+ return;
416
+ }
417
+
418
+ if (tailTurns && resumed.summary) {
419
+ process.stderr.write(`\n ${c.bold(`Summary + Last ${tailTurns} Turns`)}\n`);
420
+ process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
421
+ for (const line of resumed.summary.split('\n')) {
422
+ process.stderr.write(` ${c.dim(line)}\n`);
423
+ }
424
+ process.stderr.write('\n');
425
+ }
426
+
427
+ if (resumed.replayEvents?.length) {
428
+ const replayStartOrder = replayStartOrderForMode(resumed.history || [], resumed.historyMode);
429
+ const replayEvents = filterResumeReplayEvents(resumed.replayEvents)
430
+ .filter(item => replayStartOrder == null || !Number.isFinite(Number(item.order)) || Number(item.order) >= replayStartOrder);
431
+ const userTurns = (resumed.history || [])
432
+ .filter(m => m.role === 'user')
433
+ .filter(m => replayStartOrder == null || !Number.isFinite(Number(m.order)) || Number(m.order) >= replayStartOrder);
434
+ const replayItems = mergeResumeReplayItems(userTurns, replayEvents);
435
+ const replayTitle = tailTurns ? `Last ${tailTurns} Turns Replay` : 'Replayed Live Session Events';
436
+ process.stderr.write(`\n ${c.bold(replayTitle)} (${userTurns.length} turns, ${replayEvents.length} events)\n`);
437
+ process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
438
+ const sessionSnapshot = JSON.parse(JSON.stringify(session));
439
+ const savedOrbit = orbitRef.current;
440
+ const savedSessionMgr = sessionMgrRef.current;
441
+ orbitRef.current = null;
442
+ sessionMgrRef.current = null;
443
+ try {
444
+ startContentStream();
445
+ for (const item of replayItems) {
446
+ if (item.kind === 'user') {
447
+ flushContent();
448
+ stopSpinner();
449
+ const content = String(item.message.content || '').replace(/\s+/g, ' ').trim();
450
+ process.stderr.write(`\n ${historyRoleLabel('user')}: ${content}\n`);
451
+ continue;
452
+ }
453
+ renderEvent(item.event.event);
454
+ }
455
+ flushContent();
456
+ stopSpinner();
457
+ } finally {
458
+ orbitRef.current = savedOrbit;
459
+ sessionMgrRef.current = savedSessionMgr;
460
+ for (const key of Object.keys(session)) delete session[key];
461
+ Object.assign(session, sessionSnapshot);
462
+ }
463
+ process.stderr.write('\n');
464
+ return;
465
+ }
466
+
467
+ if (resumed.history?.length) {
468
+ renderHistoryEntries(resumed.history, {
469
+ limit: Infinity,
470
+ maxChars: 220,
471
+ title: tailTurns ? `Last ${tailTurns} Turns` : 'Replayed Session History',
472
+ });
473
+ }
474
+ }
475
+
476
+ export async function summarizeResumeTranscript({
477
+ auth,
478
+ toolExecutor,
479
+ sessionId,
480
+ projectPath,
481
+ messages,
482
+ }) {
483
+ const creds = auth?.loadCredentials?.() || {};
484
+ if (!creds.backendUrl || !creds.token || !Array.isArray(messages) || messages.length === 0) {
485
+ return {
486
+ ok: false,
487
+ source: 'local',
488
+ reason: !creds.backendUrl || !creds.token ? 'missing backend credentials' : 'empty transcript',
489
+ };
490
+ }
491
+ try {
492
+ const client = new TarangStreamClient({
493
+ baseUrl: creds.backendUrl,
494
+ token: creds.token,
495
+ toolExecutor,
496
+ });
497
+ const result = await client.summarizeSession(messages, {
498
+ sessionId,
499
+ projectPath,
500
+ maxTokens: 800,
501
+ timeoutMs: 15000,
502
+ });
503
+ return { ...result, ok: true };
504
+ } catch (err) {
505
+ return {
506
+ ok: false,
507
+ source: 'local',
508
+ reason: err?.message || 'backend summary request failed',
509
+ };
510
+ }
511
+ }
512
+
513
+ export async function compactCurrentSession(ctx, rest = '') {
514
+ const tailCount = parseCompactTailCount(rest, 8);
515
+ const preparedLive = prepareCompactHistory({
516
+ agentHistory: session.agentHistory,
517
+ tailCount,
518
+ });
519
+ if (!preparedLive.ok) {
520
+ process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
521
+ return;
522
+ }
523
+
524
+ const progress = startResumeProgress('compact');
525
+ progress.update('preparing compact summary', 18);
526
+ let sourceMessages = preparedLive.sourceMessages;
527
+ let priorSummary = preparedLive.previousSummary || '';
528
+ let previousSourceMessageCount = 0;
529
+ let fullMessageCount = preparedLive.beforeCount;
530
+ let projectPath = safeCwd();
531
+ let sourceFrom = 'live';
532
+ let summaryWarning = '';
533
+
534
+ try {
535
+ if (session.id && ctx.jsonlWriter?.flush) {
536
+ progress.update('reading transcript checkpoint', 28);
537
+ await ctx.jsonlWriter.flush();
538
+ const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
539
+ if (detail) {
540
+ const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
541
+ if (richHistory.sourceMessages?.length) {
542
+ sourceMessages = richHistory.sourceMessages;
543
+ priorSummary = richHistory.priorSummary || '';
544
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
545
+ fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
546
+ projectPath = detail.meta?.project || safeCwd();
547
+ sourceFrom = 'transcript';
548
+ } else if (richHistory.priorSummary) {
549
+ priorSummary = richHistory.priorSummary;
550
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
551
+ fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
552
+ }
553
+ }
554
+ }
555
+
556
+ if (!sourceMessages.length) {
557
+ progress.stop();
558
+ process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
559
+ return;
560
+ }
561
+
562
+ progress.update('summarizing compacted history', 46);
563
+ const backendSummary = await summarizeResumeTranscript({
564
+ auth: ctx.auth,
565
+ toolExecutor: ctx.toolExecutor,
566
+ sessionId: session.id,
567
+ projectPath,
568
+ messages: sourceMessages,
569
+ });
570
+ let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
571
+ let deltaSummary = backendSummary?.summary || '';
572
+ if (!deltaSummary) {
573
+ summaryWarning = backendSummary?.reason || 'backend summary unavailable';
574
+ deltaSummary = localCompactSummary(sourceMessages);
575
+ }
576
+ const summary = combineResumeSummaries(priorSummary, deltaSummary);
577
+
578
+ progress.update('rewriting live context', 74);
579
+ const applied = applyCompactSummary({
580
+ prepared: { ...preparedLive, sourceMessages },
581
+ summary,
582
+ sessionId: session.id,
583
+ cwd: projectPath,
584
+ originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
585
+ previousSourceMessageCount,
586
+ });
587
+ session.agentHistory = applied.agentHistory;
588
+ session.compactSummary = summary;
589
+ session.compactSourceMessageCount = applied.sourceMessageCount;
590
+
591
+ if (ctx.jsonlWriter) {
592
+ progress.update('writing summary checkpoint', 88);
593
+ ctx.jsonlWriter.writeKeplerEvent({
594
+ type: 'resume_summary',
595
+ data: {
596
+ session_id: session.id || null,
597
+ mode: 'compact',
598
+ mode_label: '/compact',
599
+ summary,
600
+ summary_source: summarySource,
601
+ summary_warning: summaryWarning || null,
602
+ source: sourceFrom,
603
+ source_message_count: applied.sourceMessageCount,
604
+ previous_source_message_count: previousSourceMessageCount,
605
+ full_message_count: fullMessageCount,
606
+ retained_tail_messages: applied.retainedCount,
607
+ live_before_messages: applied.beforeCount,
608
+ live_after_messages: applied.afterCount,
609
+ },
610
+ });
611
+ await ctx.jsonlWriter.flush?.();
612
+ }
613
+
614
+ progress.stop();
615
+ process.stderr.write(
616
+ ` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
617
+ );
618
+ if (summaryWarning) {
619
+ process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
620
+ }
621
+ } catch (err) {
622
+ progress.stop();
623
+ process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
624
+ }
625
+ }