authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-27 22:29:30-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-27 22:29:30-07:00
log2fbe313096b54a3422e101ed1bcc3589f6cf371c
tree16fea27b34e8b57301819e0bb0158a05e769cc1e
parent850e4582e4c828b1533a400bf56a532721239198
signature Signed by PGP key B5690EEEBB952194

fix(desktop): allow preview automation in agent-created threads (#8483)


4 files changed, 54 insertions(+), 5 deletions(-)

apps/desktop/src/ipc/methods/preview.test.ts+43
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { it as effectIt } from "@effect/vitest";1import { it as effectIt } from "@effect/vitest";
2import { PreviewAutomationStatus } from "@t3tools/contracts";
2import * as Cause from "effect/Cause";3import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";5import * as Exit from "effect/Exit";
...@@ -51,4 +52,46 @@ describe("preview IPC methods", () => {...@@ -51,4 +52,46 @@ describe("preview IPC methods", () => {
51 },52 },
52 ),53 ),
53 );54 );
55
56 effectIt.effect("returns automation status for long runtime tab ids", () =>
57 Effect.gen(function* () {
58 const tabId =
59 `["environment-1","thread:delegated-task:${"a".repeat(120)}",` +
60 `"server-epoch-1","preview-1"]`;
61 const status = {
62 available: false,
63 visible: true,
64 tabId,
65 url: null,
66 title: null,
67 loading: false,
68 };
69 const manager = PreviewManager.PreviewManager.of({
70 automationStatus: () => Effect.succeed(status),
71 } as unknown as PreviewManager.PreviewManager["Service"]);
72
73 expect(tabId.length).toBeGreaterThan(128);
74 expect(
75 yield* PreviewIpc.automationStatus
76 .handler({ tabId })
77 .pipe(Effect.provideService(PreviewManager.PreviewManager, manager)),
78 ).toEqual(status);
79 }),
80 );
81
82 it("keeps the public automation status tab id limit", () => {
83 const encode = Schema.encodeUnknownSync(PreviewAutomationStatus);
84 const tabId = "t".repeat(129);
85
86 expect(() =>
87 encode({
88 available: false,
89 visible: true,
90 tabId,
91 url: null,
92 title: null,
93 loading: false,
94 }),
95 ).toThrow();
96 });
54});97});
apps/desktop/src/ipc/methods/preview.ts+2-2
...@@ -5,6 +5,7 @@ import {...@@ -5,6 +5,7 @@ import {
5 DesktopPreviewAutomationEvaluateInputSchema,5 DesktopPreviewAutomationEvaluateInputSchema,
6 DesktopPreviewAutomationPressInputSchema,6 DesktopPreviewAutomationPressInputSchema,
7 DesktopPreviewAutomationScrollInputSchema,7 DesktopPreviewAutomationScrollInputSchema,
8 DesktopPreviewAutomationStatusSchema,
8 DesktopPreviewAutomationTypeInputSchema,9 DesktopPreviewAutomationTypeInputSchema,
9 DesktopPreviewAutomationWaitForInputSchema,10 DesktopPreviewAutomationWaitForInputSchema,
10 DesktopPreviewConfigInputSchema,11 DesktopPreviewConfigInputSchema,
...@@ -20,7 +21,6 @@ import {...@@ -20,7 +21,6 @@ import {
20 DesktopPreviewWebviewConfigSchema,21 DesktopPreviewWebviewConfigSchema,
21 PreviewAnnotationSubmissionResultSchema,22 PreviewAnnotationSubmissionResultSchema,
22 PreviewAutomationSnapshot,23 PreviewAutomationSnapshot,
23 PreviewAutomationStatus,
24} from "@t3tools/contracts";24} from "@t3tools/contracts";
25import * as Effect from "effect/Effect";25import * as Effect from "effect/Effect";
26import * as Schema from "effect/Schema";26import * as Schema from "effect/Schema";
...@@ -282,7 +282,7 @@ export const copyArtifactToClipboard = DesktopIpc.makeIpcMethod({...@@ -282,7 +282,7 @@ export const copyArtifactToClipboard = DesktopIpc.makeIpcMethod({
282export const automationStatus = DesktopIpc.makeIpcMethod({282export const automationStatus = DesktopIpc.makeIpcMethod({
283 channel: IpcChannels.PREVIEW_AUTOMATION_STATUS_CHANNEL,283 channel: IpcChannels.PREVIEW_AUTOMATION_STATUS_CHANNEL,
284 payload: DesktopPreviewTabInputSchema,284 payload: DesktopPreviewTabInputSchema,
285 result: PreviewAutomationStatus,285 result: DesktopPreviewAutomationStatusSchema,
286 handler: Effect.fn("desktop.ipc.preview.automationStatus")(function* ({ tabId }) {286 handler: Effect.fn("desktop.ipc.preview.automationStatus")(function* ({ tabId }) {
287 const manager = yield* PreviewManager.PreviewManager;287 const manager = yield* PreviewManager.PreviewManager;
288 return yield* manager.automationStatus(tabId);288 return yield* manager.automationStatus(tabId);
apps/desktop/src/preview/Manager.ts+2-2
...@@ -7,6 +7,7 @@...@@ -7,6 +7,7 @@
7 */7 */
8import type {8import type {
9 DesktopPreviewAnnotationTheme,9 DesktopPreviewAnnotationTheme,
10 DesktopPreviewAutomationStatus,
10 DesktopPreviewColorScheme,11 DesktopPreviewColorScheme,
11 DesktopPreviewFavicon,12 DesktopPreviewFavicon,
12 DesktopPreviewPointerEvent,13 DesktopPreviewPointerEvent,
...@@ -25,7 +26,6 @@ import type {...@@ -25,7 +26,6 @@ import type {
25 PreviewAutomationNetworkEntry,26 PreviewAutomationNetworkEntry,
26 PreviewAutomationScrollInput,27 PreviewAutomationScrollInput,
27 PreviewAutomationSnapshot,28 PreviewAutomationSnapshot,
28 PreviewAutomationStatus,
29 PreviewAutomationTypeInput,29 PreviewAutomationTypeInput,
30 PreviewAutomationWaitForInput,30 PreviewAutomationWaitForInput,
31} from "@t3tools/contracts";31} from "@t3tools/contracts";
...@@ -4089,7 +4089,7 @@ export class PreviewManager extends Context.Service<...@@ -4089,7 +4089,7 @@ export class PreviewManager extends Context.Service<
4089 ) => Effect.Effect<DesktopPreviewRecordingArtifact, PreviewManagerError>;4089 ) => Effect.Effect<DesktopPreviewRecordingArtifact, PreviewManagerError>;
4090 readonly automationStatus: (4090 readonly automationStatus: (
4091 tabId: string,4091 tabId: string,
4092 ) => Effect.Effect<PreviewAutomationStatus, PreviewManagerError>;4092 ) => Effect.Effect<DesktopPreviewAutomationStatus, PreviewManagerError>;
4093 readonly automationSnapshot: (4093 readonly automationSnapshot: (
4094 tabId: string,4094 tabId: string,
4095 ) => Effect.Effect<PreviewAutomationSnapshot, PreviewManagerError>;4095 ) => Effect.Effect<PreviewAutomationSnapshot, PreviewManagerError>;
packages/contracts/src/ipc.ts+7-1
...@@ -584,6 +584,12 @@ export const DesktopPreviewTabIdSchema = Schema.String.check(Schema.isTrimmed())...@@ -584,6 +584,12 @@ export const DesktopPreviewTabIdSchema = Schema.String.check(Schema.isTrimmed())
584 Schema.isNonEmpty(),584 Schema.isNonEmpty(),
585);585);
586586
587export const DesktopPreviewAutomationStatusSchema = Schema.Struct({
588 ...PreviewAutomationStatus.fields,
589 tabId: Schema.NullOr(DesktopPreviewTabIdSchema),
590});
591export type DesktopPreviewAutomationStatus = typeof DesktopPreviewAutomationStatusSchema.Type;
592
587export const DesktopPreviewNavStatusSchema = Schema.Union([593export const DesktopPreviewNavStatusSchema = Schema.Union([
588 Schema.Struct({ kind: Schema.Literal("Idle") }),594 Schema.Struct({ kind: Schema.Literal("Idle") }),
589 Schema.Struct({595 Schema.Struct({
...@@ -1217,7 +1223,7 @@ export interface DesktopPreviewBridge {...@@ -1217,7 +1223,7 @@ export interface DesktopPreviewBridge {
1217 onFrame: (listener: (frame: DesktopPreviewRecordingFrame) => void) => () => void;1223 onFrame: (listener: (frame: DesktopPreviewRecordingFrame) => void) => () => void;
1218 };1224 };
1219 automation: {1225 automation: {
1220 status: (tabId: string) => Promise<PreviewAutomationStatus>;1226 status: (tabId: string) => Promise<DesktopPreviewAutomationStatus>;
1221 snapshot: (tabId: string) => Promise<PreviewAutomationSnapshot>;1227 snapshot: (tabId: string) => Promise<PreviewAutomationSnapshot>;
1222 click: (tabId: string, input: PreviewAutomationClickInput) => Promise<void>;1228 click: (tabId: string, input: PreviewAutomationClickInput) => Promise<void>;
1223 type: (tabId: string, input: PreviewAutomationTypeInput) => Promise<void>;1229 type: (tabId: string, input: PreviewAutomationTypeInput) => Promise<void>;