Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/desktop/renderer-architecture.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -770,7 +770,7 @@
"useNewTaskChoice": 1,
"useOnboardingSnapshot": 1,
"usePlanModeState": 1,
"useRef": 24,
"useRef": 23,
"useSessionCollaborationDialog": 1,
"useSessionEventHealthPolling": 1,
"useSessionNavigationReads": 1,
Expand DownExpand Up@@ -891,7 +891,7 @@
"react": 1
},
"importSpecifiers": 148,
"nonTriviaTokens": 15602
"nonTriviaTokens": 15600
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,10 +30,10 @@ import {
writeNewTaskReloadDraft,
} from '../../renderer/new-task-reload-intent.js';

type Summary = { id: string; lastMessageAt?: number };
type Summary = { id: string; lastMessageAt?: number; isArchived: boolean };

function session(id: string, lastMessageAt?: number): Summary {
return { id, lastMessageAt };
function session(id: string, lastMessageAt?: number, isArchived = false): Summary {
return { id, lastMessageAt, isArchived };
}

function harness(activeId?: string) {
Expand DownExpand Up@@ -88,6 +88,33 @@ describe('bootstrap selection lease', () => {
assert.equal(state.activeId(), undefined);
});

for (const { name, initialActiveId, sessions, expected } of [
{
name: 'the freshest session is archived',
initialActiveId: undefined,
sessions: [session('archived', 2, true), session('active', 1)],
expected: 'active',
},
{
name: 'the bootstrap-owned selection is archived',
initialActiveId: 'archived',
sessions: [session('archived', 2, true), session('active', 1)],
expected: 'active',
},
{
name: 'every session is archived',
initialActiveId: 'archived',
sessions: [session('archived', 1, true)],
expected: undefined,
},
] as const) {
it(`skips archived sessions when ${name}`, () => {
const state = harness(initialActiveId);
assert.equal(state.lease.reconcile(sessions), true);
assert.equal(state.activeId(), expected);
});
}

it('does not reconcile after release', () => {
const state = harness();
state.lease.release();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,7 +108,7 @@ function ports(
return {
activeIdRef: { current: activeSessionId },
sessionsRef: { current: sessions },
pendingSessionRowActionsRef: { current: new Set<string>() },
acquireAutomaticQueryBlock: () => ({ release: () => undefined }),
activateSession: (sessionId) => calls.push(`activate:${sessionId ?? 'none'}`),
clearActiveMessages: () => calls.push('clear-messages'),
clearSessionRendererState: (sessionId) => calls.push(`clear:${sessionId}`),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,14 +90,22 @@ describe('revision-family session row actions', () => {
});
const branch = summary('branch', { parentSessionId: 'root', branchOfTurnId: 'turn-1' });
const activeIdRef = { current: 'root' as string | undefined };
const service = createService(calls);
const actions = createSessionNavigationRowActions({
uiLocale: 'en',
activeIdRef,
acquireAutomaticQueryBlock: (ids) => {
calls.push(`acquire:${ids.join(',')}`);
return { release: () => calls.push('release') };
},
clearActiveMessages: () => undefined,
clearSessionRendererState: (id) => { cleared.push(id); },
pendingSessionRowActionsRef: { current: new Set<string>() },
refreshSessions: async () => [root, version, branch],
service: createService(calls),
refreshSessions: async () => {
calls.push('refresh');
return [root, version, branch];
},
service,
sessionsRef: { current: [root, version, branch] },
setActiveId: (id) => { selections.push(id); activeIdRef.current = id; },
toastApi: {
Expand All@@ -115,17 +123,84 @@ describe('revision-family session row actions', () => {

assert.deepEqual(calls, [
'flag:version:true:true',
'refresh',
'rename:branch:Independent branch:true',
'refresh',
'acquire:root,version',
'archive:version:true',
'refresh',
'release',
// The delete asks the Host how many subtasks it would archive before the
// confirm, then removes.
'preview:root',
'acquire:root,version',
// `root` is not archived, so the delete states no archived premise —
// requiring one would refuse every delete from the rail.
'remove:root:true:false',
'refresh',
'release',
]);
assert.deepEqual(selections, [undefined, undefined]);
assert.deepEqual(cleared, ['root', 'version', 'root', 'version']);

service.archive = async () => { throw new Error('archive failed'); };
await actions.archiveSession('root');
assert.deepEqual(calls.slice(-2), ['acquire:root,version', 'release']);
});

it('holds one query block through a bulk archive refresh', async () => {
const calls: string[] = [];
const root = summary('root');
const version = summary('version', {
revisionRootSessionId: 'root',
revisionParentSessionId: 'root',
});
const other = summary('other');
let rejectRefresh = false;
const actions = createSessionNavigationRowActions({
uiLocale: 'en',
activeIdRef: { current: undefined },
acquireAutomaticQueryBlock: (ids) => {
calls.push(`acquire:${ids.join(',')}`);
return { release: () => calls.push('release') };
},
clearActiveMessages: () => undefined,
clearSessionRendererState: () => undefined,
pendingSessionRowActionsRef: { current: new Set<string>() },
refreshSessions: async () => {
calls.push('refresh');
if (rejectRefresh) throw new Error('refresh failed');
return [];
},
service: createService(calls),
sessionsRef: { current: [root, version, other] },
setActiveId: () => undefined,
toastApi: {
success: () => undefined,
error: () => undefined,
confirm: async () => true,
},
});

await actions.archiveSelected(['version', 'other']);

assert.deepEqual(calls, [
'acquire:root,version,other',
'archive:version:true',
'archive:other:true',
'refresh',
'release',
]);

calls.length = 0;
rejectRefresh = true;
await assert.rejects(actions.archiveSelected(['other']), /refresh failed/);
assert.deepEqual(calls, [
'acquire:other',
'archive:other:true',
'refresh',
'release',
]);
});
});

Expand All@@ -138,9 +213,11 @@ function deleteHarness(
const calls: string[] = [];
const confirms: Array<{ title: string; description: string }> = [];
const successes: Array<{ title: string; description?: string }> = [];
let leaseReleased = false;
const actions = createSessionNavigationRowActions({
uiLocale: 'en',
activeIdRef: { current: undefined },
acquireAutomaticQueryBlock: () => ({ release: () => { leaseReleased = true; } }),
clearActiveMessages: () => undefined,
clearSessionRendererState: () => undefined,
pendingSessionRowActionsRef: { current: new Set<string>() },
Expand All@@ -154,7 +231,7 @@ function deleteHarness(
confirm: async (options) => { confirms.push({ title: options.title, description: options.description }); return true; },
},
});
return { actions, calls, confirms, successes };
return { actions, calls, confirms, successes, wasLeaseReleased: () => leaseReleased };
}

describe('delete confirm warns off the Host preview, toast reports the Host count', () => {
Expand DownExpand Up@@ -219,7 +296,7 @@ describe('delete confirm warns off the Host preview, toast reports the Host coun
});

it('stays silent on the toast when a concurrent restore calls the delete off', async () => {
const { actions, confirms, successes } = deleteHarness(
const { actions, confirms, successes, wasLeaseReleased } = deleteHarness(
[summary('parent', { name: 'hi' })],
'restored',
0,
Expand All@@ -232,5 +309,6 @@ describe('delete confirm warns off the Host preview, toast reports the Host coun
assert.match(confirms[0].description, /kept and moved to Archived/);
// But nothing was deleted, so nothing moved to the archive.
assert.deepEqual(successes, [{ title: 'hi was restored, so it was kept', description: undefined }]);
assert.equal(wasLeaseReleased(), true);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -142,6 +142,7 @@ function createActions(input: {
return createSessionNavigationRowActions({
uiLocale: 'en',
activeIdRef: input.activeIdRef,
acquireAutomaticQueryBlock: () => ({ release: () => undefined }),
clearActiveMessages: () => undefined,
clearSessionRendererState: (id) => {
input.harness.cleared.push(id);
Expand Down
Loading