klyro 0.1.50 → 0.1.51
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.d.ts +2 -0
- package/dist/tui/app.js +50 -16
- package/dist/tui/app.test.js +37 -0
- package/dist/tui/transcript-commands.d.ts +34 -0
- package/dist/tui/transcript-commands.js +32 -0
- package/package.json +1 -1
package/dist/tui/app.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { StatusSnapshot } from './status.js';
|
|
|
7
7
|
import type { TranscriptItem } from './transcript.js';
|
|
8
8
|
import { TuiApprovalBridge } from './approval.js';
|
|
9
9
|
import type { PlanStep } from '../agent/runtime.js';
|
|
10
|
+
import { type TranscriptScrollHandle } from './transcript-commands.js';
|
|
10
11
|
export interface AppProps {
|
|
11
12
|
initialModel: string;
|
|
12
13
|
maxSteps: number;
|
|
@@ -26,6 +27,7 @@ export interface AppProps {
|
|
|
26
27
|
scrollToBottom: () => void;
|
|
27
28
|
scrollHalfPage: (dir: -1 | 1) => void;
|
|
28
29
|
scrollToTop: () => void;
|
|
30
|
+
transcript: TranscriptScrollHandle;
|
|
29
31
|
}) => void;
|
|
30
32
|
version?: string;
|
|
31
33
|
isFullscreen?: boolean;
|
package/dist/tui/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { tokens, g } from './tokens.js';
|
|
|
12
12
|
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, } from './scroll-model.js';
|
|
13
13
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
14
14
|
import { renderMarkdownLines } from './markdown.js';
|
|
15
|
+
import { getTranscriptCommand, } from './transcript-commands.js';
|
|
15
16
|
let _id = 0;
|
|
16
17
|
function nextId(p) { _id++; return `${p}-${_id}`; }
|
|
17
18
|
function Header({ cwd, model, version, width }) {
|
|
@@ -184,13 +185,21 @@ export function App(props) {
|
|
|
184
185
|
if (status.status !== 'running')
|
|
185
186
|
ctrlCArmed.current = false;
|
|
186
187
|
}, [status.status]);
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
// scroll.md §7: submit bumps a version key; the settle effect below pins
|
|
189
|
+
// to bottom immediate + microtask + timeout so layout settles first.
|
|
190
|
+
// (Decoupled from render: never scroll directly inside the submit handler.)
|
|
191
|
+
const [submitKey, setSubmitKey] = useState(0);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!submitKey)
|
|
194
|
+
return;
|
|
195
|
+
const toBottom = () => {
|
|
196
|
+
scrollCmdsRef.current.bottom();
|
|
197
|
+
};
|
|
198
|
+
toBottom();
|
|
199
|
+
queueMicrotask(toBottom);
|
|
200
|
+
const t = setTimeout(toBottom, 0);
|
|
201
|
+
return () => clearTimeout(t);
|
|
202
|
+
}, [submitKey]);
|
|
194
203
|
// Shift+Enter intent: explicit shift+return, kitty/CSI-u sequence, legacy
|
|
195
204
|
// ESC+CR pair, or Esc immediately followed by Return (75ms, non-empty input).
|
|
196
205
|
const escReturnAt = useRef(0);
|
|
@@ -383,9 +392,28 @@ export function App(props) {
|
|
|
383
392
|
const scrollToBottom = useCallback(() => { scrollCmdsRef.current.bottom(); }, []);
|
|
384
393
|
const scrollHalfPage = useCallback((dir) => { scrollCmdsRef.current.halfPage(dir); }, []);
|
|
385
394
|
const scrollToTop = useCallback(() => { scrollCmdsRef.current.top(); }, []);
|
|
395
|
+
// scroll.md §2/§10: the four-command TranscriptScrollHandle.
|
|
396
|
+
const transcriptHandle = useMemo(() => ({
|
|
397
|
+
runTranscriptCommand: (command) => {
|
|
398
|
+
switch (command) {
|
|
399
|
+
case 'messages_half_page_up':
|
|
400
|
+
scrollCmdsRef.current.halfPage(-1);
|
|
401
|
+
return;
|
|
402
|
+
case 'messages_half_page_down':
|
|
403
|
+
scrollCmdsRef.current.halfPage(1);
|
|
404
|
+
return;
|
|
405
|
+
case 'messages_first':
|
|
406
|
+
scrollCmdsRef.current.top();
|
|
407
|
+
return;
|
|
408
|
+
case 'messages_last':
|
|
409
|
+
scrollCmdsRef.current.bottom();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
}), []);
|
|
386
414
|
const onMountedRef = useRef(props.onMounted);
|
|
387
415
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
388
|
-
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop]);
|
|
416
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcript: transcriptHandle }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcriptHandle]);
|
|
389
417
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
390
418
|
n.delete(id);
|
|
391
419
|
else
|
|
@@ -406,26 +434,32 @@ export function App(props) {
|
|
|
406
434
|
return;
|
|
407
435
|
}
|
|
408
436
|
// Scroll keys (work in any mode, including while running).
|
|
437
|
+
// scroll.md §6 flow: Keyboard → getTranscriptCommand → handle.
|
|
409
438
|
// design.md §11: PageUp/Ctrl+U half-up, PageDown/Ctrl+D half-down,
|
|
410
|
-
// Ctrl+Home/Ctrl+End first/last. (Ctrl+B/F kept
|
|
439
|
+
// Ctrl+Home/Ctrl+End first/last. (Plain Home/End + Ctrl+B/F/G kept.)
|
|
411
440
|
if (isFullscreen && maxTop > 0) {
|
|
441
|
+
const tcmd = getTranscriptCommand(inputStr, key);
|
|
442
|
+
if (tcmd) {
|
|
443
|
+
transcriptHandle.runTranscriptCommand(tcmd);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
412
446
|
if (key.home) {
|
|
413
447
|
commands.jumpTop();
|
|
414
448
|
return;
|
|
415
|
-
}
|
|
449
|
+
}
|
|
416
450
|
if (key.end) {
|
|
417
451
|
commands.jumpBottom();
|
|
418
452
|
return;
|
|
419
|
-
}
|
|
453
|
+
}
|
|
420
454
|
if (key.ctrl && inputStr === 'g') {
|
|
421
455
|
commands.jumpBottom();
|
|
422
456
|
return;
|
|
423
457
|
} // Ctrl+G → bottom
|
|
424
|
-
if (
|
|
458
|
+
if ((key.ctrl && inputStr === 'b')) {
|
|
425
459
|
commands.pageUp();
|
|
426
460
|
return;
|
|
427
461
|
}
|
|
428
|
-
if (
|
|
462
|
+
if ((key.ctrl && inputStr === 'f')) {
|
|
429
463
|
commands.pageDown();
|
|
430
464
|
return;
|
|
431
465
|
}
|
|
@@ -490,7 +524,7 @@ export function App(props) {
|
|
|
490
524
|
setQueuedInputs((prev) => [...prev, v]);
|
|
491
525
|
setInput('');
|
|
492
526
|
pushHistory(v);
|
|
493
|
-
|
|
527
|
+
setSubmitKey((k) => k + 1);
|
|
494
528
|
return;
|
|
495
529
|
}
|
|
496
530
|
if (key.backspace || key.delete) {
|
|
@@ -555,7 +589,7 @@ export function App(props) {
|
|
|
555
589
|
pushHistory(v);
|
|
556
590
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
|
|
557
591
|
streamingIdRef.current = null;
|
|
558
|
-
|
|
592
|
+
setSubmitKey((k) => k + 1);
|
|
559
593
|
const cmd = parseSlash(v);
|
|
560
594
|
if (cmd.kind === 'prompt')
|
|
561
595
|
void props.onPrompt(cmd.text);
|
|
@@ -654,5 +688,5 @@ export function App(props) {
|
|
|
654
688
|
if (it.kind === 'diff')
|
|
655
689
|
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));
|
|
656
690
|
return null;
|
|
657
|
-
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking..." }), _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 > 0 ? (_jsx(Box, { justifyContent: "flex-end", paddingX: 1, marginTop: -1, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ↓ ', pendingNew >= 1000 ? '999+ new' : `${pendingNew} new`, ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, 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, _jsxs(Box, { flexDirection: "column", 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", children: [_jsxs(Text, { color: tokens.colors.dim, children: [baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
691
|
+
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _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 > 0 ? (_jsx(Box, { justifyContent: "flex-end", paddingX: 1, marginTop: -1, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ↓ ', pendingNew >= 1000 ? '999+ new' : `${pendingNew} new`, ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, 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, _jsxs(Box, { flexDirection: "column", 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", children: [_jsxs(Text, { color: tokens.colors.dim, children: [baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
658
692
|
}
|
package/dist/tui/app.test.js
CHANGED
|
@@ -319,6 +319,43 @@ describe('App', () => {
|
|
|
319
319
|
await new Promise((r) => setTimeout(r, 50));
|
|
320
320
|
expect(onPrompt).toHaveBeenCalledWith('line one\nline two');
|
|
321
321
|
});
|
|
322
|
+
it('submitting while pinned snaps back to bottom (scroll.md §7)', async () => {
|
|
323
|
+
const onPrompt = vi.fn(async () => { });
|
|
324
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
325
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
326
|
+
stdin.write(KEY_HOME);
|
|
327
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
328
|
+
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
329
|
+
stdin.write('hello again');
|
|
330
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
331
|
+
stdin.write('\x0d');
|
|
332
|
+
// Settle effect: immediate + microtask + timeout(0) bottom pin.
|
|
333
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
334
|
+
expect(onPrompt).toHaveBeenCalledWith('hello again');
|
|
335
|
+
expect(lastFrame() ?? '').toMatch(/MSG-24-tag/);
|
|
336
|
+
});
|
|
337
|
+
it('onMounted transcript handle runs the four commands (scroll.md §2)', async () => {
|
|
338
|
+
let handle = null;
|
|
339
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25), onMounted: (h) => {
|
|
340
|
+
handle = h.transcript;
|
|
341
|
+
} }));
|
|
342
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
343
|
+
expect(handle).not.toBeNull();
|
|
344
|
+
handle.runTranscriptCommand('messages_half_page_up');
|
|
345
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
346
|
+
let frame = lastFrame() ?? '';
|
|
347
|
+
// Half page (10 lines) up from bottom (row 30 → 20): MSG-24 gone, MSG-14 in view.
|
|
348
|
+
expect(frame).not.toMatch(/MSG-24-tag/);
|
|
349
|
+
expect(frame).toMatch(/MSG-14-tag/);
|
|
350
|
+
handle.runTranscriptCommand('messages_first');
|
|
351
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
352
|
+
frame = lastFrame() ?? '';
|
|
353
|
+
expect(frame).toMatch(/MSG-00-tag/);
|
|
354
|
+
expect(frame).not.toMatch(/MSG-24-tag/);
|
|
355
|
+
handle.runTranscriptCommand('messages_last');
|
|
356
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
357
|
+
expect(lastFrame() ?? '').toMatch(/MSG-24-tag/);
|
|
358
|
+
});
|
|
322
359
|
it('Shift+Up / Shift+Down scroll by one line', async () => {
|
|
323
360
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
324
361
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll.md §§2/4/5/6 — TranscriptCommand abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Only four scroll commands exist (Cline parity):
|
|
5
|
+
* messages_half_page_up / messages_half_page_down /
|
|
6
|
+
* messages_first / messages_last
|
|
7
|
+
*
|
|
8
|
+
* Flow (§6): Keyboard → getTranscriptCommand() → TranscriptScrollHandle →
|
|
9
|
+
* ChatMessageList. Keys are matched in ONE place (the §5 binding table);
|
|
10
|
+
* the App routes everything else (history, autocomplete, approvals).
|
|
11
|
+
*
|
|
12
|
+
* Note on the stack: the doc's `<scrollbox stickyScroll>` is OpenTUI-only
|
|
13
|
+
* and this app renders with Ink, which has no scrollbox primitive — so the
|
|
14
|
+
* handle drives the measured anchor viewport (tui/scroll-model.ts) instead.
|
|
15
|
+
* The command vocabulary and all §21 behaviors are identical.
|
|
16
|
+
*/
|
|
17
|
+
export type TranscriptCommand = 'messages_half_page_up' | 'messages_half_page_down' | 'messages_first' | 'messages_last';
|
|
18
|
+
export interface TranscriptScrollHandle {
|
|
19
|
+
runTranscriptCommand(command: TranscriptCommand): void;
|
|
20
|
+
}
|
|
21
|
+
/** Minimal Ink key shape needed for the binding table. */
|
|
22
|
+
export interface TranscriptKey {
|
|
23
|
+
pageUp?: boolean;
|
|
24
|
+
pageDown?: boolean;
|
|
25
|
+
home?: boolean;
|
|
26
|
+
end?: boolean;
|
|
27
|
+
ctrl?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* scroll.md §5 binding table (pure — unit-tested):
|
|
31
|
+
* PageUp / Ctrl+U → up · PageDown / Ctrl+D → down ·
|
|
32
|
+
* Ctrl+Home → first · Ctrl+End → last · anything else → undefined.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getTranscriptCommand(input: string, key: TranscriptKey): TranscriptCommand | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll.md §§2/4/5/6 — TranscriptCommand abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Only four scroll commands exist (Cline parity):
|
|
5
|
+
* messages_half_page_up / messages_half_page_down /
|
|
6
|
+
* messages_first / messages_last
|
|
7
|
+
*
|
|
8
|
+
* Flow (§6): Keyboard → getTranscriptCommand() → TranscriptScrollHandle →
|
|
9
|
+
* ChatMessageList. Keys are matched in ONE place (the §5 binding table);
|
|
10
|
+
* the App routes everything else (history, autocomplete, approvals).
|
|
11
|
+
*
|
|
12
|
+
* Note on the stack: the doc's `<scrollbox stickyScroll>` is OpenTUI-only
|
|
13
|
+
* and this app renders with Ink, which has no scrollbox primitive — so the
|
|
14
|
+
* handle drives the measured anchor viewport (tui/scroll-model.ts) instead.
|
|
15
|
+
* The command vocabulary and all §21 behaviors are identical.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* scroll.md §5 binding table (pure — unit-tested):
|
|
19
|
+
* PageUp / Ctrl+U → up · PageDown / Ctrl+D → down ·
|
|
20
|
+
* Ctrl+Home → first · Ctrl+End → last · anything else → undefined.
|
|
21
|
+
*/
|
|
22
|
+
export function getTranscriptCommand(input, key) {
|
|
23
|
+
if (key.pageUp || (key.ctrl && input === 'u'))
|
|
24
|
+
return 'messages_half_page_up';
|
|
25
|
+
if (key.pageDown || (key.ctrl && input === 'd'))
|
|
26
|
+
return 'messages_half_page_down';
|
|
27
|
+
if (key.home && key.ctrl)
|
|
28
|
+
return 'messages_first';
|
|
29
|
+
if (key.end && key.ctrl)
|
|
30
|
+
return 'messages_last';
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
package/package.json
CHANGED