@bahulam/code 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +93 -0
  2. package/package.json +56 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +223 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +312 -0
  136. package/src/commands/agent.mjs +221 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +202 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/model-catalog.mjs +57 -0
  143. package/src/config/settings-loader.mjs +45 -0
  144. package/src/config/settings.mjs +132 -0
  145. package/src/context/ast-parser.mjs +298 -0
  146. package/src/context/bm25.mjs +85 -0
  147. package/src/context/prose-chunker.mjs +255 -0
  148. package/src/context/retriever.mjs +425 -0
  149. package/src/context/skeleton.mjs +134 -0
  150. package/src/context/symbol-indexer.mjs +375 -0
  151. package/src/core/agent-history.mjs +111 -0
  152. package/src/core/agent-loop.mjs +486 -0
  153. package/src/core/approval-log.mjs +145 -0
  154. package/src/core/approval.mjs +700 -0
  155. package/src/core/attachments.mjs +666 -0
  156. package/src/core/backend-url.mjs +68 -0
  157. package/src/core/bundled-runtime.mjs +418 -0
  158. package/src/core/cache-control.mjs +92 -0
  159. package/src/core/cache.mjs +105 -0
  160. package/src/core/callback-client.mjs +180 -0
  161. package/src/core/checkpoints.mjs +142 -0
  162. package/src/core/compact-history.mjs +127 -0
  163. package/src/core/context-envelope.mjs +54 -0
  164. package/src/core/context-manager.mjs +198 -0
  165. package/src/core/error-guidance.mjs +331 -0
  166. package/src/core/file-diff.mjs +217 -0
  167. package/src/core/headless.mjs +460 -0
  168. package/src/core/hooks-manager.mjs +87 -0
  169. package/src/core/jsonl-writer.mjs +449 -0
  170. package/src/core/local-agent.mjs +538 -0
  171. package/src/core/local-store.mjs +836 -0
  172. package/src/core/mode-selector.mjs +51 -0
  173. package/src/core/output-filter.mjs +177 -0
  174. package/src/core/paths.mjs +190 -0
  175. package/src/core/policy-resolver.mjs +156 -0
  176. package/src/core/pricing.mjs +336 -0
  177. package/src/core/project-artifacts.mjs +39 -0
  178. package/src/core/project-context-loader.mjs +139 -0
  179. package/src/core/providers.mjs +219 -0
  180. package/src/core/rate-limit-display.mjs +121 -0
  181. package/src/core/rate-limiter.mjs +119 -0
  182. package/src/core/resume-mode.mjs +192 -0
  183. package/src/core/risk-tier.mjs +388 -0
  184. package/src/core/safety.mjs +260 -0
  185. package/src/core/scheduler.mjs +173 -0
  186. package/src/core/session-manager.mjs +360 -0
  187. package/src/core/session.mjs +143 -0
  188. package/src/core/settings-sync.mjs +85 -0
  189. package/src/core/stagnation.mjs +57 -0
  190. package/src/core/stream-client.mjs +957 -0
  191. package/src/core/streaming.mjs +182 -0
  192. package/src/core/system-prompt.mjs +140 -0
  193. package/src/core/tasks.mjs +196 -0
  194. package/src/core/tool-executor.mjs +2231 -0
  195. package/src/core/trust.mjs +160 -0
  196. package/src/core/work-scope.mjs +248 -0
  197. package/src/hooks/engine.mjs +162 -0
  198. package/src/mcp/client.mjs +253 -0
  199. package/src/mcp/transport-shttp.mjs +130 -0
  200. package/src/mcp/transport-sse.mjs +131 -0
  201. package/src/mcp/transport-ws.mjs +134 -0
  202. package/src/onboarding/preflight.mjs +374 -0
  203. package/src/permissions/checker.mjs +57 -0
  204. package/src/permissions/command-classifier.mjs +700 -0
  205. package/src/permissions/injection-check.mjs +60 -0
  206. package/src/permissions/path-check.mjs +102 -0
  207. package/src/permissions/prompt.mjs +73 -0
  208. package/src/permissions/sandbox.mjs +112 -0
  209. package/src/plugins/loader.mjs +138 -0
  210. package/src/skills/installer.mjs +188 -0
  211. package/src/skills/loader.mjs +252 -0
  212. package/src/skills/runner.mjs +55 -0
  213. package/src/state/orbit.mjs +263 -0
  214. package/src/state/verbosity.mjs +99 -0
  215. package/src/telemetry/index.mjs +122 -0
  216. package/src/terminal/agents.mjs +353 -0
  217. package/src/terminal/analytics.mjs +292 -0
  218. package/src/terminal/ansi.mjs +695 -0
  219. package/src/terminal/init.mjs +145 -0
  220. package/src/terminal/main.mjs +310 -0
  221. package/src/terminal/repl-ask-form.mjs +120 -0
  222. package/src/terminal/repl-explore.mjs +44 -0
  223. package/src/terminal/repl-format.mjs +317 -0
  224. package/src/terminal/repl-model-form.mjs +132 -0
  225. package/src/terminal/repl-render.mjs +833 -0
  226. package/src/terminal/repl-resume.mjs +640 -0
  227. package/src/terminal/repl-state.mjs +120 -0
  228. package/src/terminal/repl-utils.mjs +34 -0
  229. package/src/terminal/repl.mjs +5032 -0
  230. package/src/terminal/skills.mjs +54 -0
  231. package/src/terminal/tool-display.mjs +392 -0
  232. package/src/tools/agent.mjs +137 -0
  233. package/src/tools/ask-user.mjs +61 -0
  234. package/src/tools/bash.mjs +231 -0
  235. package/src/tools/cron-create.mjs +120 -0
  236. package/src/tools/cron-delete.mjs +49 -0
  237. package/src/tools/cron-list.mjs +37 -0
  238. package/src/tools/edit.mjs +82 -0
  239. package/src/tools/enter-worktree.mjs +69 -0
  240. package/src/tools/exit-worktree.mjs +57 -0
  241. package/src/tools/glob.mjs +117 -0
  242. package/src/tools/grep.mjs +129 -0
  243. package/src/tools/lint.mjs +71 -0
  244. package/src/tools/ls.mjs +58 -0
  245. package/src/tools/lsp.mjs +115 -0
  246. package/src/tools/multi-edit.mjs +94 -0
  247. package/src/tools/notebook-edit.mjs +96 -0
  248. package/src/tools/project-overview.mjs +703 -0
  249. package/src/tools/read-mcp-resource.mjs +57 -0
  250. package/src/tools/read.mjs +138 -0
  251. package/src/tools/registry.mjs +116 -0
  252. package/src/tools/remote-trigger.mjs +84 -0
  253. package/src/tools/send-message.mjs +64 -0
  254. package/src/tools/skill.mjs +52 -0
  255. package/src/tools/test-runner.mjs +49 -0
  256. package/src/tools/todo-write.mjs +68 -0
  257. package/src/tools/tool-search.mjs +77 -0
  258. package/src/tools/web-fetch.mjs +65 -0
  259. package/src/tools/web-search.mjs +89 -0
  260. package/src/tools/write.mjs +55 -0
  261. package/src/ui/approval.mjs +510 -0
  262. package/src/ui/banner.mjs +232 -0
  263. package/src/ui/commands.mjs +537 -0
  264. package/src/ui/formatter.mjs +409 -0
  265. package/src/ui/icons.mjs +170 -0
  266. package/src/ui/input-dock.mjs +772 -0
  267. package/src/ui/markdown.mjs +278 -0
  268. package/src/ui/mission-report.mjs +296 -0
  269. package/src/ui/palette.mjs +189 -0
  270. package/src/ui/render-queue.mjs +500 -0
  271. package/src/ui/slash-commands.mjs +257 -0
  272. package/src/ui/spinner.mjs +116 -0
  273. package/src/ui/sub-agent.mjs +167 -0
  274. package/src/ui/term.mjs +174 -0
  275. package/src/ui/text-layout.mjs +127 -0
  276. package/src/ui/tool-card.mjs +740 -0
  277. package/src/ui/tool-details.mjs +504 -0
  278. package/src/ui/transcript-block.mjs +20 -0
