klyro 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/app.js
CHANGED
|
@@ -10,7 +10,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
10
10
|
import { TuiApprovalBridge, ApprovalModal } from './approval.js';
|
|
11
11
|
import { parse as parseSlash, suggestCommands } from '../cli/slash/parser.js';
|
|
12
12
|
import { tokens, g } from './tokens.js';
|
|
13
|
-
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, } from './scroll-model.js';
|
|
13
|
+
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, badgeLabel, } from './scroll-model.js';
|
|
14
14
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
15
15
|
import { renderMarkdownLines } from './markdown.js';
|
|
16
16
|
import { readVersion } from '../version.js';
|
|
@@ -322,7 +322,7 @@ export function App(props) {
|
|
|
322
322
|
const blockKeys = useMemo(() => blocks.map((b) => b.key), [blocks]);
|
|
323
323
|
const blockHeights = useMemo(() => blocks.map((b) => cache.heightFor(b.key, b.desc, width)), [blocks, width, cache]);
|
|
324
324
|
const scroll = useChatScroll({ keys: blockKeys, heights: blockHeights, viewportH, width });
|
|
325
|
-
const { topRow, maxTop, pinned, pendingNew, commands } = scroll;
|
|
325
|
+
const { topRow, maxTop, pinned, pendingNew, commands, atBottom } = scroll;
|
|
326
326
|
const trackH = viewportH;
|
|
327
327
|
const thumbPos = maxTop === 0 ? 0 : Math.round((topRow / maxTop) * (trackH - 1));
|
|
328
328
|
// Slice *display lines* [topRow, topRow+viewportH): intersecting blocks only
|
|
@@ -538,6 +538,10 @@ export function App(props) {
|
|
|
538
538
|
commands.jumpBottom();
|
|
539
539
|
return;
|
|
540
540
|
}
|
|
541
|
+
if (inputStr === ' ' && pinned && pendingNew > 0) {
|
|
542
|
+
commands.jumpBottom();
|
|
543
|
+
return;
|
|
544
|
+
} // Space → jump to unread tail
|
|
541
545
|
if (key.ctrl && inputStr === 'g') {
|
|
542
546
|
commands.jumpBottom();
|
|
543
547
|
return;
|
|
@@ -788,5 +792,5 @@ export function App(props) {
|
|
|
788
792
|
if (it.kind === 'diff')
|
|
789
793
|
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
|
|
790
794
|
return null;
|
|
791
|
-
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", p.title] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && pendingNew
|
|
795
|
+
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", p.title] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen && !atBottom ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && badgeLabel(atBottom, pendingNew, status.status !== 'running') ? (_jsx(Box, { justifyContent: "center", flexShrink: 0, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ', badgeLabel(atBottom, pendingNew, status.status !== 'running'), ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [slashSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " /", s.name, " \u2014 ", s.hint] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, _jsx(ApprovalModal, { bridge: bridge }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.colors.dim, children: "Message Klyro..." }), "|"] })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { color: tokens.colors.dim, children: [status.status === 'running' ? (_jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " working \u00B7 "] })) : null, baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [maxTop > 0 && isFullscreen ? `⇅ ${topRow}/${maxTop} · ` : '', cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
792
796
|
}
|
package/dist/tui/app.test.js
CHANGED
|
@@ -541,13 +541,13 @@ describe('App', () => {
|
|
|
541
541
|
await waitForMatch(lastFrame, /⇅ 0\/\d+/);
|
|
542
542
|
hooks.append({ id: 'late-1', kind: 'text', text: 'LATE-1-tag', role: 'assistant' });
|
|
543
543
|
// Badge counts lines grown while pinned.
|
|
544
|
-
await waitForMatch(lastFrame, /↓ \d+
|
|
544
|
+
await waitForMatch(lastFrame, /↓ \d+ unread/);
|
|
545
545
|
hooks.clearTranscript();
|
|
546
546
|
// Wait for transcript content to actually clear (no stale MSG-* items) —
|
|
547
547
|
// this is the "no stale badge" invariant: after a clear, the view must
|
|
548
548
|
// drop back to follow-tail with the pin/count reset.
|
|
549
549
|
await waitForAbsent(lastFrame, /MSG-\d{2}-tag/);
|
|
550
|
-
await waitForAbsent(lastFrame, /↓ \d+
|
|
550
|
+
await waitForAbsent(lastFrame, /↓ \d+ unread/);
|
|
551
551
|
// Fresh session: seed 25 more, let them settle to a bottom-anchored view.
|
|
552
552
|
for (let i = 0; i < 25; i++) {
|
|
553
553
|
hooks.append({ id: `n-${i}`, kind: 'text', text: `NEW-${i.toString().padStart(2, '0')}-tag`, role: 'user' });
|
|
@@ -558,7 +558,7 @@ describe('App', () => {
|
|
|
558
558
|
await waitForMatch(lastFrame, /NEWLATE-tag/);
|
|
559
559
|
await new Promise((r) => setTimeout(r, 100));
|
|
560
560
|
const afterFollow = lastFrame() ?? '';
|
|
561
|
-
expect(afterFollow).not.toMatch(/↓ \d+
|
|
561
|
+
expect(afterFollow).not.toMatch(/↓ \d+ unread/);
|
|
562
562
|
expect(afterFollow).not.toMatch(/MSG-\d{2}-tag/);
|
|
563
563
|
});
|
|
564
564
|
it('status bar shows scroll position when content overflows', async () => {
|
|
@@ -66,7 +66,7 @@ describe('scroll flow diagnostics', () => {
|
|
|
66
66
|
}
|
|
67
67
|
const frozen = lastFrame() ?? '';
|
|
68
68
|
expect(frozen).toContain('MSG-00-tag'); // viewport did not yank down
|
|
69
|
-
expect(frozen).toMatch(/↓ \d+
|
|
69
|
+
expect(frozen).toMatch(/↓ \d+ unread/); // badge visible
|
|
70
70
|
expect(rowsOf(frozen)).toBeLessThanOrEqual(32);
|
|
71
71
|
stdin.write('\x1b[F'); // End → follow
|
|
72
72
|
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
|
}
|