klyro 1.0.12 → 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
@@ -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 · ↑/↓ input history · 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 · PgUp/Dn 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,11 +1052,17 @@ export function App(props) {
1052
1052
  void props.onSlash({ kind: 'quit' });
1053
1053
  return;
1054
1054
  }
1055
- // Contextual ↑/↓ (§8.3): history always wins when entries exist —
1056
- // empty input + ↑ recalls the last prompt (standard REPL behavior),
1057
- // typing filters by prefix is unnecessary so plain recall applies;
1058
- // the viewport scrolls only when there is no history to show.
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.
1059
1061
  if (key.upArrow && !key.shift && !key.ctrl) {
1062
+ if (isFullscreen && maxTop > 0 && !atBottom && histIdx === null && input.trim() === '') {
1063
+ commands.lineUp();
1064
+ return;
1065
+ }
1060
1066
  if (history.length > 0) {
1061
1067
  const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
1062
1068
  setHistIdx(next);
@@ -257,29 +257,40 @@ describe('App', () => {
257
257
  await new Promise((r) => setTimeout(r, 30));
258
258
  expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
259
259
  });
260
- it('↑ on empty input recalls history newest-first; ↓ browses back (user report)', async () => {
260
+ it('↑ at live tail recalls history; scrolled up it scrolls (both kept)', async () => {
261
261
  const onPrompt = vi.fn(async () => { });
262
- const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { } }));
263
- stdin.write('hi');
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');
264
265
  await new Promise((r) => setTimeout(r, 20));
265
266
  stdin.write('\x0d');
266
267
  await new Promise((r) => setTimeout(r, 50));
267
- stdin.write('second prompt');
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');
268
281
  await new Promise((r) => setTimeout(r, 20));
269
282
  stdin.write('\x0d');
270
283
  await new Promise((r) => setTimeout(r, 50));
271
- expect(onPrompt).toHaveBeenCalledTimes(2);
272
- // Input is empty after each submit; plain ↑ must recall, not scroll.
273
- stdin.write('\x1b[A');
284
+ stdin.write(KEY_HOME);
274
285
  await new Promise((r) => setTimeout(r, 30));
275
- expect(lastFrame() ?? '').toContain('second prompt');
286
+ // Scrolled up + empty input + ↑ → scroll (MSG-00 stays), input untouched:
287
+ // typing '!' must NOT extend the submitted prompt.
276
288
  stdin.write('\x1b[A');
277
289
  await new Promise((r) => setTimeout(r, 30));
278
- expect(lastFrame() ?? '').toContain('hi');
279
- // ↓ walks back toward newer entries.
280
- stdin.write('\x1b[B');
290
+ expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
291
+ stdin.write('!');
281
292
  await new Promise((r) => setTimeout(r, 30));
282
- expect(lastFrame() ?? '').toContain('second prompt');
293
+ expect(lastFrame() ?? '').not.toContain('zznoscroll!');
283
294
  });
284
295
  it('/c shows top-6 suggestions and Tab completes', async () => {
285
296
  const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.12",
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",