klyro 1.0.13 → 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 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 · ↑ history at live tail, scrolls while reading above · PgUp/Dn scroll (KLYRO_MOUSE=1 adds wheel ±3 lines)',
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
@@ -1056,10 +1056,15 @@ export function App(props) {
1056
1056
  // Scrolled up reading the transcript (not at the live tail, not already
1057
1057
  // browsing) → plain ↑ keeps scrolling so chat scroll is never lost.
1058
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.
1061
- if (key.upArrow && !key.shift && !key.ctrl) {
1062
- if (isFullscreen && maxTop > 0 && !atBottom && histIdx === null && input.trim() === '') {
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() === '') {
1063
1068
  commands.lineUp();
1064
1069
  return;
1065
1070
  }
@@ -1068,14 +1073,13 @@ export function App(props) {
1068
1073
  setHistIdx(next);
1069
1074
  setInput(history[next] ?? '');
1070
1075
  setVimCursor(null);
1071
- return;
1072
1076
  }
1073
- if (isFullscreen && maxTop > 0) {
1077
+ else if (!key.ctrl && isFullscreen && maxTop > 0) {
1074
1078
  commands.lineUp();
1075
- return;
1076
1079
  }
1080
+ return;
1077
1081
  }
1078
- if (key.downArrow && !key.shift && !key.ctrl) {
1082
+ if (wantHistNext) {
1079
1083
  if (histIdx !== null) {
1080
1084
  const next = histIdx + 1;
1081
1085
  if (next >= history.length) {
@@ -1089,8 +1093,8 @@ export function App(props) {
1089
1093
  setVimCursor(null);
1090
1094
  return;
1091
1095
  }
1092
- if (input.trim() !== '')
1093
- return; // single line with text, nothing newer
1096
+ if (input.trim() !== '' || key.ctrl)
1097
+ return; // text present, or Ctrl+N: nothing newer
1094
1098
  if (isFullscreen && maxTop > 0) {
1095
1099
  commands.lineDown();
1096
1100
  return;
@@ -292,6 +292,35 @@ describe('App', () => {
292
292
  await new Promise((r) => setTimeout(r, 30));
293
293
  expect(lastFrame() ?? '').not.toContain('zznoscroll!');
294
294
  });
295
+ it('Ctrl+P / Ctrl+N browse history without arrow keys', async () => {
296
+ const onPrompt = vi.fn(async () => { });
297
+ const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { } }));
298
+ stdin.write('zzfirst');
299
+ await new Promise((r) => setTimeout(r, 20));
300
+ stdin.write('\x0d');
301
+ await new Promise((r) => setTimeout(r, 50));
302
+ stdin.write('zzsecond');
303
+ await new Promise((r) => setTimeout(r, 20));
304
+ stdin.write('\x0d');
305
+ await new Promise((r) => setTimeout(r, 50));
306
+ expect(onPrompt).toHaveBeenCalledTimes(2);
307
+ // Ctrl+P recalls newest-first with zero escape sequences involved.
308
+ stdin.write('\x10');
309
+ await new Promise((r) => setTimeout(r, 30));
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');
319
+ await new Promise((r) => setTimeout(r, 30));
320
+ stdin.write('#');
321
+ await new Promise((r) => setTimeout(r, 30));
322
+ expect(lastFrame() ?? '').toContain('zzsecond#');
323
+ });
295
324
  it('/c shows top-6 suggestions and Tab completes', async () => {
296
325
  const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
297
326
  stdin.write('/c');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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",