cvc-tui 0.4.0 → 0.4.2
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/dist/entry.js +71148 -61
- package/package.json +2 -2
- package/dist/app/completion.js +0 -102
- package/dist/app/createGatewayEventHandler.js +0 -508
- package/dist/app/createSlashHandler.js +0 -101
- package/dist/app/delegationStore.js +0 -51
- package/dist/app/gatewayContext.js +0 -17
- package/dist/app/historyStore.js +0 -123
- package/dist/app/inputBuffer.js +0 -120
- package/dist/app/inputSelectionStore.js +0 -8
- package/dist/app/inputStore.js +0 -28
- package/dist/app/interfaces.js +0 -6
- package/dist/app/overlayStore.js +0 -40
- package/dist/app/promptStore.js +0 -44
- package/dist/app/queueStore.js +0 -25
- package/dist/app/scroll.js +0 -44
- package/dist/app/setupHandoff.js +0 -28
- package/dist/app/slash/commands/core.js +0 -479
- package/dist/app/slash/commands/debug.js +0 -44
- package/dist/app/slash/commands/ops.js +0 -498
- package/dist/app/slash/commands/session.js +0 -431
- package/dist/app/slash/commands/setup.js +0 -20
- package/dist/app/slash/commands/toggles.js +0 -40
- package/dist/app/slash/registry.js +0 -18
- package/dist/app/slash/types.js +0 -1
- package/dist/app/spawnHistoryStore.js +0 -105
- package/dist/app/turnController.js +0 -650
- package/dist/app/turnStore.js +0 -48
- package/dist/app/uiStore.js +0 -36
- package/dist/app/useComposerState.js +0 -265
- package/dist/app/useConfigSync.js +0 -144
- package/dist/app/useInputHandlers.js +0 -403
- package/dist/app/useLongRunToolCharms.js +0 -50
- package/dist/app/useMainApp.js +0 -629
- package/dist/app/useSessionLifecycle.js +0 -175
- package/dist/app/useSubmission.js +0 -287
- package/dist/app.js +0 -15
- package/dist/banner.js +0 -57
- package/dist/components/agentsOverlay.js +0 -474
- package/dist/components/appChrome.js +0 -252
- package/dist/components/appLayout.js +0 -121
- package/dist/components/appOverlays.js +0 -65
- package/dist/components/branding.js +0 -97
- package/dist/components/fpsOverlay.js +0 -22
- package/dist/components/helpHint.js +0 -21
- package/dist/components/markdown.js +0 -501
- package/dist/components/maskedPrompt.js +0 -12
- package/dist/components/messageLine.js +0 -82
- package/dist/components/modelPicker.js +0 -254
- package/dist/components/overlayControls.js +0 -30
- package/dist/components/overlays/confirmPrompt.js +0 -25
- package/dist/components/overlays/helpOverlay.js +0 -76
- package/dist/components/overlays/historySearch.js +0 -49
- package/dist/components/overlays/modelPicker.js +0 -60
- package/dist/components/overlays/overlayUtils.js +0 -19
- package/dist/components/overlays/secretPrompt.js +0 -36
- package/dist/components/overlays/sessionPicker.js +0 -93
- package/dist/components/overlays/skillsHub.js +0 -71
- package/dist/components/prompts.js +0 -95
- package/dist/components/queuedMessages.js +0 -24
- package/dist/components/sessionPicker.js +0 -130
- package/dist/components/skillsHub.js +0 -165
- package/dist/components/streamingAssistant.js +0 -35
- package/dist/components/streamingMarkdown.js +0 -144
- package/dist/components/textInput.js +0 -794
- package/dist/components/themed.js +0 -12
- package/dist/components/thinking.js +0 -496
- package/dist/components/todoPanel.js +0 -40
- package/dist/components/transcript.js +0 -22
- package/dist/config/env.js +0 -18
- package/dist/config/limits.js +0 -22
- package/dist/config/timing.js +0 -18
- package/dist/content/charms.js +0 -5
- package/dist/content/faces.js +0 -21
- package/dist/content/fortunes.js +0 -29
- package/dist/content/hotkeys.js +0 -38
- package/dist/content/placeholders.js +0 -15
- package/dist/content/setup.js +0 -14
- package/dist/content/verbs.js +0 -41
- package/dist/domain/details.js +0 -53
- package/dist/domain/messages.js +0 -63
- package/dist/domain/paths.js +0 -16
- package/dist/domain/providers.js +0 -11
- package/dist/domain/roles.js +0 -6
- package/dist/domain/slash.js +0 -11
- package/dist/domain/usage.js +0 -1
- package/dist/domain/viewport.js +0 -33
- package/dist/gateway/client.js +0 -312
- package/dist/gatewayClient.js +0 -574
- package/dist/gatewayTypes.js +0 -1
- package/dist/hooks/useCompletion.js +0 -86
- package/dist/hooks/useGitBranch.js +0 -58
- package/dist/hooks/useInputHistory.js +0 -12
- package/dist/hooks/useQueue.js +0 -57
- package/dist/hooks/useVirtualHistory.js +0 -401
- package/dist/lib/circularBuffer.js +0 -43
- package/dist/lib/clipboard.js +0 -126
- package/dist/lib/editor.js +0 -41
- package/dist/lib/editor.test.js +0 -58
- package/dist/lib/emoji.js +0 -49
- package/dist/lib/externalCli.js +0 -11
- package/dist/lib/forceTruecolor.js +0 -26
- package/dist/lib/fpsStore.js +0 -36
- package/dist/lib/gracefulExit.js +0 -29
- package/dist/lib/history.js +0 -69
- package/dist/lib/inputMetrics.js +0 -143
- package/dist/lib/liveProgress.js +0 -51
- package/dist/lib/liveProgress.test.js +0 -89
- package/dist/lib/mathUnicode.js +0 -685
- package/dist/lib/memory.js +0 -123
- package/dist/lib/memoryMonitor.js +0 -76
- package/dist/lib/messages.js +0 -3
- package/dist/lib/messages.test.js +0 -25
- package/dist/lib/osc52.js +0 -53
- package/dist/lib/perfPane.js +0 -94
- package/dist/lib/platform.js +0 -312
- package/dist/lib/precisionWheel.js +0 -25
- package/dist/lib/reasoning.js +0 -39
- package/dist/lib/rpc.js +0 -26
- package/dist/lib/subagentTree.js +0 -287
- package/dist/lib/syntax.js +0 -89
- package/dist/lib/terminalModes.js +0 -46
- package/dist/lib/terminalParity.js +0 -48
- package/dist/lib/terminalSetup.js +0 -321
- package/dist/lib/text.js +0 -203
- package/dist/lib/text.test.js +0 -18
- package/dist/lib/todo.js +0 -2
- package/dist/lib/todo.test.js +0 -22
- package/dist/lib/viewportStore.js +0 -82
- package/dist/lib/virtualHeights.js +0 -61
- package/dist/lib/wheelAccel.js +0 -143
- package/dist/theme.js +0 -398
- package/dist/types.js +0 -1
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { transcriptBodyWidth } from './inputMetrics.js';
|
|
2
|
-
import { boundedHistoryRenderText } from './text.js';
|
|
3
|
-
const hashText = (text) => {
|
|
4
|
-
let h = 5381;
|
|
5
|
-
for (let i = 0; i < text.length; i++) {
|
|
6
|
-
h = ((h << 5) + h) ^ text.charCodeAt(i);
|
|
7
|
-
}
|
|
8
|
-
return (h >>> 0).toString(36);
|
|
9
|
-
};
|
|
10
|
-
export const messageHeightKey = (msg) => {
|
|
11
|
-
const todoSig = msg.todos?.map(t => `${t.status}:${t.content}`).join('\u0001') ?? '';
|
|
12
|
-
const panelSig = msg.panelData?.sections
|
|
13
|
-
.map(s => `${s.title ?? ''}:${s.text?.length ?? 0}:${s.items?.length ?? 0}:${s.rows?.length ?? 0}`)
|
|
14
|
-
.join('\u0001') ?? '';
|
|
15
|
-
const introSig = msg.kind === 'intro' ? (msg.info?.version ?? '') : '';
|
|
16
|
-
return [
|
|
17
|
-
msg.role,
|
|
18
|
-
msg.kind ?? '',
|
|
19
|
-
hashText([msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? '', todoSig, panelSig, introSig].join('\0'))
|
|
20
|
-
].join(':');
|
|
21
|
-
};
|
|
22
|
-
export const wrappedLines = (text, width) => {
|
|
23
|
-
const w = Math.max(1, width);
|
|
24
|
-
return text.split('\n').reduce((n, line) => n + Math.max(1, Math.ceil(line.length / w)), 0);
|
|
25
|
-
};
|
|
26
|
-
export const estimatedMsgHeight = (msg, cols, { compact, details, limitHistory = false, userPrompt = '', withSeparator = false }) => {
|
|
27
|
-
if (msg.kind === 'intro') {
|
|
28
|
-
return msg.info?.version ? 9 : 5;
|
|
29
|
-
}
|
|
30
|
-
if (msg.kind === 'panel') {
|
|
31
|
-
return Math.max(3, (msg.panelData?.sections.length ?? 1) * 2 + 1);
|
|
32
|
-
}
|
|
33
|
-
if (msg.kind === 'trail' && msg.todos?.length) {
|
|
34
|
-
if (msg.todoCollapsedByDefault) {
|
|
35
|
-
return 2;
|
|
36
|
-
}
|
|
37
|
-
return Math.max(2, msg.todos.length + 2);
|
|
38
|
-
}
|
|
39
|
-
const bodyWidth = transcriptBodyWidth(cols, msg.role, userPrompt);
|
|
40
|
-
const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text;
|
|
41
|
-
let h = wrappedLines(text || ' ', bodyWidth);
|
|
42
|
-
if (!compact && msg.role === 'assistant') {
|
|
43
|
-
h += Math.min(6, (text.match(/\n\s*\n/g) ?? []).length);
|
|
44
|
-
}
|
|
45
|
-
if (details) {
|
|
46
|
-
h += (msg.tools?.length ?? 0) + wrappedLines(msg.thinking ?? '', bodyWidth);
|
|
47
|
-
}
|
|
48
|
-
if (msg.role === 'user' || msg.kind === 'diff') {
|
|
49
|
-
h += 2;
|
|
50
|
-
}
|
|
51
|
-
else if (msg.kind === 'slash') {
|
|
52
|
-
h++;
|
|
53
|
-
}
|
|
54
|
-
// Inter-turn separator above non-first user messages (1 rule row + 1
|
|
55
|
-
// top-margin row). The render-side gate is in appLayout.tsx; we trust
|
|
56
|
-
// the caller to pass `withSeparator` only when it matches that gate.
|
|
57
|
-
if (withSeparator) {
|
|
58
|
-
h += 2;
|
|
59
|
-
}
|
|
60
|
-
return Math.max(1, h);
|
|
61
|
-
};
|
package/dist/lib/wheelAccel.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
// Wheel-scroll acceleration state machine.
|
|
6
|
-
//
|
|
7
|
-
// One event = 1 row feels sluggish on trackpads (200+ ev/s) and sustained
|
|
8
|
-
// mouse-wheel; one event = 6 rows teleports and ruins precision.
|
|
9
|
-
// Heuristic on inter-event gap + direction flips:
|
|
10
|
-
//
|
|
11
|
-
// gap < 5ms → same-batch burst → 1 row/event
|
|
12
|
-
// gap < 40ms (native) → ramp +0.3, cap 6
|
|
13
|
-
// gap 80-500ms (xterm.js) → mult = 1 + (mult-1)·0.5^(gap/150) + 5·decay
|
|
14
|
-
// cap 3 slow / 6 fast
|
|
15
|
-
// gap > 500ms → reset (deliberate click stays responsive)
|
|
16
|
-
// flip + flip-back ≤200ms → encoder bounce → engage wheel-mode (sticky cap)
|
|
17
|
-
// 5 consecutive <5ms events → trackpad flick → disengage wheel-mode
|
|
18
|
-
//
|
|
19
|
-
// Native terminals (Ghostty, iTerm2) and xterm.js embedders (VS Code,
|
|
20
|
-
// Cursor) emit wheel events with different cadences, hence two paths.
|
|
21
|
-
import { isXtermJs } from '@cvc/ink';
|
|
22
|
-
// ── Native (ghostty, iTerm2, WezTerm, …) ───────────────────────────────
|
|
23
|
-
const WHEEL_ACCEL_WINDOW_MS = 40;
|
|
24
|
-
const WHEEL_ACCEL_STEP = 0.3;
|
|
25
|
-
const WHEEL_ACCEL_MAX = 6;
|
|
26
|
-
// ── Encoder bounce / wheel-mode (mechanical wheels) ────────────────────
|
|
27
|
-
const WHEEL_BOUNCE_GAP_MAX_MS = 200;
|
|
28
|
-
const WHEEL_MODE_STEP = 15;
|
|
29
|
-
const WHEEL_MODE_CAP = 15;
|
|
30
|
-
const WHEEL_MODE_RAMP = 3;
|
|
31
|
-
const WHEEL_MODE_IDLE_DISENGAGE_MS = 1500;
|
|
32
|
-
// ── xterm.js (VS Code / Cursor / browser terminals) ────────────────────
|
|
33
|
-
const WHEEL_DECAY_HALFLIFE_MS = 150;
|
|
34
|
-
const WHEEL_DECAY_STEP = 5;
|
|
35
|
-
const WHEEL_BURST_MS = 5;
|
|
36
|
-
const WHEEL_DECAY_GAP_MS = 80;
|
|
37
|
-
const WHEEL_DECAY_CAP_SLOW = 3;
|
|
38
|
-
const WHEEL_DECAY_CAP_FAST = 6;
|
|
39
|
-
const WHEEL_DECAY_IDLE_MS = 500;
|
|
40
|
-
export function initWheelAccel(xtermJs = false, base = 1) {
|
|
41
|
-
return { burstCount: 0, base, dir: 0, frac: 0, mult: base, pendingFlip: false, time: 0, wheelMode: false, xtermJs };
|
|
42
|
-
}
|
|
43
|
-
/** CVC_TUI_SCROLL_SPEED (or CLAUDE_CODE_SCROLL_SPEED for portability).
|
|
44
|
-
* Default 1, clamped (0, 20]. */
|
|
45
|
-
export function readScrollSpeedBase() {
|
|
46
|
-
const n = parseFloat(process.env.CVC_TUI_SCROLL_SPEED ?? process.env.CLAUDE_CODE_SCROLL_SPEED ?? '');
|
|
47
|
-
return Number.isFinite(n) && n > 0 ? Math.min(n, 20) : 1;
|
|
48
|
-
}
|
|
49
|
-
export function initWheelAccelForHost() {
|
|
50
|
-
return initWheelAccel(isXtermJs(), readScrollSpeedBase());
|
|
51
|
-
}
|
|
52
|
-
/** Compute rows for one wheel event, mutating `state`. Returns 0 when a
|
|
53
|
-
* direction flip is deferred for bounce detection — call sites should
|
|
54
|
-
* no-op on 0. */
|
|
55
|
-
export function computeWheelStep(state, dir, now) {
|
|
56
|
-
return state.xtermJs ? xtermJsStep(state, dir, now) : nativeStep(state, dir, now);
|
|
57
|
-
}
|
|
58
|
-
function nativeStep(state, dir, now) {
|
|
59
|
-
// Idle disengage runs first so a pending bounce can't mask "user paused
|
|
60
|
-
// 1.5s then mouse-clicked" as a real reversal.
|
|
61
|
-
if (state.wheelMode && now - state.time > WHEEL_MODE_IDLE_DISENGAGE_MS) {
|
|
62
|
-
state.wheelMode = false;
|
|
63
|
-
state.burstCount = 0;
|
|
64
|
-
state.mult = state.base;
|
|
65
|
-
}
|
|
66
|
-
if (state.pendingFlip) {
|
|
67
|
-
state.pendingFlip = false;
|
|
68
|
-
if (dir !== state.dir || now - state.time > WHEEL_BOUNCE_GAP_MAX_MS) {
|
|
69
|
-
// Real reversal (flip persisted OR flip-back too late). Commit.
|
|
70
|
-
// The deferred event's 1 row is lost — acceptable latency.
|
|
71
|
-
state.dir = dir;
|
|
72
|
-
state.time = now;
|
|
73
|
-
state.mult = state.base;
|
|
74
|
-
return Math.floor(state.mult);
|
|
75
|
-
}
|
|
76
|
-
state.wheelMode = true;
|
|
77
|
-
}
|
|
78
|
-
const gap = now - state.time;
|
|
79
|
-
if (dir !== state.dir && state.dir !== 0) {
|
|
80
|
-
state.pendingFlip = true;
|
|
81
|
-
state.time = now;
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
state.dir = dir;
|
|
85
|
-
state.time = now;
|
|
86
|
-
if (state.wheelMode) {
|
|
87
|
-
if (gap < WHEEL_BURST_MS) {
|
|
88
|
-
// Same-batch burst (SGR proportional) OR trackpad flick. 1 row/event;
|
|
89
|
-
// trackpad flick trips the burst-count disengage.
|
|
90
|
-
if (++state.burstCount >= 5) {
|
|
91
|
-
state.wheelMode = false;
|
|
92
|
-
state.burstCount = 0;
|
|
93
|
-
state.mult = state.base;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
return 1;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
state.burstCount = 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (state.wheelMode) {
|
|
104
|
-
const m = Math.pow(0.5, gap / WHEEL_DECAY_HALFLIFE_MS);
|
|
105
|
-
const cap = Math.max(WHEEL_MODE_CAP, state.base * 2);
|
|
106
|
-
const next = 1 + (state.mult - 1) * m + WHEEL_MODE_STEP * m;
|
|
107
|
-
state.mult = Math.min(cap, next, state.mult + WHEEL_MODE_RAMP);
|
|
108
|
-
return Math.floor(state.mult);
|
|
109
|
-
}
|
|
110
|
-
// Trackpad / hi-res native: tight 40ms window — sub-window ramps,
|
|
111
|
-
// anything slower resets to baseline.
|
|
112
|
-
if (gap > WHEEL_ACCEL_WINDOW_MS) {
|
|
113
|
-
state.mult = state.base;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
const cap = Math.max(WHEEL_ACCEL_MAX, state.base * 2);
|
|
117
|
-
state.mult = Math.min(cap, state.mult + WHEEL_ACCEL_STEP);
|
|
118
|
-
}
|
|
119
|
-
return Math.floor(state.mult);
|
|
120
|
-
}
|
|
121
|
-
function xtermJsStep(state, dir, now) {
|
|
122
|
-
const gap = now - state.time;
|
|
123
|
-
const sameDir = dir === state.dir;
|
|
124
|
-
state.time = now;
|
|
125
|
-
state.dir = dir;
|
|
126
|
-
if (sameDir && gap < WHEEL_BURST_MS) {
|
|
127
|
-
return 1;
|
|
128
|
-
}
|
|
129
|
-
if (!sameDir || gap > WHEEL_DECAY_IDLE_MS) {
|
|
130
|
-
// Reversal or long idle — start at 2 so first click after a pause moves visibly.
|
|
131
|
-
state.mult = 2;
|
|
132
|
-
state.frac = 0;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
const m = Math.pow(0.5, gap / WHEEL_DECAY_HALFLIFE_MS);
|
|
136
|
-
const cap = gap >= WHEEL_DECAY_GAP_MS ? WHEEL_DECAY_CAP_SLOW : WHEEL_DECAY_CAP_FAST;
|
|
137
|
-
state.mult = Math.min(cap, 1 + (state.mult - 1) * m + WHEEL_DECAY_STEP * m);
|
|
138
|
-
}
|
|
139
|
-
const total = state.mult + state.frac;
|
|
140
|
-
const rows = Math.floor(total);
|
|
141
|
-
state.frac = total - rows;
|
|
142
|
-
return rows;
|
|
143
|
-
}
|
package/dist/theme.js
DELETED
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
3
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
4
|
-
// ── Color math ───────────────────────────────────────────────────────
|
|
5
|
-
function parseHex(h) {
|
|
6
|
-
const m = /^#?([0-9a-f]{6})$/i.exec(h);
|
|
7
|
-
if (!m) {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
const n = parseInt(m[1], 16);
|
|
11
|
-
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
12
|
-
}
|
|
13
|
-
function mix(a, b, t) {
|
|
14
|
-
const pa = parseHex(a);
|
|
15
|
-
const pb = parseHex(b);
|
|
16
|
-
if (!pa || !pb) {
|
|
17
|
-
return a;
|
|
18
|
-
}
|
|
19
|
-
const lerp = (i) => Math.round(pa[i] + (pb[i] - pa[i]) * t);
|
|
20
|
-
return '#' + ((1 << 24) | (lerp(0) << 16) | (lerp(1) << 8) | lerp(2)).toString(16).slice(1);
|
|
21
|
-
}
|
|
22
|
-
const XTERM_6_LEVELS = [0, 95, 135, 175, 215, 255];
|
|
23
|
-
const ANSI_LIGHT_MAX_LUMINANCE = 0.72;
|
|
24
|
-
const ANSI_LIGHT_TARGET_LUMINANCE = 0.34;
|
|
25
|
-
const ANSI_LIGHT_MIN_SATURATION = 0.22;
|
|
26
|
-
const ANSI_MUTED_BUCKET = 245;
|
|
27
|
-
const ANSI_NORMALIZED_FOREGROUNDS = [
|
|
28
|
-
'text',
|
|
29
|
-
'label',
|
|
30
|
-
'ok',
|
|
31
|
-
'error',
|
|
32
|
-
'warn',
|
|
33
|
-
'prompt',
|
|
34
|
-
'statusFg',
|
|
35
|
-
'statusGood',
|
|
36
|
-
'statusWarn',
|
|
37
|
-
'statusBad',
|
|
38
|
-
'statusCritical',
|
|
39
|
-
'shellDollar'
|
|
40
|
-
];
|
|
41
|
-
const ANSI_MUTED_FOREGROUNDS = ['muted', 'sessionLabel', 'sessionBorder'];
|
|
42
|
-
function xtermEightBitRgb(colorNumber) {
|
|
43
|
-
if (colorNumber >= 232) {
|
|
44
|
-
const value = 8 + (colorNumber - 232) * 10;
|
|
45
|
-
return [value, value, value];
|
|
46
|
-
}
|
|
47
|
-
if (colorNumber >= 16) {
|
|
48
|
-
const offset = colorNumber - 16;
|
|
49
|
-
return [
|
|
50
|
-
XTERM_6_LEVELS[Math.floor(offset / 36) % 6],
|
|
51
|
-
XTERM_6_LEVELS[Math.floor(offset / 6) % 6],
|
|
52
|
-
XTERM_6_LEVELS[offset % 6]
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
return [0, 0, 0];
|
|
56
|
-
}
|
|
57
|
-
function channelLuminance(value) {
|
|
58
|
-
const normalized = value / 255;
|
|
59
|
-
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
|
60
|
-
}
|
|
61
|
-
function relativeLuminance(red, green, blue) {
|
|
62
|
-
return 0.2126 * channelLuminance(red) + 0.7152 * channelLuminance(green) + 0.0722 * channelLuminance(blue);
|
|
63
|
-
}
|
|
64
|
-
function rgbToHsl(red, green, blue) {
|
|
65
|
-
const rn = red / 255;
|
|
66
|
-
const gn = green / 255;
|
|
67
|
-
const bn = blue / 255;
|
|
68
|
-
const max = Math.max(rn, gn, bn);
|
|
69
|
-
const min = Math.min(rn, gn, bn);
|
|
70
|
-
const lightness = (max + min) / 2;
|
|
71
|
-
if (max === min) {
|
|
72
|
-
return [0, 0, lightness];
|
|
73
|
-
}
|
|
74
|
-
const delta = max - min;
|
|
75
|
-
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
76
|
-
const hue = max === rn
|
|
77
|
-
? (gn - bn) / delta + (gn < bn ? 6 : 0)
|
|
78
|
-
: max === gn
|
|
79
|
-
? (bn - rn) / delta + 2
|
|
80
|
-
: (rn - gn) / delta + 4;
|
|
81
|
-
return [hue / 6, saturation, lightness];
|
|
82
|
-
}
|
|
83
|
-
function circularDistance(a, b) {
|
|
84
|
-
const distance = Math.abs(a - b);
|
|
85
|
-
return Math.min(distance, 1 - distance);
|
|
86
|
-
}
|
|
87
|
-
// Mirrors @cvc/ink's colorize.ts. Keep local: app code compiles from
|
|
88
|
-
// ui-tui/src, while @cvc/ink is bundled separately from packages/.
|
|
89
|
-
function richEightBitColorNumber(red, green, blue) {
|
|
90
|
-
const [, saturation, lightness] = rgbToHsl(red, green, blue);
|
|
91
|
-
if (saturation < 0.15) {
|
|
92
|
-
const gray = Math.round(lightness * 25);
|
|
93
|
-
return gray === 0 ? 16 : gray === 25 ? 231 : 231 + gray;
|
|
94
|
-
}
|
|
95
|
-
const sixRed = red < 95 ? red / 95 : 1 + (red - 95) / 40;
|
|
96
|
-
const sixGreen = green < 95 ? green / 95 : 1 + (green - 95) / 40;
|
|
97
|
-
const sixBlue = blue < 95 ? blue / 95 : 1 + (blue - 95) / 40;
|
|
98
|
-
return 16 + 36 * Math.round(sixRed) + 6 * Math.round(sixGreen) + Math.round(sixBlue);
|
|
99
|
-
}
|
|
100
|
-
function bestReadableAnsiColor(red, green, blue) {
|
|
101
|
-
const [hue, saturation, lightness] = rgbToHsl(red, green, blue);
|
|
102
|
-
let bestColor = richEightBitColorNumber(red, green, blue);
|
|
103
|
-
let bestScore = Number.POSITIVE_INFINITY;
|
|
104
|
-
for (let colorNumber = 16; colorNumber <= 255; colorNumber += 1) {
|
|
105
|
-
const [candidateRed, candidateGreen, candidateBlue] = xtermEightBitRgb(colorNumber);
|
|
106
|
-
const candidateLuminance = relativeLuminance(candidateRed, candidateGreen, candidateBlue);
|
|
107
|
-
if (candidateLuminance > ANSI_LIGHT_MAX_LUMINANCE) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const [candidateHue, candidateSaturation, candidateLightness] = rgbToHsl(candidateRed, candidateGreen, candidateBlue);
|
|
111
|
-
const saturationFloorPenalty = candidateSaturation < ANSI_LIGHT_MIN_SATURATION ? (ANSI_LIGHT_MIN_SATURATION - candidateSaturation) * 3 : 0;
|
|
112
|
-
const score = circularDistance(candidateHue, hue) * 4 +
|
|
113
|
-
Math.abs(candidateSaturation - Math.max(ANSI_LIGHT_MIN_SATURATION, saturation)) * 0.8 +
|
|
114
|
-
Math.abs(candidateLightness - Math.min(lightness, ANSI_LIGHT_TARGET_LUMINANCE)) * 2 +
|
|
115
|
-
saturationFloorPenalty;
|
|
116
|
-
if (score < bestScore) {
|
|
117
|
-
bestColor = colorNumber;
|
|
118
|
-
bestScore = score;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return bestColor;
|
|
122
|
-
}
|
|
123
|
-
function normalizeAnsiForeground(color) {
|
|
124
|
-
const rgb = parseHex(color);
|
|
125
|
-
if (!rgb) {
|
|
126
|
-
return color;
|
|
127
|
-
}
|
|
128
|
-
const richAnsi = richEightBitColorNumber(rgb[0], rgb[1], rgb[2]);
|
|
129
|
-
const richRgb = xtermEightBitRgb(richAnsi);
|
|
130
|
-
const ansi = relativeLuminance(richRgb[0], richRgb[1], richRgb[2]) > ANSI_LIGHT_MAX_LUMINANCE
|
|
131
|
-
? bestReadableAnsiColor(rgb[0], rgb[1], rgb[2])
|
|
132
|
-
: richAnsi;
|
|
133
|
-
return `ansi256(${ansi})`;
|
|
134
|
-
}
|
|
135
|
-
// ── Defaults ─────────────────────────────────────────────────────────
|
|
136
|
-
const BRAND = {
|
|
137
|
-
name: 'CVC',
|
|
138
|
-
icon: '◆',
|
|
139
|
-
prompt: '❯',
|
|
140
|
-
welcome: 'Type /help for commands · Ctrl+C to exit',
|
|
141
|
-
goodbye: 'Goodbye! ◆',
|
|
142
|
-
tool: '┊',
|
|
143
|
-
helpHeader: '(^_^)? Commands'
|
|
144
|
-
};
|
|
145
|
-
const cleanPromptSymbol = (s, fallback) => {
|
|
146
|
-
const cleaned = String(s ?? '')
|
|
147
|
-
.replace(/\s+/g, ' ')
|
|
148
|
-
.trim();
|
|
149
|
-
return cleaned || fallback;
|
|
150
|
-
};
|
|
151
|
-
export const DARK_THEME = {
|
|
152
|
-
color: {
|
|
153
|
-
primary: '#e63946',
|
|
154
|
-
accent: '#4a9eff',
|
|
155
|
-
border: '#a4202c',
|
|
156
|
-
text: '#FFF8DC',
|
|
157
|
-
muted: '#8B7070',
|
|
158
|
-
completionBg: '#0d1b2a',
|
|
159
|
-
completionCurrentBg: '#1b2d44',
|
|
160
|
-
completionMetaBg: '#0d1b2a',
|
|
161
|
-
completionMetaCurrentBg: '#1b2d44',
|
|
162
|
-
label: '#e63946',
|
|
163
|
-
ok: '#4caf50',
|
|
164
|
-
error: '#ef5350',
|
|
165
|
-
warn: '#ffa726',
|
|
166
|
-
prompt: '#FFF8DC',
|
|
167
|
-
sessionLabel: '#8B7070',
|
|
168
|
-
sessionBorder: '#8B7070',
|
|
169
|
-
statusBg: '#0d1b2a',
|
|
170
|
-
statusFg: '#C0C0C0',
|
|
171
|
-
statusGood: '#8FBC8F',
|
|
172
|
-
statusWarn: '#e63946',
|
|
173
|
-
statusBad: '#FF8C00',
|
|
174
|
-
statusCritical: '#FF6B6B',
|
|
175
|
-
selectionBg: '#1b2d44',
|
|
176
|
-
diffAdded: 'rgb(220,255,220)',
|
|
177
|
-
diffRemoved: 'rgb(255,220,220)',
|
|
178
|
-
diffAddedWord: 'rgb(36,138,61)',
|
|
179
|
-
diffRemovedWord: 'rgb(207,34,46)',
|
|
180
|
-
shellDollar: '#4a9eff'
|
|
181
|
-
},
|
|
182
|
-
brand: BRAND,
|
|
183
|
-
bannerLogo: '',
|
|
184
|
-
bannerHero: ''
|
|
185
|
-
};
|
|
186
|
-
// Light-terminal palette: deeper CVC reds + navy accents for legibility on
|
|
187
|
-
// white backgrounds. Same shape as DARK_THEME so `fromSkin` still layers on
|
|
188
|
-
// top cleanly.
|
|
189
|
-
export const LIGHT_THEME = {
|
|
190
|
-
color: {
|
|
191
|
-
primary: '#a4202c',
|
|
192
|
-
accent: '#1f5dab',
|
|
193
|
-
border: '#7a1820',
|
|
194
|
-
text: '#1a1a2e',
|
|
195
|
-
muted: '#5a4a4a',
|
|
196
|
-
completionBg: '#F5F5F5',
|
|
197
|
-
completionCurrentBg: mix('#F5F5F5', '#1f5dab', 0.25),
|
|
198
|
-
completionMetaBg: '#F5F5F5',
|
|
199
|
-
completionMetaCurrentBg: mix('#F5F5F5', '#1f5dab', 0.25),
|
|
200
|
-
label: '#a4202c',
|
|
201
|
-
ok: '#2E7D32',
|
|
202
|
-
error: '#C62828',
|
|
203
|
-
warn: '#E65100',
|
|
204
|
-
prompt: '#1a1a2e',
|
|
205
|
-
sessionLabel: '#5a4a4a',
|
|
206
|
-
sessionBorder: '#5a4a4a',
|
|
207
|
-
statusBg: '#F5F5F5',
|
|
208
|
-
statusFg: '#333333',
|
|
209
|
-
statusGood: '#2E7D32',
|
|
210
|
-
statusWarn: '#a4202c',
|
|
211
|
-
statusBad: '#D84315',
|
|
212
|
-
statusCritical: '#B71C1C',
|
|
213
|
-
selectionBg: '#D4E4F7',
|
|
214
|
-
diffAdded: 'rgb(200,240,200)',
|
|
215
|
-
diffRemoved: 'rgb(240,200,200)',
|
|
216
|
-
diffAddedWord: 'rgb(27,94,32)',
|
|
217
|
-
diffRemovedWord: 'rgb(183,28,28)',
|
|
218
|
-
shellDollar: '#1565C0'
|
|
219
|
-
},
|
|
220
|
-
brand: BRAND,
|
|
221
|
-
bannerLogo: '',
|
|
222
|
-
bannerHero: ''
|
|
223
|
-
};
|
|
224
|
-
const TRUE_RE = /^(?:1|true|yes|on)$/;
|
|
225
|
-
const FALSE_RE = /^(?:0|false|no|off)$/;
|
|
226
|
-
// TERM_PROGRAM fallback allow-list for terminals whose default profile is
|
|
227
|
-
// light and which may not expose COLORFGBG. This currently includes Apple
|
|
228
|
-
// Terminal. Explicit CVC_TUI_THEME / COLORFGBG signals above still win,
|
|
229
|
-
// so dark Apple Terminal profiles that advertise a dark background stay dark.
|
|
230
|
-
const LIGHT_DEFAULT_TERM_PROGRAMS = new Set(['Apple_Terminal']);
|
|
231
|
-
// Best-effort RGB → luminance check. Currently only accepts a 3- or
|
|
232
|
-
// 6-digit hex value (with or without a leading `#`); the env var name
|
|
233
|
-
// `CVC_TUI_BACKGROUND` is intentionally generic so a future OSC11
|
|
234
|
-
// query helper can cache its answer there too, but additional formats
|
|
235
|
-
// (rgb()/hsl()/named colours) would need explicit parsing here first.
|
|
236
|
-
const LUMA_LIGHT_THRESHOLD = 0.6;
|
|
237
|
-
// Strict allow-list: parseInt(..., 16) silently truncates at the first
|
|
238
|
-
// non-hex character (e.g. `fffgff` would parse as `fff` and yield a
|
|
239
|
-
// false-positive "white" reading), so reject anything that doesn't match
|
|
240
|
-
// the canonical 3- or 6-digit shape up front.
|
|
241
|
-
const HEX_3_RE = /^[0-9a-f]{3}$/;
|
|
242
|
-
const HEX_6_RE = /^[0-9a-f]{6}$/;
|
|
243
|
-
function backgroundLuminance(raw) {
|
|
244
|
-
const v = raw.trim().toLowerCase();
|
|
245
|
-
if (!v) {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
const hex = v.startsWith('#') ? v.slice(1) : v;
|
|
249
|
-
const rgb = HEX_6_RE.test(hex)
|
|
250
|
-
? [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
|
|
251
|
-
: HEX_3_RE.test(hex)
|
|
252
|
-
? [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16)]
|
|
253
|
-
: null;
|
|
254
|
-
if (!rgb) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
// Rec. 709 luma — close enough for "is this background bright".
|
|
258
|
-
return (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
|
|
259
|
-
}
|
|
260
|
-
// Pick light vs dark with ordered, explainable signals (#11300):
|
|
261
|
-
//
|
|
262
|
-
// 1. `CVC_TUI_LIGHT` boolean — `1`/`true`/`yes`/`on` → light;
|
|
263
|
-
// `0`/`false`/`no`/`off` → dark. Either explicit value wins
|
|
264
|
-
// regardless of any later signal.
|
|
265
|
-
// 2. `CVC_TUI_THEME` named override — `light` / `dark` win over
|
|
266
|
-
// every signal below.
|
|
267
|
-
// 3. `CVC_TUI_BACKGROUND` hex hint (3- or 6-digit) — luminance
|
|
268
|
-
// ≥ LUMA_LIGHT_THRESHOLD → light.
|
|
269
|
-
// 4. `COLORFGBG` last field — XFCE / rxvt / Terminal.app emit
|
|
270
|
-
// slot 7 or 15 on light profiles; 0–15 ranges are otherwise
|
|
271
|
-
// treated as authoritatively dark so the TERM_PROGRAM
|
|
272
|
-
// allow-list below cannot override an explicit dark profile.
|
|
273
|
-
// 5. `TERM_PROGRAM` light-default allow-list.
|
|
274
|
-
//
|
|
275
|
-
// Anything we can't decide stays dark — the default CVC palette
|
|
276
|
-
// is the dark one.
|
|
277
|
-
export function detectLightMode(env = process.env,
|
|
278
|
-
// Injectable so tests can prove the COLORFGBG-over-TERM_PROGRAM
|
|
279
|
-
// precedence rule even though the production allow-list is empty.
|
|
280
|
-
lightDefaultTermPrograms = LIGHT_DEFAULT_TERM_PROGRAMS) {
|
|
281
|
-
const lightFlag = (env.CVC_TUI_LIGHT ?? '').trim().toLowerCase();
|
|
282
|
-
if (TRUE_RE.test(lightFlag)) {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
if (FALSE_RE.test(lightFlag)) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
const themeFlag = (env.CVC_TUI_THEME ?? '').trim().toLowerCase();
|
|
289
|
-
if (themeFlag === 'light') {
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
if (themeFlag === 'dark') {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
const bgHint = backgroundLuminance(env.CVC_TUI_BACKGROUND ?? '');
|
|
296
|
-
if (bgHint !== null) {
|
|
297
|
-
return bgHint >= LUMA_LIGHT_THRESHOLD;
|
|
298
|
-
}
|
|
299
|
-
const colorfgbg = (env.COLORFGBG ?? '').trim();
|
|
300
|
-
if (colorfgbg) {
|
|
301
|
-
// Validate as a decimal integer before coercing — `Number('')` is 0,
|
|
302
|
-
// so a malformed `COLORFGBG='15;'` would otherwise look like an
|
|
303
|
-
// authoritative dark slot and incorrectly block the TERM_PROGRAM
|
|
304
|
-
// allow-list. Anything that isn't pure digits falls through.
|
|
305
|
-
const lastField = colorfgbg.split(';').at(-1) ?? '';
|
|
306
|
-
if (/^\d+$/.test(lastField)) {
|
|
307
|
-
const bg = Number(lastField);
|
|
308
|
-
if (bg === 7 || bg === 15) {
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
// Slots 0–6 and 8–14 are the dark half of the 0–15 ANSI range.
|
|
312
|
-
// When COLORFGBG is set we trust it as authoritative — a non-light
|
|
313
|
-
// value here shouldn't get overridden by the TERM_PROGRAM allow-list.
|
|
314
|
-
if (bg >= 0 && bg < 16) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const termProgram = (env.TERM_PROGRAM ?? '').trim();
|
|
320
|
-
return lightDefaultTermPrograms.has(termProgram);
|
|
321
|
-
}
|
|
322
|
-
function shouldNormalizeAnsiLightTheme(env = process.env, isLight = detectLightMode(env)) {
|
|
323
|
-
const colorTerm = (env.COLORTERM ?? '').trim().toLowerCase();
|
|
324
|
-
const termProgram = (env.TERM_PROGRAM ?? '').trim();
|
|
325
|
-
return termProgram === 'Apple_Terminal' && colorTerm !== 'truecolor' && colorTerm !== '24bit' && isLight;
|
|
326
|
-
}
|
|
327
|
-
export function normalizeThemeForAnsiLightTerminal(theme, env = process.env, isLight = detectLightMode(env)) {
|
|
328
|
-
if (!shouldNormalizeAnsiLightTheme(env, isLight)) {
|
|
329
|
-
return theme;
|
|
330
|
-
}
|
|
331
|
-
const color = { ...theme.color };
|
|
332
|
-
for (const key of ANSI_NORMALIZED_FOREGROUNDS) {
|
|
333
|
-
color[key] = normalizeAnsiForeground(color[key]);
|
|
334
|
-
}
|
|
335
|
-
for (const key of ANSI_MUTED_FOREGROUNDS) {
|
|
336
|
-
color[key] = `ansi256(${ANSI_MUTED_BUCKET})`;
|
|
337
|
-
}
|
|
338
|
-
return { ...theme, color };
|
|
339
|
-
}
|
|
340
|
-
const DEFAULT_LIGHT_MODE = detectLightMode();
|
|
341
|
-
export const DEFAULT_THEME = normalizeThemeForAnsiLightTerminal(DEFAULT_LIGHT_MODE ? LIGHT_THEME : DARK_THEME, process.env, DEFAULT_LIGHT_MODE);
|
|
342
|
-
// ── Skin → Theme ─────────────────────────────────────────────────────
|
|
343
|
-
export function fromSkin(colors, branding, bannerLogo = '', bannerHero = '', toolPrefix = '', helpHeader = '') {
|
|
344
|
-
const d = DEFAULT_THEME;
|
|
345
|
-
const c = (k) => colors[k];
|
|
346
|
-
const hasSkinColors = Object.keys(colors).length > 0;
|
|
347
|
-
const accent = c('ui_accent') ?? c('banner_accent') ?? d.color.accent;
|
|
348
|
-
const bannerAccent = c('banner_accent') ?? c('banner_title') ?? d.color.accent;
|
|
349
|
-
const muted = c('banner_dim') ?? d.color.muted;
|
|
350
|
-
const completionBg = c('completion_menu_bg') ?? d.color.completionBg;
|
|
351
|
-
const completionCurrentBg = c('completion_menu_current_bg') ??
|
|
352
|
-
(hasSkinColors ? mix(completionBg, bannerAccent, 0.25) : d.color.completionCurrentBg);
|
|
353
|
-
const completionMetaBg = c('completion_menu_meta_bg') ?? completionBg;
|
|
354
|
-
const completionMetaCurrentBg = c('completion_menu_meta_current_bg') ?? completionCurrentBg;
|
|
355
|
-
return normalizeThemeForAnsiLightTerminal({
|
|
356
|
-
color: {
|
|
357
|
-
primary: c('ui_primary') ?? c('banner_title') ?? d.color.primary,
|
|
358
|
-
accent,
|
|
359
|
-
border: c('ui_border') ?? c('banner_border') ?? d.color.border,
|
|
360
|
-
text: c('ui_text') ?? c('banner_text') ?? d.color.text,
|
|
361
|
-
muted,
|
|
362
|
-
completionBg,
|
|
363
|
-
completionCurrentBg,
|
|
364
|
-
completionMetaBg,
|
|
365
|
-
completionMetaCurrentBg,
|
|
366
|
-
label: c('ui_label') ?? d.color.label,
|
|
367
|
-
ok: c('ui_ok') ?? d.color.ok,
|
|
368
|
-
error: c('ui_error') ?? d.color.error,
|
|
369
|
-
warn: c('ui_warn') ?? d.color.warn,
|
|
370
|
-
prompt: c('prompt') ?? c('banner_text') ?? d.color.prompt,
|
|
371
|
-
sessionLabel: c('session_label') ?? muted,
|
|
372
|
-
sessionBorder: c('session_border') ?? muted,
|
|
373
|
-
statusBg: d.color.statusBg,
|
|
374
|
-
statusFg: d.color.statusFg,
|
|
375
|
-
statusGood: c('ui_ok') ?? d.color.statusGood,
|
|
376
|
-
statusWarn: c('ui_warn') ?? d.color.statusWarn,
|
|
377
|
-
statusBad: d.color.statusBad,
|
|
378
|
-
statusCritical: d.color.statusCritical,
|
|
379
|
-
selectionBg: c('selection_bg') ?? c('completion_menu_current_bg') ?? (hasSkinColors ? completionCurrentBg : d.color.selectionBg),
|
|
380
|
-
diffAdded: d.color.diffAdded,
|
|
381
|
-
diffRemoved: d.color.diffRemoved,
|
|
382
|
-
diffAddedWord: d.color.diffAddedWord,
|
|
383
|
-
diffRemovedWord: d.color.diffRemovedWord,
|
|
384
|
-
shellDollar: c('shell_dollar') ?? d.color.shellDollar
|
|
385
|
-
},
|
|
386
|
-
brand: {
|
|
387
|
-
name: branding.agent_name ?? d.brand.name,
|
|
388
|
-
icon: d.brand.icon,
|
|
389
|
-
prompt: cleanPromptSymbol(branding.prompt_symbol, d.brand.prompt),
|
|
390
|
-
welcome: branding.welcome ?? d.brand.welcome,
|
|
391
|
-
goodbye: branding.goodbye ?? d.brand.goodbye,
|
|
392
|
-
tool: toolPrefix || d.brand.tool,
|
|
393
|
-
helpHeader: branding.help_header ?? (helpHeader || d.brand.helpHeader)
|
|
394
|
-
},
|
|
395
|
-
bannerLogo,
|
|
396
|
-
bannerHero
|
|
397
|
-
}, process.env, DEFAULT_LIGHT_MODE);
|
|
398
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|