@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.
- package/README.md +93 -0
- package/package.json +56 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +223 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +312 -0
- package/src/commands/agent.mjs +221 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +202 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/model-catalog.mjs +57 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/prose-chunker.mjs +255 -0
- package/src/context/retriever.mjs +425 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +145 -0
- package/src/core/approval.mjs +700 -0
- package/src/core/attachments.mjs +666 -0
- package/src/core/backend-url.mjs +68 -0
- package/src/core/bundled-runtime.mjs +418 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +331 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +460 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +538 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +388 -0
- package/src/core/safety.mjs +260 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +957 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +2231 -0
- package/src/core/trust.mjs +160 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +374 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +700 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +122 -0
- package/src/terminal/agents.mjs +353 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +310 -0
- package/src/terminal/repl-ask-form.mjs +120 -0
- package/src/terminal/repl-explore.mjs +44 -0
- package/src/terminal/repl-format.mjs +317 -0
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +833 -0
- package/src/terminal/repl-resume.mjs +640 -0
- package/src/terminal/repl-state.mjs +120 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +5032 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +392 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +703 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +510 -0
- package/src/ui/banner.mjs +232 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +170 -0
- package/src/ui/input-dock.mjs +772 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +257 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +167 -0
- package/src/ui/term.mjs +174 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +740 -0
- package/src/ui/tool-details.mjs +504 -0
- 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
|
+
}
|