klyro 1.0.12 → 1.0.14
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/cli/repl.js +1 -1
- package/dist/tui/app.js +21 -11
- package/dist/tui/app.test.js +51 -11
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -2689,7 +2689,7 @@ export async function startRepl(opts = {}) {
|
|
|
2689
2689
|
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2690
2690
|
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2691
2691
|
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2692
|
-
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line ·
|
|
2692
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑ history at live tail, scrolls while reading above · Ctrl+P/N history always · PgUp/Dn or Ctrl+U/D scroll (KLYRO_MOUSE=1 adds wheel ±3 lines)',
|
|
2693
2693
|
' Text selection/copy and right-click paste work natively · Shift+Enter newline · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2694
2694
|
].join('\n'),
|
|
2695
2695
|
});
|
package/dist/tui/app.js
CHANGED
|
@@ -1052,24 +1052,34 @@ export function App(props) {
|
|
|
1052
1052
|
void props.onSlash({ kind: 'quit' });
|
|
1053
1053
|
return;
|
|
1054
1054
|
}
|
|
1055
|
-
// Contextual ↑/↓ (§8.3):
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
// the
|
|
1059
|
-
|
|
1055
|
+
// Contextual ↑/↓ (§8.3): both behaviors, split by where you are.
|
|
1056
|
+
// Scrolled up reading the transcript (not at the live tail, not already
|
|
1057
|
+
// browsing) → plain ↑ keeps scrolling so chat scroll is never lost.
|
|
1058
|
+
// At the live tail (or no scroll surface) → ↑ browses input history
|
|
1059
|
+
// newest-first, so send + ↑ restores the last prompt. Ctrl+P / Ctrl+N
|
|
1060
|
+
// are escape-free aliases for history prev/next: single-byte codes that
|
|
1061
|
+
// always browse, even on terminals that mangle arrow-key sequences.
|
|
1062
|
+
// ↓ is otherwise unchanged: browse newer while browsing, else scroll
|
|
1063
|
+
// toward the tail.
|
|
1064
|
+
const wantHistPrev = (key.upArrow && !key.shift && !key.ctrl) || (key.ctrl && inputStr === 'p');
|
|
1065
|
+
const wantHistNext = (key.downArrow && !key.shift && !key.ctrl) || (key.ctrl && inputStr === 'n');
|
|
1066
|
+
if (wantHistPrev) {
|
|
1067
|
+
if (!key.ctrl && isFullscreen && maxTop > 0 && !atBottom && histIdx === null && input.trim() === '') {
|
|
1068
|
+
commands.lineUp();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1060
1071
|
if (history.length > 0) {
|
|
1061
1072
|
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
1062
1073
|
setHistIdx(next);
|
|
1063
1074
|
setInput(history[next] ?? '');
|
|
1064
1075
|
setVimCursor(null);
|
|
1065
|
-
return;
|
|
1066
1076
|
}
|
|
1067
|
-
if (isFullscreen && maxTop > 0) {
|
|
1077
|
+
else if (!key.ctrl && isFullscreen && maxTop > 0) {
|
|
1068
1078
|
commands.lineUp();
|
|
1069
|
-
return;
|
|
1070
1079
|
}
|
|
1080
|
+
return;
|
|
1071
1081
|
}
|
|
1072
|
-
if (
|
|
1082
|
+
if (wantHistNext) {
|
|
1073
1083
|
if (histIdx !== null) {
|
|
1074
1084
|
const next = histIdx + 1;
|
|
1075
1085
|
if (next >= history.length) {
|
|
@@ -1083,8 +1093,8 @@ export function App(props) {
|
|
|
1083
1093
|
setVimCursor(null);
|
|
1084
1094
|
return;
|
|
1085
1095
|
}
|
|
1086
|
-
if (input.trim() !== '')
|
|
1087
|
-
return; //
|
|
1096
|
+
if (input.trim() !== '' || key.ctrl)
|
|
1097
|
+
return; // text present, or Ctrl+N: nothing newer
|
|
1088
1098
|
if (isFullscreen && maxTop > 0) {
|
|
1089
1099
|
commands.lineDown();
|
|
1090
1100
|
return;
|
package/dist/tui/app.test.js
CHANGED
|
@@ -257,29 +257,69 @@ describe('App', () => {
|
|
|
257
257
|
await new Promise((r) => setTimeout(r, 30));
|
|
258
258
|
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
259
259
|
});
|
|
260
|
-
it('↑
|
|
260
|
+
it('↑ at live tail recalls history; scrolled up it scrolls (both kept)', async () => {
|
|
261
|
+
const onPrompt = vi.fn(async () => { });
|
|
262
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
263
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
264
|
+
stdin.write('zzrecallme');
|
|
265
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
266
|
+
stdin.write('\x0d');
|
|
267
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
268
|
+
expect(onPrompt).toHaveBeenCalledWith('zzrecallme');
|
|
269
|
+
// At the live tail, empty input + ↑ recalls into the input line: typing
|
|
270
|
+
// '!' must extend the recalled text (only the input holds 'zzrecallme!').
|
|
271
|
+
stdin.write('\x1b[A');
|
|
272
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
273
|
+
stdin.write('!');
|
|
274
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
275
|
+
expect(lastFrame() ?? '').toContain('zzrecallme!');
|
|
276
|
+
});
|
|
277
|
+
it('↑ while scrolled up keeps scrolling and leaves input empty', async () => {
|
|
278
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
279
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
280
|
+
stdin.write('zznoscroll');
|
|
281
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
282
|
+
stdin.write('\x0d');
|
|
283
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
284
|
+
stdin.write(KEY_HOME);
|
|
285
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
286
|
+
// Scrolled up + empty input + ↑ → scroll (MSG-00 stays), input untouched:
|
|
287
|
+
// typing '!' must NOT extend the submitted prompt.
|
|
288
|
+
stdin.write('\x1b[A');
|
|
289
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
290
|
+
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
291
|
+
stdin.write('!');
|
|
292
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
293
|
+
expect(lastFrame() ?? '').not.toContain('zznoscroll!');
|
|
294
|
+
});
|
|
295
|
+
it('Ctrl+P / Ctrl+N browse history without arrow keys', async () => {
|
|
261
296
|
const onPrompt = vi.fn(async () => { });
|
|
262
297
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { } }));
|
|
263
|
-
stdin.write('
|
|
298
|
+
stdin.write('zzfirst');
|
|
264
299
|
await new Promise((r) => setTimeout(r, 20));
|
|
265
300
|
stdin.write('\x0d');
|
|
266
301
|
await new Promise((r) => setTimeout(r, 50));
|
|
267
|
-
stdin.write('
|
|
302
|
+
stdin.write('zzsecond');
|
|
268
303
|
await new Promise((r) => setTimeout(r, 20));
|
|
269
304
|
stdin.write('\x0d');
|
|
270
305
|
await new Promise((r) => setTimeout(r, 50));
|
|
271
306
|
expect(onPrompt).toHaveBeenCalledTimes(2);
|
|
272
|
-
//
|
|
273
|
-
stdin.write('\
|
|
307
|
+
// Ctrl+P recalls newest-first with zero escape sequences involved.
|
|
308
|
+
stdin.write('\x10');
|
|
274
309
|
await new Promise((r) => setTimeout(r, 30));
|
|
275
|
-
|
|
276
|
-
|
|
310
|
+
stdin.write('!');
|
|
311
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
312
|
+
expect(lastFrame() ?? '').toContain('zzsecond!');
|
|
313
|
+
// Ctrl+P twice walks older with no typing between, Ctrl+N walks newer.
|
|
314
|
+
stdin.write('\x10');
|
|
315
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
316
|
+
stdin.write('\x10');
|
|
317
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
318
|
+
stdin.write('\x0e');
|
|
277
319
|
await new Promise((r) => setTimeout(r, 30));
|
|
278
|
-
|
|
279
|
-
// ↓ walks back toward newer entries.
|
|
280
|
-
stdin.write('\x1b[B');
|
|
320
|
+
stdin.write('#');
|
|
281
321
|
await new Promise((r) => setTimeout(r, 30));
|
|
282
|
-
expect(lastFrame() ?? '').toContain('
|
|
322
|
+
expect(lastFrame() ?? '').toContain('zzsecond#');
|
|
283
323
|
});
|
|
284
324
|
it('/c shows top-6 suggestions and Tab completes', async () => {
|
|
285
325
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|