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 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
- process.stdout.write(MOUSE_ENABLE); // wheel events (SGR), see tui/mouse.ts
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
- process.stdout.write(MOUSE_DISABLE);
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 · ↑/↓ history (with text) else scroll · wheel ±3 lines',
2685
- ' Shift+drag selects · /vim toggles vim input mode · /keymap <note> saves a display note',
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): text in buffer (or browsing) → history;
1056
- // empty buffer → scroll viewport one line.
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() !== '' || histIdx !== null) {
1059
- if (history.length > 0) {
1060
- const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
1061
- setHistIdx(next);
1062
- setInput(history[next] ?? '');
1063
- setVimCursor(null);
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) {
@@ -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');
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",