@@ -791,7 +790,7 @@ function App() {
commands={active?.commands ?? []}
onClose={() => setPalette(false)}
onWorkspace={b.selectWorkspace}
- onSession={b.showSession}
+ onSession={(session) => void b.previewSavedSession(session)}
onNavigate={b.navigate}
onFile={(file) => {
setPaletteFiles((previous) => ({
diff --git a/src/renderer/components/CommandPalette.tsx b/src/renderer/components/CommandPalette.tsx
index f977503..07e2d79 100644
--- a/src/renderer/components/CommandPalette.tsx
+++ b/src/renderer/components/CommandPalette.tsx
@@ -97,7 +97,7 @@ export function CommandPalette({
...sessions.map((s) => ({
id: `session:${s.file}`,
title: s.title,
- detail: `${s.messageCount} messages / ${s.model || 'Pi'}`,
+ detail: `Preview saved conversation / ${s.messageCount} messages / ${s.model || 'Pi'}`,
category: 'Sessions' as const,
run: () => onSession(s),
})),
diff --git a/src/renderer/useWorkbench.ts b/src/renderer/useWorkbench.ts
index 663cc5d..2328e08 100644
--- a/src/renderer/useWorkbench.ts
+++ b/src/renderer/useWorkbench.ts
@@ -490,12 +490,6 @@ export function useWorkbench() {
setBusy(false);
}
};
- const showSession = (info: SessionInfo) =>
- run(async () => {
- const token = generation.current;
- const tab = await openSession(info);
- if (tab && token === generation.current) setView('session');
- });
const forkSession = () =>
preview?.ready &&
preview.tree?.canContinue &&
@@ -540,6 +534,11 @@ export function useWorkbench() {
},
[api, report],
);
+ const previewSavedSession = (info: SessionInfo) => {
+ if (opening.current) return;
+ navigate('history');
+ return showPreview(info);
+ };
useEffect(() => {
if (view !== 'history' || previewLoading) return;
if (preview && !preview.loaded)
@@ -780,7 +779,7 @@ export function useWorkbench() {
refresh,
selectWorkspace,
addWorkspace,
- showSession,
+ previewSavedSession,
closeSession,
forkSession,
prepareHandoff,
diff --git a/tests/e2e/app.test.ts b/tests/e2e/app.test.ts
index ccc9f89..dcb912c 100644
--- a/tests/e2e/app.test.ts
+++ b/tests/e2e/app.test.ts
@@ -231,7 +231,7 @@ describe('Figma workbench in Electron', () => {
await win
.getByLabel('Message pi')
.fill('Check keyboard focus in the composer.');
- await win.getByRole('button', { name: 'Send', exact: true }).click();
+ await win.getByRole('button', { name: 'Resume', exact: true }).click();
await win.getByRole('button', { name: 'Stop', exact: true }).waitFor();
await win.screenshot({ path: path.join(shots, '02-active-session.png') });
await win.getByRole('button', { name: 'Stop', exact: true }).click();
diff --git a/tests/e2e/long-session.test.ts b/tests/e2e/long-session.test.ts
index 8823d1a..0c70f28 100644
--- a/tests/e2e/long-session.test.ts
+++ b/tests/e2e/long-session.test.ts
@@ -72,6 +72,7 @@ describe('long session in Electron', () => {
it('opens 4,500 mixed entries with a bounded DOM and revisits them quickly', async () => {
const start = performance.now();
await page.locator('.wb-sidebar .wb-session-item').first().click();
+ await page.getByRole('button', { name: 'Fork from here', exact: true }).click();
await page
.locator('.wb-transcript')
.getByText('Iteration 1499', { exact: true })
diff --git a/tests/e2e/session-start.test.ts b/tests/e2e/session-start.test.ts
index 9294017..83be233 100644
--- a/tests/e2e/session-start.test.ts
+++ b/tests/e2e/session-start.test.ts
@@ -143,6 +143,54 @@ describe('session start choice in Electron', () => {
await resize(1280, 1000);
});
+ it('previews sidebar shortcuts with keyboard selection and preserves drafts at the minimum window size', async () => {
+ await newSession();
+ await page.getByLabel('Message pi').fill('Keep this separate task for later.');
+ const sidebar = page.getByRole('complementary', { name: 'Workspace navigation' });
+ const selected = sidebar.getByRole('button', { name: 'Preview Check keyboard focus', exact: true });
+ await selected.focus();
+ await page.keyboard.press('Enter');
+ await page.locator('.wb-transcript').getByText('Saved response for Check keyboard focus.', { exact: true }).waitFor();
+ expect(await selected.getAttribute('aria-current')).toBe('page');
+ expect(await selected.textContent()).toContain('Preview');
+ expect(await page.getByLabel('Message pi').inputValue()).toBe('Keep this unsent continuation too.');
+ await capture('sidebar-saved-preview.png');
+ await resize(900, 600);
+ const older = sidebar.getByRole('button', { name: 'Preview Older saved task', exact: true });
+ await older.scrollIntoViewIfNeeded();
+ await older.click();
+ await page.locator('.wb-transcript').getByText('Saved response for Older saved task.', { exact: true }).waitFor();
+ expect(await selected.getAttribute('aria-current')).toBeNull();
+ expect(await older.getAttribute('aria-current')).toBe('page');
+ await page.getByRole('button', { name: 'Resume', exact: true }).scrollIntoViewIfNeeded();
+ await capture('sidebar-saved-preview-narrow.png');
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
+ expect(await sessionRecords()).toEqual([]);
+ expect(await fs.readFile(source, 'utf8')).toBe(sourceBytes);
+ await newSession();
+ expect(await page.getByLabel('Message pi').inputValue()).toBe('Keep this separate task for later.');
+ await resize(1280, 1000);
+ });
+
+ it('previews Quick switch results by keyboard without opening a runtime', async () => {
+ await page.getByRole('button', { name: 'Quick switch', exact: true }).click();
+ const palette = page.getByRole('dialog', { name: 'Quick switch' });
+ await palette.getByRole('tab', { name: /Sessions/ }).click();
+ const search = palette.getByRole('combobox');
+ await search.fill('Check keyboard focus');
+ expect(await palette.getByRole('option').count()).toBe(1);
+ expect(await palette.getByRole('option').textContent()).toContain('Preview saved conversation');
+ await capture('quick-switch-saved-preview.png');
+ await search.press('Enter');
+ await palette.waitFor({ state: 'hidden' });
+ await page.locator('.wb-transcript').getByText('Saved response for Check keyboard focus.', { exact: true }).waitFor();
+ expect(await page.getByLabel('Message pi').inputValue()).toBe('Keep this unsent continuation too.');
+ expect(await sessionRecords()).toEqual([]);
+ expect(await fs.readFile(source, 'utf8')).toBe(sourceBytes);
+ await newSession();
+ expect(await page.getByLabel('Message pi').inputValue()).toBe('Keep this separate task for later.');
+ });
+
it('scopes choices to the selected root and starts a fresh conversation only after Send', async () => {
await newSession();
await page.getByLabel('Message pi').fill('Keep this separate task for later.');
@@ -168,4 +216,32 @@ describe('session start choice in Electron', () => {
await page.getByRole('button', { name: 'Stop', exact: true }).click();
expect(errors).toEqual([]);
});
+
+ it('resumes an explicitly selected sidebar preview and focuses its live tab without another launch or prompt', async () => {
+ const sidebar = page.getByRole('complementary', { name: 'Workspace navigation' });
+ const saved = sidebar.getByRole('button', { name: 'Preview Check keyboard focus', exact: true });
+ await saved.click();
+ await page.locator('.wb-transcript').getByText('Saved response for Check keyboard focus.', { exact: true }).waitFor();
+ const before = await sessionRecords();
+ expect(before.filter((record) => record.type === 'prompt')).toHaveLength(1);
+ await page.getByLabel('Message pi').fill('Continue this saved keyboard task.');
+ await page.getByRole('button', { name: 'Resume', exact: true }).click();
+ await page.getByRole('button', { name: 'Stop', exact: true }).waitFor();
+ await expect.poll(async () => (await sessionRecords()).filter((record) => record.type === 'prompt').length).toBe(2);
+ const resumed = await sessionRecords();
+ expect(resumed.filter((record) => record.type === 'prompt').at(-1)).toMatchObject({
+ cwd: project, message: 'Continue this saved keyboard task.',
+ sessionFile: path.join(path.dirname(source), '1.jsonl'),
+ });
+ await newSession();
+ const live = sidebar.locator('.wb-session-item').filter({ hasText: 'this window' }).filter({ hasText: 'Check keyboard focus' });
+ await live.click();
+ await page.getByRole('heading', { name: 'Check keyboard focus', exact: true }).waitFor();
+ expect(await page.getByRole('button', { name: 'Stop', exact: true }).isVisible()).toBe(true);
+ expect(await sessionRecords()).toEqual(resumed);
+ await capture('open-session-direct-focus.png');
+ await page.getByRole('button', { name: 'Stop', exact: true }).click();
+ expect(await fs.readFile(source, 'utf8')).toBe(sourceBytes);
+ expect(errors).toEqual([]);
+ });
});
diff --git a/tests/integration/CommandPalette.test.tsx b/tests/integration/CommandPalette.test.tsx
index 7289877..fb7af5e 100644
--- a/tests/integration/CommandPalette.test.tsx
+++ b/tests/integration/CommandPalette.test.tsx
@@ -69,6 +69,7 @@ describe('command palette', () => {
);
fireEvent.click(screen.getByRole('tab', { name: /Sessions/ }));
expect(screen.getAllByRole('option')).toHaveLength(2);
+ expect(screen.getAllByText(/Preview saved conversation/)).toHaveLength(2);
rerender(
);
fireEvent.click(screen.getByRole('option', { name: /Second archive/ }));
expect(callbacks.onSession).toHaveBeenCalledExactlyOnceWith(second);
diff --git a/tests/integration/useWorkbench.test.tsx b/tests/integration/useWorkbench.test.tsx
index 5ffd088..1940796 100644
--- a/tests/integration/useWorkbench.test.tsx
+++ b/tests/integration/useWorkbench.test.tsx
@@ -81,8 +81,7 @@ describe('workbench root switching', () => {
await waitFor(() => expect(result.current.ws?.name).toBe('api'));
act(() => result.current.onDraft('Keep my separate task'));
await act(async () => {
- result.current.navigate('history');
- await result.current.showPreview(source);
+ await result.current.previewSavedSession(source);
});
expect(result.current.view).toBe('history');
expect(result.current.preview?.info.file).toBe(source.file);
@@ -122,6 +121,39 @@ describe('workbench root switching', () => {
expect(result.current.detailsRoot).toBe('web');
});
+ it('previews saved work for a missing root and reports unreadable archives without starting Pi', async () => {
+ const fixture = apiFixture();
+ fixture.workspaceCatalog.mockResolvedValue({
+ ...catalog,
+ roots: [{ ...catalog.roots[0], availability: 'missing' }, catalog.roots[1]],
+ });
+ fixture.sessionTree.mockResolvedValue({ data: { leafId: 'saved', canContinue: true, nodes: [] } });
+ fixture.previewSession.mockResolvedValue({ data: [
+ { role: 'user', content: [{ type: 'text', text: 'Recover this saved task' }] },
+ ] });
+ const openSession = vi.fn();
+ const sendPrompt = vi.fn();
+ Object.assign(fixture, { openSession, sendPrompt });
+ const source = {
+ cwd: '/code/api', id: 'source', file: '/sessions/source.jsonl',
+ title: 'Saved task', model: '', timestamp: '', messageCount: 1,
+ };
+ const { result } = renderHook(useWorkbench);
+ await waitFor(() => expect(result.current.ws?.availability).toBe('missing'));
+ await act(async () => { await result.current.previewSavedSession(source); });
+ expect(result.current.view).toBe('history');
+ expect(result.current.preview?.entries[0]).toMatchObject({ text: 'Recover this saved task' });
+ fixture.sessionTree.mockResolvedValue({ error: 'Archive no longer exists' });
+ await act(async () => {
+ await result.current.previewSavedSession({ ...source, file: '/sessions/removed.jsonl' });
+ });
+ expect(result.current.error).toBe('Archive no longer exists');
+ expect(result.current.preview?.ready).toBe(false);
+ expect(result.current.preview?.entries).toEqual([]);
+ expect(openSession).not.toHaveBeenCalled();
+ expect(sendPrompt).not.toHaveBeenCalled();
+ });
+
it('prepares selected history across groups, keeps both drafts and restores the source selection', async () => {
const fixture = apiFixture();
fixture.workspaceCatalog.mockResolvedValue({
@@ -225,20 +257,11 @@ describe('workbench root switching', () => {
const sessionCommands = vi
.fn
()
.mockResolvedValue({ commands: [] });
- Object.assign(fixture, { openSession, sessionCommands });
+ Object.assign(fixture, { openSession, sessionCommands, sendPrompt: vi.fn().mockResolvedValue({ success: true }) });
const { result } = renderHook(useWorkbench);
await waitFor(() => expect(result.current.ws?.name).toBe('api'));
- act(() =>
- result.current.showSession({
- cwd: '/code/api',
- id: 'archive',
- file: '/sessions/archive.jsonl',
- title: 'Build',
- model: '',
- timestamp: '',
- messageCount: 0,
- }),
- );
+ act(() => result.current.onDraft('Build'));
+ await act(async () => { await result.current.send(); });
await waitFor(() => expect(result.current.tabs).toHaveLength(1));
const onEvent = fixture.onSessionEvent.mock.calls[0][0];
const onExit = fixture.onSessionExit.mock.calls[0][0];
@@ -262,13 +285,13 @@ describe('workbench root switching', () => {
});
onExit({ sessionId: 'other-runtime' });
});
- expect(result.current.tabs[0].entries[0]).toMatchObject({ running: true });
+ expect(result.current.tabs[0].entries.at(-1)).toMatchObject({ running: true });
act(() => onExit({ sessionId: 'runtime' }));
expect(result.current.tabs[0]).toMatchObject({
running: false,
exited: true,
});
- expect(result.current.tabs[0].entries[0]).toMatchObject({
+ expect(result.current.tabs[0].entries.at(-1)).toMatchObject({
running: false,
interrupted: true,
output: 'Partial build',