Skip to content

Commit cb62977

Browse files
committed
Cap the resume picker at ten recent sessions
A long catalog made resume a wall of rows. Ten newest plus type-to-filter is how you actually pick a thread.
1 parent 2986951 commit cb62977

10 files changed

Lines changed: 94 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
2525
stay closed.
2626
- `corbits resume` lists completed, failed, and crashed sessions alongside
2727
in-progress ones, ordered by last persist rather than start time.
28-
`--force` is no longer required to see finished threads.
28+
`--force` is no longer required to see finished threads. The picker shows
29+
the 10 most recent sessions and type-to-filter narrows that list.
2930

3031
### Fixed
3132

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ corbits resume <session-id>
7575
```
7676

7777
Plain `corbits` always starts a fresh conversation. `corbits resume` opens a
78-
picker of saved sessions for this checkout, including completed ones, ordered
79-
by last persist.
78+
picker of the 10 most recently persisted sessions for this checkout,
79+
including completed ones. Type to filter by name.
8080

8181
### Mid-run steering
8282

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts`
316316
| -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317317
| _(no verb)_ | — | Interactive session; optional trailing task text |
318318
| `exec` / `run` | — | Run a prompt (non-interactive / one-shot) |
319-
| `resume` / `continue` | — | Open the session picker for this folder (project-keyed to this checkout's git toplevel). Lists every saved session, completed included, newest persist first. `--force` is not required to see finished threads. |
319+
| `resume` / `continue` | — | Open the session picker for this folder (project-keyed to this checkout's git toplevel). Lists the 10 most recently persisted sessions, completed included. Type to filter. `--force` is not required to see finished threads. |
320320
| `--resume` | — | Open the interactive session picker |
321321
| `resume <session-id>` | — | Reopen a specific session |
322322
| `resume --pick` / `--list` | — | Interactive session picker |

‎docs/PRODUCT.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ Local multi-model capability checks use this path (`bun run eval:capability`); s
7676
$ corbits resume
7777
```
7878

79-
Opens a picker of saved conversations for this checkout, including completed
80-
ones. Rows are ordered by last persist, not start time. Plain
79+
Opens a picker of the 10 most recently persisted conversations for this
80+
checkout, including completed ones. Type to filter by name. Plain
8181
`corbits` always starts a fresh conversation; `corbits resume <session-id>`
8282
is the direct, explicit resume path.
8383

‎docs/TUI.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,9 +432,11 @@ in `mouse-reporting-disabled.test.ts` for both `runListModal` and
432432
`runProviderSetup`). This is intentional: these surfaces never need
433433
click-to-expand or drag-to-scroll, so leaving mouse reporting off lets the
434434
terminal's own text selection and copy work by default, with no Alt+M dance
435-
required. The resume picker lists every saved session for this checkout —
436-
completed, failed, and crashed included — newest persist first. Recency is
437-
the last write to `run.json`, not start time; `--force` is not a list filter.
435+
required. The resume picker lists the 10 most recently persisted sessions
436+
for this checkout — completed, failed, and crashed included. Recency is
437+
the last write to `run.json`, not start time. Type to filter by name
438+
(printable keys claim the `>` row, same as the model picker); `--force`
439+
is not a list filter.
438440

439441
## The prompt box
440442

‎src/tui/list-modal.test.ts‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { afterEach, describe, expect, test } from "bun:test";
22

33
import { createHarness, type Harness } from "./harness.js";
4-
import { runListModal } from "./list-modal.js";
4+
import { runListModal, type ListModalConfig } from "./list-modal.js";
55

66
let harness: Harness | undefined;
77

@@ -10,7 +10,7 @@ afterEach(() => {
1010
harness = undefined;
1111
});
1212

13-
async function mountModal(): Promise<{
13+
async function mountModal(overrides: Partial<ListModalConfig> = {}): Promise<{
1414
choice: Promise<string | null>;
1515
harness: Harness;
1616
}> {
@@ -22,6 +22,7 @@ async function mountModal(): Promise<{
2222
{ id: "s-2", label: "Second session" },
2323
],
2424
createRenderer: async () => harness!.renderer,
25+
...overrides,
2526
});
2627
await harness.renderOnce();
2728
return { choice, harness };
@@ -62,4 +63,33 @@ describe("runListModal", () => {
6263
harness.pressKey("Escape");
6364
await choice;
6465
});
66+
67+
test("type-to-filter narrows the list and Enter selects the match", async () => {
68+
const { choice, harness } = await mountModal({ typeToFilter: true });
69+
await harness.renderOnce();
70+
expect(harness.captureCharFrame()).toContain("Second session");
71+
for (const ch of "Second") {
72+
harness.pressKey(ch);
73+
}
74+
await harness.renderOnce();
75+
const frame = harness.captureCharFrame();
76+
expect(frame).toContain("Second session");
77+
expect(frame).not.toContain("First session");
78+
harness.pressKey("Enter");
79+
expect(await choice).toBe("s-2");
80+
});
81+
82+
test("type-to-filter no-match Enter stays open", async () => {
83+
const { choice, harness } = await mountModal({ typeToFilter: true });
84+
await harness.renderOnce();
85+
for (const ch of "zzzzz") {
86+
harness.pressKey(ch);
87+
}
88+
await harness.renderOnce();
89+
expect(harness.captureCharFrame()).toContain("(no matches)");
90+
harness.pressKey("Enter");
91+
await harness.renderOnce();
92+
harness.pressKey("Escape");
93+
expect(await choice).toBeNull();
94+
});
6595
});

‎src/tui/list-modal.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export interface ListModalConfig {
2828
readonly heading?: readonly string[];
2929
readonly options: readonly ResidualCatalogEntry[];
3030
readonly activeIndex?: number;
31+
/**
32+
* Claim printable keys for a `>` filter row so the list narrows as you type.
33+
* Off by default so other satellite lists keep j/k navigation.
34+
*/
35+
readonly typeToFilter?: boolean;
3136
/** Renderer factory override for headless mounting in tests. */
3237
readonly createRenderer?: () => Promise<CliRenderer>;
3338
}
@@ -100,8 +105,12 @@ export async function runListModal(config: ListModalConfig): Promise<string | nu
100105
itemIds,
101106
frameId: "overlay-list-modal",
102107
activeIndex: config.activeIndex ?? 0,
108+
...(config.typeToFilter === true ? { typeToFilter: true } : {}),
103109
onAccept: (selection) => {
104-
settle(residualIdFromSelection(selection, itemIds) ?? null);
110+
const id = residualIdFromSelection(selection, itemIds);
111+
// Type-to-filter plants "(no matches)" with an empty id. Stay open.
112+
if (id === undefined || id.length === 0) return;
113+
settle(id);
105114
},
106115
});
107116

‎src/tui/pick-session.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect } from "bun:test";
22

3-
import { sessionResumeLabel } from "./pick-session.js";
3+
import { sessionResumeLabel, recentResumeSessions, RESUME_PICKER_LIMIT } from "./pick-session.js";
44
import type { SessionSummary } from "../session/index.js";
55

66
function summary(overrides: Partial<SessionSummary> = {}): SessionSummary {
@@ -37,3 +37,24 @@ describe("sessionResumeLabel", () => {
3737
expect(label.startsWith("Untitled session ·")).toBe(true);
3838
});
3939
});
40+
41+
describe("recentResumeSessions", () => {
42+
test("keeps at most ten newest-first rows", () => {
43+
const rows = Array.from({ length: 12 }, (_, i) =>
44+
summary({
45+
sessionId: `00000000-0000-7000-8000-${String(i).padStart(12, "0")}`,
46+
task: `session ${i}`,
47+
}),
48+
);
49+
const kept = recentResumeSessions(rows);
50+
expect(kept).toHaveLength(RESUME_PICKER_LIMIT);
51+
expect(kept[0]?.task).toBe("session 0");
52+
expect(kept[9]?.task).toBe("session 9");
53+
expect(kept.some((row) => row.task === "session 10")).toBe(false);
54+
});
55+
56+
test("leaves a shorter catalog unchanged", () => {
57+
const rows = [summary({ task: "only" })];
58+
expect(recentResumeSessions(rows)).toEqual(rows);
59+
});
60+
});

‎src/tui/pick-session.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,34 @@ import { listSessions, type SessionSummary } from "../session/index.js";
33
import { runListModal } from "./list-modal.js";
44
import { formatRelativeTime } from "./format-relative-time.js";
55

6+
export const RESUME_PICKER_LIMIT = 10;
7+
68
export function sessionResumeLabel(session: SessionSummary): string {
79
const title = session.task.trim().length > 0 ? session.task.trim() : "Untitled session";
810
return `${title} · ${formatRelativeTime(session.updatedAt)} · ${session.status}`;
911
}
1012

13+
/** Newest-first catalog already sorted; keep the most recent `limit` rows. */
14+
export function recentResumeSessions(
15+
sessions: readonly SessionSummary[],
16+
limit = RESUME_PICKER_LIMIT,
17+
): SessionSummary[] {
18+
return sessions.slice(0, limit);
19+
}
20+
1121
export async function pickSession(cwd: string): Promise<SessionSummary | null> {
12-
const sessions = await listSessions(cwd);
13-
if (sessions.length === 0) {
22+
const catalog = await listSessions(cwd);
23+
if (catalog.length === 0) {
1424
process.stderr.write(`${COMMAND_NAME}: no previous sessions found in this directory.\n`);
1525
return null;
1626
}
1727

28+
const sessions = recentResumeSessions(catalog);
1829
const picked = await runListModal({
1930
title: "Resume conversation",
2031
kind: "resume",
2132
heading: ["Choose a previous session in this checkout"],
33+
typeToFilter: true,
2234
options: sessions.map((session) => ({
2335
id: session.sessionId,
2436
label: sessionResumeLabel(session),

‎src/tui/shell.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3564,7 +3564,7 @@ export interface OpenListOverlayOpts {
35643564
readonly echoChoice?: boolean;
35653565
/**
35663566
* Claim printable keys for a `>` filter row so the list narrows as you type.
3567-
* Opt-in per open (model picker, palette). Overlays without it keep j/k
3567+
* Opt-in per open (model picker, palette, resume). Overlays without it keep j/k
35683568
* navigation; with it, j/k type into the filter and arrows still navigate.
35693569
*/
35703570
readonly typeToFilter?: boolean;
@@ -3839,9 +3839,9 @@ function repaintPalette(shell: AppShell): void {
38393839
* Keys a type-to-filter list claims while it is open, so the `>` row filters
38403840
* as you type.
38413841
*
3842-
* Opt-in per open (`typeToFilter`): palette and the flat model picker give up
3843-
* j/k navigation so printable keys feed the filter. Overlays without
3844-
* type-to-filter (permissions, resume, workers, copy, …) keep j/k. Arrow and
3842+
* Opt-in per open (`typeToFilter`): palette, the flat model picker, and the
3843+
* resume picker give up j/k navigation so printable keys feed the filter.
3844+
* Overlays without type-to-filter (permissions, workers, copy, …) keep j/k. Arrow and
38453845
* page keys are never claimed here, so they keep working in every overlay
38463846
* including type-to-filter ones.
38473847
*/

0 commit comments

Comments
 (0)