@pellux/goodvibes-tui 0.20.2 → 0.21.0
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/CHANGELOG.md +33 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/platform-sandbox-qemu.ts +60 -16
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/sandbox-qemu-templates.ts +15 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
package/src/renderer/diff.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { TerminalBuffer } from './buffer.ts';
|
|
2
2
|
import { type Cell } from '../types/grid.ts';
|
|
3
|
+
import {
|
|
4
|
+
type TermColorCaps,
|
|
5
|
+
downsampleColor,
|
|
6
|
+
wrapSynced,
|
|
7
|
+
} from './term-caps.ts';
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* DiffEngine - Generates minimal ANSI updates between two buffers.
|
|
11
|
+
*
|
|
12
|
+
* Accepts a TermColorCaps probe result so that color sequences are
|
|
13
|
+
* downsampled to the terminal's actual capability level (truecolor /
|
|
14
|
+
* ansi256 / basic16 / none), and frames are wrapped in DEC 2026
|
|
15
|
+
* synchronized-output markers when supported.
|
|
6
16
|
*/
|
|
7
17
|
export class DiffEngine {
|
|
8
18
|
private lastFg = '';
|
|
@@ -13,6 +23,19 @@ export class DiffEngine {
|
|
|
13
23
|
private lastItalic = false;
|
|
14
24
|
private lastStrikethrough = false;
|
|
15
25
|
private lastLink = '';
|
|
26
|
+
private caps: TermColorCaps;
|
|
27
|
+
/**
|
|
28
|
+
* Run-coalescing state: tracks the last cell position emitted.
|
|
29
|
+
* When the next cell is at (lastEmitX+1, lastEmitY) and SGR is unchanged,
|
|
30
|
+
* we skip cursor re-addressing and append the char directly.
|
|
31
|
+
* Reset per diff() call to avoid stale state across frames.
|
|
32
|
+
*/
|
|
33
|
+
private lastEmitX = -1;
|
|
34
|
+
private lastEmitY = -1;
|
|
35
|
+
|
|
36
|
+
constructor(caps: TermColorCaps = { capability: 'truecolor', syncedOutput: true }) {
|
|
37
|
+
this.caps = caps;
|
|
38
|
+
}
|
|
16
39
|
|
|
17
40
|
public reset(): void {
|
|
18
41
|
this.lastFg = '';
|
|
@@ -27,7 +50,10 @@ export class DiffEngine {
|
|
|
27
50
|
|
|
28
51
|
public diff(oldBuffer: TerminalBuffer | null, newBuffer: TerminalBuffer): string {
|
|
29
52
|
let output = '';
|
|
30
|
-
|
|
53
|
+
// Reset run-coalescing state per frame: last emitted cursor position.
|
|
54
|
+
this.lastEmitX = -1;
|
|
55
|
+
this.lastEmitY = -1;
|
|
56
|
+
|
|
31
57
|
for (let y = 0; y < newBuffer.height; y++) {
|
|
32
58
|
// Skip rows that were not written in either the old or new buffer.
|
|
33
59
|
// If neither side touched the row, both must match the prior frame:
|
|
@@ -43,9 +69,20 @@ export class DiffEngine {
|
|
|
43
69
|
if (!newCell || newCell.char === '') continue;
|
|
44
70
|
|
|
45
71
|
if (this.isCellDifferent(oldCell, newCell)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
const sgrOutput = this.applyStyles(newCell);
|
|
73
|
+
// Run-coalescing: when the previous emitted cell was (x-1, y) and
|
|
74
|
+
// the SGR state did not change, skip cursor re-addressing.
|
|
75
|
+
// Emit a cursor move only on run breaks (new row, gap, or style change).
|
|
76
|
+
if (sgrOutput === '' && this.lastEmitY === y && this.lastEmitX === x - 1) {
|
|
77
|
+
// Contiguous run on same row, same SGR — just append the char.
|
|
78
|
+
output += newCell.char;
|
|
79
|
+
} else {
|
|
80
|
+
output += `\x1b[${y + 1};${x + 1}H`;
|
|
81
|
+
output += sgrOutput;
|
|
82
|
+
output += newCell.char;
|
|
83
|
+
}
|
|
84
|
+
this.lastEmitX = x;
|
|
85
|
+
this.lastEmitY = y;
|
|
49
86
|
}
|
|
50
87
|
}
|
|
51
88
|
}
|
|
@@ -56,7 +93,7 @@ export class DiffEngine {
|
|
|
56
93
|
this.lastLink = '';
|
|
57
94
|
}
|
|
58
95
|
|
|
59
|
-
return output;
|
|
96
|
+
return wrapSynced(output, this.caps);
|
|
60
97
|
}
|
|
61
98
|
|
|
62
99
|
private isCellDifferent(a: Cell | undefined, b: Cell): boolean {
|
|
@@ -66,6 +103,13 @@ export class DiffEngine {
|
|
|
66
103
|
(a.link ?? '') !== (b.link ?? '');
|
|
67
104
|
}
|
|
68
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Convert a raw color string (hex or r;g;b) to the "r;g;b" form expected
|
|
108
|
+
* by applyStyles. For non-RGB palette indices the value is returned as-is.
|
|
109
|
+
* This mirrors the original sanitizeColor contract but is now capability-aware
|
|
110
|
+
* only in the sense of normalizing hex → "r;g;b" — downsampling happens in
|
|
111
|
+
* applyStyles via downsampleColor.
|
|
112
|
+
*/
|
|
69
113
|
private sanitizeColor(color: string): string {
|
|
70
114
|
if (color.startsWith('#')) {
|
|
71
115
|
const r = parseInt(color.slice(1, 3), 16);
|
|
@@ -77,7 +121,7 @@ export class DiffEngine {
|
|
|
77
121
|
}
|
|
78
122
|
|
|
79
123
|
private applyStyles(cell: Cell): string {
|
|
80
|
-
|
|
124
|
+
// Normalize hex → r;g;b (for change-detection against lastFg/lastBg)
|
|
81
125
|
const fg = this.sanitizeColor(cell.fg);
|
|
82
126
|
const bg = this.sanitizeColor(cell.bg);
|
|
83
127
|
const link = cell.link ?? '';
|
|
@@ -87,21 +131,45 @@ export class DiffEngine {
|
|
|
87
131
|
cell.underline !== this.lastUnderline || cell.italic !== this.lastItalic ||
|
|
88
132
|
cell.strikethrough !== this.lastStrikethrough;
|
|
89
133
|
|
|
134
|
+
let style = '';
|
|
135
|
+
|
|
90
136
|
if (changed) {
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
if (cell.italic) style += '\x1b[3m';
|
|
95
|
-
if (cell.underline) style += '\x1b[4m';
|
|
96
|
-
if (cell.strikethrough) style += '\x1b[9m';
|
|
97
|
-
|
|
98
|
-
if (fg) {
|
|
99
|
-
const isRgb = fg.includes(';');
|
|
100
|
-
style += isRgb ? `\x1b[38;2;${fg}m` : `\x1b[38;5;${fg}m`;
|
|
137
|
+
// Reset all attributes first
|
|
138
|
+
if (this.caps.capability !== 'none') {
|
|
139
|
+
style += '\x1b[0m';
|
|
101
140
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
|
|
142
|
+
// Text attributes are always emitted when capability > none
|
|
143
|
+
if (this.caps.capability !== 'none') {
|
|
144
|
+
if (cell.bold) style += '\x1b[1m';
|
|
145
|
+
if (cell.dim) style += '\x1b[2m';
|
|
146
|
+
if (cell.italic) style += '\x1b[3m';
|
|
147
|
+
if (cell.underline) style += '\x1b[4m';
|
|
148
|
+
if (cell.strikethrough) style += '\x1b[9m';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Foreground color — capability-downsampled
|
|
152
|
+
const fgOut = downsampleColor(fg, this.caps, 'fg');
|
|
153
|
+
if (fgOut !== null) {
|
|
154
|
+
if (this.caps.capability === 'basic16') {
|
|
155
|
+
// fgOut is already the numeric SGR code (e.g. "31", "92")
|
|
156
|
+
style += `\x1b[${fgOut}m`;
|
|
157
|
+
} else {
|
|
158
|
+
const isRgb = fgOut.includes(';');
|
|
159
|
+
style += isRgb ? `\x1b[38;2;${fgOut}m` : `\x1b[38;5;${fgOut}m`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Background color — capability-downsampled
|
|
164
|
+
const bgOut = downsampleColor(bg, this.caps, 'bg');
|
|
165
|
+
if (bgOut !== null) {
|
|
166
|
+
if (this.caps.capability === 'basic16') {
|
|
167
|
+
// bgOut is already the numeric SGR code (e.g. "41", "102")
|
|
168
|
+
style += `\x1b[${bgOut}m`;
|
|
169
|
+
} else {
|
|
170
|
+
const isRgb = bgOut.includes(';');
|
|
171
|
+
style += isRgb ? `\x1b[48;2;${bgOut}m` : `\x1b[48;5;${bgOut}m`;
|
|
172
|
+
}
|
|
105
173
|
}
|
|
106
174
|
|
|
107
175
|
this.lastFg = fg;
|
|
@@ -114,15 +182,20 @@ export class DiffEngine {
|
|
|
114
182
|
}
|
|
115
183
|
|
|
116
184
|
// OSC 8 hyperlink: emit open/close/change sequences only when link changes
|
|
117
|
-
|
|
185
|
+
// Hyperlinks are suppressed in no-color mode (dumb/no-color terminals
|
|
186
|
+
// cannot render them and the OSC sequence is just noise).
|
|
187
|
+
if (link !== this.lastLink && this.caps.capability !== 'none') {
|
|
118
188
|
if (link) {
|
|
119
189
|
// Open new hyperlink (close previous if any was open)
|
|
120
190
|
style += `\x1b]8;;${link}\x1b\\`;
|
|
121
191
|
} else {
|
|
122
192
|
// Close hyperlink
|
|
123
|
-
style +=
|
|
193
|
+
style += '\x1b]8;;\x1b\\';
|
|
124
194
|
}
|
|
125
195
|
this.lastLink = link;
|
|
196
|
+
} else if (link !== this.lastLink) {
|
|
197
|
+
// capability === 'none': track state but emit nothing
|
|
198
|
+
this.lastLink = link;
|
|
126
199
|
}
|
|
127
200
|
|
|
128
201
|
return style;
|
package/src/renderer/markdown.ts
CHANGED
|
@@ -3,9 +3,15 @@ import { UIFactory } from './ui-factory.ts';
|
|
|
3
3
|
import { renderCodeBlock } from './code-block.ts';
|
|
4
4
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
5
|
import { LAYOUT } from './layout.ts';
|
|
6
|
+
import { DARK_THEME } from './theme.ts';
|
|
7
|
+
|
|
8
|
+
/** Module-level resolved token set (dark default; replace with resolveTheme(mode) when mode detection lands). */
|
|
9
|
+
const T = DARK_THEME;
|
|
6
10
|
|
|
7
11
|
export interface MarkdownRenderOptions {
|
|
8
12
|
codeBlockLineNumbers?: boolean;
|
|
13
|
+
/** When true, suppresses tree-sitter parse scheduling for streaming code blocks. */
|
|
14
|
+
isStreaming?: boolean;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
/** Module-level set of inline markdown special characters (hoisted out of hot loop). */
|
|
@@ -63,24 +69,31 @@ export function renderMarkdownTracked(
|
|
|
63
69
|
let inCodeBlock = false;
|
|
64
70
|
let codeBlockLang = '';
|
|
65
71
|
let codeBlockLines: string[] = [];
|
|
72
|
+
let fenceChar = '`';
|
|
73
|
+
let fenceIndent = 0;
|
|
66
74
|
const indent = LAYOUT.LEFT_MARGIN;
|
|
67
75
|
const contentWidth = LAYOUT.contentWidth(width);
|
|
68
76
|
|
|
69
77
|
for (let i = 0; i < rawLines.length; i++) {
|
|
70
78
|
const raw = rawLines[i];
|
|
71
79
|
|
|
72
|
-
const fenceMatch = raw.match(
|
|
80
|
+
const fenceMatch = raw.match(/^(\s*)(```|~~~)\s*([\w-]*)/);
|
|
73
81
|
if (fenceMatch && !inCodeBlock) {
|
|
74
82
|
inCodeBlock = true;
|
|
75
|
-
|
|
83
|
+
fenceIndent = fenceMatch[1].length;
|
|
84
|
+
fenceChar = fenceMatch[2][0]; // '`' or '~'
|
|
85
|
+
codeBlockLang = fenceMatch[3] || '';
|
|
76
86
|
codeBlockLines = [];
|
|
77
87
|
continue;
|
|
78
88
|
}
|
|
79
89
|
if (inCodeBlock) {
|
|
80
|
-
|
|
90
|
+
// Close fence: same char, same or less indentation, at least 3 of that char
|
|
91
|
+
const closeFenceRe = new RegExp(`^\\s{0,${fenceIndent}}${fenceChar === '`' ? '```' : '~~~'}`);
|
|
92
|
+
if (closeFenceRe.test(raw)) {
|
|
81
93
|
const blockStart = lines.length;
|
|
82
94
|
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
83
95
|
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
96
|
+
isStreaming: options.isStreaming ?? false,
|
|
84
97
|
});
|
|
85
98
|
codeBlocks.push({
|
|
86
99
|
startOffset: blockStart,
|
|
@@ -91,6 +104,8 @@ export function renderMarkdownTracked(
|
|
|
91
104
|
inCodeBlock = false;
|
|
92
105
|
codeBlockLang = '';
|
|
93
106
|
codeBlockLines = [];
|
|
107
|
+
fenceChar = '`';
|
|
108
|
+
fenceIndent = 0;
|
|
94
109
|
} else {
|
|
95
110
|
codeBlockLines.push(raw);
|
|
96
111
|
}
|
|
@@ -106,17 +121,17 @@ export function renderMarkdownTracked(
|
|
|
106
121
|
const h2 = raw.match(/^## (.+)/);
|
|
107
122
|
const h1 = raw.match(/^# (.+)/);
|
|
108
123
|
if (h1) {
|
|
109
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg:
|
|
124
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h1[1].toUpperCase(), width, { fg: T.heading1, bold: true }));
|
|
110
125
|
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '━'.repeat(Math.min(getDisplayWidth(h1[1]), contentWidth)), width, { fg: '244' }));
|
|
111
126
|
continue;
|
|
112
127
|
}
|
|
113
128
|
if (h2) {
|
|
114
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg:
|
|
129
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h2[1], width, { fg: T.heading2, bold: true }));
|
|
115
130
|
lines.push(UIFactory.stringToLine(' '.repeat(indent) + '─'.repeat(Math.min(getDisplayWidth(h2[1]), contentWidth)), width, { fg: '240' }));
|
|
116
131
|
continue;
|
|
117
132
|
}
|
|
118
133
|
if (h3) {
|
|
119
|
-
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg:
|
|
134
|
+
lines.push(UIFactory.stringToLine(' '.repeat(indent) + h3[1], width, { fg: T.heading3, bold: true }));
|
|
120
135
|
continue;
|
|
121
136
|
}
|
|
122
137
|
|
|
@@ -130,7 +145,7 @@ export function renderMarkdownTracked(
|
|
|
130
145
|
const rendered = renderInlineMarkdown(taskMatch[3]);
|
|
131
146
|
const prefix = ' '.repeat(bulletX) + checkbox;
|
|
132
147
|
const style = checked ? { fg: '244', strikethrough: true } : {};
|
|
133
|
-
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ?
|
|
148
|
+
lines.push(...compositeInlineLine(prefix, rendered, width, { fg: checked ? T.checkboxChecked : '252', ...style }, textStartX));
|
|
134
149
|
continue;
|
|
135
150
|
}
|
|
136
151
|
|
|
@@ -190,6 +205,7 @@ export function renderMarkdownTracked(
|
|
|
190
205
|
const blockStart = lines.length;
|
|
191
206
|
const rendered = renderCodeBlock(codeBlockLines, codeBlockLang, width, {
|
|
192
207
|
showLineNumbers: options.codeBlockLineNumbers ?? true,
|
|
208
|
+
isStreaming: options.isStreaming ?? false,
|
|
193
209
|
});
|
|
194
210
|
codeBlocks.push({
|
|
195
211
|
startOffset: blockStart,
|
|
@@ -300,14 +316,14 @@ function renderTable(rows: string[], width: number, indent: number): Line[] {
|
|
|
300
316
|
}
|
|
301
317
|
let style: Partial<Cell> = {};
|
|
302
318
|
if (token.type === 'code') {
|
|
303
|
-
style = { fg:
|
|
319
|
+
style = { fg: T.inlineCodeFg, bold: true };
|
|
304
320
|
} else if (token.type === 'link') {
|
|
305
|
-
style = { fg:
|
|
321
|
+
style = { fg: T.link, underline: true };
|
|
306
322
|
} else {
|
|
307
323
|
style = { ...token.style };
|
|
308
324
|
}
|
|
309
325
|
if (isHdr) {
|
|
310
|
-
style.fg = style.fg ||
|
|
326
|
+
style.fg = style.fg || T.heading1;
|
|
311
327
|
style.bold = true;
|
|
312
328
|
} else {
|
|
313
329
|
style.fg = style.fg || '252';
|
|
@@ -320,7 +336,7 @@ function renderTable(rows: string[], width: number, indent: number): Line[] {
|
|
|
320
336
|
|
|
321
337
|
// Pad remaining space
|
|
322
338
|
while (w < maxW) {
|
|
323
|
-
cells.push(createStyledCell(' ', isHdr ? { fg:
|
|
339
|
+
cells.push(createStyledCell(' ', isHdr ? { fg: T.heading1 } : { fg: '252' }));
|
|
324
340
|
w++;
|
|
325
341
|
}
|
|
326
342
|
return cells;
|
|
@@ -528,14 +544,14 @@ function compositeInlineLine(
|
|
|
528
544
|
if (token.type === 'text') {
|
|
529
545
|
for (const ch of token.text) chars.push({ char: ch, style: token.style });
|
|
530
546
|
} else if (token.type === 'code') {
|
|
531
|
-
for (const ch of token.text) chars.push({ char: ch, style: { fg:
|
|
547
|
+
for (const ch of token.text) chars.push({ char: ch, style: { fg: T.inlineCodeFg, bold: true } });
|
|
532
548
|
} else if (token.type === 'link') {
|
|
533
549
|
// Resolve URL: if url is empty or relative, treat as text; if it's a file path, use file:// protocol
|
|
534
550
|
let resolvedUrl = token.url;
|
|
535
551
|
if (resolvedUrl && !resolvedUrl.startsWith('http') && !resolvedUrl.startsWith('file://') && resolvedUrl.startsWith('/')) {
|
|
536
552
|
resolvedUrl = `file://${resolvedUrl}`;
|
|
537
553
|
}
|
|
538
|
-
for (const ch of token.text) chars.push({ char: ch, style: { fg:
|
|
554
|
+
for (const ch of token.text) chars.push({ char: ch, style: { fg: T.link, underline: true, link: resolvedUrl || undefined } });
|
|
539
555
|
}
|
|
540
556
|
}
|
|
541
557
|
|
|
@@ -111,7 +111,7 @@ export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
111
111
|
'ui.systemMessages': 'System Message Target',
|
|
112
112
|
'ui.operationalMessages': 'Operational Message Target',
|
|
113
113
|
'ui.wrfcMessages': 'WRFC Message Target',
|
|
114
|
-
'ui.voiceEnabled': '
|
|
114
|
+
'ui.voiceEnabled': 'Always Speak',
|
|
115
115
|
'behavior.autoCompactThreshold': 'Auto-Compact %',
|
|
116
116
|
'behavior.staleContextWarnings': 'Context Warnings',
|
|
117
117
|
'behavior.returnContextMode': 'Return Context',
|
|
@@ -248,8 +248,8 @@ function buildSubscriptionContext(modal: SettingsModal, entry: SubscriptionEntry
|
|
|
248
248
|
const routeReason = inferSubscriptionRouteReason(entry);
|
|
249
249
|
const logout = entry.state === 'active' || entry.state === 'pending'
|
|
250
250
|
? modal.subscriptionLogoutConfirmationTarget === entry.provider
|
|
251
|
-
? `
|
|
252
|
-
: 'Press Enter to
|
|
251
|
+
? `Sign out ${entry.provider}? Enter/y to confirm, n/Esc to cancel.`
|
|
252
|
+
: 'Press Enter to begin sign-out for this provider session.'
|
|
253
253
|
: `Use /subscription login ${entry.provider} start to begin OAuth sign-in for this provider.`;
|
|
254
254
|
return [
|
|
255
255
|
entry.provider,
|
|
@@ -268,6 +268,28 @@ function buildSubscriptionContext(modal: SettingsModal, entry: SubscriptionEntry
|
|
|
268
268
|
|
|
269
269
|
function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
270
270
|
const category = modal.currentCategory;
|
|
271
|
+
|
|
272
|
+
// Search mode: show context for the selected search result, or a help blurb
|
|
273
|
+
if (modal.searchFocused) {
|
|
274
|
+
const selected = modal.getSelected();
|
|
275
|
+
const lines: string[] = ['Search Results'];
|
|
276
|
+
if (selected) {
|
|
277
|
+
lines.push(...buildSettingContext(modal, selected));
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(
|
|
280
|
+
modal.searchQuery.trim().length === 0
|
|
281
|
+
? 'Type a query to search across all settings categories.'
|
|
282
|
+
: 'No settings matched the search query.',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const wrapped: string[] = [];
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
if (line === '') { wrapped.push(''); continue; }
|
|
288
|
+
wrapped.push(...paddedWrapped(line, width));
|
|
289
|
+
}
|
|
290
|
+
return wrapped;
|
|
291
|
+
}
|
|
292
|
+
|
|
271
293
|
const lines: string[] = [
|
|
272
294
|
`${CATEGORY_LABELS[category]} configuration`,
|
|
273
295
|
];
|
|
@@ -458,7 +480,51 @@ function renderSubscriptionRows(modal: SettingsModal, width: number, height: num
|
|
|
458
480
|
return rows.slice(0, height);
|
|
459
481
|
}
|
|
460
482
|
|
|
483
|
+
function renderSearchRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
484
|
+
const rows: string[] = [];
|
|
485
|
+
const query = modal.searchQuery;
|
|
486
|
+
// Search-prompt row shows the current query
|
|
487
|
+
const promptRow = `/ ${query}${GLYPHS.surface.cursor}`;
|
|
488
|
+
rows.push(promptRow);
|
|
489
|
+
|
|
490
|
+
const results = modal.searchResults;
|
|
491
|
+
if (query.trim().length === 0 || results.length === 0) {
|
|
492
|
+
rows.push(query.trim().length === 0 ? 'Type to search across all categories.' : 'No results.');
|
|
493
|
+
return rows.slice(0, height);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const selectedIndex = clamp(modal.selectedIndex, 0, results.length - 1);
|
|
497
|
+
const typeWidth = 9;
|
|
498
|
+
const sourceWidth = 12;
|
|
499
|
+
const categoryWidth = 14;
|
|
500
|
+
const available = Math.max(24, width - typeWidth - sourceWidth - categoryWidth - 16);
|
|
501
|
+
const keyWidth = clamp(Math.floor(available * 0.56), 18, 52);
|
|
502
|
+
const valueWidth = Math.max(10, available - keyWidth);
|
|
503
|
+
rows.push(` ${padDisplay('Setting', keyWidth)} ${padDisplay('Value', valueWidth)} ${padDisplay('Type', typeWidth)} ${padDisplay('Category', categoryWidth)} ${padDisplay('Source', sourceWidth)}`);
|
|
504
|
+
|
|
505
|
+
const visibleCount = Math.max(1, height - 3);
|
|
506
|
+
const window = stableWindow(results.length, selectedIndex, visibleCount);
|
|
507
|
+
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more result(s) above`);
|
|
508
|
+
|
|
509
|
+
for (let index = window.start; index < window.end; index += 1) {
|
|
510
|
+
const entry = results[index]!;
|
|
511
|
+
const selected = index === selectedIndex;
|
|
512
|
+
const marker = selected ? GLYPHS.navigation.selected : entry.isDefault ? ' ' : '◇';
|
|
513
|
+
const value = currentSettingValue(modal, entry, selected);
|
|
514
|
+
const source = `${entry.effectiveSource ?? 'default'}${entry.locked ? ' locked' : ''}${entry.conflict ? ' conflict' : ''}`;
|
|
515
|
+
const label = getSettingLabel(entry);
|
|
516
|
+
// Derive category label from setting key prefix
|
|
517
|
+
const keyPrefix = entry.setting.key.split('.')[0] ?? '';
|
|
518
|
+
const categoryLabel = CATEGORY_LABELS[keyPrefix as SettingsCategory] ?? keyPrefix;
|
|
519
|
+
rows.push(`${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay(entry.setting.type, typeWidth)} ${padDisplay(categoryLabel, categoryWidth)} ${padDisplay(source, sourceWidth)}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (window.end < results.length) rows.push(`${GLYPHS.navigation.moreBelow} ${results.length - window.end} more result(s) below`);
|
|
523
|
+
return rows.slice(0, height);
|
|
524
|
+
}
|
|
525
|
+
|
|
461
526
|
function renderControlRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
527
|
+
if (modal.searchFocused) return renderSearchRows(modal, width, height);
|
|
462
528
|
if (modal.currentCategory === 'flags') return renderFlagRows(modal, width, height);
|
|
463
529
|
if (modal.currentCategory === 'mcp') return renderMcpRows(modal, width, height);
|
|
464
530
|
if (modal.currentCategory === 'subscriptions') return renderSubscriptionRows(modal, width, height);
|
|
@@ -474,12 +540,13 @@ function rowColorForSetting(modal: SettingsModal, rowText: string): string {
|
|
|
474
540
|
}
|
|
475
541
|
|
|
476
542
|
function footerText(modal: SettingsModal): string {
|
|
543
|
+
if (modal.searchFocused) return 'Search · type to filter · Up/Down navigate results · Enter select · Esc exit search';
|
|
477
544
|
if (modal.editingMode) return 'Enter Confirm edit · Esc Cancel edit · text keys edit the selected field';
|
|
478
|
-
if (modal.focusPane === 'categories') return 'Focus categories · Up/Down choose · Right/Enter settings · Tab pane · Esc close';
|
|
479
|
-
if (modal.currentCategory === 'subscriptions') return 'Focus settings · Up/Down provider · Left categories · Tab pane · Enter review/sign out · Esc close';
|
|
480
|
-
if (modal.currentCategory === 'mcp') return 'Focus settings · Up/Down server · Left categories · Tab pane · Enter edit trust · Esc close';
|
|
481
|
-
if (modal.currentCategory === 'flags') return 'Focus feature flags · Up/Down flag · Left categories · Tab pane · Enter/Space toggle · Esc close';
|
|
482
|
-
return 'Focus settings · Up/Down setting · Left categories · Tab pane · Enter/Space edit/toggle · R reset · Esc close';
|
|
545
|
+
if (modal.focusPane === 'categories') return 'Focus categories · Up/Down choose · Right/Enter settings · Tab pane · / search · Esc close';
|
|
546
|
+
if (modal.currentCategory === 'subscriptions') return 'Focus settings · Up/Down provider · Left categories · Tab pane · / search · Enter review/sign out · Esc close';
|
|
547
|
+
if (modal.currentCategory === 'mcp') return 'Focus settings · Up/Down server · Left categories · Tab pane · / search · Enter edit trust · Esc close';
|
|
548
|
+
if (modal.currentCategory === 'flags') return 'Focus feature flags · Up/Down flag · Left categories · Tab pane · / search · Enter/Space toggle · Esc close';
|
|
549
|
+
return 'Focus settings · Up/Down setting · Left categories · Tab pane · / search · Enter/Space edit/toggle · R reset · Esc close';
|
|
483
550
|
}
|
|
484
551
|
|
|
485
552
|
export function renderSettingsModal(
|
|
@@ -523,7 +590,9 @@ export function renderSettingsModal(
|
|
|
523
590
|
height: viewportHeight,
|
|
524
591
|
title: 'Configuration Workspace / Settings',
|
|
525
592
|
leftHeader: 'Categories',
|
|
526
|
-
mainHeader:
|
|
593
|
+
mainHeader: modal.searchFocused
|
|
594
|
+
? `Search: ${modal.searchQuery || '…'} (${modal.searchResults.length} result${modal.searchResults.length === 1 ? '' : 's'})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`
|
|
595
|
+
: `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`,
|
|
527
596
|
leftRows: categoryRows.map((row): WorkspaceRow => ({
|
|
528
597
|
text: row.text,
|
|
529
598
|
selected: row.selected,
|
|
@@ -466,8 +466,13 @@ export class SyntaxHighlighter {
|
|
|
466
466
|
* If the highlight cache has a result for this code+language, returns it.
|
|
467
467
|
* Otherwise, schedules an async parse in background and returns null.
|
|
468
468
|
* Callers should fall back to regex-based tokenization when null is returned.
|
|
469
|
+
*
|
|
470
|
+
* @param isStreaming - When true, suppresses background parse scheduling.
|
|
471
|
+
* The regex tokenizer serves during streaming; tree-sitter is scheduled only
|
|
472
|
+
* when the block is finalized (isStreaming=false or omitted) to avoid ~50
|
|
473
|
+
* wasted async parses per streamed block that thrash the FIFO cache.
|
|
469
474
|
*/
|
|
470
|
-
highlight(code: string, fenceTag: string): HighlightedLine[] | null {
|
|
475
|
+
highlight(code: string, fenceTag: string, isStreaming = false): HighlightedLine[] | null {
|
|
471
476
|
const langId = this.fenceToLangId(fenceTag);
|
|
472
477
|
if (!langId) return null; // unsupported language
|
|
473
478
|
|
|
@@ -475,8 +480,10 @@ export class SyntaxHighlighter {
|
|
|
475
480
|
const cached = this.cache.get(key);
|
|
476
481
|
if (cached) return cached;
|
|
477
482
|
|
|
478
|
-
//
|
|
479
|
-
|
|
483
|
+
// Do not schedule background parse while the block is still being streamed.
|
|
484
|
+
// The regex tokenizer serves during streaming (as designed). Schedule parse
|
|
485
|
+
// only when isStreaming=false, i.e., the block has been finalized.
|
|
486
|
+
if (!isStreaming && !this.pending.has(key)) {
|
|
480
487
|
this.scheduleParse(code, langId, key);
|
|
481
488
|
}
|
|
482
489
|
|