@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,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kepler palette — semantic color tokens for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every feature module imports from here, never from raw ANSI. Tokens resolve
|
|
5
|
+
* at call time so changing terminal capabilities (resize, `refresh()`) is
|
|
6
|
+
* picked up without restarting the process.
|
|
7
|
+
*
|
|
8
|
+
* import { paint } from './palette.mjs';
|
|
9
|
+
* process.stdout.write(paint.brand.primary('KEPLER'));
|
|
10
|
+
*
|
|
11
|
+
* Composition (multiple styles on one string):
|
|
12
|
+
*
|
|
13
|
+
* paint.bold(paint.brand.primary('KEPLER'))
|
|
14
|
+
*
|
|
15
|
+
* Tier behavior:
|
|
16
|
+
* truecolor → 24-bit RGB
|
|
17
|
+
* ansi256 → nearest 256-color index
|
|
18
|
+
* ansi16 → nearest basic color
|
|
19
|
+
* none → identity (input returned unchanged)
|
|
20
|
+
*
|
|
21
|
+
* Brand identity (Mission Control PRD-055):
|
|
22
|
+
* primary Abundance Cyan #06b6d4
|
|
23
|
+
* accent Stellar Magenta #ec4899
|
|
24
|
+
* data Neon Cyan #22d3ee
|
|
25
|
+
* success Aligned green #22c55e
|
|
26
|
+
* warn Soft amber #eab308
|
|
27
|
+
* danger Failure red #ef4444
|
|
28
|
+
* dim Sub-agent / hint #6b7280
|
|
29
|
+
* text Primary text #c9d1d9
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { term } from './term.mjs';
|
|
33
|
+
|
|
34
|
+
const ESC = '\x1b[';
|
|
35
|
+
const RESET = `${ESC}0m`;
|
|
36
|
+
|
|
37
|
+
// ── Brand tokens ─────────────────────────────────────────────────────────
|
|
38
|
+
// Each token is { rgb: [r,g,b], ansi256: n, ansi16: 'fgName' }.
|
|
39
|
+
// `ansi16` maps to a key in BASIC_FG below.
|
|
40
|
+
|
|
41
|
+
export const TOKENS = Object.freeze({
|
|
42
|
+
// Brand
|
|
43
|
+
'brand.primary': { rgb: [6, 182, 212], ansi256: 44, ansi16: 'cyan' }, // #06b6d4
|
|
44
|
+
'brand.accent': { rgb: [236, 72, 153], ansi256: 198, ansi16: 'magenta' }, // #ec4899
|
|
45
|
+
'brand.data': { rgb: [34, 211, 238], ansi256: 87, ansi16: 'cyan' }, // #22d3ee
|
|
46
|
+
|
|
47
|
+
// State
|
|
48
|
+
'state.success': { rgb: [34, 197, 94], ansi256: 41, ansi16: 'green' }, // #22c55e
|
|
49
|
+
'state.warn': { rgb: [234, 179, 8], ansi256: 220, ansi16: 'yellow' }, // #eab308
|
|
50
|
+
'state.danger': { rgb: [239, 68, 68], ansi256: 196, ansi16: 'red' }, // #ef4444
|
|
51
|
+
|
|
52
|
+
// Text
|
|
53
|
+
'text.primary': { rgb: [201, 209, 217], ansi256: 250, ansi16: 'white' }, // #c9d1d9
|
|
54
|
+
'text.dim': { rgb: [107, 114, 128], ansi256: 245, ansi16: 'gray' }, // #6b7280
|
|
55
|
+
'text.muted': { rgb: [156, 163, 175], ansi256: 247, ansi16: 'gray' }, // #9ca3af
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── ANSI 16-color foreground codes ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const BASIC_FG = {
|
|
61
|
+
black: 30,
|
|
62
|
+
red: 31,
|
|
63
|
+
green: 32,
|
|
64
|
+
yellow: 33,
|
|
65
|
+
blue: 34,
|
|
66
|
+
magenta: 35,
|
|
67
|
+
cyan: 36,
|
|
68
|
+
white: 37,
|
|
69
|
+
gray: 90,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ── Style codes (work at every tier) ─────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const STYLE_CODES = {
|
|
75
|
+
bold: [1, 22],
|
|
76
|
+
dim: [2, 22],
|
|
77
|
+
italic: [3, 23],
|
|
78
|
+
underline: [4, 24],
|
|
79
|
+
inverse: [7, 27],
|
|
80
|
+
strike: [9, 29],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ── Open / close sequence builders ───────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function openForToken(token, capability) {
|
|
86
|
+
const def = TOKENS[token];
|
|
87
|
+
if (!def) return '';
|
|
88
|
+
|
|
89
|
+
if (capability === 'truecolor') {
|
|
90
|
+
const [r, g, b] = def.rgb;
|
|
91
|
+
return `${ESC}38;2;${r};${g};${b}m`;
|
|
92
|
+
}
|
|
93
|
+
if (capability === 'ansi256') {
|
|
94
|
+
return `${ESC}38;5;${def.ansi256}m`;
|
|
95
|
+
}
|
|
96
|
+
if (capability === 'ansi16') {
|
|
97
|
+
return `${ESC}${BASIC_FG[def.ansi16] || BASIC_FG.white}m`;
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wrap(open) {
|
|
103
|
+
if (!open) return (input) => String(input ?? '');
|
|
104
|
+
// Re-open after every embedded reset so nested styles compose.
|
|
105
|
+
// Cheap and predictable; most tool output is short enough that the cost
|
|
106
|
+
// is negligible compared to writing to the TTY.
|
|
107
|
+
return (input) => {
|
|
108
|
+
const text = String(input ?? '');
|
|
109
|
+
if (!text) return '';
|
|
110
|
+
if (!text.includes(RESET)) return `${open}${text}${RESET}`;
|
|
111
|
+
return `${open}${text.split(RESET).join(`${RESET}${open}`)}${RESET}`;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function styleWrap(openCode, closeCode) {
|
|
116
|
+
return (input) => {
|
|
117
|
+
const text = String(input ?? '');
|
|
118
|
+
if (!text) return '';
|
|
119
|
+
// No color tier check here — styles like bold/dim work even in ansi16.
|
|
120
|
+
if (!term().color) return text;
|
|
121
|
+
const open = `${ESC}${openCode}m`;
|
|
122
|
+
const close = `${ESC}${closeCode}m`;
|
|
123
|
+
if (!text.includes(close)) return `${open}${text}${close}`;
|
|
124
|
+
return `${open}${text.split(close).join(`${close}${open}`)}${close}`;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Build a structured `paint` object once per token ─────────────────────
|
|
129
|
+
|
|
130
|
+
function buildPaint() {
|
|
131
|
+
const paint = {};
|
|
132
|
+
|
|
133
|
+
// Brand / state / text colorizers, nested by namespace.
|
|
134
|
+
for (const token of Object.keys(TOKENS)) {
|
|
135
|
+
const [ns, name] = token.split('.');
|
|
136
|
+
if (!paint[ns]) paint[ns] = {};
|
|
137
|
+
paint[ns][name] = (input) => {
|
|
138
|
+
const t = term();
|
|
139
|
+
if (!t.color) return String(input ?? '');
|
|
140
|
+
return wrap(openForToken(token, t.colorLevel))(input);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Style colorizers (callable directly).
|
|
145
|
+
for (const [style, [open, close]] of Object.entries(STYLE_CODES)) {
|
|
146
|
+
paint[style] = styleWrap(open, close);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compose helper — apply multiple styles left-to-right.
|
|
150
|
+
paint.compose = (...fns) => (input) =>
|
|
151
|
+
fns.reduce((acc, fn) => (typeof fn === 'function' ? fn(acc) : acc), input);
|
|
152
|
+
|
|
153
|
+
// Raw token accessor for callers that need to inject codes around their
|
|
154
|
+
// own text (e.g. status bar repaint loops that re-style a buffer).
|
|
155
|
+
paint.token = (key) => {
|
|
156
|
+
const t = term();
|
|
157
|
+
if (!t.color) return { open: '', close: '' };
|
|
158
|
+
return { open: openForToken(key, t.colorLevel), close: RESET };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return paint;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const paint = buildPaint();
|
|
165
|
+
|
|
166
|
+
// ── Plain-text helper (always strips colors) ─────────────────────────────
|
|
167
|
+
|
|
168
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
169
|
+
|
|
170
|
+
export function strip(input) {
|
|
171
|
+
return String(input ?? '').replace(ANSI_RE, '');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Visible length of a string (ignoring ANSI codes).
|
|
176
|
+
* Surrogate pairs (emoji) count as 1 visual cell for layout purposes — this
|
|
177
|
+
* is consistent with most terminals' rendering of single-codepoint emoji.
|
|
178
|
+
*/
|
|
179
|
+
export function width(input) {
|
|
180
|
+
const plain = strip(input);
|
|
181
|
+
// Strip variation selectors so "🛰️" measures as one cell.
|
|
182
|
+
return [...plain.replace(/︎|️/g, '')].length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Backwards compatibility re-exports ───────────────────────────────────
|
|
186
|
+
// `ansi.mjs` and other legacy modules import these names. New code should
|
|
187
|
+
// prefer `paint.brand.primary(...)` etc.
|
|
188
|
+
|
|
189
|
+
export const RESET_CODE = RESET;
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render queue — THE single writer for the rich-TTY screen.
|
|
3
|
+
*
|
|
4
|
+
* Every glitch class the old pipeline suffered (flicker, dock overlap,
|
|
5
|
+
* wrap artifacts, resize mangling, approval gaps) traced to the same root:
|
|
6
|
+
* multiple modules moved the cursor independently — spinner interval,
|
|
7
|
+
* content flush timer, dock repaints, approval prompts — with the single
|
|
8
|
+
* VT100 save/restore slot as their only (broken) coordination.
|
|
9
|
+
*
|
|
10
|
+
* Discipline (Ink's core insight, without adopting Ink):
|
|
11
|
+
* 1. Exactly ONE module writes to the terminal in rich mode. This one.
|
|
12
|
+
* 2. Every operation is a complete transaction: position → write →
|
|
13
|
+
* re-park. No saveCursor/restoreCursor anywhere.
|
|
14
|
+
* 3. Cursor position is tracked exactly — possible only because raw
|
|
15
|
+
* writes are banned. process.stdout/stderr are patched to REDIRECT
|
|
16
|
+
* through queue.content() (not merely observe), so a stray
|
|
17
|
+
* console.log from any dependency is serialized instead of
|
|
18
|
+
* corrupting the screen.
|
|
19
|
+
* 4. The transient status line (spinner) is coalesced last-wins and is
|
|
20
|
+
* always cleared before content lands, then redrawn after — content
|
|
21
|
+
* and spinner can no longer interleave.
|
|
22
|
+
*
|
|
23
|
+
* Plain / non-TTY mode: the queue degrades to a pass-through (content →
|
|
24
|
+
* stdout, status dropped, no cursor ops) so piped transcripts stay clean.
|
|
25
|
+
*
|
|
26
|
+
* Cell-width accounting: `cellWidth()` measures terminal CELLS (East
|
|
27
|
+
* Asian Wide/Fullwidth = 2, combining marks/ZWJ = 0, tabs → next stop),
|
|
28
|
+
* unlike palette.width() which counts codepoints. Wrap math that feeds
|
|
29
|
+
* cursor tracking MUST use cells, or CJK/emoji output drifts the dock.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { term, onResize } from './term.mjs';
|
|
33
|
+
|
|
34
|
+
const ESC = '\x1b[';
|
|
35
|
+
|
|
36
|
+
// ── ANSI / OSC stripping ─────────────────────────────────────────────────
|
|
37
|
+
// CSI (incl. private + intermediate bytes), OSC (BEL or ST terminated),
|
|
38
|
+
// charset selection, and simple ESC-letter sequences.
|
|
39
|
+
const ANSI_RE = new RegExp([
|
|
40
|
+
'\\x1b\\[[0-?]*[ -/]*[@-~]', // CSI
|
|
41
|
+
'\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)', // OSC ... BEL|ST
|
|
42
|
+
'\\x1b[()][A-Za-z0-9]', // charset
|
|
43
|
+
'\\x1b[@-Z\\\\-_]', // 2-byte ESC sequences
|
|
44
|
+
].join('|'), 'g');
|
|
45
|
+
|
|
46
|
+
export function stripSequences(text) {
|
|
47
|
+
return String(text ?? '').replace(ANSI_RE, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Cell width ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function isZeroWidth(cp) {
|
|
53
|
+
return (
|
|
54
|
+
(cp >= 0x0300 && cp <= 0x036f) || // combining diacriticals
|
|
55
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
56
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
57
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
58
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
59
|
+
cp === 0x200d || // ZWJ
|
|
60
|
+
cp === 0xfeff
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isWide(cp) {
|
|
65
|
+
return (
|
|
66
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
67
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals, punctuation
|
|
68
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana..CJK compat
|
|
69
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
70
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified
|
|
71
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
72
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
73
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
74
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
75
|
+
(cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
|
|
76
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
77
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji blocks
|
|
78
|
+
(cp >= 0x20000 && cp <= 0x3fffd)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Terminal cell width of `text` after stripping escape sequences. */
|
|
83
|
+
export function cellWidth(text) {
|
|
84
|
+
const plain = stripSequences(text);
|
|
85
|
+
let w = 0;
|
|
86
|
+
for (const ch of plain) {
|
|
87
|
+
const cp = ch.codePointAt(0);
|
|
88
|
+
if (cp === 0x09) { w = (Math.floor(w / 8) + 1) * 8; continue; }
|
|
89
|
+
if (cp < 0x20 || cp === 0x7f) continue;
|
|
90
|
+
if (isZeroWidth(cp)) continue;
|
|
91
|
+
w += isWide(cp) ? 2 : 1;
|
|
92
|
+
}
|
|
93
|
+
return w;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Queue state ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
let active = false; // rich mode engaged (dock mounted)
|
|
99
|
+
let out = process.stderr; // the one true stream in rich mode
|
|
100
|
+
let rawStderrWrite = null; // originals captured at activate()
|
|
101
|
+
let rawStdoutWrite = null;
|
|
102
|
+
|
|
103
|
+
let row = 1; // tracked content cursor (1-based)
|
|
104
|
+
let col = 1;
|
|
105
|
+
let regionBottom = null; // scroll-region bottom (content area)
|
|
106
|
+
let statusLines = []; // current transient status block ([] = none)
|
|
107
|
+
let statusPaintedRows = 0; // rows painted by the last paintStatus()
|
|
108
|
+
let statusVisible = false;
|
|
109
|
+
let parked = null; // {row, col} where input echo expects the cursor
|
|
110
|
+
let inTransaction = 0; // reentrancy guard for queue-internal writes
|
|
111
|
+
let contentEscapeCarry = ''; // incomplete ESC sequence crossing write chunks
|
|
112
|
+
|
|
113
|
+
function rawWrite(s) {
|
|
114
|
+
// Always bypass the redirect patch for queue-internal writes.
|
|
115
|
+
(rawStderrWrite || process.stderr.write.bind(process.stderr))(s);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function seq(s) { rawWrite(ESC + s); }
|
|
119
|
+
function moveTo(r, c) { seq(`${r};${c}H`); }
|
|
120
|
+
function clearLine() { seq('2K'); }
|
|
121
|
+
|
|
122
|
+
function cols() {
|
|
123
|
+
return Math.max(20, term().columns || out.columns || 80);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bottom() {
|
|
127
|
+
return regionBottom ?? Math.max(10, term().rows || 24);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Exact tracking ───────────────────────────────────────────────────────
|
|
131
|
+
// Advance the tracked (row, col) as the terminal will after printing
|
|
132
|
+
// `text`. Only printable content passes through here — queue ops position
|
|
133
|
+
// with moveTo(), never with embedded CSI in content.
|
|
134
|
+
|
|
135
|
+
function advance(text) {
|
|
136
|
+
const width = cols();
|
|
137
|
+
const plain = stripSequences(text);
|
|
138
|
+
for (const ch of plain) {
|
|
139
|
+
const cp = ch.codePointAt(0);
|
|
140
|
+
if (cp === 0x0a) { row = Math.min(bottom(), row + 1); col = 1; continue; }
|
|
141
|
+
if (cp === 0x0d) { col = 1; continue; }
|
|
142
|
+
if (cp === 0x09) { col = (Math.floor((col - 1) / 8) + 1) * 8 + 1; }
|
|
143
|
+
else if (cp < 0x20 || cp === 0x7f || isZeroWidth(cp)) { continue; }
|
|
144
|
+
else { col += isWide(cp) ? 2 : 1; }
|
|
145
|
+
if (col > width) { row = Math.min(bottom(), row + 1); col = 1; }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Status line (spinner) discipline ─────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function eraseStatus() {
|
|
152
|
+
if (!statusVisible) return;
|
|
153
|
+
const anchor = Math.min(bottom(), row);
|
|
154
|
+
const count = Math.max(1, statusPaintedRows);
|
|
155
|
+
for (let i = 0; i < count && anchor + i <= bottom(); i++) {
|
|
156
|
+
moveTo(anchor + i, 1);
|
|
157
|
+
clearLine();
|
|
158
|
+
}
|
|
159
|
+
statusPaintedRows = 0;
|
|
160
|
+
statusVisible = false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ANSI-aware truncation to N terminal cells. Escape sequences pass
|
|
164
|
+
// through at zero width; printable chars accumulate cell width until the
|
|
165
|
+
// budget is hit. A trailing reset prevents color bleed on truncation.
|
|
166
|
+
const TOKEN_RE = new RegExp(`(${ANSI_RE.source})|([\\s\\S])`, 'g');
|
|
167
|
+
|
|
168
|
+
export function fitCells(text, maxCells) {
|
|
169
|
+
const s = String(text ?? '');
|
|
170
|
+
let out = '';
|
|
171
|
+
let w = 0;
|
|
172
|
+
let truncated = false;
|
|
173
|
+
TOKEN_RE.lastIndex = 0;
|
|
174
|
+
let m;
|
|
175
|
+
while ((m = TOKEN_RE.exec(s)) !== null) {
|
|
176
|
+
if (m[1] !== undefined) { out += m[1]; continue; }
|
|
177
|
+
const ch = m[2];
|
|
178
|
+
const cp = ch.codePointAt(0);
|
|
179
|
+
if (cp < 0x20 || cp === 0x7f) continue; // control chars never printable here
|
|
180
|
+
const cw = isZeroWidth(cp) ? 0 : (isWide(cp) ? 2 : 1);
|
|
181
|
+
if (w + cw > maxCells - 1) { truncated = true; break; }
|
|
182
|
+
out += ch;
|
|
183
|
+
w += cw;
|
|
184
|
+
}
|
|
185
|
+
if (truncated) out += '…\x1b[0m';
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function paintStatus() {
|
|
190
|
+
if (!statusLines.length) return;
|
|
191
|
+
const anchor = Math.min(bottom(), row);
|
|
192
|
+
const width = Math.max(8, cols() - 1);
|
|
193
|
+
let painted = 0;
|
|
194
|
+
for (const line of statusLines) {
|
|
195
|
+
const r = anchor + painted;
|
|
196
|
+
if (r > bottom()) break;
|
|
197
|
+
moveTo(r, 1);
|
|
198
|
+
clearLine();
|
|
199
|
+
// Clamp to the drawable width — a status line that reaches the final
|
|
200
|
+
// column triggers terminal autowrap, which scrolls the region and
|
|
201
|
+
// turns the "transient" line into permanent transcript content (one
|
|
202
|
+
// leaked line per animation frame). Newlines are equally forbidden.
|
|
203
|
+
rawWrite(fitCells(String(line).replace(/[\r\n]+/g, ' '), width));
|
|
204
|
+
painted++;
|
|
205
|
+
}
|
|
206
|
+
statusPaintedRows = painted;
|
|
207
|
+
statusVisible = painted > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function repark() {
|
|
211
|
+
if (parked) moveTo(parked.row, parked.col);
|
|
212
|
+
else moveTo(Math.min(bottom(), row), col);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Engage rich mode. The queue captures both std streams and becomes the
|
|
219
|
+
* sole writer. `initial` seeds the tracked cursor (dock mount computes it).
|
|
220
|
+
*/
|
|
221
|
+
export function activate({ initialRow = 1, initialCol = 1, bottom: b = null } = {}) {
|
|
222
|
+
if (active) return true;
|
|
223
|
+
const t = term();
|
|
224
|
+
if (!t.isTTY || t.plain) return false;
|
|
225
|
+
active = true;
|
|
226
|
+
row = Math.max(1, initialRow);
|
|
227
|
+
col = Math.max(1, initialCol);
|
|
228
|
+
regionBottom = b;
|
|
229
|
+
rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
230
|
+
rawStderrWrite = process.stderr.write.bind(process.stderr);
|
|
231
|
+
// REDIRECT, don't observe: stray writers become serialized content.
|
|
232
|
+
process.stdout.write = redirectedWrite;
|
|
233
|
+
process.stderr.write = redirectedWrite;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function deactivate() {
|
|
238
|
+
if (!active) return;
|
|
239
|
+
if (rawStdoutWrite) { process.stdout.write = rawStdoutWrite; rawStdoutWrite = null; }
|
|
240
|
+
if (rawStderrWrite) { process.stderr.write = rawStderrWrite; rawStderrWrite = null; }
|
|
241
|
+
active = false;
|
|
242
|
+
regionBottom = null;
|
|
243
|
+
statusLines = [];
|
|
244
|
+
statusPaintedRows = 0;
|
|
245
|
+
statusVisible = false;
|
|
246
|
+
parked = null;
|
|
247
|
+
contentEscapeCarry = '';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isActive() { return active; }
|
|
251
|
+
|
|
252
|
+
function redirectedWrite(chunk, encoding, cb) {
|
|
253
|
+
if (inTransaction > 0) {
|
|
254
|
+
// Queue-internal writes that (incorrectly) went through the patched
|
|
255
|
+
// stream — pass straight through.
|
|
256
|
+
rawWrite(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? ''));
|
|
257
|
+
} else {
|
|
258
|
+
content(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? ''));
|
|
259
|
+
}
|
|
260
|
+
if (typeof encoding === 'function') encoding();
|
|
261
|
+
else if (typeof cb === 'function') cb();
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Content is TEXT, not cursor commands (Ink's discipline). SGR color
|
|
266
|
+
// sequences pass through; cursor movement / erase / scroll sequences are
|
|
267
|
+
// stripped — a content writer that embeds CUU/ED/DECSTBM would move the
|
|
268
|
+
// real cursor without tracking following (the old approval-redraw gap
|
|
269
|
+
// bug). Positioning belongs to the queue alone.
|
|
270
|
+
function sanitizeContent(s) {
|
|
271
|
+
const input = contentEscapeCarry + String(s ?? '');
|
|
272
|
+
contentEscapeCarry = '';
|
|
273
|
+
let out = '';
|
|
274
|
+
|
|
275
|
+
for (let i = 0; i < input.length;) {
|
|
276
|
+
const ch = input[i];
|
|
277
|
+
if (ch !== '\x1b') {
|
|
278
|
+
out += ch;
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (i + 1 >= input.length) {
|
|
284
|
+
contentEscapeCarry = input.slice(i);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const next = input[i + 1];
|
|
289
|
+
|
|
290
|
+
// CSI: ESC [ params/intermediates final. Preserve only SGR (`m`).
|
|
291
|
+
if (next === '[') {
|
|
292
|
+
let j = i + 2;
|
|
293
|
+
while (j < input.length) {
|
|
294
|
+
const code = input.charCodeAt(j);
|
|
295
|
+
if (code >= 0x40 && code <= 0x7e) break;
|
|
296
|
+
j++;
|
|
297
|
+
}
|
|
298
|
+
if (j >= input.length) {
|
|
299
|
+
contentEscapeCarry = input.slice(i);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
const seqText = input.slice(i, j + 1);
|
|
303
|
+
if (input[j] === 'm') out += seqText;
|
|
304
|
+
i = j + 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// OSC: ESC ] ... BEL or ST. Strip, buffering incomplete sequences.
|
|
309
|
+
if (next === ']') {
|
|
310
|
+
let j = i + 2;
|
|
311
|
+
let completeAt = -1;
|
|
312
|
+
while (j < input.length) {
|
|
313
|
+
if (input[j] === '\x07') { completeAt = j; break; }
|
|
314
|
+
if (input[j] === '\x1b' && input[j + 1] === '\\') { completeAt = j + 1; break; }
|
|
315
|
+
j++;
|
|
316
|
+
}
|
|
317
|
+
if (completeAt < 0) {
|
|
318
|
+
contentEscapeCarry = input.slice(i);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
i = completeAt + 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Charset selection: ESC ( X / ESC ) X. Strip, buffering if split.
|
|
326
|
+
if (next === '(' || next === ')') {
|
|
327
|
+
if (i + 2 >= input.length) {
|
|
328
|
+
contentEscapeCarry = input.slice(i);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
i += 3;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Other two-byte ESC commands. Strip the ESC command byte too.
|
|
336
|
+
i += 2;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Append transcript content at the tracked position. Multi-line safe.
|
|
344
|
+
* Clears the status line first and repaints it after, so spinner and
|
|
345
|
+
* content can never interleave.
|
|
346
|
+
*/
|
|
347
|
+
export function content(text) {
|
|
348
|
+
const s = String(text ?? '');
|
|
349
|
+
if (!s) return;
|
|
350
|
+
if (!active) { (rawStdoutWrite || process.stdout.write.bind(process.stdout))(s); return; }
|
|
351
|
+
const clean = sanitizeContent(s);
|
|
352
|
+
if (!clean) return;
|
|
353
|
+
inTransaction++;
|
|
354
|
+
try {
|
|
355
|
+
eraseStatus();
|
|
356
|
+
moveTo(Math.min(bottom(), row), col);
|
|
357
|
+
rawWrite(clean);
|
|
358
|
+
advance(clean);
|
|
359
|
+
paintStatus();
|
|
360
|
+
repark();
|
|
361
|
+
} finally {
|
|
362
|
+
inTransaction--;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Set / update the transient status block. Last-wins; a repaint happens
|
|
368
|
+
* only when the rendered content actually changed. Single-line callers
|
|
369
|
+
* use status(line); multi-line (sub-agent tool window) use
|
|
370
|
+
* statusBlock(lines). Rows are clamped to the space between the content
|
|
371
|
+
* cursor and the region bottom.
|
|
372
|
+
*/
|
|
373
|
+
export function statusBlock(lines) {
|
|
374
|
+
if (!active) return;
|
|
375
|
+
const next = (Array.isArray(lines) ? lines : [lines])
|
|
376
|
+
.map(l => String(l ?? ''))
|
|
377
|
+
.filter((l, i) => l !== '' || i === 0);
|
|
378
|
+
const isEmpty = !next.length || (next.length === 1 && !next[0]);
|
|
379
|
+
const same = statusVisible
|
|
380
|
+
&& next.length === statusLines.length
|
|
381
|
+
&& next.every((l, i) => l === statusLines[i]);
|
|
382
|
+
if (same) return;
|
|
383
|
+
inTransaction++;
|
|
384
|
+
try {
|
|
385
|
+
eraseStatus();
|
|
386
|
+
statusLines = isEmpty ? [] : next;
|
|
387
|
+
if (!isEmpty) paintStatus();
|
|
388
|
+
repark();
|
|
389
|
+
} finally {
|
|
390
|
+
inTransaction--;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function status(line) {
|
|
395
|
+
statusBlock([String(line ?? '')]);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function clearStatus() { statusBlock([]); }
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Absolute-positioned write for dock frame rows. Does NOT touch content
|
|
402
|
+
* tracking. `clear` wipes the line first.
|
|
403
|
+
*/
|
|
404
|
+
export function at(r, c, text, { clear = true } = {}) {
|
|
405
|
+
if (!active) return;
|
|
406
|
+
inTransaction++;
|
|
407
|
+
try {
|
|
408
|
+
moveTo(r, c);
|
|
409
|
+
if (clear) clearLine();
|
|
410
|
+
if (text) rawWrite(String(text));
|
|
411
|
+
} finally {
|
|
412
|
+
inTransaction--;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Set the scroll region (content area 1..b). */
|
|
417
|
+
export function setRegion(top, b) {
|
|
418
|
+
if (!active) return;
|
|
419
|
+
inTransaction++;
|
|
420
|
+
try {
|
|
421
|
+
seq(`${top};${b}r`);
|
|
422
|
+
regionBottom = b;
|
|
423
|
+
row = Math.min(b, row);
|
|
424
|
+
} finally {
|
|
425
|
+
inTransaction--;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function clearRegion() {
|
|
430
|
+
if (!active) return;
|
|
431
|
+
inTransaction++;
|
|
432
|
+
try { seq('r'); regionBottom = null; } finally { inTransaction--; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Batch several at()/setRegion() calls (a dock repaint) and finish with
|
|
437
|
+
* the cursor parked for input echo. Single place where parking happens.
|
|
438
|
+
*/
|
|
439
|
+
export function frame(fn, { park = null } = {}) {
|
|
440
|
+
if (!active) { if (typeof fn === 'function') fn(); return; }
|
|
441
|
+
inTransaction++;
|
|
442
|
+
try {
|
|
443
|
+
if (typeof fn === 'function') fn();
|
|
444
|
+
if (park) parked = { row: park.row, col: park.col };
|
|
445
|
+
repark();
|
|
446
|
+
} finally {
|
|
447
|
+
inTransaction--;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Serialized raw write for trusted frame painters (the input dock). The
|
|
453
|
+
* caller owns positioning via embedded escape sequences; content tracking
|
|
454
|
+
* is not touched. Bypasses the redirect patch.
|
|
455
|
+
*/
|
|
456
|
+
export function raw(s) {
|
|
457
|
+
if (!active) { (rawStderrWrite || process.stderr.write.bind(process.stderr))(String(s ?? '')); return; }
|
|
458
|
+
inTransaction++;
|
|
459
|
+
try { rawWrite(String(s ?? '')); } finally { inTransaction--; }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Park the input-echo cursor. Every op re-parks here afterwards. */
|
|
463
|
+
export function park(r, c) {
|
|
464
|
+
if (!active) return;
|
|
465
|
+
parked = r == null ? null : { row: r, col: c ?? 1 };
|
|
466
|
+
inTransaction++;
|
|
467
|
+
try { repark(); } finally { inTransaction--; }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Tracked content cursor (exact — no simulation drift). */
|
|
471
|
+
export function contentCursor() {
|
|
472
|
+
return { row, col };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Hard re-anchor after resize: tracking through a reflow is fiction. */
|
|
476
|
+
export function reanchor({ row: r, col: c = 1, bottom: b = null } = {}) {
|
|
477
|
+
if (!active) return;
|
|
478
|
+
if (b != null) regionBottom = b;
|
|
479
|
+
row = Math.max(1, Math.min(bottom(), r ?? bottom()));
|
|
480
|
+
col = Math.max(1, c);
|
|
481
|
+
statusVisible = false; // old status row is gone after reflow
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Reset tracked cursor without touching the screen (dock mount). */
|
|
485
|
+
export function seed({ row: r = 1, col: c = 1, bottom: b = null } = {}) {
|
|
486
|
+
row = Math.max(1, r);
|
|
487
|
+
col = Math.max(1, c);
|
|
488
|
+
if (b != null) regionBottom = b;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Test-only accessors.
|
|
492
|
+
export function _internals() {
|
|
493
|
+
return {
|
|
494
|
+
state: () => ({ active, row, col, regionBottom, statusLines, statusPaintedRows, statusVisible, parked }),
|
|
495
|
+
advance,
|
|
496
|
+
cellWidth,
|
|
497
|
+
stripSequences,
|
|
498
|
+
sanitizeContent,
|
|
499
|
+
};
|
|
500
|
+
}
|