Skip to content

Commit d86d1ee

Browse files
fix(desktop): reopen artifact previews after reconnect
1 parent 56fe46f commit d86d1ee

4 files changed

Lines changed: 48 additions & 25 deletions

File tree

‎apps/desktop/src/main/__tests__/managed-artifact-preview.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ test('isolates leases by origin and rejects credentials for another preview', as
8585
await assert.rejects(fetch(first.url));
8686
assert.equal(await (await fetch(second.url)).text(), 'second');
8787
await assert.rejects(service.prepare('host1', client(), 's1', 'a1'), /closed/);
88+
service.openScope('host1');
89+
assert.equal(await (await fetch((await service.prepare('host1', client(), 's1', 'a1')).url)).text(), html);
8890
await service.revoke('host2', 's1', 'a1');
8991
await assert.rejects(fetch(second.url));
9092
} finally { await service.close(); }

‎apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts‎

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -523,47 +523,63 @@ test('tears down the whole candidate when the Host connection closes', async ()
523523
assert.equal(host.closeCalls, 1);
524524
});
525525

526-
test('closes managed Artifact previews when the Host connection closes', async () => {
526+
test('closes old managed Artifact previews and reopens the scope after reconnect', async () => {
527527
const ipc = ipcHarness();
528-
const host = connectionHarness('preview-closed');
528+
const firstHost = connectionHarness('preview-first');
529529
const preview = new ManagedArtifactPreview();
530530
const bytes = Buffer.from('<!doctype html><title>Preview</title>');
531-
const candidate = await createDesktopRuntimeHostCandidate(host.connection, {
531+
const candidateDeps = {
532532
...deps(ipc),
533-
registerClientIpc: (_client, _ipc, _controls, _target, scope) =>
534-
() => preview.closeScope(scope.targetEpoch),
535-
});
533+
registerClientIpc: (_client, _ipc, _controls, _target, scope) => {
534+
preview.openScope(scope.targetEpoch);
535+
return () => preview.closeScope(scope.targetEpoch);
536+
},
537+
} satisfies DesktopRuntimeHostCandidateDeps;
538+
const firstCandidate = await createDesktopRuntimeHostCandidate(firstHost.connection, candidateDeps);
539+
let secondCandidate: Awaited<ReturnType<typeof createDesktopRuntimeHostCandidate>> | undefined;
540+
const source = {
541+
getArtifact: async () => ({
542+
id: 'artifact-1',
543+
sessionId: 'session-1',
544+
turnId: 'turn-1',
545+
createdAt: 0,
546+
name: 'preview.html',
547+
kind: 'html' as const,
548+
sizeBytes: bytes.length,
549+
source: 'tool_result' as const,
550+
}),
551+
streamArtifact: async (_sessionId: string, _artifactId: string, write: (chunk: Uint8Array) => Promise<void>) => {
552+
await write(bytes);
553+
return bytes.length;
554+
},
555+
};
536556

537557
try {
538558
const endpoint = await preview.prepare(
539559
TEST_TARGET_EPOCH,
540-
{
541-
getArtifact: async () => ({
542-
id: 'artifact-1',
543-
sessionId: 'session-1',
544-
turnId: 'turn-1',
545-
createdAt: 0,
546-
name: 'preview.html',
547-
kind: 'html',
548-
sizeBytes: bytes.length,
549-
source: 'tool_result',
550-
}),
551-
streamArtifact: async (_sessionId, _artifactId, write) => {
552-
await write(bytes);
553-
return bytes.length;
554-
},
555-
},
560+
source,
556561
'session-1',
557562
'artifact-1',
558563
);
559564
assert.equal(await (await fetch(endpoint.url)).text(), bytes.toString());
560565

561-
host.disconnect();
562-
await candidate.closed;
566+
firstHost.disconnect();
567+
await firstCandidate.closed;
563568

564569
await assert.rejects(fetch(endpoint.url));
570+
571+
const secondHost = connectionHarness('preview-second');
572+
secondCandidate = await createDesktopRuntimeHostCandidate(secondHost.connection, candidateDeps);
573+
const replacement = await preview.prepare(
574+
TEST_TARGET_EPOCH,
575+
source,
576+
'session-1',
577+
'artifact-1',
578+
);
579+
assert.equal(await (await fetch(replacement.url)).text(), bytes.toString());
565580
} finally {
566-
await candidate.close();
581+
await firstCandidate.close();
582+
await secondCandidate?.close();
567583
await preview.close();
568584
}
569585
});

‎apps/desktop/src/main/managed-artifact-preview.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export class ManagedArtifactPreview {
5656

5757
constructor(private readonly ttlMs = PREVIEW_TTL_MS) {}
5858

59+
openScope(scope: string): void {
60+
this.retiredScopes.delete(scope);
61+
}
62+
5963
async releaseUrl(url: string): Promise<void> {
6064
const lease = [...this.leases].find((entry) => entry.url === url);
6165
if (lease) await this.release(lease);

‎apps/desktop/src/main/runtime-host-boot.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,7 @@ function registerHostClientIpc(
18351835
});
18361836
registerOnboardingIpc({ onboardingService, ipcMain: scopedIpc });
18371837
registerTaskSubmissionReadinessIpc(taskSubmissionReadinessService, scopedIpc);
1838+
managedArtifactPreview.openScope(scope.targetEpoch);
18381839
return async () => {
18391840
clientPluginTransport.release(client);
18401841
unsubscribeConfigurationChanges();

0 commit comments

Comments
 (0)