@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
package/src/ui/term.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves once at import. Re-evaluation requires `refresh()` (used by tests
|
|
5
|
+
* and the rare command that toggles a relevant env var mid-process).
|
|
6
|
+
*
|
|
7
|
+
* Capability tiers (highest first):
|
|
8
|
+
* truecolor - 24-bit RGB (e.g. iTerm2, modern xterm, Windows Terminal)
|
|
9
|
+
* ansi256 - 256-color palette
|
|
10
|
+
* ansi16 - basic 16 colors
|
|
11
|
+
* none - no color (NO_COLOR=1, dumb terminal, non-TTY without override)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const TRUECOLOR_TERMS = new Set([
|
|
15
|
+
'truecolor',
|
|
16
|
+
'24bit',
|
|
17
|
+
'24-bit',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const ANSI256_TERMS = [
|
|
21
|
+
/-256(color)?$/i,
|
|
22
|
+
/^xterm/i,
|
|
23
|
+
/^screen/i,
|
|
24
|
+
/^tmux/i,
|
|
25
|
+
/^rxvt-unicode/i,
|
|
26
|
+
/^alacritty/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const DUMB_TERMS = new Set(['', 'dumb', 'unknown']);
|
|
30
|
+
|
|
31
|
+
function readEnv() {
|
|
32
|
+
const env = process.env || {};
|
|
33
|
+
return {
|
|
34
|
+
NO_COLOR: env.NO_COLOR,
|
|
35
|
+
FORCE_COLOR: env.FORCE_COLOR,
|
|
36
|
+
BAHULAM_PLAIN: env.BAHULAM_PLAIN,
|
|
37
|
+
KEPLER_PLAIN: env.KEPLER_PLAIN,
|
|
38
|
+
BAHULAM_TTY_MODE: env.BAHULAM_TTY_MODE,
|
|
39
|
+
KEPLER_TTY_MODE: env.KEPLER_TTY_MODE,
|
|
40
|
+
COLORTERM: (env.COLORTERM || '').toLowerCase(),
|
|
41
|
+
TERM: (env.TERM || '').toLowerCase(),
|
|
42
|
+
TERM_PROGRAM: env.TERM_PROGRAM || '',
|
|
43
|
+
CI: env.CI,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function detectColorLevel(env, isTTY) {
|
|
48
|
+
// Hard opt-out (https://no-color.org). Honored even on TTYs.
|
|
49
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
|
|
50
|
+
if (env.BAHULAM_PLAIN === '1') return 'none';
|
|
51
|
+
if (env.KEPLER_PLAIN === '1') return 'none';
|
|
52
|
+
|
|
53
|
+
// Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
|
|
54
|
+
// FORCE_COLOR with no value or =true falls through to detection.
|
|
55
|
+
if (env.FORCE_COLOR !== undefined) {
|
|
56
|
+
const v = String(env.FORCE_COLOR).trim();
|
|
57
|
+
if (v === '0' || v === 'false') return 'none';
|
|
58
|
+
if (v === '1') return 'ansi16';
|
|
59
|
+
if (v === '2') return 'ansi256';
|
|
60
|
+
if (v === '3') return 'truecolor';
|
|
61
|
+
// Any other truthy value: continue detection but allow non-TTY.
|
|
62
|
+
isTTY = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// No TTY and not forced: no color.
|
|
66
|
+
if (!isTTY) return 'none';
|
|
67
|
+
|
|
68
|
+
if (DUMB_TERMS.has(env.TERM)) return 'none';
|
|
69
|
+
|
|
70
|
+
if (TRUECOLOR_TERMS.has(env.COLORTERM)) return 'truecolor';
|
|
71
|
+
|
|
72
|
+
// Some terminal emulators advertise truecolor through TERM_PROGRAM.
|
|
73
|
+
if (env.TERM_PROGRAM === 'iTerm.app' || env.TERM_PROGRAM === 'WezTerm') {
|
|
74
|
+
return 'truecolor';
|
|
75
|
+
}
|
|
76
|
+
if (env.TERM_PROGRAM === 'vscode' || env.TERM_PROGRAM === 'Apple_Terminal') {
|
|
77
|
+
return 'ansi256';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ANSI256_TERMS.some(re => re.test(env.TERM))) return 'ansi256';
|
|
81
|
+
|
|
82
|
+
return 'ansi16';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectUnicode(env) {
|
|
86
|
+
if (env.BAHULAM_PLAIN === '1') return false;
|
|
87
|
+
if (env.KEPLER_PLAIN === '1') return false;
|
|
88
|
+
// Most modern terminals on macOS/Linux handle UTF-8.
|
|
89
|
+
// Windows ConEmu / older terminals are the main holdouts; conservative
|
|
90
|
+
// fallback when LANG and LC_* are missing or explicitly POSIX.
|
|
91
|
+
const lang = (process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '').toLowerCase();
|
|
92
|
+
if (!lang) return process.platform !== 'win32';
|
|
93
|
+
if (lang.includes('utf')) return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function detectTtyMode(env) {
|
|
98
|
+
const raw = String(env.BAHULAM_TTY_MODE || env.KEPLER_TTY_MODE || '').trim().toLowerCase();
|
|
99
|
+
if (env.BAHULAM_PLAIN === '1' || env.KEPLER_PLAIN === '1') return 'plain';
|
|
100
|
+
if (raw === 'stable' || raw === 'simple' || raw === 'static' || raw === 'transcript') return 'stable';
|
|
101
|
+
return 'rich';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compute() {
|
|
105
|
+
const env = readEnv();
|
|
106
|
+
const isTTY = !!(process.stdout && process.stdout.isTTY);
|
|
107
|
+
const level = detectColorLevel(env, isTTY);
|
|
108
|
+
const ttyMode = detectTtyMode(env);
|
|
109
|
+
return {
|
|
110
|
+
isTTY,
|
|
111
|
+
colorLevel: level, // 'none' | 'ansi16' | 'ansi256' | 'truecolor'
|
|
112
|
+
color: level !== 'none',
|
|
113
|
+
truecolor: level === 'truecolor',
|
|
114
|
+
ansi256: level === 'ansi256' || level === 'truecolor',
|
|
115
|
+
unicode: detectUnicode(env),
|
|
116
|
+
ttyMode, // 'rich' | 'stable' | 'plain'
|
|
117
|
+
plain: ttyMode === 'plain',
|
|
118
|
+
fixedInput: ttyMode === 'rich',
|
|
119
|
+
columns: (process.stdout && process.stdout.columns) || 80,
|
|
120
|
+
rows: (process.stdout && process.stdout.rows) || 24,
|
|
121
|
+
ci: !!env.CI,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let _capabilities = compute();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Current terminal capabilities. Stable until `refresh()` is called.
|
|
129
|
+
*/
|
|
130
|
+
export function term() {
|
|
131
|
+
return _capabilities;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Re-run capability detection. Tests, `/config` reloads, or runtime env changes.
|
|
136
|
+
*/
|
|
137
|
+
export function refresh() {
|
|
138
|
+
_capabilities = compute();
|
|
139
|
+
return _capabilities;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Listen for terminal resizes. Returns an unsubscribe function.
|
|
144
|
+
* Callers receive the latest capabilities object (with updated columns/rows).
|
|
145
|
+
*/
|
|
146
|
+
export function onResize(handler) {
|
|
147
|
+
if (typeof handler !== 'function') return () => {};
|
|
148
|
+
const stream = process.stdout;
|
|
149
|
+
if (!stream || typeof stream.on !== 'function') return () => {};
|
|
150
|
+
|
|
151
|
+
const onChange = () => {
|
|
152
|
+
_capabilities = {
|
|
153
|
+
..._capabilities,
|
|
154
|
+
columns: stream.columns || _capabilities.columns,
|
|
155
|
+
rows: stream.rows || _capabilities.rows,
|
|
156
|
+
};
|
|
157
|
+
handler(_capabilities);
|
|
158
|
+
};
|
|
159
|
+
stream.on('resize', onChange);
|
|
160
|
+
return () => stream.off('resize', onChange);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Force a capability level. Test-only escape hatch.
|
|
165
|
+
* Pass `null` to clear and re-detect.
|
|
166
|
+
*/
|
|
167
|
+
export function _setForTesting(overrides) {
|
|
168
|
+
if (overrides === null) {
|
|
169
|
+
_capabilities = compute();
|
|
170
|
+
return _capabilities;
|
|
171
|
+
}
|
|
172
|
+
_capabilities = { ..._capabilities, ...overrides };
|
|
173
|
+
return _capabilities;
|
|
174
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text layout helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted so the input dock's line-wrapping and clipping logic can be
|
|
5
|
+
* unit-tested without a live terminal. Everything here is a pure function
|
|
6
|
+
* of its inputs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { strip as stripAnsi, width as visibleWidth } from './palette.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrap `text` into lines of at most `maxWidth` visible columns.
|
|
13
|
+
*
|
|
14
|
+
* - Preserves existing '\n' as hard breaks.
|
|
15
|
+
* - Wraps long lines at whitespace when possible.
|
|
16
|
+
* - Breaks mid-word only when a single word exceeds maxWidth.
|
|
17
|
+
* - Empty input returns [''].
|
|
18
|
+
* - ANSI escapes are preserved; wrap decisions use visible width.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} text
|
|
21
|
+
* @param {number} maxWidth
|
|
22
|
+
* @returns {string[]}
|
|
23
|
+
*/
|
|
24
|
+
export function wrapToLines(text, maxWidth) {
|
|
25
|
+
const width = Math.max(1, Math.floor(maxWidth));
|
|
26
|
+
const source = String(text ?? '');
|
|
27
|
+
if (!source) return [''];
|
|
28
|
+
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const rawLine of source.split('\n')) {
|
|
31
|
+
if (rawLine === '') { out.push(''); continue; }
|
|
32
|
+
if (visibleWidth(rawLine) <= width) { out.push(rawLine); continue; }
|
|
33
|
+
|
|
34
|
+
// Long line — wrap at whitespace where possible.
|
|
35
|
+
let current = '';
|
|
36
|
+
const tokens = tokenizeForWrap(rawLine);
|
|
37
|
+
for (const token of tokens) {
|
|
38
|
+
const isSpace = /^\s+$/.test(stripAnsi(token));
|
|
39
|
+
const candidate = current + token;
|
|
40
|
+
if (visibleWidth(candidate) <= width) { current = candidate; continue; }
|
|
41
|
+
if (current) { out.push(current.replace(/\s+$/, '')); current = ''; }
|
|
42
|
+
if (isSpace) continue; // don't start a new line with pure whitespace
|
|
43
|
+
// Token alone still too wide — chunk it.
|
|
44
|
+
if (visibleWidth(token) > width) {
|
|
45
|
+
for (const chunk of chunkByVisibleWidth(token, width)) out.push(chunk);
|
|
46
|
+
} else {
|
|
47
|
+
current = token;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (current) out.push(current.replace(/\s+$/, ''));
|
|
51
|
+
}
|
|
52
|
+
return out.length ? out : [''];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Take the last `maxRows` of `lines`. If more lines were dropped, prefix
|
|
57
|
+
* the first surviving line with a dim '…' marker so the truncation is
|
|
58
|
+
* explicit (per PRD §5.1).
|
|
59
|
+
*
|
|
60
|
+
* Returns { visible, truncated: boolean, dropped: number }.
|
|
61
|
+
*/
|
|
62
|
+
export function tailWithEllipsis(lines, maxRows, ellipsis = '… ') {
|
|
63
|
+
const rows = Math.max(1, Math.floor(maxRows));
|
|
64
|
+
const arr = Array.isArray(lines) ? lines : [];
|
|
65
|
+
if (arr.length <= rows) return { visible: arr.slice(), truncated: false, dropped: 0 };
|
|
66
|
+
const dropped = arr.length - rows;
|
|
67
|
+
const visible = arr.slice(dropped);
|
|
68
|
+
visible[0] = `${ellipsis}${visible[0] || ''}`;
|
|
69
|
+
return { visible, truncated: true, dropped };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Given the wrapped visible lines and a cursor position (as a linear
|
|
74
|
+
* character offset into the *joined* wrapped output), return the
|
|
75
|
+
* { row, col } inside the visible block (0-indexed).
|
|
76
|
+
*
|
|
77
|
+
* Used to place the terminal cursor after rendering the input buffer.
|
|
78
|
+
*/
|
|
79
|
+
export function cursorPositionInLines(visibleLines, offset) {
|
|
80
|
+
const arr = Array.isArray(visibleLines) ? visibleLines : [];
|
|
81
|
+
let remaining = Math.max(0, Math.floor(offset));
|
|
82
|
+
for (let row = 0; row < arr.length; row++) {
|
|
83
|
+
const line = arr[row] || '';
|
|
84
|
+
const w = visibleWidth(line);
|
|
85
|
+
if (remaining <= w) return { row, col: remaining };
|
|
86
|
+
remaining -= w;
|
|
87
|
+
// Newline between wrapped lines doesn't count as a visible column,
|
|
88
|
+
// but consumes zero of the remaining offset either.
|
|
89
|
+
}
|
|
90
|
+
const lastRow = Math.max(0, arr.length - 1);
|
|
91
|
+
return { row: lastRow, col: visibleWidth(arr[lastRow] || '') };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
// Tokenize into runs of non-whitespace and whitespace so wrapWords can
|
|
97
|
+
// decide break points without merging spaces into words. ANSI escapes stay
|
|
98
|
+
// attached to the visible token they precede.
|
|
99
|
+
function tokenizeForWrap(line) {
|
|
100
|
+
const tokens = [];
|
|
101
|
+
const re = /(\s+|\S+)/g;
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = re.exec(line)) !== null) tokens.push(m[0]);
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Split a single overly-long token into chunks of at most `maxWidth`
|
|
108
|
+
// visible cells, preserving ANSI escape sequences (they don't count).
|
|
109
|
+
function chunkByVisibleWidth(token, maxWidth) {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let buf = '';
|
|
112
|
+
let bufWidth = 0;
|
|
113
|
+
const chars = Array.from(String(token));
|
|
114
|
+
for (const ch of chars) {
|
|
115
|
+
// Naive: treat each codepoint as 1 cell. Good enough for path-like
|
|
116
|
+
// tokens; wide-char inputs would need a fuller width table.
|
|
117
|
+
if (bufWidth + 1 > maxWidth) {
|
|
118
|
+
chunks.push(buf);
|
|
119
|
+
buf = '';
|
|
120
|
+
bufWidth = 0;
|
|
121
|
+
}
|
|
122
|
+
buf += ch;
|
|
123
|
+
bufWidth += 1;
|
|
124
|
+
}
|
|
125
|
+
if (buf) chunks.push(buf);
|
|
126
|
+
return chunks;
|
|
127
|
+
}
|