@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,152 @@
1
+ /**
2
+ * Sub-agent block renderer — Mission Control (PRD-055 §7).
3
+ *
4
+ * Renders the open/close pair for a sub-agent block, dimmed throughout so
5
+ * the primary agent reads bright by contrast. Inner tool cards are indented
6
+ * via the `subAgentIndent()` helper so they nest visually under the header.
7
+ *
8
+ * 🛰️ explore "JWT lifecycle" ▸ running
9
+ * 🔭 Search code "expire" → 6 matches
10
+ * 🔭 Read file auth.py L120-180 → 60 lines
11
+ * └ ✅ returned 3 files identified · $0.004 · 2.1s
12
+ *
13
+ * Maintains a depth stack so concurrent / nested sub-agents indent further
14
+ * and so callers can ask `inSubAgent()` / `depth()` without threading state.
15
+ *
16
+ * No I/O — caller writes the returned strings to stderr. This keeps the
17
+ * module testable from a plain Node script.
18
+ */
19
+
20
+ import { paint } from './palette.mjs';
21
+ import { icons } from './icons.mjs';
22
+
23
+ const SUB_ICONS = {
24
+ explore: '🔭',
25
+ plan: '📐',
26
+ verify: '✅',
27
+ debug: '🪲',
28
+ refactor:'♻️',
29
+ };
30
+
31
+ // ── Active stack ─────────────────────────────────────────────────────────
32
+
33
+ const _stack = []; // [{ id, type, startedAt }]
34
+
35
+ /** How many sub-agents are currently open. */
36
+ export function depth() { return _stack.length; }
37
+ export function inSubAgent() { return _stack.length > 0; }
38
+
39
+ /**
40
+ * Indent string for a tool card line nested under N sub-agents.
41
+ * 5 cols per level matches the existing `' '` legacy indent.
42
+ */
43
+ export function subAgentIndent(extraDepth = 0) {
44
+ const d = _stack.length + extraDepth;
45
+ if (d <= 0) return ' ';
46
+ return ' '.repeat(2 + d * 3);
47
+ }
48
+
49
+ // ── Render ───────────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Open a sub-agent block. Pushes onto the stack; returns the lines to print.
53
+ *
54
+ * @returns {string} ANSI-styled multi-line block (no trailing newline).
55
+ */
56
+ export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
57
+ const t = type || 'sub-agent';
58
+ const depthBefore = _stack.length;
59
+ _stack.push({ id: id || `${t}-${depthBefore}-${tag()}`, type: t, startedAt: Date.now() });
60
+
61
+ const indent = ' '.repeat(2 + depthBefore * 3);
62
+ const iconChar = SUB_ICONS[t] || icons.subAgent;
63
+ const head = `${indent}${iconChar} ${paint.brand.data(t)} ${paint.text.dim(`"${query || ''}"`)}`;
64
+ const tag1 = paint.text.dim('▸ running');
65
+
66
+ return query
67
+ ? `\n${head} ${tag1}`
68
+ : `\n${indent}${iconChar} ${paint.brand.data(t)} ${tag1}`;
69
+ }
70
+
71
+ /**
72
+ * Close the most recent sub-agent block. Pops the stack; returns the close
73
+ * line with optional cost / token / duration attribution per PRD §7.3.
74
+ *
75
+ * └ ✅ returned 3 files identified · 1.2k tok · $0.004 · 2.1s
76
+ * └ ✗ explore agent failed
77
+ *
78
+ * Caller passes `success` (default true), `summary` ("returned N files"),
79
+ * and any of `{ costUsd, tokens, durationS, toolCalls, iterations }`.
80
+ */
81
+ export function renderSubAgentClose({
82
+ type,
83
+ success = true,
84
+ summary = '',
85
+ costUsd,
86
+ tokens,
87
+ durationS,
88
+ toolCalls,
89
+ iterations,
90
+ error,
91
+ } = {}) {
92
+ // Match-pop: if the type doesn't match the top of stack we still pop the
93
+ // top entry — backends never emit interleaved open/close, so this is the
94
+ // safe behavior.
95
+ const opened = _stack.pop();
96
+ const t = type || opened?.type || 'sub-agent';
97
+ const indent = ' '.repeat(2 + _stack.length * 3);
98
+
99
+ if (!success) {
100
+ const line = `${indent}${paint.text.dim('└')} ${paint.state.danger('✗')} ${paint.text.dim(`${t} agent failed`)}`;
101
+ if (error) {
102
+ return `${line}\n${indent} ${paint.state.danger(truncate(error, 140))}`;
103
+ }
104
+ return line;
105
+ }
106
+
107
+ const parts = [];
108
+ if (toolCalls > 0) parts.push(`${toolCalls} tools`);
109
+ if (iterations > 0) parts.push(`${iterations} iter`);
110
+ if (tokens > 0) parts.push(`${formatTokens(tokens)} tok`);
111
+ if (typeof costUsd === 'number' && costUsd > 0) parts.push(formatCost(costUsd));
112
+ if (durationS != null) parts.push(`${Number(durationS).toFixed(1)}s`);
113
+ const detail = parts.length ? paint.text.dim(' · ' + parts.join(' · ')) : '';
114
+
115
+ const body = summary
116
+ ? paint.text.dim(summary)
117
+ : paint.text.dim(`${t} returned`);
118
+
119
+ return `${indent}${paint.text.dim('└')} ${paint.state.success('✅')} ${body}${detail}`;
120
+ }
121
+
122
+ /**
123
+ * Force-clear the stack. Use after a `complete` event or when cancelling so
124
+ * a stale entry doesn't keep indenting future output.
125
+ */
126
+ export function resetSubAgents() { _stack.length = 0; }
127
+
128
+ // ── helpers ──────────────────────────────────────────────────────────────
129
+
130
+ function truncate(text, n) {
131
+ const s = String(text || '');
132
+ return s.length <= n ? s : s.slice(0, n - 1) + '…';
133
+ }
134
+
135
+ function formatTokens(n) {
136
+ if (!Number.isFinite(n)) return '0';
137
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
138
+ return String(Math.round(n));
139
+ }
140
+
141
+ function formatCost(usd) {
142
+ if (usd < 0.001) return `$${usd.toFixed(5)}`;
143
+ if (usd < 0.01) return `$${usd.toFixed(4)}`;
144
+ return `$${usd.toFixed(3)}`;
145
+ }
146
+
147
+ function tag() {
148
+ // Avoid Date.now()/Math.random() drift across re-renders — depth+counter is
149
+ // enough to keep ids unique within a process.
150
+ tag._n = (tag._n || 0) + 1;
151
+ return tag._n.toString(36);
152
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Terminal capability detection.
3
+ *
4
+ * Resolves once at import. Re-evaluation requires `refresh()` (used by tests
5
+ * and the rare command that toggles a relevant env var mid-process).
6
+ *
7
+ * Capability tiers (highest first):
8
+ * truecolor - 24-bit RGB (e.g. iTerm2, modern xterm, Windows Terminal)
9
+ * ansi256 - 256-color palette
10
+ * ansi16 - basic 16 colors
11
+ * none - no color (NO_COLOR=1, dumb terminal, non-TTY without override)
12
+ */
13
+
14
+ const TRUECOLOR_TERMS = new Set([
15
+ 'truecolor',
16
+ '24bit',
17
+ '24-bit',
18
+ ]);
19
+
20
+ const ANSI256_TERMS = [
21
+ /-256(color)?$/i,
22
+ /^xterm/i,
23
+ /^screen/i,
24
+ /^tmux/i,
25
+ /^rxvt-unicode/i,
26
+ /^alacritty/i,
27
+ ];
28
+
29
+ const DUMB_TERMS = new Set(['', 'dumb', 'unknown']);
30
+
31
+ function readEnv() {
32
+ const env = process.env || {};
33
+ return {
34
+ NO_COLOR: env.NO_COLOR,
35
+ FORCE_COLOR: env.FORCE_COLOR,
36
+ KEPLER_PLAIN: env.KEPLER_PLAIN,
37
+ COLORTERM: (env.COLORTERM || '').toLowerCase(),
38
+ TERM: (env.TERM || '').toLowerCase(),
39
+ TERM_PROGRAM: env.TERM_PROGRAM || '',
40
+ CI: env.CI,
41
+ };
42
+ }
43
+
44
+ function detectColorLevel(env, isTTY) {
45
+ // Hard opt-out (https://no-color.org). Honored even on TTYs.
46
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
47
+ if (env.KEPLER_PLAIN === '1') return 'none';
48
+
49
+ // Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
50
+ // FORCE_COLOR with no value or =true falls through to detection.
51
+ if (env.FORCE_COLOR !== undefined) {
52
+ const v = String(env.FORCE_COLOR).trim();
53
+ if (v === '0' || v === 'false') return 'none';
54
+ if (v === '1') return 'ansi16';
55
+ if (v === '2') return 'ansi256';
56
+ if (v === '3') return 'truecolor';
57
+ // Any other truthy value: continue detection but allow non-TTY.
58
+ isTTY = true;
59
+ }
60
+
61
+ // No TTY and not forced: no color.
62
+ if (!isTTY) return 'none';
63
+
64
+ if (DUMB_TERMS.has(env.TERM)) return 'none';
65
+
66
+ if (TRUECOLOR_TERMS.has(env.COLORTERM)) return 'truecolor';
67
+
68
+ // Some terminal emulators advertise truecolor through TERM_PROGRAM.
69
+ if (env.TERM_PROGRAM === 'iTerm.app' || env.TERM_PROGRAM === 'WezTerm') {
70
+ return 'truecolor';
71
+ }
72
+ if (env.TERM_PROGRAM === 'vscode' || env.TERM_PROGRAM === 'Apple_Terminal') {
73
+ return 'ansi256';
74
+ }
75
+
76
+ if (ANSI256_TERMS.some(re => re.test(env.TERM))) return 'ansi256';
77
+
78
+ return 'ansi16';
79
+ }
80
+
81
+ function detectUnicode(env) {
82
+ if (env.KEPLER_PLAIN === '1') return false;
83
+ // Most modern terminals on macOS/Linux handle UTF-8.
84
+ // Windows ConEmu / older terminals are the main holdouts; conservative
85
+ // fallback when LANG and LC_* are missing or explicitly POSIX.
86
+ const lang = (process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '').toLowerCase();
87
+ if (!lang) return process.platform !== 'win32';
88
+ if (lang.includes('utf')) return true;
89
+ return false;
90
+ }
91
+
92
+ function compute() {
93
+ const env = readEnv();
94
+ const isTTY = !!(process.stdout && process.stdout.isTTY);
95
+ const level = detectColorLevel(env, isTTY);
96
+ return {
97
+ isTTY,
98
+ colorLevel: level, // 'none' | 'ansi16' | 'ansi256' | 'truecolor'
99
+ color: level !== 'none',
100
+ truecolor: level === 'truecolor',
101
+ ansi256: level === 'ansi256' || level === 'truecolor',
102
+ unicode: detectUnicode(env),
103
+ plain: env.KEPLER_PLAIN === '1',
104
+ columns: (process.stdout && process.stdout.columns) || 80,
105
+ rows: (process.stdout && process.stdout.rows) || 24,
106
+ ci: !!env.CI,
107
+ };
108
+ }
109
+
110
+ let _capabilities = compute();
111
+
112
+ /**
113
+ * Current terminal capabilities. Stable until `refresh()` is called.
114
+ */
115
+ export function term() {
116
+ return _capabilities;
117
+ }
118
+
119
+ /**
120
+ * Re-run capability detection. Tests, `/config` reloads, or runtime env changes.
121
+ */
122
+ export function refresh() {
123
+ _capabilities = compute();
124
+ return _capabilities;
125
+ }
126
+
127
+ /**
128
+ * Listen for terminal resizes. Returns an unsubscribe function.
129
+ * Callers receive the latest capabilities object (with updated columns/rows).
130
+ */
131
+ export function onResize(handler) {
132
+ if (typeof handler !== 'function') return () => {};
133
+ const stream = process.stdout;
134
+ if (!stream || typeof stream.on !== 'function') return () => {};
135
+
136
+ const onChange = () => {
137
+ _capabilities = {
138
+ ..._capabilities,
139
+ columns: stream.columns || _capabilities.columns,
140
+ rows: stream.rows || _capabilities.rows,
141
+ };
142
+ handler(_capabilities);
143
+ };
144
+ stream.on('resize', onChange);
145
+ return () => stream.off('resize', onChange);
146
+ }
147
+
148
+ /**
149
+ * Force a capability level. Test-only escape hatch.
150
+ * Pass `null` to clear and re-detect.
151
+ */
152
+ export function _setForTesting(overrides) {
153
+ if (overrides === null) {
154
+ _capabilities = compute();
155
+ return _capabilities;
156
+ }
157
+ _capabilities = { ..._capabilities, ...overrides };
158
+ return _capabilities;
159
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pure text layout helpers.
3
+ *
4
+ * Extracted so the input dock's line-wrapping and clipping logic can be
5
+ * unit-tested without a live terminal. Everything here is a pure function
6
+ * of its inputs.
7
+ */
8
+
9
+ import { strip as stripAnsi, width as visibleWidth } from './palette.mjs';
10
+
11
+ /**
12
+ * Wrap `text` into lines of at most `maxWidth` visible columns.
13
+ *
14
+ * - Preserves existing '\n' as hard breaks.
15
+ * - Wraps long lines at whitespace when possible.
16
+ * - Breaks mid-word only when a single word exceeds maxWidth.
17
+ * - Empty input returns [''].
18
+ * - ANSI escapes are preserved; wrap decisions use visible width.
19
+ *
20
+ * @param {string} text
21
+ * @param {number} maxWidth
22
+ * @returns {string[]}
23
+ */
24
+ export function wrapToLines(text, maxWidth) {
25
+ const width = Math.max(1, Math.floor(maxWidth));
26
+ const source = String(text ?? '');
27
+ if (!source) return [''];
28
+
29
+ const out = [];
30
+ for (const rawLine of source.split('\n')) {
31
+ if (rawLine === '') { out.push(''); continue; }
32
+ if (visibleWidth(rawLine) <= width) { out.push(rawLine); continue; }
33
+
34
+ // Long line — wrap at whitespace where possible.
35
+ let current = '';
36
+ const tokens = tokenizeForWrap(rawLine);
37
+ for (const token of tokens) {
38
+ const isSpace = /^\s+$/.test(stripAnsi(token));
39
+ const candidate = current + token;
40
+ if (visibleWidth(candidate) <= width) { current = candidate; continue; }
41
+ if (current) { out.push(current.replace(/\s+$/, '')); current = ''; }
42
+ if (isSpace) continue; // don't start a new line with pure whitespace
43
+ // Token alone still too wide — chunk it.
44
+ if (visibleWidth(token) > width) {
45
+ for (const chunk of chunkByVisibleWidth(token, width)) out.push(chunk);
46
+ } else {
47
+ current = token;
48
+ }
49
+ }
50
+ if (current) out.push(current.replace(/\s+$/, ''));
51
+ }
52
+ return out.length ? out : [''];
53
+ }
54
+
55
+ /**
56
+ * Take the last `maxRows` of `lines`. If more lines were dropped, prefix
57
+ * the first surviving line with a dim '…' marker so the truncation is
58
+ * explicit (per PRD §5.1).
59
+ *
60
+ * Returns { visible, truncated: boolean, dropped: number }.
61
+ */
62
+ export function tailWithEllipsis(lines, maxRows, ellipsis = '… ') {
63
+ const rows = Math.max(1, Math.floor(maxRows));
64
+ const arr = Array.isArray(lines) ? lines : [];
65
+ if (arr.length <= rows) return { visible: arr.slice(), truncated: false, dropped: 0 };
66
+ const dropped = arr.length - rows;
67
+ const visible = arr.slice(dropped);
68
+ visible[0] = `${ellipsis}${visible[0] || ''}`;
69
+ return { visible, truncated: true, dropped };
70
+ }
71
+
72
+ /**
73
+ * Given the wrapped visible lines and a cursor position (as a linear
74
+ * character offset into the *joined* wrapped output), return the
75
+ * { row, col } inside the visible block (0-indexed).
76
+ *
77
+ * Used to place the terminal cursor after rendering the input buffer.
78
+ */
79
+ export function cursorPositionInLines(visibleLines, offset) {
80
+ const arr = Array.isArray(visibleLines) ? visibleLines : [];
81
+ let remaining = Math.max(0, Math.floor(offset));
82
+ for (let row = 0; row < arr.length; row++) {
83
+ const line = arr[row] || '';
84
+ const w = visibleWidth(line);
85
+ if (remaining <= w) return { row, col: remaining };
86
+ remaining -= w;
87
+ // Newline between wrapped lines doesn't count as a visible column,
88
+ // but consumes zero of the remaining offset either.
89
+ }
90
+ const lastRow = Math.max(0, arr.length - 1);
91
+ return { row: lastRow, col: visibleWidth(arr[lastRow] || '') };
92
+ }
93
+
94
+ // ── internals ────────────────────────────────────────────────────────────
95
+
96
+ // Tokenize into runs of non-whitespace and whitespace so wrapWords can
97
+ // decide break points without merging spaces into words. ANSI escapes stay
98
+ // attached to the visible token they precede.
99
+ function tokenizeForWrap(line) {
100
+ const tokens = [];
101
+ const re = /(\s+|\S+)/g;
102
+ let m;
103
+ while ((m = re.exec(line)) !== null) tokens.push(m[0]);
104
+ return tokens;
105
+ }
106
+
107
+ // Split a single overly-long token into chunks of at most `maxWidth`
108
+ // visible cells, preserving ANSI escape sequences (they don't count).
109
+ function chunkByVisibleWidth(token, maxWidth) {
110
+ const chunks = [];
111
+ let buf = '';
112
+ let bufWidth = 0;
113
+ const chars = Array.from(String(token));
114
+ for (const ch of chars) {
115
+ // Naive: treat each codepoint as 1 cell. Good enough for path-like
116
+ // tokens; wide-char inputs would need a fuller width table.
117
+ if (bufWidth + 1 > maxWidth) {
118
+ chunks.push(buf);
119
+ buf = '';
120
+ bufWidth = 0;
121
+ }
122
+ buf += ch;
123
+ bufWidth += 1;
124
+ }
125
+ if (buf) chunks.push(buf);
126
+ return chunks;
127
+ }