@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,444 @@
1
+ /**
2
+ * Fixed input dock — Bahulam Code identity + meta + tips.
3
+ *
4
+ * Reserves rows at the bottom of the terminal so agent/tool output scrolls
5
+ * above the prompt. The input area GROWS DYNAMICALLY as the user types or
6
+ * pastes multi-line content: starts at 1 row, expands up to `MAX_INPUT_ROWS`
7
+ * (default 6) to fit the wrapped buffer, shrinks back when the buffer is
8
+ * cleared. On resize, the scroll region moves so content above reflows
9
+ * smoothly.
10
+ *
11
+ * When the wrapped buffer still exceeds the max, only the LAST max rows
12
+ * render with a leading '…' marker (explicit truncation).
13
+ *
14
+ * Multi-line paste — including bracketed paste bursts — renders as one
15
+ * visible block in the dock and submits as a single message. The dock
16
+ * itself doesn't parse newlines; wrapToLines treats them as hard breaks.
17
+ *
18
+ * Layout (fixed 8 rows + N input rows):
19
+ * ── ▎Bahulam Code ══════════════ 12.3k tok · 2m 41s ═══ ← top rule (colored)
20
+ * ← spacer
21
+ * + add instruction › foo bar ← input row 1
22
+ * baz quux ← input row 2 (added as needed)
23
+ * ... ← up to MAX_INPUT_ROWS
24
+ * ← spacer
25
+ * ══════════════════════════════════════════════════════ ← bottom rule (colored)
26
+ * deepseek-chat-v3 · Tarang-Orca ⎇ main · turn 4 ← meta row
27
+ * [Enter] send · [/] commands … ← tips row
28
+ * (one blank row at very bottom for cursor safety)
29
+ *
30
+ * Config:
31
+ * BAHULAM_INPUT_ROWS_MAX / KEPLER_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
32
+ * BAHULAM_FIXED_INPUT=0 / KEPLER_FIXED_INPUT=0 disable dock entirely (fallback readline)
33
+ */
34
+
35
+ import { paint, width as visibleWidth } from './palette.mjs';
36
+ import { term, onResize } from './term.mjs';
37
+ import { wrapToLines, tailWithEllipsis, cursorPositionInLines } from './text-layout.mjs';
38
+
39
+ const ESC = '\x1b[';
40
+ const OUT = process.stderr;
41
+
42
+ const BRAND_LABEL = 'Bahulam Code';
43
+ const LABEL_ACCENT = '▎'; // left-edge accent bar
44
+
45
+ // Horizontal margin: text starts INPUT_INDENT cols from the left edge and
46
+ // leaves INPUT_RIGHT_PAD cols of clear space on the right so wrapped lines
47
+ // don't kiss the terminal edge.
48
+ const INPUT_INDENT = 5;
49
+ const INPUT_RIGHT_PAD = 2;
50
+ const META_INDENT = 4;
51
+
52
+ const DEFAULT_MAX_INPUT_ROWS = 6;
53
+ const MIN_INPUT_ROWS = 1;
54
+ const MAX_INPUT_ROWS_CAP = 12;
55
+
56
+ // Fixed rows around the input area:
57
+ // 1 top rule + 1 spacer above + input(N) + 1 spacer below + 1 bottom rule
58
+ // + 1 meta row + 1 tips row + 1 safety row
59
+ // = 7 + N
60
+ const FIXED_ROWS = 7;
61
+
62
+ let mounted = false;
63
+ let inputRowsMax = DEFAULT_MAX_INPUT_ROWS;
64
+ let inputRows = MIN_INPUT_ROWS;
65
+ let reservedRows = FIXED_ROWS + MIN_INPUT_ROWS;
66
+ let unsubResize = null;
67
+ let lastFrame = { context: '', meta: '', tips: '' };
68
+ let resetting = false;
69
+
70
+ function write(s) { try { OUT.write(s); } catch {} }
71
+ function setScrollRegion(top, bottom) { write(`${ESC}${top};${bottom}r`); }
72
+ function clearScrollRegion() { write(`${ESC}r`); }
73
+ function saveCursor() { write(`${ESC}s`); }
74
+ function restoreCursor() { write(`${ESC}u`); }
75
+ function moveTo(row, col) { write(`${ESC}${row};${col}H`); }
76
+ function clearLine() { write(`${ESC}2K`); }
77
+
78
+ function rows() {
79
+ return Math.max(10, term().rows || 24);
80
+ }
81
+
82
+ function cols() {
83
+ return Math.max(40, term().columns || 80);
84
+ }
85
+
86
+ function contentBottomRow() {
87
+ return Math.max(1, rows() - reservedRows);
88
+ }
89
+
90
+ // Row map (top → bottom of the reserved region).
91
+ function topRuleRow() { return contentBottomRow() + 1; }
92
+ function spacerAboveRow() { return topRuleRow() + 1; }
93
+ function inputRowStart() { return spacerAboveRow() + 1; }
94
+ function inputRowEnd() { return inputRowStart() + inputRows - 1; }
95
+ function spacerBelowRow() { return inputRowEnd() + 1; }
96
+ function bottomRuleRow() { return spacerBelowRow() + 1; }
97
+ function metaRow() { return bottomRuleRow() + 1; }
98
+ function tipsRow() { return metaRow() + 1; }
99
+
100
+ function inputTextBudget() {
101
+ return Math.max(8, cols() - INPUT_INDENT - INPUT_RIGHT_PAD - 1);
102
+ }
103
+
104
+ function resolveMaxInputRows(requested) {
105
+ const raw = requested != null
106
+ ? requested
107
+ : (process.env.BAHULAM_INPUT_ROWS_MAX || process.env.KEPLER_INPUT_ROWS_MAX);
108
+ const n = Number.parseInt(String(raw), 10);
109
+ if (!Number.isFinite(n)) return DEFAULT_MAX_INPUT_ROWS;
110
+ return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
111
+ }
112
+
113
+ // How many input rows does this (prefix + value) buffer need? Wrapped line
114
+ // count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
115
+ // takes over inside drawInputLines so at most inputRowsMax rows render.
116
+ function computeInputRowsForBuffer(prefix, value) {
117
+ const budget = inputTextBudget();
118
+ const wrapped = wrapToLines(`${prefix || ''}${value || ''}`, budget);
119
+ const wanted = Math.max(MIN_INPUT_ROWS, wrapped.length);
120
+ return Math.min(inputRowsMax, wanted);
121
+ }
122
+
123
+ // Resize the input area to a new row count. Moves the scroll region so
124
+ // content above shifts accordingly; the dock frame is redrawn at the
125
+ // new position. No-op if the count didn't change.
126
+ function setInputRowsTo(nextRows) {
127
+ const clamped = Math.max(MIN_INPUT_ROWS, Math.min(inputRowsMax, Math.floor(nextRows)));
128
+ if (clamped === inputRows) return false;
129
+ inputRows = clamped;
130
+ reservedRows = FIXED_ROWS + inputRows;
131
+ if (mounted) {
132
+ const bottom = contentBottomRow();
133
+ setScrollRegion(1, bottom);
134
+ renderFrame(lastFrame);
135
+ }
136
+ return true;
137
+ }
138
+
139
+ function padLine(text) {
140
+ const value = String(text || '');
141
+ const pad = Math.max(0, cols() - visibleWidth(value));
142
+ return value + ' '.repeat(pad);
143
+ }
144
+
145
+ function fitText(text, maxWidth) {
146
+ const plain = String(text || '').replace(/\s+/g, ' ').trim();
147
+ if (visibleWidth(plain) <= maxWidth) return plain;
148
+ if (maxWidth <= 1) return '';
149
+ return plain.slice(0, Math.max(0, maxWidth - 1)) + '…';
150
+ }
151
+
152
+ function ruleChars(count) {
153
+ return '═'.repeat(Math.max(0, count));
154
+ }
155
+
156
+ // Top rule: `── ▎Bahulam Code ═════════════ [context] ═══`
157
+ // - Leading two rule chars for a clean edge
158
+ // - Colored accent bar + brand label on the left
159
+ // - Optional right-aligned context strip (session tokens/elapsed) with rule
160
+ // fill between label and context
161
+ function topRuleLine(context = '') {
162
+ const w = Math.max(0, cols() - 1);
163
+ const brand = paint.brand.primary;
164
+ const bold = paint.bold;
165
+
166
+ const leadRule = brand(ruleChars(2));
167
+ const accent = brand(LABEL_ACCENT);
168
+ const label = bold(brand(BRAND_LABEL));
169
+ const leftBlock = `${leadRule} ${accent}${label}`;
170
+ const leftWidth = visibleWidth(leftBlock);
171
+
172
+ const ctx = fitText(context, Math.max(0, Math.floor(w / 2)));
173
+ const ctxWidth = ctx ? visibleWidth(ctx) : 0;
174
+
175
+ // Rule fill in the middle. Reserve 1 space around ctx when present.
176
+ const gap = 1;
177
+ const rightPadRule = 2;
178
+ const middleWidth = Math.max(1, w - leftWidth - gap - ctxWidth - (ctx ? gap + rightPadRule : 0));
179
+ const middleRule = brand(ruleChars(middleWidth));
180
+
181
+ if (!ctx) {
182
+ return `${leftBlock} ${middleRule}`;
183
+ }
184
+ const tailRule = brand(ruleChars(rightPadRule));
185
+ return `${leftBlock} ${middleRule} ${paint.text.dim(ctx)} ${tailRule}`;
186
+ }
187
+
188
+ function bottomRuleLine() {
189
+ return paint.brand.primary(ruleChars(Math.max(0, cols() - 1)));
190
+ }
191
+
192
+ function applyLayout() {
193
+ if (!mounted) return;
194
+ const bottom = contentBottomRow();
195
+ setScrollRegion(1, bottom);
196
+ renderFrame(lastFrame);
197
+ moveTo(bottom, 1);
198
+ }
199
+
200
+ function renderFrame(frame = {}) {
201
+ if (!mounted) return;
202
+ lastFrame = { ...lastFrame, ...frame };
203
+ saveCursor();
204
+
205
+ moveTo(topRuleRow(), 1);
206
+ clearLine();
207
+ write(padLine(topRuleLine(lastFrame.context)));
208
+
209
+ // Spacer above input.
210
+ moveTo(spacerAboveRow(), 1);
211
+ clearLine();
212
+
213
+ // (input rows are written by drawInputLines / clearInputRows)
214
+
215
+ // Spacer below input.
216
+ moveTo(spacerBelowRow(), 1);
217
+ clearLine();
218
+
219
+ moveTo(bottomRuleRow(), 1);
220
+ clearLine();
221
+ write(padLine(bottomRuleLine()));
222
+
223
+ // Meta row: model · cwd ⎇ branch · turn N · tokens · elapsed
224
+ moveTo(metaRow(), 1);
225
+ clearLine();
226
+ const metaIndent = ' '.repeat(META_INDENT);
227
+ const metaBudget = Math.max(0, cols() - META_INDENT - 1);
228
+ const metaText = lastFrame.meta ? paint.text.muted(fitText(lastFrame.meta, metaBudget)) : '';
229
+ write(padLine(`${metaIndent}${metaText}`));
230
+
231
+ // Tips row.
232
+ moveTo(tipsRow(), 1);
233
+ clearLine();
234
+ const tipsIndent = ' '.repeat(META_INDENT);
235
+ const tipsBudget = Math.max(0, cols() - META_INDENT - 1);
236
+ write(padLine(`${tipsIndent}${paint.text.dim(fitText(lastFrame.tips, tipsBudget))}`));
237
+
238
+ // Safety row.
239
+ moveTo(rows(), 1);
240
+ clearLine();
241
+ restoreCursor();
242
+ }
243
+
244
+ function clearInputRows() {
245
+ saveCursor();
246
+ for (let row = inputRowStart(); row <= inputRowEnd(); row++) {
247
+ moveTo(row, 1);
248
+ clearLine();
249
+ }
250
+ restoreCursor();
251
+ }
252
+
253
+ export function clearDockArea({ restore = true } = {}) {
254
+ if (!mounted) return false;
255
+ if (restore) saveCursor();
256
+ for (let row = topRuleRow(); row <= rows(); row++) {
257
+ moveTo(row, 1);
258
+ clearLine();
259
+ }
260
+ if (restore) restoreCursor();
261
+ return true;
262
+ }
263
+
264
+ /**
265
+ * Wrap `prefix + value` into visible lines, keeping only the last N rows
266
+ * with an ellipsis marker on overflow. Returned lines are already indented
267
+ * for the input area.
268
+ */
269
+ function layoutInput(prefix, value) {
270
+ const budget = inputTextBudget();
271
+ const combined = `${prefix || ''}${value || ''}`;
272
+ const wrapped = wrapToLines(combined, budget);
273
+ const tail = tailWithEllipsis(wrapped, inputRows);
274
+ return {
275
+ lines: tail.visible,
276
+ truncated: tail.truncated,
277
+ dropped: tail.dropped,
278
+ wrapped,
279
+ prefix: prefix || '',
280
+ };
281
+ }
282
+
283
+ function drawInputLines(lines) {
284
+ saveCursor();
285
+ const indent = ' '.repeat(INPUT_INDENT);
286
+ for (let i = 0; i < inputRows; i++) {
287
+ const row = inputRowStart() + i;
288
+ moveTo(row, 1);
289
+ clearLine();
290
+ if (i < lines.length) {
291
+ write(`${indent}${lines[i]}`);
292
+ }
293
+ }
294
+ restoreCursor();
295
+ }
296
+
297
+ export function isInputDockMounted() {
298
+ return mounted;
299
+ }
300
+
301
+ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
302
+ const t = term();
303
+ if (!t.isTTY || t.plain) return false;
304
+ if (process.env.BAHULAM_FIXED_INPUT === '0' || process.env.KEPLER_FIXED_INPUT === '0') return false;
305
+ if (mounted) return true;
306
+
307
+ inputRowsMax = resolveMaxInputRows(requestedMax);
308
+ inputRows = MIN_INPUT_ROWS;
309
+ reservedRows = FIXED_ROWS + inputRows;
310
+ mounted = true;
311
+ applyLayout();
312
+
313
+ unsubResize = onResize(() => applyLayout());
314
+ process.once('exit', safeUnmount);
315
+ process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
316
+ return true;
317
+ }
318
+
319
+ export function unmountInputDock() {
320
+ if (!mounted || resetting) return;
321
+ resetting = true;
322
+ try {
323
+ const exitRow = topRuleRow();
324
+ clearDockArea({ restore: false });
325
+ clearScrollRegion();
326
+ moveTo(Math.min(rows(), exitRow), 1);
327
+ if (unsubResize) { unsubResize(); unsubResize = null; }
328
+ } finally {
329
+ mounted = false;
330
+ resetting = false;
331
+ }
332
+ }
333
+
334
+ function safeUnmount() { try { unmountInputDock(); } catch {} }
335
+
336
+ export function moveToContent() {
337
+ if (!mounted) return false;
338
+ moveTo(contentBottomRow(), 1);
339
+ return true;
340
+ }
341
+
342
+ // One row above the bottom of the scroll region. Writes here won't trigger
343
+ // the scroll-on-LF that happens when writing '\n' at the region's bottom row —
344
+ // exactly what the persistent explore/spinner line needs to overwrite in place
345
+ // without piling copies of itself into scrollback.
346
+ export function pinnedStatusRow() {
347
+ if (!mounted) return null;
348
+ return Math.max(1, contentBottomRow() - 1);
349
+ }
350
+
351
+ export function drawPinnedStatus(line) {
352
+ if (!mounted) return false;
353
+ const row = pinnedStatusRow();
354
+ if (row == null) return false;
355
+ saveCursor();
356
+ moveTo(row, 1);
357
+ clearLine();
358
+ write(String(line || ''));
359
+ restoreCursor();
360
+ return true;
361
+ }
362
+
363
+ export function clearPinnedStatus() {
364
+ if (!mounted) return false;
365
+ const row = pinnedStatusRow();
366
+ if (row == null) return false;
367
+ saveCursor();
368
+ moveTo(row, 1);
369
+ clearLine();
370
+ restoreCursor();
371
+ return true;
372
+ }
373
+
374
+ // Redraw just the frame (rules, spacers, meta, tips, safety row) without
375
+ // touching the input rows. Callers that write into the dock's non-input
376
+ // rows (e.g. slash-hint overlays) use this to restore the frame after
377
+ // clearing their overlay.
378
+ export function redrawDockFrame() {
379
+ if (!mounted) return false;
380
+ renderFrame(lastFrame);
381
+ return true;
382
+ }
383
+
384
+ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
385
+ if (!mounted) return false;
386
+ setInputRowsTo(MIN_INPUT_ROWS);
387
+ clearInputRows();
388
+ renderFrame({ context, tips, meta });
389
+ moveTo(inputRowStart(), INPUT_INDENT + 1);
390
+ return true;
391
+ }
392
+
393
+ export function clearInputPrompt() {
394
+ if (!mounted) return false;
395
+ clearInputRows();
396
+ renderFrame(lastFrame);
397
+ return true;
398
+ }
399
+
400
+ export function renderDockInput(prefix, value, { context = '', tips = '', meta = '' } = {}) {
401
+ if (!mounted) return false;
402
+ setInputRowsTo(computeInputRowsForBuffer(prefix, value));
403
+ renderFrame({ context, tips, meta });
404
+ const layout = layoutInput(prefix, value);
405
+ drawInputLines(layout.lines);
406
+ focusDockInput(prefix, value);
407
+ return true;
408
+ }
409
+
410
+ /**
411
+ * Move the terminal cursor to the position that corresponds to the logical
412
+ * end of `prefix + value` within the (possibly wrapped and truncated) input
413
+ * area. If the buffer overflowed, the cursor lands on the last visible row.
414
+ */
415
+ export function focusDockInput(prefix, value = '') {
416
+ if (!mounted) return false;
417
+ const layout = layoutInput(prefix, value);
418
+ const offset = visibleWidth(`${prefix || ''}${value || ''}`);
419
+ const pos = cursorPositionInLines(layout.wrapped, offset);
420
+ const visibleRowIdx = Math.max(
421
+ 0,
422
+ Math.min(inputRows - 1, pos.row - Math.max(0, layout.wrapped.length - inputRows)),
423
+ );
424
+ const row = inputRowStart() + visibleRowIdx;
425
+ const col = Math.min(cols(), INPUT_INDENT + 1 + Math.max(0, pos.col));
426
+ moveTo(row, col);
427
+ return true;
428
+ }
429
+
430
+ export function inputRowColumn() {
431
+ return INPUT_INDENT + 1;
432
+ }
433
+
434
+ // Test-only accessors.
435
+ export function _internals() {
436
+ return {
437
+ inputRows: () => inputRows,
438
+ reservedRows: () => reservedRows,
439
+ layoutInput,
440
+ inputTextBudget,
441
+ FIXED_ROWS,
442
+ BRAND_LABEL,
443
+ };
444
+ }