diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d1771ab..1d62c37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename or interrupted worker and returns immediately. `wait_agents` collects the reply. `send_input` steers only an in-flight running turn. Closed workers stay closed. +- `corbits resume` lists completed, failed, and crashed sessions alongside + in-progress ones, ordered by last persist rather than start time. + `--force` is no longer required to see finished threads. The picker shows + the 10 most recent sessions and type-to-filter narrows that list. ### Fixed diff --git a/README.md b/README.md index 87ce80aa..7caa7098 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,8 @@ corbits resume ``` Plain `corbits` always starts a fresh conversation. `corbits resume` opens a -picker of saved sessions for the working directory. +picker of the 10 most recently persisted sessions for this checkout, +including completed ones. Type to filter by name. ### Mid-run steering diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index e80faa19..92e118bf 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -316,7 +316,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts` | -------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _(no verb)_ | — | Interactive session; optional trailing task text | | `exec` / `run` | — | Run a prompt (non-interactive / one-shot) | -| `resume` / `continue` | — | Open the session picker for this folder (project-keyed to this checkout's git toplevel) | +| `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. | | `--resume` | — | Open the interactive session picker | | `resume ` | — | Reopen a specific session | | `resume --pick` / `--list` | — | Interactive session picker | diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index ee440995..fe9aec1c 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -76,7 +76,8 @@ Local multi-model capability checks use this path (`bun run eval:capability`); s $ corbits resume ``` -Opens a picker of saved conversations for the working directory. Plain +Opens a picker of the 10 most recently persisted conversations for this +checkout, including completed ones. Type to filter by name. Plain `corbits` always starts a fresh conversation; `corbits resume ` is the direct, explicit resume path. diff --git a/docs/TUI.md b/docs/TUI.md index e271feb3..55af3bbd 100644 --- a/docs/TUI.md +++ b/docs/TUI.md @@ -432,7 +432,11 @@ in `mouse-reporting-disabled.test.ts` for both `runListModal` and `runProviderSetup`). This is intentional: these surfaces never need click-to-expand or drag-to-scroll, so leaving mouse reporting off lets the terminal's own text selection and copy work by default, with no Alt+M dance -required. +required. The resume picker lists the 10 most recently persisted sessions +for this checkout — completed, failed, and crashed included. Recency is +the last write to `run.json`, not start time. Type to filter by name +(printable keys claim the `>` row, same as the model picker); `--force` +is not a list filter. ## The prompt box diff --git a/src/session/index.ts b/src/session/index.ts index 3c2845cf..ffdb701d 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -202,6 +202,8 @@ export interface SessionSummary { sessionId: string; task: string; startedAt: number; + /** Last persist time (`run.json` mtime, else session-dir mtime). Sort key for resume. */ + updatedAt: number; status: RunState["status"]; } @@ -230,7 +232,19 @@ async function collectSessionIds(cwd: string, home: string): Promise { return [...ids]; } -/** List on-disk sessions for a project, newest first. */ +async function sessionUpdatedAt(dir: string, fallbackMs: number): Promise { + try { + return (await stat(join(dir, "run.json"))).mtimeMs; + } catch { + try { + return (await stat(dir)).mtimeMs; + } catch { + return fallbackMs; + } + } +} + +/** List on-disk sessions for a project, most recently persisted first. */ export async function listSessions( cwd: string, home: string = homedir(), @@ -240,12 +254,14 @@ export async function listSessions( const summaries: SessionSummary[] = []; for (const entry of entries) { await migrateLegacySessionIfNeeded(cwd, entry, home); + const dir = sessionDir(cwd, entry, home); const loaded = await loadState(cwd, entry, home); if (loaded.kind === "ok") { summaries.push({ sessionId: entry, task: loaded.state.task, startedAt: loaded.state.startedAt, + updatedAt: await sessionUpdatedAt(dir, loaded.state.startedAt), status: loaded.state.status, }); continue; @@ -258,12 +274,14 @@ export async function listSessions( // and therefore isn't actually running: report it as crashed rather // than fabricating liveness. try { - const dirStat = await stat(sessionDir(cwd, entry, home)); + const dirStat = await stat(dir); await stat(sessionContextDir(cwd, entry, home)); + const startedAt = dirStat.birthtimeMs > 0 ? dirStat.birthtimeMs : dirStat.mtimeMs; summaries.push({ sessionId: entry, task: "(conversation)", - startedAt: dirStat.birthtimeMs > 0 ? dirStat.birthtimeMs : dirStat.mtimeMs, + startedAt, + updatedAt: dirStat.mtimeMs, status: "crashed", }); } catch { @@ -271,7 +289,7 @@ export async function listSessions( } } - summaries.sort((a, b) => b.startedAt - a.startedAt); + summaries.sort((a, b) => b.updatedAt - a.updatedAt); return Promise.all( summaries.map(async (row) => ({ ...row, diff --git a/src/session/list-sessions.test.ts b/src/session/list-sessions.test.ts index 7d412927..a773bafd 100644 --- a/src/session/list-sessions.test.ts +++ b/src/session/list-sessions.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; -import { mkdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, rm, utimes, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -203,3 +203,60 @@ test("listSessions stays silent when many sibling runs failed with an error", as expect(logged).not.toContain("unreadable session state"); expect(logged).not.toContain(home); }); + +async function writeRun( + sessionId: string, + body: { status: string; task: string; startedAt: number; turnsUsed?: number }, +): Promise { + await initSessionDir(cwd, sessionId, home); + const dir = sessionDir(cwd, sessionId, home); + await writeFile(join(dir, "run.json"), JSON.stringify({ turnsUsed: 1, ...body })); + return join(dir, "run.json"); +} + +test("listSessions includes completed and failed sessions", async () => { + const doneId = generateSessionId(); + const failedId = generateSessionId(); + await writeRun(doneId, { status: "done", task: "finished work", startedAt: 1 }); + await writeRun(failedId, { status: "failed", task: "broke", startedAt: 2 }); + const listed = await listSessions(cwd, home); + expect(listed.find((s) => s.sessionId === doneId)?.status).toBe("done"); + expect(listed.find((s) => s.sessionId === failedId)?.status).toBe("failed"); +}); + +test("listSessions sorts by run.json mtime, not startedAt", async () => { + const olderStart = generateSessionId(); + const newerStart = generateSessionId(); + const olderPath = await writeRun(olderStart, { + status: "done", + task: "started first, touched last", + startedAt: 1_000, + }); + const newerPath = await writeRun(newerStart, { + status: "running", + task: "started later, stale", + startedAt: 9_000, + }); + const now = Date.now(); + await utimes(newerPath, now / 1000 - 60, now / 1000 - 60); + await utimes(olderPath, now / 1000, now / 1000); + const listed = await listSessions(cwd, home); + expect(listed[0]?.sessionId).toBe(olderStart); + expect(listed[1]?.sessionId).toBe(newerStart); + expect(listed[0]?.updatedAt).toBeGreaterThan(listed[1]?.updatedAt ?? 0); +}); + +test("listSessions reports updatedAt from run.json mtime", async () => { + const sessionId = generateSessionId(); + const path = await writeRun(sessionId, { + status: "done", + task: "mtime title", + startedAt: 1, + }); + const stamp = Date.now() - 120_000; + await utimes(path, stamp / 1000, stamp / 1000); + const listed = await listSessions(cwd, home); + const row = listed.find((s) => s.sessionId === sessionId); + expect(row?.updatedAt).toBeGreaterThanOrEqual(stamp - 2000); + expect(row?.updatedAt).toBeLessThanOrEqual(stamp + 2000); +}); diff --git a/src/tui/list-modal.test.ts b/src/tui/list-modal.test.ts index d965b4a5..1800cf1f 100644 --- a/src/tui/list-modal.test.ts +++ b/src/tui/list-modal.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test"; import { createHarness, type Harness } from "./harness.js"; -import { runListModal } from "./list-modal.js"; +import { runListModal, type ListModalConfig } from "./list-modal.js"; let harness: Harness | undefined; @@ -10,7 +10,7 @@ afterEach(() => { harness = undefined; }); -async function mountModal(): Promise<{ +async function mountModal(overrides: Partial = {}): Promise<{ choice: Promise; harness: Harness; }> { @@ -22,6 +22,7 @@ async function mountModal(): Promise<{ { id: "s-2", label: "Second session" }, ], createRenderer: async () => harness!.renderer, + ...overrides, }); await harness.renderOnce(); return { choice, harness }; @@ -62,4 +63,40 @@ describe("runListModal", () => { harness.pressKey("Escape"); await choice; }); + + test("type-to-filter narrows the list and Enter selects the match", async () => { + const { choice, harness } = await mountModal({ typeToFilter: true }); + await harness.renderOnce(); + expect(harness.captureCharFrame()).toContain("Second session"); + for (const ch of "Second") { + harness.pressKey(ch); + } + await harness.renderOnce(); + const frame = harness.captureCharFrame(); + expect(frame).toContain("Second session"); + expect(frame).not.toContain("First session"); + harness.pressKey("Enter"); + expect(await choice).toBe("s-2"); + }); + + test("type-to-filter no-match Enter stays open", async () => { + const { choice, harness } = await mountModal({ typeToFilter: true }); + await harness.renderOnce(); + for (const ch of "zzzzz") { + harness.pressKey(ch); + } + await harness.renderOnce(); + expect(harness.captureCharFrame()).toContain("(no matches)"); + harness.pressKey("Enter"); + await harness.renderOnce(); + const afterEnter = harness.captureCharFrame(); + expect(afterEnter).toContain("(no matches)"); + expect(afterEnter).toContain(">"); + for (let i = 0; i < 5; i++) { + harness.pressKey("Backspace"); + } + await harness.renderOnce(); + harness.pressKey("Enter"); + expect(await choice).toBe("s-1"); + }); }); diff --git a/src/tui/list-modal.ts b/src/tui/list-modal.ts index ce9141fd..df98d1b6 100644 --- a/src/tui/list-modal.ts +++ b/src/tui/list-modal.ts @@ -28,6 +28,11 @@ export interface ListModalConfig { readonly heading?: readonly string[]; readonly options: readonly ResidualCatalogEntry[]; readonly activeIndex?: number; + /** + * Claim printable keys for a `>` filter row so the list narrows as you type. + * Off by default so other satellite lists keep j/k navigation. + */ + readonly typeToFilter?: boolean; /** Renderer factory override for headless mounting in tests. */ readonly createRenderer?: () => Promise; } @@ -100,8 +105,11 @@ export async function runListModal(config: ListModalConfig): Promise { - settle(residualIdFromSelection(selection, itemIds) ?? null); + const id = residualIdFromSelection(selection, itemIds); + if (id === undefined) return; + settle(id); }, }); diff --git a/src/tui/overlays.test.ts b/src/tui/overlays.test.ts index 939c7816..5cdc0ec2 100644 --- a/src/tui/overlays.test.ts +++ b/src/tui/overlays.test.ts @@ -2,7 +2,7 @@ * Wave 5: primary overlays — open / navigate / Esc restore + resize floors. */ import { describe, expect, test } from "bun:test"; -import { rgbToHex } from "@opentui/core"; +import { rgbToHex, type KeyEvent } from "@opentui/core"; import { IDLE_TRANSCRIPT_FLOOR, OVERLAY_TRANSCRIPT_FLOOR } from "./geometry/index"; import { focusOwner, scrollLease } from "./focus/index"; import { withTestRenderer } from "./harness"; @@ -18,6 +18,7 @@ import { clearShellOverlayHooks, closeInsetOverlay, createAppShell, + handleListFilterKey, moveOverlaySelection, openListOverlay, pageOverlaySelection, @@ -450,6 +451,44 @@ describe("overlay accept callbacks", () => { }); }); +describe("type-to-filter list overlay", () => { + test("no-match Enter leaves overlayList set and does not echo Chose (no matches)", async () => { + await withTestRenderer( + async (h) => { + const shell = createAppShell(h.renderer, { + terminal: { columns: 80, rows: 24 }, + wireKeys: false, + }); + try { + openListOverlay(shell, { + kind: "resume", + items: ["First session", "Second session"], + itemIds: ["s-1", "s-2"], + typeToFilter: true, + }); + const press = (seq: string): boolean => + handleListFilterKey(shell, { + name: seq, + sequence: seq, + ctrl: false, + meta: false, + option: false, + } as unknown as KeyEvent); + for (const ch of "zzzzz") press(ch); + expect(shell.overlayItems).toEqual(["(no matches)"]); + acceptOverlaySelection(shell); + expect(shell.overlayList).not.toBeNull(); + expect(shell.overlayItems).toEqual(["(no matches)"]); + expect(shell.streamLog.some((row) => /Chose \(no matches\)/.test(row.text))).toBe(false); + } finally { + shell.dispose(); + } + }, + { width: 80, height: 24 }, + ); + }); +}); + describe("resize mid-overlay", () => { test("80×24 ↔ larger keeps floors; closed restores idle floor", async () => { await withTestRenderer( diff --git a/src/tui/palette-paint.test.ts b/src/tui/palette-paint.test.ts index 29e1ed87..0f79aa18 100644 --- a/src/tui/palette-paint.test.ts +++ b/src/tui/palette-paint.test.ts @@ -9,6 +9,7 @@ import type { KeyEvent } from "@opentui/core"; import { withTestRenderer } from "./harness"; import type { PaletteCommand } from "./command-catalog"; import { + acceptOverlaySelection, createAppShell, handlePaletteFilterKey, moveOverlaySelection, @@ -183,6 +184,17 @@ describe("palette filters as you type", () => { expect(shell.overlayKind).toBe("palette"); }); }); + + test("type-to-filter no-match Enter leaves the palette open", async () => { + await withPalette((shell) => { + for (const ch of "zzqq") press(shell, ch); + expect(shell.overlayItems).toEqual(["(no matches)"]); + acceptOverlaySelection(shell); + expect(shell.overlayKind).toBe("palette"); + expect(shell.overlayList).not.toBeNull(); + expect(shell.overlayItems).toEqual(["(no matches)"]); + }); + }); }); const DESCRIBED_CATALOG: readonly PaletteCommand[] = [ diff --git a/src/tui/pick-session.test.ts b/src/tui/pick-session.test.ts index 2077b5bd..1f91e7e8 100644 --- a/src/tui/pick-session.test.ts +++ b/src/tui/pick-session.test.ts @@ -1,17 +1,60 @@ import { describe, test, expect } from "bun:test"; -import { isResumableByDefault } from "./pick-session.js"; -describe("isResumableByDefault", () => { - test("in-progress sessions are resumable", () => { - expect(isResumableByDefault({ status: "running" })).toBe(true); +import { sessionResumeLabel, recentResumeSessions, RESUME_PICKER_LIMIT } from "./pick-session.js"; +import type { SessionSummary } from "../session/index.js"; + +function summary(overrides: Partial = {}): SessionSummary { + return { + sessionId: "00000000-0000-7000-8000-000000000000", + task: "Ship picker", + startedAt: 1_700_000_000_000, + updatedAt: 1_700_000_000_000, + status: "done", + ...overrides, + }; +} + +describe("sessionResumeLabel", () => { + test("uses updatedAt for relative age, not startedAt", () => { + const now = Date.now(); + const label = sessionResumeLabel( + summary({ + startedAt: now - 48 * 60 * 60 * 1000, + updatedAt: now - 5 * 60 * 1000, + status: "done", + }), + ); + expect(label).toBe("Ship picker · 5m ago · done"); + }); + + test("includes completed and crashed statuses in the row", () => { + expect(sessionResumeLabel(summary({ status: "failed" }))).toContain("failed"); + expect(sessionResumeLabel(summary({ status: "crashed" }))).toContain("crashed"); }); - test("interrupted sessions are resumable without --force", () => { - expect(isResumableByDefault({ status: "cancelled" })).toBe(true); + test("falls back to Untitled session when the task is blank", () => { + const label = sessionResumeLabel(summary({ task: " " })); + expect(label.startsWith("Untitled session ·")).toBe(true); + }); +}); + +describe("recentResumeSessions", () => { + test("keeps at most ten newest-first rows", () => { + const rows = Array.from({ length: 12 }, (_, i) => + summary({ + sessionId: `00000000-0000-7000-8000-${String(i).padStart(12, "0")}`, + task: `session ${i}`, + }), + ); + const kept = recentResumeSessions(rows); + expect(kept).toHaveLength(RESUME_PICKER_LIMIT); + expect(kept[0]?.task).toBe("session 0"); + expect(kept[9]?.task).toBe("session 9"); + expect(kept.some((row) => row.task === "session 10")).toBe(false); }); - test("completed and failed sessions need --force", () => { - expect(isResumableByDefault({ status: "done" })).toBe(false); - expect(isResumableByDefault({ status: "failed" })).toBe(false); + test("leaves a shorter catalog unchanged", () => { + const rows = [summary({ task: "only" })]; + expect(recentResumeSessions(rows)).toEqual(rows); }); }); diff --git a/src/tui/pick-session.ts b/src/tui/pick-session.ts index 82a9f313..5547a6d7 100644 --- a/src/tui/pick-session.ts +++ b/src/tui/pick-session.ts @@ -3,38 +3,34 @@ import { listSessions, type SessionSummary } from "../session/index.js"; import { runListModal } from "./list-modal.js"; import { formatRelativeTime } from "./format-relative-time.js"; -// Interrupted (cancelled) sessions are prime resume candidates alongside -// in-progress ones; only done/failed runs need --force to reopen. -export function isResumableByDefault(session: Pick): boolean { - return session.status === "running" || session.status === "cancelled"; -} +export const RESUME_PICKER_LIMIT = 10; export function sessionResumeLabel(session: SessionSummary): string { const title = session.task.trim().length > 0 ? session.task.trim() : "Untitled session"; - return `${title} · ${formatRelativeTime(session.startedAt)} · ${session.status}`; + return `${title} · ${formatRelativeTime(session.updatedAt)} · ${session.status}`; } -export async function pickSession( - cwd: string, - options?: { includeCompleted?: boolean }, -): Promise { - let sessions = await listSessions(cwd); - if (options?.includeCompleted !== true) { - sessions = sessions.filter(isResumableByDefault); - } - if (sessions.length === 0) { - process.stderr.write( - options?.includeCompleted === true - ? `${COMMAND_NAME}: no previous sessions found in this directory.\n` - : `${COMMAND_NAME}: no in-progress sessions found (use --force to resume completed runs).\n`, - ); +/** Newest-first catalog already sorted; keep the most recent `limit` rows. */ +export function recentResumeSessions( + sessions: readonly SessionSummary[], + limit = RESUME_PICKER_LIMIT, +): SessionSummary[] { + return sessions.slice(0, limit); +} + +export async function pickSession(cwd: string): Promise { + const catalog = await listSessions(cwd); + if (catalog.length === 0) { + process.stderr.write(`${COMMAND_NAME}: no previous sessions found in this directory.\n`); return null; } + const sessions = recentResumeSessions(catalog); const picked = await runListModal({ title: "Resume conversation", kind: "resume", heading: ["Choose a previous session in this checkout"], + typeToFilter: true, options: sessions.map((session) => ({ id: session.sessionId, label: sessionResumeLabel(session), diff --git a/src/tui/product-host.test.ts b/src/tui/product-host.test.ts index 98836964..e9e1c438 100644 --- a/src/tui/product-host.test.ts +++ b/src/tui/product-host.test.ts @@ -569,6 +569,8 @@ describe("flat type-to-filter model picker", () => { expect(host.shell.overlayItems).toEqual(["(no matches)"]); acceptOverlaySelection(host.shell); expect(selected).toEqual([]); + expect(host.shell.overlayList).not.toBeNull(); + expect(host.shell.overlayItems).toEqual(["(no matches)"]); } finally { host.dispose(); harness.destroy(); diff --git a/src/tui/product-host.ts b/src/tui/product-host.ts index 839333e8..915411e7 100644 --- a/src/tui/product-host.ts +++ b/src/tui/product-host.ts @@ -576,7 +576,7 @@ export async function mountProductHost(config: ProductHostConfig): Promise currentDescribeModel?.(itemId) ?? null, diff --git a/src/tui/runner.ts b/src/tui/runner.ts index 6bb21b07..bd2efa1c 100644 --- a/src/tui/runner.ts +++ b/src/tui/runner.ts @@ -668,7 +668,7 @@ export async function runTUI(initialConfig: Config): Promise { let resumeSeed: ResumeSeed = FRESH_RESUME_SEED; if (config.resumePicker) { - const picked = await pickSession(config.cwd, { includeCompleted: config.force }); + const picked = await pickSession(config.cwd); if (picked === null) return 0; sessionId = picked.sessionId; resumeSkipInitialTask = true; diff --git a/src/tui/shell.ts b/src/tui/shell.ts index 6a88de53..9a66c75a 100644 --- a/src/tui/shell.ts +++ b/src/tui/shell.ts @@ -3564,7 +3564,7 @@ export interface OpenListOverlayOpts { readonly echoChoice?: boolean; /** * Claim printable keys for a `>` filter row so the list narrows as you type. - * Opt-in per open (model picker, palette). Overlays without it keep j/k + * Opt-in per open (model picker, palette, resume). Overlays without it keep j/k * navigation; with it, j/k type into the filter and arrows still navigate. */ readonly typeToFilter?: boolean; @@ -3839,9 +3839,9 @@ function repaintPalette(shell: AppShell): void { * Keys a type-to-filter list claims while it is open, so the `>` row filters * as you type. * - * Opt-in per open (`typeToFilter`): palette and the flat model picker give up - * j/k navigation so printable keys feed the filter. Overlays without - * type-to-filter (permissions, resume, workers, copy, …) keep j/k. Arrow and + * Opt-in per open (`typeToFilter`): palette, the flat model picker, and the + * resume picker give up j/k navigation so printable keys feed the filter. + * Overlays without type-to-filter (permissions, workers, copy, …) keep j/k. Arrow and * page keys are never claimed here, so they keep working in every overlay * including type-to-filter ones. */ @@ -4338,22 +4338,25 @@ export function acceptOverlaySelection(shell: AppShell): void { const idx = shell.overlayList.activeIndex; const label = shell.overlayItems[idx] ?? `item ${idx}`; const kind = shell.overlayKind ?? "demo"; + const bag = internals.get(shell); if (kind === "palette") { const cmd = shell.paletteCommands[idx]; - closeInsetOverlay(shell); - if (cmd) dispatchPaletteSelection(shell, cmd); - else { - appendStreamRow(shell, { - role: "system", - text: `palette: no action for ${label}`, - }); + if (!cmd) { + // Type-to-filter plants a "(no matches)" row with no command. Stay open. + // Slash popup (`typeToFilter: false`) still closes — intentional dismiss. + if (bag?.paletteFilter?.typeToFilter === true && !isSlashPopupOpen(shell)) return; + closeInsetOverlay(shell); + return; } + closeInsetOverlay(shell); + dispatchPaletteSelection(shell, cmd); return; } - const bag = internals.get(shell); const id = bag?.overlayItemIds[idx]; + // Type-to-filter plants "(no matches)" with an empty-id sentinel. Stay open. + if (id === "") return; const value = bag?.overlayItemValues[idx]; const selection: OverlaySelection = { kind, diff --git a/tests/integration/crash-finalize.test.ts b/tests/integration/crash-finalize.test.ts index c3df4ef7..755d4e13 100644 --- a/tests/integration/crash-finalize.test.ts +++ b/tests/integration/crash-finalize.test.ts @@ -6,7 +6,6 @@ import { describe, expect, test } from "bun:test"; import { generateSessionId, sessionDir } from "../../src/session/index.js"; import type { RunState } from "../../src/session/state.js"; -import { isResumableByDefault } from "../../src/tui/pick-session.js"; const FIXTURE = join(import.meta.dirname, "../fixtures/crash-run/simulate-crash.ts"); const RUN_END_FIXTURE = join( @@ -51,7 +50,6 @@ describe("integration — crash finalizes run.json", () => { expect(state.error).toContain("simulated crash"); expect(state.task).toBe("simulated crash task"); expect(state.model).toBe("test-provider:test-model"); - expect(isResumableByDefault(state)).toBe(false); } finally { rmSync(cwd, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });