mixdog 0.9.2 → 0.9.3
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/package.json +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -122,14 +122,52 @@ export function lineEnd(text, offset) {
|
|
|
122
122
|
return end === -1 ? value.length : end;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Return the [start,end) run of same-class characters (word-like vs
|
|
126
|
+
// non-word-like) surrounding `offset`, used by double-click word selection.
|
|
127
|
+
// Mirrors typical desktop double-click behavior: clicking inside a run of
|
|
128
|
+
// letters/digits selects that whole run; clicking on a run of punctuation or
|
|
129
|
+
// whitespace selects that run instead (still deterministic, never empty).
|
|
130
|
+
export function wordRangeAt(text, offset) {
|
|
131
|
+
const value = String(text || '');
|
|
132
|
+
const units = graphemeUnits(value);
|
|
133
|
+
if (units.length === 0) return { start: 0, end: 0 };
|
|
134
|
+
const off = clampOffset(value, offset);
|
|
135
|
+
let idx = units.findIndex((u) => off >= u.start && off < u.end);
|
|
136
|
+
if (idx === -1) {
|
|
137
|
+
idx = units.length - 1;
|
|
138
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
139
|
+
if (units[i].start >= off) { idx = i > 0 ? i - 1 : 0; break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const targetIsWord = wordLike(units[idx].segment);
|
|
143
|
+
let start = idx;
|
|
144
|
+
while (start > 0 && wordLike(units[start - 1].segment) === targetIsWord) start -= 1;
|
|
145
|
+
let end = idx;
|
|
146
|
+
while (end < units.length - 1 && wordLike(units[end + 1].segment) === targetIsWord) end += 1;
|
|
147
|
+
return { start: units[start].start, end: units[end].end };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// `hasTrailingContent` (default derived from the full text) decides whether a
|
|
151
|
+
// caret that lands FLUSH on the last column (col === w) rolls to the next row.
|
|
152
|
+
// It must reflect whether ANY rendered cell follows this offset in the FULL
|
|
153
|
+
// text — not just the sliced prefix — otherwise a caret at a mid-text soft-wrap
|
|
154
|
+
// boundary reports {row N, col w} while ink already rendered the next glyph on
|
|
155
|
+
// row N+1, dropping the caret outside the box. PromptInput also appends a
|
|
156
|
+
// trailing space cell when the cursor sits at end-of-input, so "end of text"
|
|
157
|
+
// still has a following cell there; callers pass hasTrailingContent=true for
|
|
158
|
+
// that case.
|
|
159
|
+
export function caretPosition(text, offset, width, hasTrailingContent = undefined) {
|
|
126
160
|
const value = String(text || '');
|
|
127
161
|
const before = value.slice(0, offset);
|
|
128
162
|
const w = safeWidth(width);
|
|
163
|
+
const followsInFullText = hasTrailingContent === undefined
|
|
164
|
+
? offset < value.length
|
|
165
|
+
: hasTrailingContent === true;
|
|
129
166
|
let row = 0;
|
|
130
167
|
let col = 0;
|
|
131
|
-
|
|
132
|
-
for (
|
|
168
|
+
const segments = [...graphemeSegmenter.segment(before)];
|
|
169
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
170
|
+
const { segment } = segments[i];
|
|
133
171
|
if (segment === '\n') {
|
|
134
172
|
row += 1;
|
|
135
173
|
col = 0;
|
|
@@ -139,15 +177,24 @@ export function caretPosition(text, offset, width) {
|
|
|
139
177
|
const segmentWidth = stringWidth(segment);
|
|
140
178
|
if (segmentWidth === 0) continue;
|
|
141
179
|
|
|
180
|
+
// Mirror ink's wrap-ansi wrapWord (wrap="hard", wordWrap:false): a glyph
|
|
181
|
+
// that would overflow the row is pushed WHOLE to the next row (col never
|
|
182
|
+
// exceeds w for wide chars — no half-column left behind), and a glyph that
|
|
183
|
+
// lands flush on the last column starts a new row ONLY when more rendered
|
|
184
|
+
// content follows. Whether content follows is decided from the FULL text
|
|
185
|
+
// (followsInFullText), not the sliced prefix, so a caret at a mid-text wrap
|
|
186
|
+
// boundary rolls to row N+1 exactly as ink renders it. The old
|
|
187
|
+
// `col >= w → row += floor(col/w)` over-counted a row for width-2 glyphs at
|
|
188
|
+
// odd widths (col could reach w+1), landing the cursor one row too low.
|
|
142
189
|
if (col > 0 && col + segmentWidth > w) {
|
|
143
190
|
row += 1;
|
|
144
191
|
col = 0;
|
|
145
192
|
}
|
|
146
|
-
|
|
147
193
|
col += segmentWidth;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
const moreFollows = i < segments.length - 1 || followsInFullText;
|
|
195
|
+
if (col === w && moreFollows) {
|
|
196
|
+
row += 1;
|
|
197
|
+
col = 0;
|
|
151
198
|
}
|
|
152
199
|
}
|
|
153
200
|
|
|
@@ -185,13 +232,16 @@ function boundaryPositions(text, width) {
|
|
|
185
232
|
for (const unit of graphemeUnits(value)) offsets.push(unit.end);
|
|
186
233
|
|
|
187
234
|
const seen = new Set();
|
|
188
|
-
|
|
235
|
+
const positions = offsets
|
|
189
236
|
.filter((offset) => {
|
|
190
237
|
if (seen.has(offset)) return false;
|
|
191
238
|
seen.add(offset);
|
|
192
239
|
return true;
|
|
193
240
|
})
|
|
241
|
+
// Decide the flush-boundary advance from the FULL text so a soft-wrap
|
|
242
|
+
// offset reports {row N+1, col 0} (matching ink) instead of {row N, col w}.
|
|
194
243
|
.map((offset) => ({ offset, ...caretPosition(value, offset, width) }));
|
|
244
|
+
return positions;
|
|
195
245
|
}
|
|
196
246
|
|
|
197
247
|
export function verticalOffset(text, offset, width, direction, preferredColumn = null) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
moveCursor,
|
|
5
|
+
previousWordOffset,
|
|
6
|
+
nextWordOffset,
|
|
7
|
+
selectionRange,
|
|
8
|
+
verticalOffset,
|
|
9
|
+
wordRangeAt,
|
|
10
|
+
lineStart,
|
|
11
|
+
lineEnd,
|
|
12
|
+
} from './input-editing.mjs';
|
|
13
|
+
|
|
14
|
+
// Whole-word extend (Ctrl+Shift+Left/Right) relies on moveCursor(extend:true)
|
|
15
|
+
// starting/keeping an anchor while jumping by word.
|
|
16
|
+
test('ctrl+shift+right extends selection by whole word', () => {
|
|
17
|
+
const draft = { value: 'hello world foo', cursor: 0, selectionAnchor: null };
|
|
18
|
+
const next = moveCursor(draft, nextWordOffset(draft.value, draft.cursor), { extend: true });
|
|
19
|
+
assert.equal(next.cursor, 'hello'.length);
|
|
20
|
+
assert.equal(next.selectionAnchor, 0);
|
|
21
|
+
assert.deepEqual(selectionRange(next), { start: 0, end: 5 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('ctrl+shift+left extends selection backward by whole word', () => {
|
|
25
|
+
const value = 'hello world';
|
|
26
|
+
const draft = { value, cursor: value.length, selectionAnchor: null };
|
|
27
|
+
const next = moveCursor(draft, previousWordOffset(value, value.length), { extend: true });
|
|
28
|
+
assert.equal(next.selectionAnchor, value.length);
|
|
29
|
+
assert.deepEqual(selectionRange(next), { start: 'hello '.length, end: value.length });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Shift+Up on the first line yields no vertical move (targetRow < 0) → callers
|
|
33
|
+
// then extend to document start (offset 0).
|
|
34
|
+
test('verticalOffset returns same cursor on first line (up)', () => {
|
|
35
|
+
const value = 'line1\nline2';
|
|
36
|
+
const moved = verticalOffset(value, 2, 80, -1, null);
|
|
37
|
+
assert.equal(moved.cursor, 2, 'no vertical move available above first line');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('shift+up to document start extends selection to offset 0', () => {
|
|
41
|
+
const value = 'abc';
|
|
42
|
+
const draft = { value, cursor: 2, selectionAnchor: null };
|
|
43
|
+
const next = moveCursor(draft, 0, { extend: true });
|
|
44
|
+
assert.deepEqual(selectionRange(next), { start: 0, end: 2 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('shift+down to document end extends selection to value.length', () => {
|
|
48
|
+
const value = 'abc';
|
|
49
|
+
const draft = { value, cursor: 1, selectionAnchor: null };
|
|
50
|
+
const next = moveCursor(draft, value.length, { extend: true });
|
|
51
|
+
assert.deepEqual(selectionRange(next), { start: 1, end: 3 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Double-click word select: clicking anywhere inside a word run selects the
|
|
55
|
+
// whole run, and a click on punctuation/whitespace selects that run instead.
|
|
56
|
+
test('wordRangeAt selects the word run under the offset', () => {
|
|
57
|
+
const value = 'hello world foo';
|
|
58
|
+
assert.deepEqual(wordRangeAt(value, 2), { start: 0, end: 5 });
|
|
59
|
+
assert.deepEqual(wordRangeAt(value, 6), { start: 6, end: 11 });
|
|
60
|
+
assert.deepEqual(wordRangeAt(value, 0), { start: 0, end: 5 });
|
|
61
|
+
assert.deepEqual(wordRangeAt(value, value.length), { start: 12, end: 15 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('wordRangeAt selects a punctuation/whitespace run distinctly from words', () => {
|
|
65
|
+
const value = 'foo---bar';
|
|
66
|
+
assert.deepEqual(wordRangeAt(value, 4), { start: 3, end: 6 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Triple-click line select uses lineStart/lineEnd directly.
|
|
70
|
+
test('lineStart/lineEnd bound the current line for triple-click select', () => {
|
|
71
|
+
const value = 'line1\nline2\nline3';
|
|
72
|
+
const offset = value.indexOf('line2') + 2;
|
|
73
|
+
assert.equal(lineStart(value, offset), 6);
|
|
74
|
+
assert.equal(lineEnd(value, offset), 11);
|
|
75
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/tui/keyboard-protocol.mjs — terminal extended-keys capability gate.
|
|
3
3
|
*
|
|
4
|
-
* We do NOT query the terminal. Instead,
|
|
4
|
+
* We do NOT query the terminal. Instead, we enable the
|
|
5
5
|
* kitty keyboard protocol AND xterm modifyOtherKeys SYNCHRONOUSLY at the moment
|
|
6
6
|
* raw mode turns on (see App.jsx's mount effect), gated by the allowlist below.
|
|
7
7
|
* Writing both enables unconditionally is safe — a terminal honors whichever it
|
|
@@ -22,7 +22,7 @@ export const POP_KITTY = '\x1b[<u';
|
|
|
22
22
|
export const DISABLE_MODIFY_OTHER_KEYS = '\x1b[>4;0m';
|
|
23
23
|
|
|
24
24
|
// Allowlist of terminals known to honor kitty and/or xterm modifyOtherKeys.
|
|
25
|
-
//
|
|
25
|
+
// VS Code's xterm.js
|
|
26
26
|
// integrated terminal mishandles these sequences, so it is excluded first.
|
|
27
27
|
// Honors the MIXDOG_TUI_EXTENDED_KEYS opt-out (=0) / override (=1).
|
|
28
28
|
export function supportsExtendedKeys() {
|
|
@@ -80,28 +80,42 @@ export function isVoiceEnabled() {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Coalesce ensure*'s onProgress ({ phase, downloaded, total } | { phase:'extra', ... })
|
|
83
|
-
// into a
|
|
84
|
-
// model download doesn't spam the notice toast on every
|
|
85
|
-
|
|
83
|
+
// into a sticky, in-place input-hint-line progress bar (setProgressHint) so a
|
|
84
|
+
// multi-hundred-MB model download doesn't spam the notice toast on every
|
|
85
|
+
// chunk. Falls back to the old throttled (2s) toast-spam behavior when the
|
|
86
|
+
// caller (e.g. the channels pipeline) doesn't have a progress-hint slot to
|
|
87
|
+
// render into — only pushNotice is guaranteed to exist everywhere.
|
|
88
|
+
const PROGRESS_BAR_CELLS = 6;
|
|
89
|
+
function renderProgressBarText(phase, downloaded, total) {
|
|
90
|
+
const label = phaseLabel(phase);
|
|
91
|
+
if (total > 0) {
|
|
92
|
+
const ratio = Math.max(0, Math.min(1, downloaded / total));
|
|
93
|
+
const filled = Math.round(ratio * PROGRESS_BAR_CELLS);
|
|
94
|
+
const bar = '▓'.repeat(filled) + '░'.repeat(PROGRESS_BAR_CELLS - filled);
|
|
95
|
+
return `⬇ Voice ${label} ${bar} ${Math.round(ratio * 100)}%`;
|
|
96
|
+
}
|
|
97
|
+
return `⬇ Voice ${label} …`;
|
|
98
|
+
}
|
|
99
|
+
function phaseLabel(phase) {
|
|
100
|
+
if (phase === 'model') return 'model';
|
|
101
|
+
if (phase === 'ffmpeg') return 'ffmpeg';
|
|
102
|
+
if (phase === 'extra') return 'extra data';
|
|
103
|
+
return 'whisper runtime';
|
|
104
|
+
}
|
|
105
|
+
function makeThrottledProgressNotice({ pushNotice, setProgressHint } = {}, intervalMs = 500) {
|
|
86
106
|
let lastEmitAt = 0;
|
|
87
|
-
const phaseLabel = (phase) => {
|
|
88
|
-
if (phase === 'model') return 'model';
|
|
89
|
-
if (phase === 'ffmpeg') return 'ffmpeg';
|
|
90
|
-
if (phase === 'extra') return 'extra data';
|
|
91
|
-
return 'whisper runtime';
|
|
92
|
-
};
|
|
93
107
|
return (progress = {}) => {
|
|
94
|
-
if (typeof pushNotice !== 'function') return;
|
|
95
108
|
const now = Date.now();
|
|
96
109
|
if (now - lastEmitAt < intervalMs) return;
|
|
97
110
|
lastEmitAt = now;
|
|
98
111
|
const total = Number(progress.total) || 0;
|
|
99
112
|
const downloaded = Number(progress.downloaded) || 0;
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
const text = renderProgressBarText(progress.phase, downloaded, total);
|
|
114
|
+
if (typeof setProgressHint === 'function') {
|
|
115
|
+
setProgressHint(text, 'info');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (typeof pushNotice === 'function') pushNotice(text, 'info');
|
|
105
119
|
};
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -112,12 +126,12 @@ function makeThrottledProgressNotice(pushNotice, intervalMs = 2000) {
|
|
|
112
126
|
* runtime descriptor (resolveVoiceRuntime shape) once `installed` is true;
|
|
113
127
|
* throws on any ensure* failure (manifest fetch, sha256 mismatch, etc.).
|
|
114
128
|
*/
|
|
115
|
-
export async function ensureVoiceRuntimeReady({ dataDir = resolvePluginData(), pushNotice } = {}) {
|
|
129
|
+
export async function ensureVoiceRuntimeReady({ dataDir = resolvePluginData(), pushNotice, setProgressHint } = {}) {
|
|
116
130
|
const fetcher = await loadVoiceRuntimeFetcher();
|
|
117
131
|
let runtime = fetcher.resolveVoiceRuntime(dataDir);
|
|
118
132
|
if (runtime.installed) return runtime;
|
|
119
133
|
|
|
120
|
-
const onProgress = makeThrottledProgressNotice(pushNotice);
|
|
134
|
+
const onProgress = makeThrottledProgressNotice({ pushNotice, setProgressHint });
|
|
121
135
|
if (!runtime.binary || !runtime.serverCmd) {
|
|
122
136
|
await fetcher.ensureWhisperRuntime(dataDir, onProgress);
|
|
123
137
|
}
|
|
@@ -147,7 +161,7 @@ export async function ensureVoiceRuntimeReady({ dataDir = resolvePluginData(), p
|
|
|
147
161
|
* Returns the NEW enabled state (true/false) on success, or null when the
|
|
148
162
|
* toggle could not run (install already in flight) or failed.
|
|
149
163
|
*/
|
|
150
|
-
export async function toggleVoice({ pushNotice } = {}) {
|
|
164
|
+
export async function toggleVoice({ pushNotice, setProgressHint } = {}) {
|
|
151
165
|
const dataDir = resolvePluginData();
|
|
152
166
|
if (isVoiceEnabled()) {
|
|
153
167
|
// Med-5: updateSection is a synchronous file write (readAllForRmW +
|
|
@@ -172,11 +186,13 @@ export async function toggleVoice({ pushNotice } = {}) {
|
|
|
172
186
|
}
|
|
173
187
|
_voiceInstallBusy = true;
|
|
174
188
|
try {
|
|
175
|
-
await ensureVoiceRuntimeReady({ dataDir, pushNotice });
|
|
189
|
+
await ensureVoiceRuntimeReady({ dataDir, pushNotice, setProgressHint });
|
|
176
190
|
updateSection('voice', (current) => ({ ...current, enabled: true }));
|
|
191
|
+
setProgressHint?.('');
|
|
177
192
|
pushNotice?.('Voice ON — Ctrl+Space to record', 'info');
|
|
178
193
|
return true;
|
|
179
194
|
} catch (err) {
|
|
195
|
+
setProgressHint?.('');
|
|
180
196
|
pushNotice?.(`Voice setup failed: ${err?.message || err}`, 'error');
|
|
181
197
|
return null;
|
|
182
198
|
} finally {
|
|
@@ -108,8 +108,8 @@ function decodeEntities(s) {
|
|
|
108
108
|
|
|
109
109
|
// ── Fenced code block rendering ─────────────────────────────────────────────
|
|
110
110
|
// Gutter-indented, syntax-highlighted body with NO background band and no ```
|
|
111
|
-
// fences
|
|
112
|
-
//
|
|
111
|
+
// fences. Diff/patch languages (and bodies that look like unified diffs) keep
|
|
112
|
+
// the diff highlighter; other languages use
|
|
113
113
|
// cli-highlight (highlight.js) themed from our syntax* palette; unknown
|
|
114
114
|
// languages fall back to flat `mdCodeBlock` body color.
|
|
115
115
|
const DIFF_LANGS = new Set(['diff', 'patch', 'udiff', 'git-diff', 'gitdiff']);
|
|
@@ -567,7 +567,7 @@ function collectHighlightedBodyLines(text, hljsLang, bandWidth) {
|
|
|
567
567
|
const CODE_GUTTER = ' ';
|
|
568
568
|
|
|
569
569
|
/**
|
|
570
|
-
* Render a fenced `code` token
|
|
570
|
+
* Render a fenced `code` token: an optional
|
|
571
571
|
* subtle language label row, then the wrapped, syntax/diff/flat-highlighted
|
|
572
572
|
* body. Every row is left-indented by a 2-col gutter. NO background band, no
|
|
573
573
|
* full-width padding, and no visible ``` fence lines.
|
|
@@ -653,7 +653,7 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
|
|
|
653
653
|
const ex = extraColorizers();
|
|
654
654
|
switch (token.type) {
|
|
655
655
|
case 'blockquote': {
|
|
656
|
-
// Block structure carries no color
|
|
656
|
+
// Block structure carries no color: a dim bar plus
|
|
657
657
|
// body-colored italic text. Color is reserved for inline codespan/link.
|
|
658
658
|
const bar = chalk.dim(BLOCKQUOTE_BAR);
|
|
659
659
|
const quotePrefix = `${BLOCKQUOTE_BAR} `;
|
|
@@ -673,12 +673,11 @@ export function formatToken(token, listBaseIndent = 0, orderedListNumber = null,
|
|
|
673
673
|
return renderCodeBlock(token, width);
|
|
674
674
|
case 'codespan':
|
|
675
675
|
// inline code — accent color only (no background tint; a bg box behind
|
|
676
|
-
// inline spans reads as awkward against body text
|
|
677
|
-
// codespan = color-only treatment).
|
|
676
|
+
// inline spans reads as awkward against body text).
|
|
678
677
|
return accent(decodeEntities(token.text));
|
|
679
678
|
case 'em':
|
|
680
|
-
// Italic only — no color tint (
|
|
681
|
-
//
|
|
679
|
+
// Italic only — no color tint (a colored em clashes per-theme and
|
|
680
|
+
// reads loud against body prose).
|
|
682
681
|
return chalk.italic((token.tokens ?? []).map((t) => formatToken(t, 0, null, parent)).join(''));
|
|
683
682
|
case 'strong':
|
|
684
683
|
// Bold only — no color tint (stays body-colored, just heavier weight).
|
|
@@ -39,7 +39,7 @@ test('short code line is gutter-indented with no background band', () => {
|
|
|
39
39
|
const width = 60;
|
|
40
40
|
const token = lexFirst('```js\nshort\n```\n', 'code');
|
|
41
41
|
const out = formatToken(token, 0, null, null, width);
|
|
42
|
-
// No background band:
|
|
42
|
+
// No background band: fenced code renders with no `48;2;` bg SGR.
|
|
43
43
|
assert.ok(!out.includes('48;2;'), 'no background SGR');
|
|
44
44
|
const bodyLine = stripAnsi(out).split('\n').find((l) => l.includes('short'));
|
|
45
45
|
assert.ok(bodyLine, 'body line present');
|
|
@@ -305,8 +305,8 @@ test('strong and em use distinct mdStrong / mdEmph colors', () => {
|
|
|
305
305
|
const emTok = lexFirst('*ital*', 'paragraph');
|
|
306
306
|
const strongOut = formatToken(strongTok);
|
|
307
307
|
const emOut = formatToken(emTok);
|
|
308
|
-
// Bold/italic carry weight/style only — NO color tint (
|
|
309
|
-
//
|
|
308
|
+
// Bold/italic carry weight/style only — NO color tint (a colored
|
|
309
|
+
// strong/em clashes per-theme and reads too loud).
|
|
310
310
|
assert.ok(strongOut.includes('\x1b[1m'), 'strong is bold');
|
|
311
311
|
assert.ok(emOut.includes('\x1b[3m'), 'em is italic');
|
|
312
312
|
assert.ok(!strongOut.includes(rgbSgr(theme.mdStrong)), 'strong is not color-tinted');
|
|
@@ -67,6 +67,44 @@ export function imageReferenceIds(input) {
|
|
|
67
67
|
return new Set([...String(input || '').matchAll(re)].map((m) => Number(m[1]) || 0).filter(Boolean));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// --- Large pasted-text folding ------------------------------------------
|
|
71
|
+
// A big paste is replaced in the prompt by a compact token so the editor does
|
|
72
|
+
// not choke on thousands of raw characters; the original text is stored in an
|
|
73
|
+
// App-level map (mirroring pastedImages) and expanded back on submit.
|
|
74
|
+
const PASTE_TOKEN_MIN_LINES = 3;
|
|
75
|
+
const PASTE_TOKEN_MIN_CHARS = 200;
|
|
76
|
+
|
|
77
|
+
export function shouldFoldPastedText(text) {
|
|
78
|
+
const value = String(text ?? '');
|
|
79
|
+
if (!value) return false;
|
|
80
|
+
const lineCount = value.split('\n').length;
|
|
81
|
+
return lineCount >= PASTE_TOKEN_MIN_LINES || value.length > PASTE_TOKEN_MIN_CHARS;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatPastedTextRef(id, text) {
|
|
85
|
+
const lines = String(text ?? '').split('\n').length;
|
|
86
|
+
return `[Pasted text #${id} +${lines} lines]`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function pastedTextReferenceIds(input) {
|
|
90
|
+
const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
|
|
91
|
+
return new Set([...String(input || '').matchAll(re)].map((m) => Number(m[1]) || 0).filter(Boolean));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Expand every intact pasted-text token in `value` back to its original text.
|
|
95
|
+
// Broken / partially-deleted tokens simply do not match and are left as-is.
|
|
96
|
+
export function expandPastedTextTokens(value, pastedTexts = {}) {
|
|
97
|
+
let out = String(value ?? '');
|
|
98
|
+
if (!pastedTexts || typeof pastedTexts !== 'object') return out;
|
|
99
|
+
const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
|
|
100
|
+
out = out.replace(re, (match, idRaw) => {
|
|
101
|
+
const entry = pastedTexts[Number(idRaw)] || pastedTexts[idRaw];
|
|
102
|
+
if (entry && typeof entry.text === 'string') return entry.text;
|
|
103
|
+
return match;
|
|
104
|
+
});
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
70
108
|
export function buildPromptContentWithImages(text, pastedImages = {}) {
|
|
71
109
|
const value = String(text ?? '');
|
|
72
110
|
const refs = imageReferenceIds(value);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import stringWidth from 'string-width';
|
|
4
|
+
import {
|
|
5
|
+
shouldFoldPastedText,
|
|
6
|
+
formatPastedTextRef,
|
|
7
|
+
pastedTextReferenceIds,
|
|
8
|
+
expandPastedTextTokens,
|
|
9
|
+
} from './paste-attachments.mjs';
|
|
10
|
+
import { caretPosition } from './input-editing.mjs';
|
|
11
|
+
|
|
12
|
+
// --- Part 1: pasted-text token folding ----------------------------------
|
|
13
|
+
|
|
14
|
+
test('shouldFoldPastedText fires on >=3 lines or >200 chars', () => {
|
|
15
|
+
assert.equal(shouldFoldPastedText('a\nb\nc'), true); // 3 lines
|
|
16
|
+
assert.equal(shouldFoldPastedText('a\nb'), false); // 2 lines, short
|
|
17
|
+
assert.equal(shouldFoldPastedText('x'.repeat(201)), true); // > 200 chars
|
|
18
|
+
assert.equal(shouldFoldPastedText('x'.repeat(200)), false); // == 200, not over
|
|
19
|
+
assert.equal(shouldFoldPastedText(''), false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('token round-trips through format + expand', () => {
|
|
23
|
+
const original = 'line1\nline2\nline3';
|
|
24
|
+
const token = formatPastedTextRef(7, original);
|
|
25
|
+
assert.equal(token, '[Pasted text #7 +3 lines]');
|
|
26
|
+
const refs = pastedTextReferenceIds(`before ${token} after`);
|
|
27
|
+
assert.deepEqual([...refs], [7]);
|
|
28
|
+
const map = { 7: { id: 7, text: original } };
|
|
29
|
+
assert.equal(
|
|
30
|
+
expandPastedTextTokens(`before ${token} after`, map),
|
|
31
|
+
`before ${original} after`,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('broken / partially-deleted tokens do not expand', () => {
|
|
36
|
+
const map = { 3: { id: 3, text: 'HIDDEN' } };
|
|
37
|
+
// Missing the "+N lines" tail — not a valid token, left verbatim.
|
|
38
|
+
assert.equal(expandPastedTextTokens('[Pasted text #3]', map), '[Pasted text #3]');
|
|
39
|
+
// Truncated bracket.
|
|
40
|
+
assert.equal(expandPastedTextTokens('[Pasted text #3 +2 lines', map), '[Pasted text #3 +2 lines');
|
|
41
|
+
// Unknown id stays as-is.
|
|
42
|
+
assert.equal(expandPastedTextTokens('[Pasted text #9 +2 lines]', map), '[Pasted text #9 +2 lines]');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Part 2: caret row math vs ink wrap-ansi (hard, wordWrap:false) -------
|
|
46
|
+
|
|
47
|
+
// Faithful re-implementation of wrap-ansi's wrapWord loop for a single
|
|
48
|
+
// space-free line (the prompt caret math walks graphemes the same way). ink
|
|
49
|
+
// renders <Text wrap="hard"> which calls wrapAnsi(text, w, {hard,wordWrap:false}).
|
|
50
|
+
// For a run with no spaces the whole line goes through wrapWord, so the visual
|
|
51
|
+
// row count is what this produces.
|
|
52
|
+
function inkWrapWordRows(line, columns) {
|
|
53
|
+
const chars = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(line), (s) => s.segment);
|
|
54
|
+
let rows = 1;
|
|
55
|
+
let visible = 0;
|
|
56
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
57
|
+
const cw = stringWidth(chars[i]);
|
|
58
|
+
if (visible + cw <= columns) {
|
|
59
|
+
// stays on current row
|
|
60
|
+
} else {
|
|
61
|
+
rows += 1;
|
|
62
|
+
visible = 0;
|
|
63
|
+
}
|
|
64
|
+
visible += cw;
|
|
65
|
+
if (visible === columns && i < chars.length - 1) {
|
|
66
|
+
rows += 1;
|
|
67
|
+
visible = 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// caretPosition row for the END of the string == (visual row count - 1).
|
|
74
|
+
function caretRowsForWholeString(text, w) {
|
|
75
|
+
return caretPosition(text, text.length, w).row + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test('caret rows agree with ink wrap for wide-char lines at odd widths', () => {
|
|
79
|
+
// A width where a width-2 glyph cannot sit flush on the last column exposes
|
|
80
|
+
// the mismatch: caretPosition must NOT advance an extra row when the char is
|
|
81
|
+
// simply pushed to the next line (ink does the same — it never leaves a
|
|
82
|
+
// half-column then over-counts).
|
|
83
|
+
const cases = [
|
|
84
|
+
{ text: '가'.repeat(30), w: 41 },
|
|
85
|
+
{ text: '가'.repeat(30), w: 40 },
|
|
86
|
+
{ text: '가나다'.repeat(10) + 'ascii', w: 13 },
|
|
87
|
+
{ text: '한글'.repeat(8), w: 7 },
|
|
88
|
+
{ text: '가'.repeat(5), w: 2 },
|
|
89
|
+
];
|
|
90
|
+
for (const { text, w } of cases) {
|
|
91
|
+
assert.equal(
|
|
92
|
+
caretRowsForWholeString(text, w),
|
|
93
|
+
inkWrapWordRows(text, w),
|
|
94
|
+
`width ${w} on ${JSON.stringify(text.slice(0, 6))}…`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Cursor exactly at a soft-wrap boundary MID-text: ink renders the following
|
|
100
|
+
// glyph on the next row, so the caret must report {row N+1, col 0}, never
|
|
101
|
+
// {row N, col == w} (which would sit outside the box).
|
|
102
|
+
test('caret at mid-text wrap boundary rolls to next row (col 0)', () => {
|
|
103
|
+
const value = '가나다라마'; // 5 wide glyphs, width 2 each
|
|
104
|
+
const w = 4; // 2 glyphs per row
|
|
105
|
+
// offset 2 = after '나' (each glyph is ONE UTF-16 unit) = end of visual
|
|
106
|
+
// row 0; content ('다라마') follows, so the caret rolls to the next row.
|
|
107
|
+
const at = caretPosition(value, 2, w);
|
|
108
|
+
assert.deepEqual(at, { row: 1, col: 0 }, 'boundary caret must sit on next row');
|
|
109
|
+
// offset 4 = after '라' = end of visual row 1 (rows 0-1 are full).
|
|
110
|
+
assert.deepEqual(caretPosition(value, 4, w), { row: 2, col: 0 }, 'second boundary rolls too');
|
|
111
|
+
// Cursor at the very end of a FLUSH-ending text ('가나다라' fills rows 0-1
|
|
112
|
+
// exactly) with a trailing cell (PromptInput appends one) also rolls.
|
|
113
|
+
const flush = '가나다라';
|
|
114
|
+
const end = caretPosition(flush, flush.length, w, true);
|
|
115
|
+
assert.deepEqual(end, { row: 2, col: 0 }, 'flush end with trailing cell rolls to next row');
|
|
116
|
+
// Non-flush end ('마' leaves col 2) must NOT roll even with a trailing cell.
|
|
117
|
+
const nonFlush = caretPosition(value, value.length, w, true);
|
|
118
|
+
assert.deepEqual(nonFlush, { row: 2, col: 2 }, 'non-flush end stays on its row');
|
|
119
|
+
});
|
package/src/tui/themes/base.mjs
CHANGED
|
@@ -35,8 +35,8 @@ export const basePalette = {
|
|
|
35
35
|
// Picker/list focus row. This is intentionally stronger than text selection.
|
|
36
36
|
selectionText: 'rgb(230,238,255)', // readable on selectionBackground
|
|
37
37
|
selectionBackground: 'rgb(38,79,120)', // classic dark-mode selection blue
|
|
38
|
-
// Mouse/drag text-selection overlay
|
|
39
|
-
// it replaces the cell background while text remains copy-highlight readable.
|
|
38
|
+
// Mouse/drag text-selection overlay: a separate role from selectionBackground
|
|
39
|
+
// so it replaces the cell background while text remains copy-highlight readable.
|
|
40
40
|
selectionHighlightText: 'rgb(255,255,255)',
|
|
41
41
|
selectionHighlightBackground: 'rgb(38,79,120)',
|
|
42
42
|
inactive: 'rgb(132,139,152)', // light_grey
|
|
@@ -13,10 +13,10 @@ import { basePalette } from './base.mjs';
|
|
|
13
13
|
export const kanagawaPalette = {
|
|
14
14
|
...basePalette,
|
|
15
15
|
background: 'transparent', // sumiInk3 #1F1F28
|
|
16
|
-
claude: 'rgb(210,126,153)', // sakuraPink accent
|
|
16
|
+
claude: 'rgb(210,126,153)', // sakuraPink accent
|
|
17
17
|
logo: 'rgb(232,156,182)', // welcome banner — brighter than accent
|
|
18
18
|
claudeShimmer: 'rgb(156,171,202)', // springViolet2 #9CABCA (lighter accent)
|
|
19
|
-
mixdogOrange: 'rgb(210,126,153)', // sakuraPink accent
|
|
19
|
+
mixdogOrange: 'rgb(210,126,153)', // sakuraPink accent
|
|
20
20
|
mixdogAmber: 'rgb(156,171,202)', // lighter accent
|
|
21
21
|
mixdogIvory: 'rgb(220,215,186)', // fujiWhite #DCD7BA
|
|
22
22
|
spinnerGlyph: 'rgb(228,104,118)', // most-emphasized: waveRed
|
|
@@ -57,7 +57,7 @@ export const kanagawaPalette = {
|
|
|
57
57
|
mdLink: 'rgb(126,156,216)', // crystalBlue
|
|
58
58
|
mdLinkText: 'rgb(118,148,106)', // waveAqua #76946A
|
|
59
59
|
mdStrong: 'rgb(215,166,87)', // roninYellow #D7A657
|
|
60
|
-
mdEmph: 'rgb(195,141,157)', // carpYellow
|
|
60
|
+
mdEmph: 'rgb(195,141,157)', // carpYellow #C38D9D
|
|
61
61
|
mdDiffAdded: 'rgb(152,187,108)', // springGreen
|
|
62
62
|
mdDiffRemoved: 'rgb(255,93,98)', // peachRed
|
|
63
63
|
mdDiffHunk: 'rgb(45,79,103)', // waveBlue #2D4F67
|
|
@@ -71,7 +71,7 @@ export const kanagawaPalette = {
|
|
|
71
71
|
syntaxVariable: 'rgb(220,215,186)', // fujiWhite
|
|
72
72
|
syntaxString: 'rgb(152,187,108)', // springGreen
|
|
73
73
|
syntaxNumber: 'rgb(215,166,87)', // roninYellow #D7A657
|
|
74
|
-
syntaxType: 'rgb(195,141,157)', // carpYellow
|
|
74
|
+
syntaxType: 'rgb(195,141,157)', // carpYellow #C38D9D
|
|
75
75
|
syntaxOperator: 'rgb(210,126,153)', // sakuraPink
|
|
76
76
|
syntaxPunctuation: 'rgb(220,215,186)', // fg
|
|
77
77
|
userMessageBackground: 'rgb(42,42,55)', // sumiInk4 #2A2A37
|
package/src/tui/themes/teal.mjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* src/tui/themes/teal.mjs — Teal
|
|
2
|
+
* src/tui/themes/teal.mjs — Teal.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* taken verbatim: accent teal #8abeb7, body text #d4d4d4, page bg #18181e,
|
|
4
|
+
* Values: accent teal #8abeb7, body text #d4d4d4, page bg #18181e,
|
|
6
5
|
* green #b5bd68, red #cc6666, warning pure yellow #ffff00, blue #81a2be,
|
|
7
|
-
* mdHeading #f0c674, gray #808080. Syntax uses
|
|
6
|
+
* mdHeading #f0c674, gray #808080. Syntax uses a VS Code Dark+-style palette
|
|
8
7
|
* (keyword #569CD6, string #CE9178, function #DCDCAA, etc.). Spreads
|
|
9
8
|
* `basePalette` first so the full key set is always present.
|
|
10
9
|
*/
|
|
11
10
|
import { basePalette } from './base.mjs';
|
|
12
11
|
|
|
13
|
-
/** Teal —
|
|
12
|
+
/** Teal — teal accent with soft body text. */
|
|
14
13
|
export const tealPalette = {
|
|
15
14
|
...basePalette,
|
|
16
15
|
background: 'transparent', // export.pageBg #18181e
|
package/src/tui/themes/utils.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/tui/themes/utils.mjs — palette post-processing helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Design goals:
|
|
5
5
|
* - keep body text neutral and highly readable;
|
|
6
6
|
* - keep strong live-state colors for spinners/status;
|
|
7
7
|
* - use accent colors sparingly for headings, links and syntax, preferably
|