@@ -0,0 +1,772 @@
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_TTY_MODE=stable / KEPLER_TTY_MODE=stable scrollback-safe transcript, no fixed dock
33
+ * BAHULAM_PLAIN=1 / KEPLER_PLAIN=1 deterministic no-ANSI output for automation
34
+ * BAHULAM_FIXED_INPUT=0 / KEPLER_FIXED_INPUT=0 disable dock entirely (fallback readline)
35
+ */
36
+
37
+ import { paint, width as visibleWidth } from './palette.mjs';
38
+ import { term, onResize } from './term.mjs';
39
+ import { wrapToLines, tailWithEllipsis, cursorPositionInLines } from './text-layout.mjs';
40
+ import * as queue from './render-queue.mjs';
41
+
42
+ const ESC = '\x1b[';
43
+ const OUT = process.stderr;
44
+
45
+ const BRAND_LABEL = 'Bahulam Code';
46
+ const LABEL_ACCENT = '▎'; // left-edge accent bar
47
+
48
+ // Horizontal margin: text starts INPUT_INDENT cols from the left edge and
49
+ // leaves INPUT_RIGHT_PAD cols of clear space on the right so wrapped lines
50
+ // don't kiss the terminal edge.
51
+ const INPUT_INDENT = 5;
52
+ const INPUT_RIGHT_PAD = 2;
53
+ const META_INDENT = 4;
54
+
55
+ const DEFAULT_MAX_INPUT_ROWS = 6;
56
+ const DEFAULT_OVERLAY_MAX_ROWS = 8;
57
+ const MIN_INPUT_ROWS = 1;
58
+ const MAX_INPUT_ROWS_CAP = 12;
59
+
60
+ // Fixed rows around the input area:
61
+ // 1 top rule + 1 spacer above + input(N) + 1 spacer below + 1 bottom rule
62
+ // + 1 meta row + 1 tips row + 1 safety row
63
+ // = 7 + N
64
+ const FIXED_ROWS = 7;
65
+
66
+ let mounted = false;
67
+ let inputRowsMax = DEFAULT_MAX_INPUT_ROWS;
68
+ let inputRows = MIN_INPUT_ROWS;
69
+ let reservedRows = FIXED_ROWS + MIN_INPUT_ROWS;
70
+ let unsubResize = null;
71
+ // prefix + value are tracked here so any code path that redraws the dock
72
+ // (renderFrame, applyLayout, redrawDockFrame, resize) can end by parking
73
+ // the cursor at the correct input position. Without this, readline echoes
74
+ // land on rows the dock briefly moved through mid-render and characters
75
+ // appear above/below the input row.
76
+ let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
77
+ let resetting = false;
78
+ let lastGeometry = null;
79
+ let contentCursorRow = 1;
80
+ let contentCursorCol = 1;
81
+ let contentTrackingActive = false;
82
+ let suppressWriteTracking = 0;
83
+ let originalStdoutWrite = null;
84
+ let originalStderrWrite = null;
85
+
86
+ function write(s) {
87
+ // All dock frame bytes flow through the render queue's serialized raw
88
+ // channel when it is active — bypassing the content redirect so frame
89
+ // paints never land in the transcript. Legacy fallback writes straight
90
+ // to stderr with the old suppress-tracking guard.
91
+ if (queue.isActive()) {
92
+ queue.raw(s);
93
+ return;
94
+ }
95
+ try {
96
+ suppressWriteTracking++;
97
+ OUT.write(s);
98
+ } catch {
99
+ } finally {
100
+ suppressWriteTracking = Math.max(0, suppressWriteTracking - 1);
101
+ }
102
+ }
103
+ function setScrollRegion(top, bottom) {
104
+ // Keep the queue's notion of the content region in sync — its content
105
+ // cursor clamps to this bottom.
106
+ if (queue.isActive()) { queue.setRegion(top, bottom); return; }
107
+ write(`${ESC}${top};${bottom}r`);
108
+ }
109
+ function clearScrollRegion() {
110
+ if (queue.isActive()) { queue.clearRegion(); return; }
111
+ write(`${ESC}r`);
112
+ }
113
+ function saveCursor() { write(`${ESC}s`); }
114
+ function restoreCursor() { write(`${ESC}u`); }
115
+ function moveTo(row, col) { write(`${ESC}${row};${col}H`); }
116
+ function clearLine() { write(`${ESC}2K`); }
117
+
118
+ function rows() {
119
+ return Math.max(10, term().rows || 24);
120
+ }
121
+
122
+ function cols() {
123
+ return Math.max(40, term().columns || 80);
124
+ }
125
+
126
+ function drawableColumns() {
127
+ // Avoid writing into the final terminal column. Many terminals enter
128
+ // autowrap state there; during rapid resizes that can push fixed dock
129
+ // frame lines into scrollback.
130
+ return Math.max(1, cols() - 1);
131
+ }
132
+
133
+ function contentBottomRow() {
134
+ return Math.max(1, rows() - reservedRows);
135
+ }
136
+
137
+ function clampContentCursor() {
138
+ const bottom = contentBottomRow();
139
+ contentCursorRow = Math.max(1, Math.min(bottom, contentCursorRow || 1));
140
+ contentCursorCol = Math.max(1, Math.min(cols(), contentCursorCol || 1));
141
+ }
142
+
143
+ function resetContentCursor(row = 1, col = 1) {
144
+ contentCursorRow = Math.max(1, Math.min(contentBottomRow(), Math.floor(row || 1)));
145
+ contentCursorCol = Math.max(1, Math.min(cols(), Math.floor(col || 1)));
146
+ }
147
+
148
+ function trackContentWrite(chunk) {
149
+ if (!mounted || !contentTrackingActive || suppressWriteTracking > 0) return;
150
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
151
+ if (!text) return;
152
+ const clean = text
153
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
154
+ .replace(/\x1b[()][A-Za-z0-9]/g, '');
155
+ const bottom = contentBottomRow();
156
+ const width = Math.max(1, drawableColumns());
157
+ for (const ch of clean) {
158
+ if (ch === '\r') {
159
+ contentCursorCol = 1;
160
+ continue;
161
+ }
162
+ if (ch === '\n') {
163
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
164
+ contentCursorCol = 1;
165
+ continue;
166
+ }
167
+ contentCursorCol++;
168
+ if (contentCursorCol > width) {
169
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
170
+ contentCursorCol = 1;
171
+ }
172
+ }
173
+ }
174
+
175
+ function patchOutputTracking() {
176
+ if (originalStdoutWrite || originalStderrWrite) return;
177
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
178
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
179
+ process.stdout.write = function trackedStdoutWrite(chunk, ...args) {
180
+ trackContentWrite(chunk);
181
+ return originalStdoutWrite(chunk, ...args);
182
+ };
183
+ process.stderr.write = function trackedStderrWrite(chunk, ...args) {
184
+ trackContentWrite(chunk);
185
+ return originalStderrWrite(chunk, ...args);
186
+ };
187
+ }
188
+
189
+ function unpatchOutputTracking() {
190
+ if (originalStdoutWrite) {
191
+ process.stdout.write = originalStdoutWrite;
192
+ originalStdoutWrite = null;
193
+ }
194
+ if (originalStderrWrite) {
195
+ process.stderr.write = originalStderrWrite;
196
+ originalStderrWrite = null;
197
+ }
198
+ }
199
+
200
+ // Row map (top → bottom of the reserved region).
201
+ function topRuleRow() { return contentBottomRow() + 1; }
202
+ function spacerAboveRow() { return topRuleRow() + 1; }
203
+ function inputRowStart() { return spacerAboveRow() + 1; }
204
+ function inputRowEnd() { return inputRowStart() + inputRows - 1; }
205
+ function spacerBelowRow() { return inputRowEnd() + 1; }
206
+ function bottomRuleRow() { return spacerBelowRow() + 1; }
207
+ function metaRow() { return bottomRuleRow() + 1; }
208
+ function tipsRow() { return metaRow() + 1; }
209
+
210
+ function inputTextBudget() {
211
+ return Math.max(8, cols() - INPUT_INDENT - INPUT_RIGHT_PAD - 1);
212
+ }
213
+
214
+ function resolveMaxInputRows(requested) {
215
+ const raw = requested != null
216
+ ? requested
217
+ : (process.env.BAHULAM_INPUT_ROWS_MAX || process.env.KEPLER_INPUT_ROWS_MAX);
218
+ const n = Number.parseInt(String(raw), 10);
219
+ if (!Number.isFinite(n)) return DEFAULT_MAX_INPUT_ROWS;
220
+ return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
221
+ }
222
+
223
+ function resolveOverlayRowCap(requested = DEFAULT_OVERLAY_MAX_ROWS) {
224
+ const n = Number.parseInt(String(requested), 10);
225
+ if (!Number.isFinite(n)) return DEFAULT_OVERLAY_MAX_ROWS;
226
+ // Overlays (approval scripts) may need more rows than the typing cap —
227
+ // the user cannot approve what they cannot see. Allow up to half the
228
+ // terminal so the transcript stays visible; typing input keeps the
229
+ // tight MAX_INPUT_ROWS_CAP via normalizeInputRows above.
230
+ const dynamicCap = Math.max(MAX_INPUT_ROWS_CAP, Math.floor((rows() || 24) / 2));
231
+ return Math.max(MIN_INPUT_ROWS, Math.min(dynamicCap, n));
232
+ }
233
+
234
+ function overlayRowsForWrapped(wrappedLength, requestedMaxRows = DEFAULT_OVERLAY_MAX_ROWS) {
235
+ const rowCap = resolveOverlayRowCap(requestedMaxRows);
236
+ const wanted = Math.max(MIN_INPUT_ROWS, Math.floor(Number(wrappedLength) || 0));
237
+ return Math.min(rowCap, wanted);
238
+ }
239
+
240
+ // How many input rows does this (prefix + value) buffer need? Wrapped line
241
+ // count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
242
+ // takes over inside drawInputLines so at most inputRowsMax rows render.
243
+ function computeInputRowsForBuffer(prefix, value) {
244
+ const budget = inputTextBudget();
245
+ const wrapped = wrapToLines(`${prefix || ''}${value || ''}`, budget);
246
+ const wanted = Math.max(MIN_INPUT_ROWS, wrapped.length);
247
+ return Math.min(inputRowsMax, wanted);
248
+ }
249
+
250
+ // Resize the input area to a new row count. Moves the scroll region so
251
+ // content above shifts accordingly; the dock frame is redrawn at the
252
+ // new position. No-op if the count didn't change.
253
+ //
254
+ // CRITICAL: when the dock SHRINKS (inputRows decreases), the frame moves
255
+ // down and previously-dock rows become part of the scroll region. Those
256
+ // rows still hold their old dock content (rule chars, prior input text,
257
+ // meta line) which then leaks into the transcript as streamed content
258
+ // scrolls past them. We clear the old dock region BEFORE moving the frame
259
+ // so the freed rows are blank when they enter the scroll region.
260
+ function setInputRowsTo(nextRows, { maxRows = inputRowsMax } = {}) {
261
+ const clamped = Math.max(MIN_INPUT_ROWS, Math.min(maxRows, Math.floor(nextRows)));
262
+ if (clamped === inputRows) return false;
263
+ const shrinking = clamped < inputRows;
264
+ const growing = clamped > inputRows;
265
+ if (mounted && shrinking) {
266
+ // Clear the entire old reserved region — top rule through safety row —
267
+ // so nothing that lived here leaks into the scroll region after we
268
+ // move the frame down.
269
+ for (let row = topRuleRow(); row <= rows(); row++) {
270
+ moveTo(row, 1);
271
+ clearLine();
272
+ }
273
+ } else if (mounted && growing) {
274
+ // GROWING is the case that used to eat transcript. Rows near the
275
+ // bottom of the current scroll region are about to become dock rows;
276
+ // whatever transcript sat on them would be overwritten by the wider
277
+ // dock frame and NEVER enter terminal scrollback, so users lost
278
+ // history the moment approval prompts (or any overlay) mounted.
279
+ //
280
+ // Fix: push the doomed rows into scrollback BEFORE shrinking the
281
+ // region. Move cursor to the last row of the CURRENT (larger) scroll
282
+ // region and emit `growth` line-feeds. Each LF at the last row of a
283
+ // DECSTBM region scrolls the whole region up one line — top row goes
284
+ // to scrollback, everything else shifts up, bottom clears. After N
285
+ // such LFs the content that USED to be on rows we're about to reserve
286
+ // is safely in scrollback and the reserved rows are blank — ready
287
+ // for the dock frame with zero visible loss to the user.
288
+ const growth = clamped - inputRows;
289
+ const oldContentBottom = contentBottomRow(); // computed against OLD reservedRows
290
+ moveTo(oldContentBottom, 1);
291
+ for (let i = 0; i < growth; i++) write('\n');
292
+ }
293
+ inputRows = clamped;
294
+ reservedRows = FIXED_ROWS + inputRows;
295
+ if (mounted) {
296
+ const bottom = contentBottomRow();
297
+ setScrollRegion(1, bottom);
298
+ renderFrame(lastFrame);
299
+ }
300
+ return true;
301
+ }
302
+
303
+ function padLine(text) {
304
+ const value = String(text || '');
305
+ const pad = Math.max(0, drawableColumns() - visibleWidth(value));
306
+ return value + ' '.repeat(pad);
307
+ }
308
+
309
+ function fitText(text, maxWidth) {
310
+ const plain = String(text || '').replace(/\s+/g, ' ').trim();
311
+ if (visibleWidth(plain) <= maxWidth) return plain;
312
+ if (maxWidth <= 1) return '';
313
+ return plain.slice(0, Math.max(0, maxWidth - 1)) + '…';
314
+ }
315
+
316
+ function ruleChars(count) {
317
+ return '═'.repeat(Math.max(0, count));
318
+ }
319
+
320
+ // Top rule: `── ▎Bahulam Code ═════════════ [context] ═══`
321
+ // - Leading two rule chars for a clean edge
322
+ // - Colored accent bar + brand label on the left
323
+ // - Optional right-aligned context strip (session tokens/elapsed) with rule
324
+ // fill between label and context
325
+ function topRuleLine(context = '') {
326
+ const w = Math.max(0, drawableColumns());
327
+ const brand = paint.brand.primary;
328
+ const bold = paint.bold;
329
+
330
+ const leadRule = brand(ruleChars(2));
331
+ const accent = brand(LABEL_ACCENT);
332
+ const label = bold(brand(BRAND_LABEL));
333
+ const leftBlock = `${leadRule} ${accent}${label}`;
334
+ const leftWidth = visibleWidth(leftBlock);
335
+ const rightPadRule = 2;
336
+
337
+ const maxCtxWidth = Math.max(0, Math.min(
338
+ Math.floor(w / 2),
339
+ w - leftWidth - 1 /* left gap */ - 1 /* minimum middle */ - 1 /* ctx gap */ - rightPadRule - 1 /* tail gap */,
340
+ ));
341
+ const ctx = fitText(context, maxCtxWidth);
342
+ const ctxWidth = ctx ? visibleWidth(ctx) : 0;
343
+
344
+ // Rule fill in the middle. Reserve 1 space around ctx when present.
345
+ const middleWidth = ctx
346
+ ? Math.max(1, w - leftWidth - ctxWidth - rightPadRule - 3)
347
+ : Math.max(1, w - leftWidth - 1);
348
+ const middleRule = brand(ruleChars(middleWidth));
349
+
350
+ if (!ctx) {
351
+ return `${leftBlock} ${middleRule}`;
352
+ }
353
+ const tailRule = brand(ruleChars(rightPadRule));
354
+ return `${leftBlock} ${middleRule} ${paint.text.dim(ctx)} ${tailRule}`;
355
+ }
356
+
357
+ function bottomRuleLine() {
358
+ return paint.brand.primary(ruleChars(Math.max(0, drawableColumns())));
359
+ }
360
+
361
+ // Always park the cursor at the tracked (prefix, value) input position.
362
+ // Called at the END of every dock render path so readline echoes land in
363
+ // the input row, not on whichever row a mid-render moveTo left them on.
364
+ // When there is no tracked input (nothing to focus yet), park at the
365
+ // scroll-region bottom so writes at least stay above the dock frame.
366
+ function parkCursorAtInput() {
367
+ if (!mounted) return;
368
+ const prefix = lastFrame.prefix || '';
369
+ const value = lastFrame.value || '';
370
+ if (!prefix && !value) {
371
+ if (queue.isActive()) {
372
+ queue.park(inputRowStart(), INPUT_INDENT + 1);
373
+ return;
374
+ }
375
+ moveTo(inputRowStart(), INPUT_INDENT + 1);
376
+ return;
377
+ }
378
+ focusDockInput(prefix, value, lastFrame.cursor);
379
+ }
380
+
381
+ function applyLayout({ clearPrevious = false } = {}) {
382
+ if (!mounted) return;
383
+ if (clearPrevious && lastGeometry) {
384
+ clearDockArea({ restore: true, geometry: lastGeometry });
385
+ }
386
+ const bottom = contentBottomRow();
387
+ setScrollRegion(1, bottom);
388
+ renderFrame(lastFrame);
389
+ clampContentCursor();
390
+ // On (re)mount and resize, park at input if we have one; otherwise sit at
391
+ // the bottom of the content region so any pending content writes flush
392
+ // above the dock rather than into a stale mid-frame position.
393
+ if (lastFrame.prefix || lastFrame.value) {
394
+ parkCursorAtInput();
395
+ } else {
396
+ moveTo(bottom, 1);
397
+ }
398
+ }
399
+
400
+ function renderFrame(frame = {}) {
401
+ if (!mounted) return;
402
+ lastFrame = { ...lastFrame, ...frame };
403
+
404
+ moveTo(topRuleRow(), 1);
405
+ clearLine();
406
+ write(padLine(topRuleLine(lastFrame.context)));
407
+
408
+ // Spacer above input.
409
+ moveTo(spacerAboveRow(), 1);
410
+ clearLine();
411
+
412
+ // (input rows are written by drawInputLines / clearInputRows)
413
+ if (Array.isArray(lastFrame.overlayLines)) {
414
+ drawInputLines(lastFrame.overlayLines);
415
+ }
416
+
417
+ // Spacer below input.
418
+ moveTo(spacerBelowRow(), 1);
419
+ clearLine();
420
+
421
+ moveTo(bottomRuleRow(), 1);
422
+ clearLine();
423
+ write(padLine(bottomRuleLine()));
424
+
425
+ // Meta row: cwd ⎇ branch · turn N · tokens
426
+ moveTo(metaRow(), 1);
427
+ clearLine();
428
+ const metaIndent = ' '.repeat(META_INDENT);
429
+ const metaBudget = Math.max(0, cols() - META_INDENT - 1);
430
+ const metaText = lastFrame.meta ? paint.text.muted(fitText(lastFrame.meta, metaBudget)) : '';
431
+ write(padLine(`${metaIndent}${metaText}`));
432
+
433
+ // Tips row.
434
+ moveTo(tipsRow(), 1);
435
+ clearLine();
436
+ const tipsIndent = ' '.repeat(META_INDENT);
437
+ const tipsBudget = Math.max(0, cols() - META_INDENT - 1);
438
+ write(padLine(`${tipsIndent}${paint.text.dim(fitText(lastFrame.tips, tipsBudget))}`));
439
+
440
+ // Safety row.
441
+ moveTo(rows(), 1);
442
+ clearLine();
443
+
444
+ // Deliberately NOT using saveCursor/restoreCursor. VT100 has a single
445
+ // cursor-save slot per terminal; nested calls in drawInputLines /
446
+ // pinned-status writers clobber the outer save and restore to the wrong
447
+ // place. Instead, callers that need the cursor parked at the input row
448
+ // invoke parkCursorAtInput() after renderFrame returns.
449
+ lastGeometry = {
450
+ top: topRuleRow(),
451
+ bottom: rows(),
452
+ };
453
+ }
454
+
455
+ function clearDockRows(startRow, endRow) {
456
+ const maxRow = rows();
457
+ const start = Math.max(1, Math.min(maxRow, Math.floor(startRow || 1)));
458
+ const end = Math.max(start, Math.min(maxRow, Math.floor(endRow || maxRow)));
459
+ for (let row = start; row <= end; row++) {
460
+ moveTo(row, 1);
461
+ clearLine();
462
+ }
463
+ }
464
+
465
+ function clearInputRows() {
466
+ for (let row = inputRowStart(); row <= inputRowEnd(); row++) {
467
+ moveTo(row, 1);
468
+ clearLine();
469
+ }
470
+ }
471
+
472
+ export function clearDockArea({ restore = true, geometry = null } = {}) {
473
+ if (!mounted) return false;
474
+ if (restore) saveCursor();
475
+ clearDockRows(geometry?.top || topRuleRow(), geometry?.bottom || rows());
476
+ if (restore) restoreCursor();
477
+ return true;
478
+ }
479
+
480
+ /**
481
+ * Wrap `prefix + value` into visible lines, keeping only the last N rows
482
+ * with an ellipsis marker on overflow. Returned lines are already indented
483
+ * for the input area.
484
+ */
485
+ function layoutInput(prefix, value) {
486
+ const budget = inputTextBudget();
487
+ const combined = `${prefix || ''}${value || ''}`;
488
+ const wrapped = wrapToLines(combined, budget);
489
+ const tail = tailWithEllipsis(wrapped, inputRows);
490
+ return {
491
+ lines: tail.visible,
492
+ truncated: tail.truncated,
493
+ dropped: tail.dropped,
494
+ wrapped,
495
+ prefix: prefix || '',
496
+ };
497
+ }
498
+
499
+ function layoutOverlayLines(lines) {
500
+ const budget = inputTextBudget();
501
+ const wrapped = [];
502
+ for (const line of lines) {
503
+ wrapped.push(...wrapToLines(line, budget));
504
+ }
505
+ return wrapped.length ? wrapped : [''];
506
+ }
507
+
508
+ function drawInputLines(lines) {
509
+ const indent = ' '.repeat(INPUT_INDENT);
510
+ for (let i = 0; i < inputRows; i++) {
511
+ const row = inputRowStart() + i;
512
+ moveTo(row, 1);
513
+ clearLine();
514
+ if (i < lines.length) {
515
+ write(`${indent}${lines[i]}`);
516
+ }
517
+ }
518
+ // No save/restore — caller parks cursor via focusDockInput.
519
+ }
520
+
521
+ export function isInputDockMounted() {
522
+ return mounted;
523
+ }
524
+
525
+ export function mountInputDock({
526
+ inputRowsMax: requestedMax,
527
+ initialContentRow = 1,
528
+ initialContentCol = 1,
529
+ } = {}) {
530
+ const t = term();
531
+ if (!t.isTTY || t.plain) return false;
532
+ if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
533
+ if (process.env.BAHULAM_FIXED_INPUT === '0' || process.env.KEPLER_FIXED_INPUT === '0') return false;
534
+ if (mounted) return true;
535
+
536
+ inputRowsMax = resolveMaxInputRows(requestedMax);
537
+ inputRows = MIN_INPUT_ROWS;
538
+ reservedRows = FIXED_ROWS + inputRows;
539
+ resetContentCursor(initialContentRow, initialContentCol);
540
+ contentTrackingActive = false;
541
+ mounted = true;
542
+ // Render queue becomes the sole writer + exact cursor tracker. The
543
+ // legacy simulate-by-parsing patch only engages if activation is
544
+ // refused (shouldn't happen — mount gating matches activate gating).
545
+ const queued = queue.activate({
546
+ initialRow: contentCursorRow,
547
+ initialCol: contentCursorCol,
548
+ bottom: contentBottomRow(),
549
+ });
550
+ if (!queued) patchOutputTracking();
551
+ applyLayout();
552
+
553
+ unsubResize = onResize(() => {
554
+ if (queue.isActive()) {
555
+ // Terminal reflow makes any tracked position fiction — hard
556
+ // re-anchor to the new content-region bottom before repainting.
557
+ queue.reanchor({ row: contentBottomRow(), col: 1, bottom: contentBottomRow() });
558
+ resetContentCursor(contentBottomRow(), 1);
559
+ }
560
+ applyLayout({ clearPrevious: true });
561
+ });
562
+ process.once('exit', safeUnmount);
563
+ process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
564
+ return true;
565
+ }
566
+
567
+ export function unmountInputDock() {
568
+ if (!mounted || resetting) return;
569
+ resetting = true;
570
+ try {
571
+ const exitRow = topRuleRow();
572
+ clearDockArea({ restore: false });
573
+ clearScrollRegion();
574
+ moveTo(Math.min(rows(), exitRow), 1);
575
+ if (unsubResize) { unsubResize(); unsubResize = null; }
576
+ } finally {
577
+ mounted = false;
578
+ contentTrackingActive = false;
579
+ queue.deactivate();
580
+ unpatchOutputTracking();
581
+ resetting = false;
582
+ lastGeometry = null;
583
+ }
584
+ }
585
+
586
+ function safeUnmount() { try { unmountInputDock(); } catch {} }
587
+
588
+ export function moveToContent() {
589
+ if (!mounted) return false;
590
+ if (queue.isActive()) {
591
+ // Content self-positions through queue.content(); nothing to do.
592
+ return true;
593
+ }
594
+ clampContentCursor();
595
+ contentTrackingActive = true;
596
+ moveTo(contentCursorRow, contentCursorCol);
597
+ return true;
598
+ }
599
+
600
+ // The next transcript row. Spinner/status overlays live next to the latest
601
+ // content instead of near the scroll-region bottom; otherwise sparse agent
602
+ // events leave large blank holes between visible lines.
603
+ export function pinnedStatusRow() {
604
+ if (!mounted) return null;
605
+ clampContentCursor();
606
+ return Math.max(1, Math.min(contentBottomRow(), contentCursorRow));
607
+ }
608
+
609
+ export function drawPinnedStatus(line) {
610
+ if (!mounted) return false;
611
+ if (queue.isActive()) {
612
+ // Coalesced, serialized, no VT100 save-slot involvement.
613
+ queue.status(String(line || ''));
614
+ return true;
615
+ }
616
+ const row = pinnedStatusRow();
617
+ if (row == null) return false;
618
+ saveCursor();
619
+ moveTo(row, 1);
620
+ clearLine();
621
+ write(String(line || ''));
622
+ restoreCursor();
623
+ return true;
624
+ }
625
+
626
+ export function clearPinnedStatus() {
627
+ if (!mounted) return false;
628
+ if (queue.isActive()) {
629
+ queue.clearStatus();
630
+ return true;
631
+ }
632
+ const row = pinnedStatusRow();
633
+ if (row == null) return false;
634
+ saveCursor();
635
+ moveTo(row, 1);
636
+ clearLine();
637
+ restoreCursor();
638
+ return true;
639
+ }
640
+
641
+ // Redraw just the frame (rules, spacers, meta, tips, safety row) without
642
+ // touching the input rows. Callers that write into the dock's non-input
643
+ // rows (e.g. slash-hint overlays) use this to restore the frame after
644
+ // clearing their overlay.
645
+ export function redrawDockFrame() {
646
+ if (!mounted) return false;
647
+ contentTrackingActive = false;
648
+ renderFrame(lastFrame);
649
+ parkCursorAtInput();
650
+ return true;
651
+ }
652
+
653
+ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
654
+ if (!mounted) return false;
655
+ contentTrackingActive = false;
656
+ setInputRowsTo(MIN_INPUT_ROWS);
657
+ clearInputRows();
658
+ renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
659
+ moveTo(inputRowStart(), INPUT_INDENT + 1);
660
+ return true;
661
+ }
662
+
663
+ export function clearInputPrompt() {
664
+ if (!mounted) return false;
665
+ contentTrackingActive = false;
666
+ lastFrame.value = '';
667
+ lastFrame.overlayLines = null;
668
+ setInputRowsTo(MIN_INPUT_ROWS);
669
+ clearInputRows();
670
+ renderFrame(lastFrame);
671
+ parkCursorAtInput();
672
+ return true;
673
+ }
674
+
675
+ export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
676
+ if (!mounted) return false;
677
+ contentTrackingActive = false;
678
+ setInputRowsTo(computeInputRowsForBuffer(prefix, value));
679
+ renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
680
+ const layout = layoutInput(prefix, value);
681
+ drawInputLines(layout.lines);
682
+ focusDockInput(prefix, value, cursor);
683
+ return true;
684
+ }
685
+
686
+ export function renderDockOverlay({
687
+ context = '',
688
+ lines = [],
689
+ meta = '',
690
+ tips = '',
691
+ maxRows = DEFAULT_OVERLAY_MAX_ROWS,
692
+ } = {}) {
693
+ if (!mounted) return false;
694
+ contentTrackingActive = false;
695
+ const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
696
+ const wrapped = layoutOverlayLines(sourceLines);
697
+ const rowCap = resolveOverlayRowCap(maxRows);
698
+ setInputRowsTo(overlayRowsForWrapped(wrapped.length, rowCap), { maxRows: rowCap });
699
+ const tail = tailWithEllipsis(wrapped, inputRows);
700
+ renderFrame({
701
+ context,
702
+ meta,
703
+ tips,
704
+ prefix: '',
705
+ value: '',
706
+ cursor: null,
707
+ overlayLines: tail.visible,
708
+ });
709
+ moveTo(rows(), 1);
710
+ return true;
711
+ }
712
+
713
+ /**
714
+ * Move the terminal cursor to the position that corresponds to
715
+ * `prefix + value[0..cursorInValue]` within the (possibly wrapped and
716
+ * truncated) input area. When `cursorInValue` is null/undefined, the cursor
717
+ * lands at the logical end of `value` (append-mode default).
718
+ *
719
+ * `cursorInValue` is a char index into the RAW value string (matching
720
+ * readline's `rl.cursor`), not into the wrapped/rendered output.
721
+ */
722
+ export function focusDockInput(prefix, value = '', cursorInValue = null) {
723
+ if (!mounted) return false;
724
+ contentTrackingActive = false;
725
+ const layout = layoutInput(prefix, value);
726
+ const valueStr = String(value || '');
727
+ const rawCursor = cursorInValue == null
728
+ ? valueStr.length
729
+ : Math.max(0, Math.min(valueStr.length, Math.floor(cursorInValue)));
730
+ const cursorSlice = valueStr.slice(0, rawCursor);
731
+ const offset = visibleWidth(`${prefix || ''}${cursorSlice}`);
732
+ const pos = cursorPositionInLines(layout.wrapped, offset);
733
+ const visibleRowIdx = Math.max(
734
+ 0,
735
+ Math.min(inputRows - 1, pos.row - Math.max(0, layout.wrapped.length - inputRows)),
736
+ );
737
+ const row = inputRowStart() + visibleRowIdx;
738
+ const col = Math.min(cols(), INPUT_INDENT + 1 + Math.max(0, pos.col));
739
+ if (queue.isActive()) {
740
+ // Record the park position — every queue op re-parks here so readline
741
+ // echoes always land in the input row, even mid-stream.
742
+ queue.park(row, col);
743
+ return true;
744
+ }
745
+ moveTo(row, col);
746
+ return true;
747
+ }
748
+
749
+ export function inputRowColumn() {
750
+ return INPUT_INDENT + 1;
751
+ }
752
+
753
+ // Test-only accessors.
754
+ export function _internals() {
755
+ return {
756
+ inputRows: () => inputRows,
757
+ reservedRows: () => reservedRows,
758
+ layoutInput,
759
+ inputTextBudget,
760
+ topRuleLine,
761
+ bottomRuleLine,
762
+ padLine,
763
+ drawableColumns,
764
+ resetContentCursor,
765
+ contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
766
+ overlayRowsForWrapped,
767
+ FIXED_ROWS,
768
+ MAX_INPUT_ROWS_CAP,
769
+ DEFAULT_OVERLAY_MAX_ROWS,
770
+ BRAND_LABEL,
771
+ };
772
+ }