klyro 1.0.11 → 1.0.13
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 +13 -5
- package/dist/tui/app.js +15 -9
- package/dist/tui/app.test.js +36 -0
- package/dist/tui/mouse.d.ts +10 -0
- package/dist/tui/mouse.js +12 -0
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
|
25
25
|
import { parse } from './slash/parser.js';
|
|
26
26
|
import { resolveProvider, providerHelp, lastProviderError } from '../providers.js';
|
|
27
27
|
import { readVersion } from '../version.js';
|
|
28
|
-
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper } from '../tui/mouse.js';
|
|
28
|
+
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper, isMouseReportingEnabled } from '../tui/mouse.js';
|
|
29
29
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
30
30
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
31
31
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
@@ -372,13 +372,20 @@ export async function startRepl(opts = {}) {
|
|
|
372
372
|
// ── Full-screen takeover like OpenCode — always when klyro in TTY (user explicitly wants it)
|
|
373
373
|
// Scroll now works correctly via internal viewport, not native terminal scroll
|
|
374
374
|
const isAltScreen = useTui && !!process.stdout.isTTY && process.env.KLYRO_NO_ALT !== '1';
|
|
375
|
+
// Mouse reporting is opt-in (KLYRO_MOUSE=1): when on, the terminal sends
|
|
376
|
+
// clicks/wheel to the app (wheel scrolls ±3 lines) but native text
|
|
377
|
+
// selection and right-click paste stop working. Default off so select to
|
|
378
|
+
// copy and right-click paste work out of the box; bracketed paste
|
|
379
|
+
// (keyboard paste) is unaffected and always enabled below.
|
|
380
|
+
const mouseReporting = isAltScreen && isMouseReportingEnabled();
|
|
375
381
|
const enterAlt = () => {
|
|
376
382
|
if (!isAltScreen)
|
|
377
383
|
return;
|
|
378
384
|
try {
|
|
379
385
|
process.stdout.write('\x1b[?1049h\x1b[?25l'); // alt screen + hide cursor
|
|
380
386
|
process.stdout.write('\x1b[H\x1b[2J'); // home + clear
|
|
381
|
-
|
|
387
|
+
if (mouseReporting)
|
|
388
|
+
process.stdout.write(MOUSE_ENABLE); // wheel events (SGR), see tui/mouse.ts
|
|
382
389
|
}
|
|
383
390
|
catch { /* ignore */ }
|
|
384
391
|
};
|
|
@@ -386,7 +393,8 @@ export async function startRepl(opts = {}) {
|
|
|
386
393
|
if (!isAltScreen)
|
|
387
394
|
return;
|
|
388
395
|
try {
|
|
389
|
-
|
|
396
|
+
if (mouseReporting)
|
|
397
|
+
process.stdout.write(MOUSE_DISABLE);
|
|
390
398
|
process.stdout.write('\x1b[?25h\x1b[?1049l'); // show cursor + leave alt
|
|
391
399
|
}
|
|
392
400
|
catch { /* ignore */ }
|
|
@@ -2681,8 +2689,8 @@ export async function startRepl(opts = {}) {
|
|
|
2681
2689
|
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2682
2690
|
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2683
2691
|
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2684
|
-
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line ·
|
|
2685
|
-
' Shift+
|
|
2692
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑ history at live tail, scrolls while reading above · PgUp/Dn scroll (KLYRO_MOUSE=1 adds wheel ±3 lines)',
|
|
2693
|
+
' Text selection/copy and right-click paste work natively · Shift+Enter newline · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2686
2694
|
].join('\n'),
|
|
2687
2695
|
});
|
|
2688
2696
|
return;
|
package/dist/tui/app.js
CHANGED
|
@@ -1052,16 +1052,22 @@ export function App(props) {
|
|
|
1052
1052
|
void props.onSlash({ kind: 'quit' });
|
|
1053
1053
|
return;
|
|
1054
1054
|
}
|
|
1055
|
-
// Contextual ↑/↓ (§8.3):
|
|
1056
|
-
//
|
|
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. ↓ is unchanged:
|
|
1060
|
+
// browse newer while browsing, else scroll toward the tail.
|
|
1057
1061
|
if (key.upArrow && !key.shift && !key.ctrl) {
|
|
1058
|
-
if (input.trim()
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1062
|
+
if (isFullscreen && maxTop > 0 && !atBottom && histIdx === null && input.trim() === '') {
|
|
1063
|
+
commands.lineUp();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (history.length > 0) {
|
|
1067
|
+
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
1068
|
+
setHistIdx(next);
|
|
1069
|
+
setInput(history[next] ?? '');
|
|
1070
|
+
setVimCursor(null);
|
|
1065
1071
|
return;
|
|
1066
1072
|
}
|
|
1067
1073
|
if (isFullscreen && maxTop > 0) {
|
package/dist/tui/app.test.js
CHANGED
|
@@ -247,6 +247,7 @@ describe('App', () => {
|
|
|
247
247
|
expect(lastFrame() ?? '').toContain('first recallable prompt');
|
|
248
248
|
});
|
|
249
249
|
it('↑ on empty input scrolls one line instead of history', async () => {
|
|
250
|
+
// No prompts submitted yet → history is empty → viewport scrolls.
|
|
250
251
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
251
252
|
await new Promise((r) => setTimeout(r, 50));
|
|
252
253
|
stdin.write(KEY_HOME);
|
|
@@ -256,6 +257,41 @@ describe('App', () => {
|
|
|
256
257
|
await new Promise((r) => setTimeout(r, 30));
|
|
257
258
|
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
258
259
|
});
|
|
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
|
+
});
|
|
259
295
|
it('/c shows top-6 suggestions and Tab completes', async () => {
|
|
260
296
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|
|
261
297
|
stdin.write('/c');
|
package/dist/tui/mouse.d.ts
CHANGED
|
@@ -34,6 +34,16 @@ 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
|
+
/**
|
|
38
|
+
* Whether the TUI may request terminal mouse reporting (wheel scrolling).
|
|
39
|
+
*
|
|
40
|
+
* Default OFF: with button reporting enabled the terminal routes
|
|
41
|
+
* selection clicks and right-click paste to the app (which swallows them),
|
|
42
|
+
* so native select-to-copy and right-click-paste break. Native selection
|
|
43
|
+
* works out of the box; set `KLYRO_MOUSE=1` to opt into wheel scrolling
|
|
44
|
+
* (Shift+drag still selects natively in most terminals).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isMouseReportingEnabled(env?: Readonly<Record<string, string | undefined>>): boolean;
|
|
37
47
|
export declare const PASTE_START = "\u001B[200~";
|
|
38
48
|
export declare const PASTE_END = "\u001B[201~";
|
|
39
49
|
export declare const PASTE_ENABLE = "\u001B[?2004h";
|
package/dist/tui/mouse.js
CHANGED
|
@@ -92,6 +92,18 @@ 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
|
+
/**
|
|
96
|
+
* Whether the TUI may request terminal mouse reporting (wheel scrolling).
|
|
97
|
+
*
|
|
98
|
+
* Default OFF: with button reporting enabled the terminal routes
|
|
99
|
+
* selection clicks and right-click paste to the app (which swallows them),
|
|
100
|
+
* so native select-to-copy and right-click-paste break. Native selection
|
|
101
|
+
* works out of the box; set `KLYRO_MOUSE=1` to opt into wheel scrolling
|
|
102
|
+
* (Shift+drag still selects natively in most terminals).
|
|
103
|
+
*/
|
|
104
|
+
export function isMouseReportingEnabled(env = process.env) {
|
|
105
|
+
return env.KLYRO_MOUSE === '1';
|
|
106
|
+
}
|
|
95
107
|
export const PASTE_START = '\x1b[200~';
|
|
96
108
|
export const PASTE_END = '\x1b[201~';
|
|
97
109
|
export const PASTE_ENABLE = '\x1b[?2004h'; // bracketed paste: terminal wraps pastes
|