klyro 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/agent/runtime.js +11 -2
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/repl.js +116 -10
- package/dist/cli/run.js +14 -0
- package/dist/cli/slash/parser.d.ts +7 -1
- package/dist/cli/slash/parser.js +37 -10
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- package/dist/index.js +78 -8
- package/dist/mcp/config.d.ts +7 -0
- package/dist/mcp/config.js +45 -0
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +330 -52
- package/dist/tui/app.test.js +45 -6
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +4 -2
- package/dist/tui/scroll-model.d.ts +3 -1
- package/dist/tui/scroll-model.js +6 -5
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
package/dist/tui/app.test.js
CHANGED
|
@@ -261,8 +261,13 @@ describe('App', () => {
|
|
|
261
261
|
stdin.write('/c');
|
|
262
262
|
await new Promise((r) => setTimeout(r, 30));
|
|
263
263
|
const withSuggest = lastFrame() ?? '';
|
|
264
|
-
expect(withSuggest).toMatch(/\/clear/);
|
|
265
264
|
expect(withSuggest).toMatch(/tab to complete/i);
|
|
265
|
+
// Prefix dominance: every suggestion for '/c' still starts with c.
|
|
266
|
+
const sugLines = withSuggest.split('\n').filter((l) => l.includes('/') && /clear|compact|cost|config|context|commands|copy|cancel/.test(l));
|
|
267
|
+
expect(sugLines.length).toBeGreaterThan(0);
|
|
268
|
+
// Unambiguous fuzzy prefix completes deterministically.
|
|
269
|
+
stdin.write('le');
|
|
270
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
266
271
|
stdin.write('\t');
|
|
267
272
|
await new Promise((r) => setTimeout(r, 30));
|
|
268
273
|
expect(lastFrame() ?? '').toContain('/clear ');
|
|
@@ -315,9 +320,15 @@ describe('App', () => {
|
|
|
315
320
|
const kinds = onSlash.mock.calls.map((c) => c[0].kind);
|
|
316
321
|
expect(kinds).toEqual(['cancel', 'quit']);
|
|
317
322
|
});
|
|
318
|
-
it('running
|
|
323
|
+
it('running Esc×2 with empty queue cancels streaming (double-Esc convention)', async () => {
|
|
319
324
|
const onSlash = vi.fn(async () => { });
|
|
320
|
-
const { stdin } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: onSlash, initialStatus: { status: 'running' } }));
|
|
325
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: onSlash, initialStatus: { status: 'running' } }));
|
|
326
|
+
// First Esc only arms (hint shows, no cancel yet).
|
|
327
|
+
stdin.write('\x1b');
|
|
328
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
329
|
+
expect(onSlash).not.toHaveBeenCalled();
|
|
330
|
+
await waitForMatch(lastFrame, /press esc again to cancel/);
|
|
331
|
+
// Second Esc within the window cancels.
|
|
321
332
|
stdin.write('\x1b');
|
|
322
333
|
await new Promise((r) => setTimeout(r, 50));
|
|
323
334
|
expect(onSlash).toHaveBeenCalled();
|
|
@@ -519,6 +530,34 @@ describe('App', () => {
|
|
|
519
530
|
expect(frame).not.toContain('{"path"');
|
|
520
531
|
expect(frame).not.toContain('queued:');
|
|
521
532
|
});
|
|
533
|
+
it('scrubs secret shapes at the render boundary (never paints raw)', async () => {
|
|
534
|
+
let hooks = null;
|
|
535
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
|
|
536
|
+
hooks = { append: h.append };
|
|
537
|
+
} }));
|
|
538
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
539
|
+
hooks.append({ id: 'sec-1', kind: 'text', text: 'deploy with password=hunter2-hunter2', role: 'assistant' });
|
|
540
|
+
const frame = await waitForMatch(lastFrame, /REDACTED/);
|
|
541
|
+
expect(frame).not.toMatch(/hunter2-hunter2/);
|
|
542
|
+
});
|
|
543
|
+
it('vim normal mode swallows typing, shows mode, i returns to insert', async () => {
|
|
544
|
+
let hooks = null;
|
|
545
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
|
|
546
|
+
hooks = { setVimMode: h.setVimMode };
|
|
547
|
+
} }));
|
|
548
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
549
|
+
stdin.write('ok');
|
|
550
|
+
await waitForMatch(lastFrame, /ok/);
|
|
551
|
+
hooks.setVimMode('normal');
|
|
552
|
+
await waitForMatch(lastFrame, /--NORMAL--/);
|
|
553
|
+
stdin.write('zzz');
|
|
554
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
555
|
+
expect(lastFrame() ?? '').not.toMatch(/zzz/);
|
|
556
|
+
stdin.write('i');
|
|
557
|
+
await waitForAbsent(lastFrame, /--NORMAL--/);
|
|
558
|
+
stdin.write('!');
|
|
559
|
+
await waitForMatch(lastFrame, /ok!/);
|
|
560
|
+
});
|
|
522
561
|
it('aborted run leaves no thinking behind (only response may remain)', async () => {
|
|
523
562
|
let hooks = null;
|
|
524
563
|
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialStatus: { status: 'running' }, onMounted: (h) => {
|
|
@@ -541,13 +580,13 @@ describe('App', () => {
|
|
|
541
580
|
await waitForMatch(lastFrame, /⇅ 0\/\d+/);
|
|
542
581
|
hooks.append({ id: 'late-1', kind: 'text', text: 'LATE-1-tag', role: 'assistant' });
|
|
543
582
|
// Badge counts lines grown while pinned.
|
|
544
|
-
await waitForMatch(lastFrame, /↓ \d+
|
|
583
|
+
await waitForMatch(lastFrame, /↓ \d+ unread/);
|
|
545
584
|
hooks.clearTranscript();
|
|
546
585
|
// Wait for transcript content to actually clear (no stale MSG-* items) —
|
|
547
586
|
// this is the "no stale badge" invariant: after a clear, the view must
|
|
548
587
|
// drop back to follow-tail with the pin/count reset.
|
|
549
588
|
await waitForAbsent(lastFrame, /MSG-\d{2}-tag/);
|
|
550
|
-
await waitForAbsent(lastFrame, /↓ \d+
|
|
589
|
+
await waitForAbsent(lastFrame, /↓ \d+ unread/);
|
|
551
590
|
// Fresh session: seed 25 more, let them settle to a bottom-anchored view.
|
|
552
591
|
for (let i = 0; i < 25; i++) {
|
|
553
592
|
hooks.append({ id: `n-${i}`, kind: 'text', text: `NEW-${i.toString().padStart(2, '0')}-tag`, role: 'user' });
|
|
@@ -558,7 +597,7 @@ describe('App', () => {
|
|
|
558
597
|
await waitForMatch(lastFrame, /NEWLATE-tag/);
|
|
559
598
|
await new Promise((r) => setTimeout(r, 100));
|
|
560
599
|
const afterFollow = lastFrame() ?? '';
|
|
561
|
-
expect(afterFollow).not.toMatch(/↓ \d+
|
|
600
|
+
expect(afterFollow).not.toMatch(/↓ \d+ unread/);
|
|
562
601
|
expect(afterFollow).not.toMatch(/MSG-\d{2}-tag/);
|
|
563
602
|
});
|
|
564
603
|
it('status bar shows scroll position when content overflows', async () => {
|
package/dist/tui/markdown.js
CHANGED
|
@@ -103,6 +103,14 @@ export function highlightCodeLine(line, lang) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
if (STRING_DELIMS.includes(ch)) {
|
|
106
|
+
// Contraction guard: a single-quote mid-word (it's, don't, users')
|
|
107
|
+
// is prose punctuation, not a string delimiter — only open on `'`
|
|
108
|
+
// after start-of-line or a non-word char. Other delims open anywhere.
|
|
109
|
+
if (ch === "'" && i > 0 && /[A-Za-z0-9_)}\]]/.test(line[i - 1])) {
|
|
110
|
+
buf += ch;
|
|
111
|
+
i++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
106
114
|
flush();
|
|
107
115
|
inString = true;
|
|
108
116
|
strDelim = ch;
|
|
@@ -115,6 +123,7 @@ export function highlightCodeLine(line, lang) {
|
|
|
115
123
|
while (j < line.length && /[A-Za-z0-9_$]/.test(line[j]))
|
|
116
124
|
j++;
|
|
117
125
|
const tok = line.slice(i, j);
|
|
126
|
+
flush(); // pending separators (spaces, punctuation) precede the token
|
|
118
127
|
flushKw(tok);
|
|
119
128
|
i = j;
|
|
120
129
|
continue;
|
package/dist/tui/mouse.d.ts
CHANGED
|
@@ -34,6 +34,28 @@ export declare class MouseFilter {
|
|
|
34
34
|
}
|
|
35
35
|
export declare const MOUSE_ENABLE = "\u001B[?1000h\u001B[?1006h";
|
|
36
36
|
export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
|
|
37
|
+
export declare const PASTE_START = "\u001B[200~";
|
|
38
|
+
export declare const PASTE_END = "\u001B[201~";
|
|
39
|
+
export declare const PASTE_ENABLE = "\u001B[?2004h";
|
|
40
|
+
export declare const PASTE_DISABLE = "\u001B[?2004l";
|
|
41
|
+
/**
|
|
42
|
+
* Bracketed-paste splitter. Strips the `ESC[200~` / `ESC[201~` markers and
|
|
43
|
+
* delivers pasted content as one atomic string so the TUI can bulk-insert
|
|
44
|
+
* it (no per-char autocomplete/history side effects). Content split across
|
|
45
|
+
* stdin chunks is reassembled; an unterminated paste is held (up to 1 MiB,
|
|
46
|
+
* then force-flushed) so a partial escape never leaks into the input.
|
|
47
|
+
*/
|
|
48
|
+
export declare class PasteFilter {
|
|
49
|
+
private pending;
|
|
50
|
+
private inPaste;
|
|
51
|
+
private chunks;
|
|
52
|
+
private size;
|
|
53
|
+
push(chunk: Buffer): {
|
|
54
|
+
kept: Buffer;
|
|
55
|
+
pastes: string[];
|
|
56
|
+
};
|
|
57
|
+
reset(): void;
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* stdin.read() wrapper implementing the tap (see repl.ts installMouseTap).
|
|
39
61
|
* Ink 7 consumes stdin via paused-mode read() calls, so filtering happens
|
|
@@ -43,4 +65,7 @@ export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
|
|
|
43
65
|
* mouse sequences are swallowed (wheels dispatched); everything else is
|
|
44
66
|
* returned byte-identical in its original string/Buffer shape.
|
|
45
67
|
*/
|
|
46
|
-
export declare function createReadWrapper(origRead: (size?: number) => unknown, filter: MouseFilter, onWheel: (delta: number) => void
|
|
68
|
+
export declare function createReadWrapper(origRead: (size?: number) => unknown, filter: MouseFilter, onWheel: (delta: number) => void, opts?: {
|
|
69
|
+
onPaste?: (text: string) => void;
|
|
70
|
+
pasteFilter?: PasteFilter;
|
|
71
|
+
}): (size?: number) => unknown;
|
package/dist/tui/mouse.js
CHANGED
|
@@ -92,6 +92,92 @@ export class MouseFilter {
|
|
|
92
92
|
}
|
|
93
93
|
export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1006h'; // button events + SGR coords
|
|
94
94
|
export const MOUSE_DISABLE = '\x1b[?1000l\x1b[?1006l';
|
|
95
|
+
export const PASTE_START = '\x1b[200~';
|
|
96
|
+
export const PASTE_END = '\x1b[201~';
|
|
97
|
+
export const PASTE_ENABLE = '\x1b[?2004h'; // bracketed paste: terminal wraps pastes
|
|
98
|
+
export const PASTE_DISABLE = '\x1b[?2004l';
|
|
99
|
+
const PASTE_START_BUF = Buffer.from(PASTE_START, 'latin1');
|
|
100
|
+
const PASTE_END_BUF = Buffer.from(PASTE_END, 'latin1');
|
|
101
|
+
const MAX_PASTE_BYTES = 1024 * 1024;
|
|
102
|
+
/**
|
|
103
|
+
* Bracketed-paste splitter. Strips the `ESC[200~` / `ESC[201~` markers and
|
|
104
|
+
* delivers pasted content as one atomic string so the TUI can bulk-insert
|
|
105
|
+
* it (no per-char autocomplete/history side effects). Content split across
|
|
106
|
+
* stdin chunks is reassembled; an unterminated paste is held (up to 1 MiB,
|
|
107
|
+
* then force-flushed) so a partial escape never leaks into the input.
|
|
108
|
+
*/
|
|
109
|
+
export class PasteFilter {
|
|
110
|
+
pending = Buffer.alloc(0);
|
|
111
|
+
inPaste = false;
|
|
112
|
+
chunks = [];
|
|
113
|
+
size = 0;
|
|
114
|
+
push(chunk) {
|
|
115
|
+
const data = Buffer.concat([this.pending, chunk]);
|
|
116
|
+
this.pending = Buffer.alloc(0);
|
|
117
|
+
const kept = [];
|
|
118
|
+
const pastes = [];
|
|
119
|
+
let pos = 0;
|
|
120
|
+
const tailPartial = (from) => {
|
|
121
|
+
// Length of the trailing run that could be a split marker prefix.
|
|
122
|
+
const tail = data.subarray(from);
|
|
123
|
+
for (let len = Math.min(tail.length, 5); len >= 1; len--) {
|
|
124
|
+
const piece = tail.subarray(tail.length - len);
|
|
125
|
+
if (PASTE_START_BUF.subarray(0, len).equals(piece) || PASTE_END_BUF.subarray(0, len).equals(piece))
|
|
126
|
+
return len;
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
};
|
|
130
|
+
while (pos < data.length) {
|
|
131
|
+
if (!this.inPaste) {
|
|
132
|
+
const idx = data.indexOf(PASTE_START_BUF, pos);
|
|
133
|
+
if (idx === -1) {
|
|
134
|
+
const hold = tailPartial(pos);
|
|
135
|
+
kept.push(data.subarray(pos, hold > 0 ? data.length - hold : data.length));
|
|
136
|
+
if (hold > 0)
|
|
137
|
+
this.pending = data.subarray(data.length - hold);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
kept.push(data.subarray(pos, idx));
|
|
141
|
+
pos = idx + PASTE_START_BUF.length;
|
|
142
|
+
this.inPaste = true;
|
|
143
|
+
this.chunks = [];
|
|
144
|
+
this.size = 0;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const idx = data.indexOf(PASTE_END_BUF, pos);
|
|
148
|
+
if (idx === -1) {
|
|
149
|
+
const hold = tailPartial(pos);
|
|
150
|
+
const end = hold > 0 ? data.length - hold : data.length;
|
|
151
|
+
if (end > pos) {
|
|
152
|
+
this.chunks.push(data.subarray(pos, end));
|
|
153
|
+
this.size += end - pos;
|
|
154
|
+
}
|
|
155
|
+
if (hold > 0)
|
|
156
|
+
this.pending = data.subarray(data.length - hold);
|
|
157
|
+
if (this.size >= MAX_PASTE_BYTES) {
|
|
158
|
+
pastes.push(Buffer.concat(this.chunks).toString('utf8'));
|
|
159
|
+
this.chunks = [];
|
|
160
|
+
this.size = 0;
|
|
161
|
+
this.inPaste = false; // force-flush: don't hold unbounded input
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
this.chunks.push(data.subarray(pos, idx));
|
|
166
|
+
pastes.push(Buffer.concat(this.chunks).toString('utf8'));
|
|
167
|
+
this.chunks = [];
|
|
168
|
+
this.size = 0;
|
|
169
|
+
this.inPaste = false;
|
|
170
|
+
pos = idx + PASTE_END_BUF.length;
|
|
171
|
+
}
|
|
172
|
+
return { kept: Buffer.concat(kept), pastes };
|
|
173
|
+
}
|
|
174
|
+
reset() {
|
|
175
|
+
this.pending = Buffer.alloc(0);
|
|
176
|
+
this.inPaste = false;
|
|
177
|
+
this.chunks = [];
|
|
178
|
+
this.size = 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
95
181
|
/**
|
|
96
182
|
* stdin.read() wrapper implementing the tap (see repl.ts installMouseTap).
|
|
97
183
|
* Ink 7 consumes stdin via paused-mode read() calls, so filtering happens
|
|
@@ -101,23 +187,35 @@ export const MOUSE_DISABLE = '\x1b[?1000l\x1b[?1006l';
|
|
|
101
187
|
* mouse sequences are swallowed (wheels dispatched); everything else is
|
|
102
188
|
* returned byte-identical in its original string/Buffer shape.
|
|
103
189
|
*/
|
|
104
|
-
export function createReadWrapper(origRead, filter, onWheel) {
|
|
190
|
+
export function createReadWrapper(origRead, filter, onWheel, opts) {
|
|
105
191
|
return (size) => {
|
|
106
192
|
if (size !== undefined)
|
|
107
193
|
return origRead(size);
|
|
108
194
|
const chunk = origRead();
|
|
109
195
|
if (chunk == null)
|
|
110
196
|
return chunk;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
197
|
+
let buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
198
|
+
if (opts?.pasteFilter) {
|
|
199
|
+
const split = opts.pasteFilter.push(buf);
|
|
200
|
+
for (const p of split.pastes) {
|
|
201
|
+
try {
|
|
202
|
+
opts.onPaste?.(p);
|
|
203
|
+
}
|
|
204
|
+
catch { /* ignore */ }
|
|
205
|
+
}
|
|
206
|
+
buf = split.kept;
|
|
207
|
+
if (buf.length === 0 && split.pastes.length > 0)
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const msplit = filter.push(buf);
|
|
211
|
+
for (const w of msplit.wheels) {
|
|
114
212
|
try {
|
|
115
213
|
onWheel(w);
|
|
116
214
|
}
|
|
117
215
|
catch { /* ignore */ }
|
|
118
216
|
}
|
|
119
|
-
if (
|
|
217
|
+
if (msplit.kept.length === 0)
|
|
120
218
|
return null;
|
|
121
|
-
return typeof chunk === 'string' ?
|
|
219
|
+
return typeof chunk === 'string' ? msplit.kept.toString('utf8') : msplit.kept;
|
|
122
220
|
};
|
|
123
221
|
}
|
|
@@ -45,7 +45,9 @@ describe('scroll flow diagnostics', () => {
|
|
|
45
45
|
const chunk = 'STREAMCHUNK lorem ipsum dolor sit amet. ';
|
|
46
46
|
for (let i = 0; i < 12; i++) {
|
|
47
47
|
g.__klyroAppendDelta(`${chunk}#${i} `);
|
|
48
|
-
|
|
48
|
+
// Tick longer than the 64ms streaming render throttle so the flush
|
|
49
|
+
// has landed before asserting visibility (follow-tail, not latency).
|
|
50
|
+
await tick(90);
|
|
49
51
|
const frame = lastFrame() ?? '';
|
|
50
52
|
expect(rowsOf(frame)).toBeLessThanOrEqual(32);
|
|
51
53
|
// latest streamed chunk must be visible (follow-tail)
|
|
@@ -66,7 +68,7 @@ describe('scroll flow diagnostics', () => {
|
|
|
66
68
|
}
|
|
67
69
|
const frozen = lastFrame() ?? '';
|
|
68
70
|
expect(frozen).toContain('MSG-00-tag'); // viewport did not yank down
|
|
69
|
-
expect(frozen).toMatch(/↓ \d+
|
|
71
|
+
expect(frozen).toMatch(/↓ \d+ unread/); // badge visible
|
|
70
72
|
expect(rowsOf(frozen)).toBeLessThanOrEqual(32);
|
|
71
73
|
stdin.write('\x1b[F'); // End → follow
|
|
72
74
|
await tick(50);
|
|
@@ -63,5 +63,7 @@ export interface Resolved {
|
|
|
63
63
|
}
|
|
64
64
|
/** Anchor → topRow, run once per frame after the line index rebuild (§5.3). */
|
|
65
65
|
export declare function resolveTopRow(s: ScrollState, ctx: ScrollCtx): Resolved;
|
|
66
|
-
/** Badge label per §7.2
|
|
66
|
+
/** Badge label per §7.2 (Claude Code style: "N unread" / "jump to end").
|
|
67
|
+
* Empty string = hidden. `idle` = agent done streaming, the only new lines
|
|
68
|
+
* are the ones already counted — the affordance flips to "jump to end". */
|
|
67
69
|
export declare function badgeLabel(atBottom: boolean, newSinceUnstick: number, idle: boolean): string;
|
package/dist/tui/scroll-model.js
CHANGED
|
@@ -87,13 +87,14 @@ export function resolveTopRow(s, ctx) {
|
|
|
87
87
|
const topRow = clampN(raw, 0, maxTop);
|
|
88
88
|
return { topRow, atBottom: topRow >= maxTop - FOLLOW_EPSILON };
|
|
89
89
|
}
|
|
90
|
-
/** Badge label per §7.2
|
|
90
|
+
/** Badge label per §7.2 (Claude Code style: "N unread" / "jump to end").
|
|
91
|
+
* Empty string = hidden. `idle` = agent done streaming, the only new lines
|
|
92
|
+
* are the ones already counted — the affordance flips to "jump to end". */
|
|
91
93
|
export function badgeLabel(atBottom, newSinceUnstick, idle) {
|
|
92
94
|
if (atBottom || newSinceUnstick <= 0)
|
|
93
95
|
return '';
|
|
94
|
-
|
|
95
|
-
return '↓ jump to end';
|
|
96
|
+
void idle; // Ink can't capture the idle click; count is shown either way (§7.2)
|
|
96
97
|
if (newSinceUnstick >= 1000)
|
|
97
|
-
return '↓ 999+
|
|
98
|
-
return `↓ ${newSinceUnstick}
|
|
98
|
+
return '↓ 999+ unread';
|
|
99
|
+
return `↓ ${newSinceUnstick} unread`;
|
|
99
100
|
}
|
package/dist/tui/tokens.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export declare const tokens: {
|
|
|
12
12
|
readonly guide: "#3A3A3A";
|
|
13
13
|
readonly accent: "#FF6B1A";
|
|
14
14
|
readonly accentSoft: "#FFF1E6";
|
|
15
|
-
readonly ok: "#
|
|
15
|
+
readonly ok: "#2FA36B";
|
|
16
16
|
readonly err: "#E06C6C";
|
|
17
|
-
readonly warn: "#
|
|
17
|
+
readonly warn: "#D9A021";
|
|
18
18
|
readonly info: "#6FA8DC";
|
|
19
19
|
readonly diffAddBg: "#12250F";
|
|
20
20
|
readonly diffDelBg: "#2A1212";
|
|
@@ -26,15 +26,15 @@ export declare const tokens: {
|
|
|
26
26
|
readonly soft: "whiteBright";
|
|
27
27
|
readonly dim: "gray";
|
|
28
28
|
readonly guide: "gray";
|
|
29
|
-
readonly ok: "
|
|
29
|
+
readonly ok: "green";
|
|
30
30
|
readonly err: "red";
|
|
31
|
-
readonly warn: "
|
|
31
|
+
readonly warn: "yellow";
|
|
32
32
|
readonly info: "blue";
|
|
33
33
|
readonly border: "gray";
|
|
34
34
|
readonly muted: "gray";
|
|
35
|
-
readonly success: "
|
|
35
|
+
readonly success: "green";
|
|
36
36
|
readonly error: "red";
|
|
37
|
-
readonly warning: "
|
|
37
|
+
readonly warning: "yellow";
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
40
|
export declare const glyphs: {
|
package/dist/tui/tokens.js
CHANGED
|
@@ -12,9 +12,12 @@ export const tokens = {
|
|
|
12
12
|
guide: '#3A3A3A',
|
|
13
13
|
accent: '#FF6B1A',
|
|
14
14
|
accentSoft: '#FFF1E6',
|
|
15
|
-
|
|
15
|
+
// Status hues are deliberately distinct from the orange brand: ok reads
|
|
16
|
+
// green, warn reads amber, err reads red. (Previously ok/warn reused the
|
|
17
|
+
// accent hex and were indistinguishable in the transcript.)
|
|
18
|
+
ok: '#2FA36B',
|
|
16
19
|
err: '#E06C6C',
|
|
17
|
-
warn: '#
|
|
20
|
+
warn: '#D9A021',
|
|
18
21
|
info: '#6FA8DC',
|
|
19
22
|
diffAddBg: '#12250F',
|
|
20
23
|
diffDelBg: '#2A1212',
|
|
@@ -26,15 +29,15 @@ export const tokens = {
|
|
|
26
29
|
soft: 'whiteBright',
|
|
27
30
|
dim: 'gray',
|
|
28
31
|
guide: 'gray',
|
|
29
|
-
ok: '
|
|
32
|
+
ok: 'green',
|
|
30
33
|
err: 'red',
|
|
31
|
-
warn: '
|
|
34
|
+
warn: 'yellow',
|
|
32
35
|
info: 'blue',
|
|
33
36
|
border: 'gray',
|
|
34
37
|
muted: 'gray',
|
|
35
|
-
success: '
|
|
38
|
+
success: 'green',
|
|
36
39
|
error: 'red',
|
|
37
|
-
warning: '
|
|
40
|
+
warning: 'yellow',
|
|
38
41
|
},
|
|
39
42
|
};
|
|
40
43
|
export const glyphs = {
|