1import type { DesktopHostedAuthState } from "./desktopHostedAuth.ts";
2import * as Schema from "effect/Schema";
3
4import { SnapShotSource } from "./chatAttachment.ts";
5import { EnvironmentId, TrimmedNonEmptyString } from "./baseSchemas.ts";
6import { BrowserProfileId } from "./browserProfile.ts";
7import type {
8 BrowserImportResult,
9 BrowserImportSource,
10 BrowserImportSourceId,
11} from "./browserImport.ts";
12import { AuthAccessTokenResult, AuthSessionState, AuthWebSocketTicketResult } from "./auth.ts";
13import { AdvertisedEndpoint } from "./remoteAccess.ts";
14import { ExecutionEnvironmentDescriptor } from "./environment.ts";
15import { type ClientSettings, type QuitConfirmationMode, SnapShotShortcut } from "./settings.ts";
16import type { EditorId } from "./editor.ts";
17import type { PreviewForwardedShortcut } from "./keybindings.ts";
18
19import type {
20 DesktopAppActivationRequest,
21 DesktopAppActivationResponse,
22} from "./desktopAppActivation.ts";
23
24export interface ContextMenuItem<T extends string = string> {
25 id: T;
26 label: string;
27 destructive?: boolean;
28 disabled?: boolean;
29 /** Renders as a non-interactive section header label. Web fallback only — stripped on desktop native menus. */
30 header?: boolean;
31 /** Icon keyword resolved by the web fallback. Stripped on desktop native menus. */
32 icon?: string;
33 /** Inserts a visual section divider immediately before this item. */
34 separatorBefore?: boolean;
35 /** Shows a check mark. Used to mark the current option inside a submenu. */
36 checked?: boolean;
37 children?: readonly ContextMenuItem<T>[];
38}
39
40export type QuitShortcutHintEvent =
41 | { readonly state: "down"; readonly mode: Exclude<QuitConfirmationMode, "direct"> }
42 | { readonly state: "up" };
43
44export interface ContextMenuItemSchemaType {
45 readonly id: string;
46 readonly label: string;
47 readonly destructive?: boolean;
48 readonly disabled?: boolean;
49 readonly header?: boolean;
50 readonly icon?: string;
51 readonly separatorBefore?: boolean;
52 readonly checked?: boolean;
53 readonly children?: readonly ContextMenuItemSchemaType[];
54}
55
56export const ContextMenuItemSchema: Schema.Codec<ContextMenuItemSchemaType> = Schema.Struct({
57 id: Schema.String,
58 label: Schema.String,
59 destructive: Schema.optionalKey(Schema.Boolean),
60 disabled: Schema.optionalKey(Schema.Boolean),
61 header: Schema.optionalKey(Schema.Boolean),
62 icon: Schema.optionalKey(Schema.String),
63 separatorBefore: Schema.optionalKey(Schema.Boolean),
64 checked: Schema.optionalKey(Schema.Boolean),
65 children: Schema.optionalKey(
66 Schema.Array(
67 Schema.suspend((): Schema.Codec<ContextMenuItemSchemaType> => ContextMenuItemSchema),
68 ),
69 ),
70});
71
72export type DesktopUpdateStatus =
73 | "disabled"
74 | "idle"
75 | "checking"
76 | "up-to-date"
77 | "available"
78 | "downloading"
79 | "downloaded"
80 | "error";
81
82export type DesktopRuntimeArch = "arm64" | "x64" | "other";
83export type DesktopTheme = "light" | "dark" | "system";
84export type DesktopUpdateChannel = "latest" | "nightly";
85export type DesktopAppStageLabel = "Alpha" | "Dev" | "Nightly";
86
87export const DesktopUpdateStatusSchema = Schema.Literals([
88 "disabled",
89 "idle",
90 "checking",
91 "up-to-date",
92 "available",
93 "downloading",
94 "downloaded",
95 "error",
96]);
97export const DesktopRuntimeArchSchema = Schema.Literals(["arm64", "x64", "other"]);
98export const DesktopThemeSchema = Schema.Literals(["light", "dark", "system"]);
99export const DesktopUpdateChannelSchema = Schema.Literals(["latest", "nightly"]);
100export const DesktopAppStageLabelSchema = Schema.Literals(["Alpha", "Dev", "Nightly"]);
101
102export interface DesktopAppBranding {
103 baseName: string;
104 stageLabel: DesktopAppStageLabel;
105 displayName: string;
106}
107
108export const DesktopAppBrandingSchema = Schema.Struct({
109 baseName: Schema.String,
110 stageLabel: DesktopAppStageLabelSchema,
111 displayName: Schema.String,
112});
113
114export const DesktopSnapShotMode = Schema.Literals(["direct", "portal", "unavailable"]);
115export type DesktopSnapShotMode = typeof DesktopSnapShotMode.Type;
116
117export const DesktopCaptureExtensionState = Schema.Struct({
118 status: Schema.Literals([
119 "not-installed",
120 "disabled",
121 "enabled",
122 "restart-required",
123 "update-required",
124 "extensions-disabled",
125 "unsupported",
126 "error",
127 ]),
128 message: Schema.String,
129});
130export type DesktopCaptureExtensionState = typeof DesktopCaptureExtensionState.Type;
131
132export const DesktopCaptureHelperState = Schema.Struct({
133 status: Schema.Literals(["not-installed", "update-required", "ready", "error"]),
134 message: Schema.String,
135 feedbackAvailable: Schema.optional(Schema.Boolean),
136});
137export type DesktopCaptureHelperState = typeof DesktopCaptureHelperState.Type;
138
139export const DesktopSnapShotSetupAction = Schema.Literals([
140 "install-extension",
141 "enable-extension",
142 "disable-extension",
143 "install-kde-helper",
144 "remove-kde-helper",
145 "install-hyprland-helper",
146 "remove-hyprland-helper",
147 "test-mac-capture",
148 "allow-screen-recording",
149 "allow-accessibility",
150 "retry-shortcut",
151]);
152export type DesktopSnapShotSetupAction = typeof DesktopSnapShotSetupAction.Type;
153
154export const DesktopCaptureConfigRequest = Schema.Struct({
155 operation: Schema.Literals(["install", "remove"]),
156 chooseFile: Schema.Boolean,
157 shortcut: Schema.optional(Schema.String.check(Schema.isMaxLength(80))),
158});
159export type DesktopCaptureConfigRequest = typeof DesktopCaptureConfigRequest.Type;
160
161export const DesktopCaptureConfigPreview = Schema.Struct({
162 id: Schema.String,
163 path: Schema.String,
164 resolvedPath: Schema.String,
165 before: Schema.String,
166 after: Schema.String,
167 shortcut: Schema.String,
168 operation: Schema.Literals(["install", "remove"]),
169});
170export type DesktopCaptureConfigPreview = typeof DesktopCaptureConfigPreview.Type;
171
172export const DesktopCaptureConfigApplied = Schema.Struct({
173 backupPath: Schema.NullOr(Schema.String),
174 warning: Schema.NullOr(Schema.String),
175});
176export type DesktopCaptureConfigApplied = typeof DesktopCaptureConfigApplied.Type;
177
178export const DesktopSnapShotState = Schema.Struct({
179 mode: DesktopSnapShotMode,
180 windows: Schema.optional(Schema.Boolean),
181 linuxDesktop: Schema.optional(Schema.Literals(["gnome", "kde", "niri", "hyprland"])),
182 linuxBackend: Schema.optional(
183 Schema.Literals(["screenshot-portal", "gnome-extension", "niri", "kde", "hyprland", "picker"]),
184 ),
185 linuxFeedbackAvailable: Schema.optional(Schema.Boolean),
186 shortcut: SnapShotShortcut,
187 shortcutRegistered: Schema.Boolean,
188 shortcutPending: Schema.optional(Schema.Boolean),
189 shortcutCanRetry: Schema.optional(Schema.Boolean),
190 shortcutLabel: Schema.optional(Schema.String),
191 shortcutMessage: Schema.NullOr(Schema.String),
192 shortcutBinding: Schema.optional(Schema.String),
193 shortcutConfigPath: Schema.optional(Schema.String),
194 shortcutActionRegistered: Schema.optional(Schema.Boolean),
195 gnomeExtension: Schema.optional(DesktopCaptureExtensionState),
196 kdeHelper: Schema.optional(DesktopCaptureHelperState),
197 hyprlandHelper: Schema.optional(DesktopCaptureHelperState),
198 macPermissions: Schema.optional(
199 Schema.Struct({ screenRecording: Schema.Boolean, accessibility: Schema.Boolean }),
200 ),
201 shortcutVerified: Schema.optional(Schema.Boolean),
202 message: Schema.NullOr(Schema.String),
203});
204export type DesktopSnapShotState = typeof DesktopSnapShotState.Type;
205
206export const DesktopSnapShotShortcutAvailability = Schema.Struct({
207 available: Schema.Boolean,
208 message: Schema.NullOr(Schema.String),
209});
210export type DesktopSnapShotShortcutAvailability = typeof DesktopSnapShotShortcutAvailability.Type;
211
212export const DesktopSnapShotId = TrimmedNonEmptyString.check(
213 Schema.isMaxLength(64),
214 Schema.isPattern(/^[a-f0-9-]+$/i),
215);
216export type DesktopSnapShotId = typeof DesktopSnapShotId.Type;
217
218/** Main-process capture lifecycle pushes. `id` is absent for failures before a capture exists. */
219export const DesktopSnapShotEvent = Schema.Union([
220 Schema.Struct({ type: Schema.Literal("requested"), id: DesktopSnapShotId }),
221 Schema.Struct({ type: Schema.Literal("started"), id: DesktopSnapShotId }),
222 Schema.Struct({ type: Schema.Literal("ready"), id: DesktopSnapShotId }),
223 Schema.Struct({ type: Schema.Literal("failed"), id: Schema.optional(DesktopSnapShotId) }),
224 Schema.Struct({ type: Schema.Literal("shortcut-changed") }),
225]);
226export type DesktopSnapShotEvent = typeof DesktopSnapShotEvent.Type;
227
228export const DesktopPendingSnapShot = Schema.Struct({
229 id: DesktopSnapShotId,
230 name: Schema.String,
231 mimeType: Schema.Literal("image/png"),
232 sizeBytes: Schema.Int,
233 source: SnapShotSource,
234});
235export type DesktopPendingSnapShot = typeof DesktopPendingSnapShot.Type;
236
237export const DesktopSnapShot = Schema.Struct({
238 ...DesktopPendingSnapShot.fields,
239 dataUrl: Schema.String,
240});
241export type DesktopSnapShot = typeof DesktopSnapShot.Type;
242
243export const DesktopSnapShotAnimationDestination = Schema.Struct({
244 id: DesktopSnapShotId,
245 viewportFrame: Schema.Struct({
246 x: Schema.Number,
247 y: Schema.Number,
248 width: Schema.Number,
249 height: Schema.Number,
250 }),
251 backgroundColor: Schema.String,
252 borderColor: Schema.String,
253 borderWidth: Schema.Number,
254 cornerRadius: Schema.Number,
255 details: Schema.optional(
256 Schema.Struct({
257 appName: SnapShotSource.fields.appName,
258 windowTitle: SnapShotSource.fields.windowTitle,
259 appIconDataUrl: SnapShotSource.fields.appIconDataUrl,
260 }),
261 ),
262});
263export type DesktopSnapShotAnimationDestination = typeof DesktopSnapShotAnimationDestination.Type;
264
265export interface DesktopRuntimeInfo {
266 hostArch: DesktopRuntimeArch;
267 appArch: DesktopRuntimeArch;
268 runningUnderArm64Translation: boolean;
269}
270
271export interface DesktopUpdateState {
272 enabled: boolean;
273 status: DesktopUpdateStatus;
274 channel: DesktopUpdateChannel;
275 currentVersion: string;
276 hostArch: DesktopRuntimeArch;
277 appArch: DesktopRuntimeArch;
278 runningUnderArm64Translation: boolean;
279 availableVersion: string | null;
280 downloadedVersion: string | null;
281 releaseNotes: ReadonlyArray<DesktopUpdateReleaseNote>;
282 omittedReleaseCount: number;
283 downloadPercent: number | null;
284 checkedAt: string | null;
285 message: string | null;
286 errorContext: "check" | "download" | "install" | null;
287 canRetry: boolean;
288 /**
289 * The version the last install handed to the OS installer that did not
290 * apply: the app came back on its old version. Server-triggered updates do
291 * not install it again; the user can retry it from the update button.
292 */
293 notAppliedVersion?: string;
294 installKind?: "ui" | "app";
295 uiVersion?: string;
296}
297
298export interface DesktopUpdateReleaseNote {
299 version: string;
300 items: ReadonlyArray<string>;
301 totalItems: number;
302}
303
304export const DesktopUpdateReleaseNoteSchema = Schema.Struct({
305 version: Schema.String,
306 items: Schema.Array(Schema.String),
307 totalItems: Schema.Number,
308});
309
310export const DesktopUpdateStateSchema = Schema.Struct({
311 enabled: Schema.Boolean,
312 status: DesktopUpdateStatusSchema,
313 channel: DesktopUpdateChannelSchema,
314 currentVersion: Schema.String,
315 hostArch: DesktopRuntimeArchSchema,
316 appArch: DesktopRuntimeArchSchema,
317 runningUnderArm64Translation: Schema.Boolean,
318 availableVersion: Schema.NullOr(Schema.String),
319 downloadedVersion: Schema.NullOr(Schema.String),
320 releaseNotes: Schema.Array(DesktopUpdateReleaseNoteSchema),
321 omittedReleaseCount: Schema.Number,
322 downloadPercent: Schema.NullOr(Schema.Number),
323 checkedAt: Schema.NullOr(Schema.String),
324 message: Schema.NullOr(Schema.String),
325 errorContext: Schema.NullOr(Schema.Literals(["check", "download", "install"])),
326 canRetry: Schema.Boolean,
327 notAppliedVersion: Schema.optionalKey(Schema.String),
328 installKind: Schema.optionalKey(Schema.Literals(["ui", "app"])),
329 uiVersion: Schema.optionalKey(Schema.String),
330});
331
332/** The desktop app's `t3` command on PATH, managed from Settings. */
333export const DesktopCliCommandStateSchema = Schema.Struct({
334 /** Only installed builds have a launcher to put on PATH. */
335 supported: Schema.Boolean,
336 /** The `t3` the app installed, or null when it is not installed. */
337 installedPath: Schema.NullOr(Schema.String),
338 /** Whether a new terminal finds it; false when the folder is not on PATH yet. */
339 onPath: Schema.Boolean,
340 /**
341 * Another `t3` a new terminal runs instead, earlier on PATH. Install refuses
342 * while it is there, since a link behind it would never run.
343 */
344 shadowedBy: Schema.optionalKey(Schema.String),
345});
346export type DesktopCliCommandState = typeof DesktopCliCommandStateSchema.Type;
347
348export interface DesktopUpdateActionResult {
349 accepted: boolean;
350 completed: boolean;
351 state: DesktopUpdateState;
352}
353
354export const DesktopUpdateActionResultSchema = Schema.Struct({
355 accepted: Schema.Boolean,
356 completed: Schema.Boolean,
357 state: DesktopUpdateStateSchema,
358});
359
360export interface DesktopUpdateCheckResult {
361 checked: boolean;
362 state: DesktopUpdateState;
363}
364
365export const DesktopUpdateCheckResultSchema = Schema.Struct({
366 checked: Schema.Boolean,
367 state: DesktopUpdateStateSchema,
368});
369
370// Stable id for the Windows-native primary backend. Desktop side wraps
371// this with a brand inside DesktopBackendManager; web side keeps it as
372// a plain string so the env-runtime can compare against it without
373// importing brand machinery from the desktop package.
374export const PRIMARY_LOCAL_ENVIRONMENT_ID = "primary";
375
376export interface DesktopEnvironmentBootstrap {
377 // Stable backend instance id (e.g. "primary" or "wsl:ubuntu"). The
378 // web env runtime keys local environments off this so projects
379 // routed to a specific backend reopen against the same one.
380 id: string;
381 label: string;
382 // Concrete WSL distro used by the current backend run. This stays separate
383 // from id because a default-tracking instance keeps the stable
384 // "wsl:default" IPC target while each run launches a specific distro.
385 runningDistro?: string | null;
386 httpBaseUrl: string | null;
387 wsBaseUrl: string | null;
388 bootstrapToken?: string;
389}
390
391export const DesktopEnvironmentBootstrapSchema = Schema.Struct({
392 id: Schema.String,
393 label: Schema.String,
394 runningDistro: Schema.optionalKey(Schema.NullOr(Schema.String)),
395 httpBaseUrl: Schema.NullOr(Schema.String),
396 wsBaseUrl: Schema.NullOr(Schema.String),
397 bootstrapToken: Schema.optionalKey(Schema.String),
398});
399
400export const DesktopSshEnvironmentTargetSchema = Schema.Struct({
401 alias: Schema.String,
402 hostname: Schema.String,
403 username: Schema.NullOr(Schema.String),
404 port: Schema.NullOr(Schema.Number),
405});
406export type DesktopSshEnvironmentTarget = typeof DesktopSshEnvironmentTargetSchema.Type;
407
408export type DesktopSshHostSource = "ssh-config" | "known-hosts";
409export const DesktopSshHostSourceSchema = Schema.Literals(["ssh-config", "known-hosts"]);
410
411export interface DesktopDiscoveredSshHost extends DesktopSshEnvironmentTarget {
412 source: DesktopSshHostSource;
413}
414
415export const DesktopDiscoveredSshHostSchema = Schema.Struct({
416 alias: Schema.String,
417 hostname: Schema.String,
418 username: Schema.NullOr(Schema.String),
419 port: Schema.NullOr(Schema.Number),
420 source: DesktopSshHostSourceSchema,
421});
422
423export interface DesktopSshEnvironmentBootstrap {
424 target: DesktopSshEnvironmentTarget;
425 httpBaseUrl: string;
426 wsBaseUrl: string;
427 pairingToken: string | null;
428 remotePort?: number;
429 remoteServerKind?: "external" | "managed";
430}
431
432export const DesktopSshEnvironmentBootstrapSchema = Schema.Struct({
433 target: DesktopSshEnvironmentTargetSchema,
434 httpBaseUrl: Schema.String,
435 wsBaseUrl: Schema.String,
436 pairingToken: Schema.NullOr(Schema.String),
437 remotePort: Schema.optionalKey(Schema.Number),
438 remoteServerKind: Schema.optionalKey(Schema.Literals(["external", "managed"])),
439});
440
441export interface DesktopSshPasswordPromptRequest {
442 requestId: string;
443 destination: string;
444 username: string | null;
445 prompt: string;
446 expiresAt: string;
447}
448
449export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const;
450
451export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({
452 type: Schema.Literal(DesktopSshPasswordPromptCancelledType),
453 message: Schema.String,
454});
455
456export const DesktopSshEnvironmentEnsureOptionsSchema = Schema.Struct({
457 issuePairingToken: Schema.optionalKey(Schema.Boolean),
458});
459
460export const DesktopSshEnvironmentEnsureInputSchema = Schema.Struct({
461 target: DesktopSshEnvironmentTargetSchema,
462 options: Schema.optionalKey(DesktopSshEnvironmentEnsureOptionsSchema),
463});
464
465export const DesktopSshEnvironmentEnsureResultSchema = Schema.Union([
466 DesktopSshEnvironmentBootstrapSchema,
467 DesktopSshPasswordPromptCancelledResultSchema,
468]);
469
470export const DesktopSshHttpBaseUrlInputSchema = Schema.Struct({
471 httpBaseUrl: Schema.String,
472});
473
474export const DesktopSshBearerRequestInputSchema = Schema.Struct({
475 httpBaseUrl: Schema.String,
476 bearerToken: Schema.String,
477});
478
479export const DesktopSshBearerBootstrapInputSchema = Schema.Struct({
480 httpBaseUrl: Schema.String,
481 credential: Schema.String,
482});
483
484export const DesktopSshPasswordPromptResolutionInputSchema = Schema.Struct({
485 requestId: Schema.String,
486 password: Schema.NullOr(Schema.String),
487});
488
489export const PersistedSavedEnvironmentRecordSchema = Schema.Struct({
490 environmentId: EnvironmentId,
491 label: Schema.String,
492 wsBaseUrl: Schema.String,
493 httpBaseUrl: Schema.String,
494 createdAt: Schema.String,
495 lastConnectedAt: Schema.NullOr(Schema.String),
496 desktopSsh: Schema.optionalKey(DesktopSshEnvironmentTargetSchema),
497 relayManaged: Schema.optionalKey(
498 Schema.Struct({
499 relayUrl: Schema.String,
500 }),
501 ),
502});
503export type PersistedSavedEnvironmentRecord = typeof PersistedSavedEnvironmentRecordSchema.Type;
504
505export type DesktopServerExposureMode = "local-only" | "network-accessible";
506
507export const DesktopServerExposureModeSchema = Schema.Literals([
508 "local-only",
509 "network-accessible",
510]);
511
512export interface DesktopServerExposureState {
513 mode: DesktopServerExposureMode;
514 endpointUrl: string | null;
515 advertisedHost: string | null;
516 tailscaleServeEnabled: boolean;
517 tailscaleServePort: number;
518}
519
520export const DesktopServerExposureStateSchema = Schema.Struct({
521 mode: DesktopServerExposureModeSchema,
522 endpointUrl: Schema.NullOr(Schema.String),
523 advertisedHost: Schema.NullOr(Schema.String),
524 tailscaleServeEnabled: Schema.Boolean,
525 tailscaleServePort: Schema.Number,
526});
527
528export interface PickFolderOptions {
529 initialPath?: string | null;
530 // When set, the desktop dialog opens against the named backend's
531 // filesystem instead of the primary's. Used by callers that already
532 // know which local environment they're targeting (e.g. opening a
533 // project that lives inside WSL). Omitting it keeps the historical
534 // behavior so non-WSL users never see a different picker.
535 targetEnvironmentId?: string;
536}
537
538export const PickFolderOptionsSchema = Schema.Struct({
539 initialPath: Schema.optionalKey(Schema.NullOr(Schema.String)),
540 targetEnvironmentId: Schema.optionalKey(Schema.String),
541});
542
543/**
544 * A file returned by the desktop theme-file picker. Oversized files carry an
545 * empty text so the renderer can reject them by size without the main
546 * process ever holding their contents.
547 */
548export interface PickedThemeFile {
549 name: string;
550 size: number;
551 text: string;
552}
553
554export const PickedThemeFileSchema = Schema.Struct({
555 name: Schema.String,
556 size: Schema.Number,
557 text: Schema.String,
558});
559
560export interface DesktopWslDistro {
561 name: string;
562 isDefault: boolean;
563 version: 1 | 2;
564}
565
566export const DesktopWslDistroSchema = Schema.Struct({
567 name: Schema.String,
568 isDefault: Schema.Boolean,
569 version: Schema.Literals([1, 2]),
570});
571
572export interface DesktopWslState {
573 // True when the user has opted the WSL backend in; the actual backend
574 // process is registered with the desktop pool independently of this
575 // flag and may take a moment to come up after the user enables it.
576 enabled: boolean;
577 // null means "track the current WSL default distro".
578 distro: string | null;
579 available: boolean;
580 // When true (and `enabled` is also true) the desktop runs only the
581 // WSL backend as the primary; the Windows-side Node backend is not
582 // started. Toggling this requires an app restart because the
583 // primary backend's spec is captured once at layer init.
584 wslOnly: boolean;
585 distros: readonly DesktopWslDistro[];
586 // Reason the dual-mode WSL backend last failed preflight (no node, wrong
587 // version, missing build tools), or null. Surfaced inline in Connections
588 // settings. Always null in wsl-only mode — that path shows a dialog and
589 // falls back to Windows instead.
590 preflightError: string | null;
591}
592
593export const DesktopWslStateSchema = Schema.Struct({
594 enabled: Schema.Boolean,
595 distro: Schema.NullOr(Schema.String),
596 available: Schema.Boolean,
597 wslOnly: Schema.Boolean,
598 distros: Schema.Array(DesktopWslDistroSchema),
599 preflightError: Schema.NullOr(Schema.String),
600});
601
602/**
603 * Renderer-facing snapshot of a desktop preview tab. Mirrors the main-process
604 * PreviewTabState shape but uses serialisable primitives only.
605 */
606export type DesktopPreviewNavStatus =
607 | { kind: "Idle" }
608 | { kind: "Loading"; url: string; title: string }
609 | { kind: "Success"; url: string; title: string }
610 | {
611 kind: "LoadFailed";
612 url: string;
613 title: string;
614 code: number;
615 description: string;
616 };
617
618/**
619 * Emulated `prefers-color-scheme` for the guest page. "system" clears the
620 * override so the page follows the OS appearance.
621 */
622export type DesktopPreviewColorScheme = "system" | "light" | "dark";
623
624export const DesktopPreviewColorSchemeSchema: Schema.Codec<DesktopPreviewColorScheme> =
625 Schema.Literals(["system", "light", "dark"]);
626
627export const FAVICON_DATA_URL_MAX_LENGTH = 8192;
628export const FAVICON_CAPTURED_AT_MAX = 8_640_000_000_000_000;
629
630export interface DesktopPreviewFavicon {
631 dataUrl: string;
632 pageUrl: string;
633 capturedAt: number;
634}
635
636export interface DesktopPreviewTabState {
637 tabId: string;
638 webContentsId: number | null;
639 navStatus: DesktopPreviewNavStatus;
640 canGoBack: boolean;
641 canGoForward: boolean;
642 /** Current zoom factor (1.0 = 100%). */
643 zoomFactor: number;
644 /** Whether this tab is currently mirrored into a desktop picture-in-picture window. */
645 pictureInPicture: boolean;
646 colorScheme: DesktopPreviewColorScheme;
647 /**
648 * Whether the user has silenced this tab. Per tab rather than per origin, so
649 * two tabs on the same site mute independently. Survives navigation and
650 * webview swaps, but is dropped when the tab closes.
651 */
652 audioMuted: boolean;
653 /**
654 * Whether the guest is currently emitting audio. Observed from Chromium, and
655 * independent of {@link audioMuted}: a muted tab that is playing still reports
656 * `true`, which is what lets the tab strip distinguish "muted and making
657 * sound" from "muted and silent".
658 */
659 audible: boolean;
660 controller: "human" | "agent" | "none";
661 favicon?: DesktopPreviewFavicon;
662 updatedAt: string;
663}
664
665export const DesktopPreviewTabIdSchema = Schema.String.check(Schema.isTrimmed()).check(
666 Schema.isNonEmpty(),
667);
668
669export interface DesktopPreviewPointerEvent {
670 tabId: string;
671 phase: "move" | "click";
672 x: number;
673 y: number;
674 sequence: number;
675 createdAt: string;
676}
677
678/** A `target="_blank"` link the previewed page asked to open beside itself. */
679export interface DesktopPreviewOpenLinkEvent {
680 tabId: string;
681 url: string;
682 /** True for middle-click / Cmd-click, which should not take focus. */
683 background: boolean;
684}
685
686/** Recording decorations are forwarded separately from the captured page pixels. */
687export const DesktopPreviewRecordingInputSchema = Schema.Union([
688 Schema.Struct({
689 type: Schema.Literal("pointer"),
690 phase: Schema.Literals(["move", "down", "up", "click"]),
691 x: Schema.Finite,
692 y: Schema.Finite,
693 width: Schema.Finite.check(Schema.isGreaterThan(0)),
694 height: Schema.Finite.check(Schema.isGreaterThan(0)),
695 }),
696 Schema.Struct({
697 type: Schema.Literal("key"),
698 label: Schema.NullOr(Schema.String.check(Schema.isMaxLength(100))),
699 held: Schema.Boolean,
700 width: Schema.Finite.check(Schema.isGreaterThan(0)),
701 }),
702 Schema.Struct({ type: Schema.Literal("clear") }),
703]);
704export type DesktopPreviewRecordingInput = typeof DesktopPreviewRecordingInputSchema.Type;
705export interface DesktopPreviewRecordingInputEvent {
706 readonly tabId: string;
707 readonly input: DesktopPreviewRecordingInput;
708}
709
710/**
711 * Static config a renderer needs to mount a preview `<webview>`. Returned
712 * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer
713 * doesn't have to wait on three separate IPC round-trips before the webview
714 * can attach.
715 */
716export interface DesktopPreviewWebviewConfig {
717 /** `persist:t3code-preview` (or whatever the desktop chose). */
718 partition: string;
719 /**
720 * Canonical `<webview webpreferences="...">` string. Encodes the security
721 * posture (sandboxed but contextIsolation off so the picker preload can
722 * read the page's React DevTools hook). Always present.
723 */
724 webPreferences: string;
725 /**
726 * Absolute `file://`-style URL to the picker preload bundle. Set to null
727 * when the bundle isn't present (older builds, broken install) — the
728 * renderer must then disable element-pick affordances.
729 */
730 preloadUrl: string | null;
731}
732
733export const DesktopPreviewWebviewConfigSchema: Schema.Codec<DesktopPreviewWebviewConfig> =
734 Schema.Struct({
735 partition: Schema.String,
736 webPreferences: Schema.String,
737 preloadUrl: Schema.NullOr(Schema.String),
738 });
739
740export interface DesktopPreviewAnnotationTheme {
741 colorScheme: "light" | "dark";
742 radius: string;
743 background: string;
744 foreground: string;
745 popover: string;
746 popoverForeground: string;
747 primary: string;
748 primaryForeground: string;
749 muted: string;
750 mutedForeground: string;
751 accent: string;
752 accentForeground: string;
753 border: string;
754 input: string;
755 ring: string;
756 fontSans: string;
757 fontMono: string;
758}
759
760export const DesktopPreviewAnnotationThemeSchema: Schema.Codec<DesktopPreviewAnnotationTheme> =
761 Schema.Struct({
762 colorScheme: Schema.Literals(["light", "dark"]),
763 radius: Schema.String,
764 background: Schema.String,
765 foreground: Schema.String,
766 popover: Schema.String,
767 popoverForeground: Schema.String,
768 primary: Schema.String,
769 primaryForeground: Schema.String,
770 muted: Schema.String,
771 mutedForeground: Schema.String,
772 accent: Schema.String,
773 accentForeground: Schema.String,
774 border: Schema.String,
775 input: Schema.String,
776 ring: Schema.String,
777 fontSans: Schema.String,
778 fontMono: Schema.String,
779 });
780
781export interface DesktopPreviewRecordingFrame {
782 tabId: string;
783 data: string;
784 width: number;
785 height: number;
786 receivedAt: string;
787}
788
789export interface DesktopPreviewRecordingArtifact {
790 id: string;
791 tabId: string;
792 path: string;
793 mimeType: string;
794 sizeBytes: number;
795 createdAt: string;
796}
797
798export const DesktopPreviewRecordingArtifactSchema: Schema.Codec<DesktopPreviewRecordingArtifact> =
799 Schema.Struct({
800 id: Schema.String,
801 tabId: DesktopPreviewTabIdSchema,
802 path: Schema.String,
803 mimeType: Schema.String,
804 sizeBytes: Schema.Int,
805 createdAt: Schema.String,
806 });
807
808export interface DesktopPreviewScreenshotArtifact {
809 id: string;
810 tabId: string;
811 path: string;
812 mimeType: "image/png";
813 sizeBytes: number;
814 createdAt: string;
815}
816
817export const DesktopPreviewScreenshotArtifactSchema: Schema.Codec<DesktopPreviewScreenshotArtifact> =
818 Schema.Struct({
819 id: Schema.String,
820 tabId: DesktopPreviewTabIdSchema,
821 path: Schema.String,
822 mimeType: Schema.Literal("image/png"),
823 sizeBytes: Schema.Int,
824 createdAt: Schema.String,
825 });
826
827/**
828 * Single stack frame captured by react-grab's `getElementContext`. We surface
829 * the source file/line so coding agents can jump straight to the JSX that
830 * produced the picked DOM node.
831 */
832export interface PickedElementStackFrame {
833 functionName: string | null;
834 fileName: string | null;
835 lineNumber: number | null;
836 columnNumber: number | null;
837}
838
839export const PickedElementStackFrameSchema: Schema.Codec<PickedElementStackFrame> = Schema.Struct({
840 functionName: Schema.NullOr(Schema.String),
841 fileName: Schema.NullOr(Schema.String),
842 lineNumber: Schema.NullOr(Schema.Number),
843 columnNumber: Schema.NullOr(Schema.Number),
844});
845
846/**
847 * A successful element pick from the preview webview. All fields are
848 * best-effort — pages that don't ship a React fiber tree (or aren't running
849 * in dev) will still produce a usable payload (selector + html preview),
850 * just without component / source attribution.
851 */
852export interface PickedElementPayload {
853 /** URL of the page the element was picked on. */
854 pageUrl: string;
855 /** Optional `<title>` of that page (best-effort). */
856 pageTitle: string | null;
857 /** Lowercase tag name, e.g. `"button"`. */
858 tagName: string;
859 /** CSS selector resolving back to the element on a re-render. */
860 selector: string | null;
861 /** Truncated outer-HTML preview (matches react-grab's `htmlPreview`). */
862 htmlPreview: string;
863 /** Nearest React component display name, or null when unavailable. */
864 componentName: string | null;
865 /** First source-mapped stack frame (file + line of the JSX source). */
866 source: PickedElementStackFrame | null;
867 /** Full owner-stack frames; can be empty. Useful for richer context. */
868 stack: ReadonlyArray<PickedElementStackFrame>;
869 /** Author CSS only (UA defaults stripped) — react-grab's `styles`. */
870 styles: string;
871 /** Wall-clock pick time as ISO-8601 string. */
872 pickedAt: string;
873}
874
875export const PickedElementPayloadSchema: Schema.Codec<PickedElementPayload> = Schema.Struct({
876 pageUrl: Schema.String,
877 pageTitle: Schema.NullOr(Schema.String),
878 tagName: Schema.String,
879 selector: Schema.NullOr(Schema.String),
880 htmlPreview: Schema.String,
881 componentName: Schema.NullOr(Schema.String),
882 source: Schema.NullOr(PickedElementStackFrameSchema),
883 stack: Schema.Array(PickedElementStackFrameSchema),
884 styles: Schema.String,
885 pickedAt: Schema.String,
886});
887
888export interface PreviewAnnotationRect {
889 x: number;
890 y: number;
891 width: number;
892 height: number;
893}
894
895export const PreviewAnnotationRectSchema: Schema.Codec<PreviewAnnotationRect> = Schema.Struct({
896 x: Schema.Number,
897 y: Schema.Number,
898 width: Schema.Number,
899 height: Schema.Number,
900});
901
902export interface PreviewAnnotationPoint {
903 x: number;
904 y: number;
905}
906
907export const PreviewAnnotationPointSchema: Schema.Codec<PreviewAnnotationPoint> = Schema.Struct({
908 x: Schema.Number,
909 y: Schema.Number,
910});
911
912export interface PreviewAnnotationElementTarget {
913 id: string;
914 element: PickedElementPayload;
915 rect: PreviewAnnotationRect;
916}
917
918export const PreviewAnnotationElementTargetSchema: Schema.Codec<PreviewAnnotationElementTarget> =
919 Schema.Struct({
920 id: Schema.String,
921 element: PickedElementPayloadSchema,
922 rect: PreviewAnnotationRectSchema,
923 });
924
925export interface PreviewAnnotationRegionTarget {
926 id: string;
927 rect: PreviewAnnotationRect;
928}
929
930export const PreviewAnnotationRegionTargetSchema: Schema.Codec<PreviewAnnotationRegionTarget> =
931 Schema.Struct({
932 id: Schema.String,
933 rect: PreviewAnnotationRectSchema,
934 });
935
936export interface PreviewAnnotationStrokeTarget {
937 id: string;
938 color: string;
939 width: number;
940 points: ReadonlyArray<PreviewAnnotationPoint>;
941 bounds: PreviewAnnotationRect;
942}
943
944export const PreviewAnnotationStrokeTargetSchema: Schema.Codec<PreviewAnnotationStrokeTarget> =
945 Schema.Struct({
946 id: Schema.String,
947 color: Schema.String,
948 width: Schema.Number,
949 points: Schema.Array(PreviewAnnotationPointSchema),
950 bounds: PreviewAnnotationRectSchema,
951 });
952
953export interface PreviewAnnotationStyleChange {
954 targetId: string;
955 selector: string | null;
956 property: string;
957 previousValue: string;
958 value: string;
959}
960
961export const PreviewAnnotationStyleChangeSchema: Schema.Codec<PreviewAnnotationStyleChange> =
962 Schema.Struct({
963 targetId: Schema.String,
964 selector: Schema.NullOr(Schema.String),
965 property: Schema.String,
966 previousValue: Schema.String,
967 value: Schema.String,
968 });
969
970export interface PreviewAnnotationScreenshot {
971 dataUrl: string;
972 width: number;
973 height: number;
974 cropRect: PreviewAnnotationRect;
975}
976
977export const PreviewAnnotationScreenshotSchema: Schema.Codec<PreviewAnnotationScreenshot> =
978 Schema.Struct({
979 dataUrl: Schema.String,
980 width: Schema.Number,
981 height: Schema.Number,
982 cropRect: PreviewAnnotationRectSchema,
983 });
984
985/**
986 * A submitted preview annotation. One annotation may reference multiple DOM
987 * elements, freeform regions, and ink strokes. The desktop main process adds
988 * the screenshot after the guest preload submits the structured draft.
989 */
990export interface PreviewAnnotationPayload {
991 id: string;
992 pageUrl: string;
993 pageTitle: string | null;
994 comment: string;
995 elements: ReadonlyArray<PreviewAnnotationElementTarget>;
996 regions: ReadonlyArray<PreviewAnnotationRegionTarget>;
997 strokes: ReadonlyArray<PreviewAnnotationStrokeTarget>;
998 styleChanges: ReadonlyArray<PreviewAnnotationStyleChange>;
999 screenshot: PreviewAnnotationScreenshot | null;
1000 createdAt: string;
1001}
1002
1003export const PreviewAnnotationPayloadSchema: Schema.Codec<PreviewAnnotationPayload> = Schema.Struct(
1004 {
1005 id: Schema.String,
1006 pageUrl: Schema.String,
1007 pageTitle: Schema.NullOr(Schema.String),
1008 comment: Schema.String,
1009 elements: Schema.Array(PreviewAnnotationElementTargetSchema),
1010 regions: Schema.Array(PreviewAnnotationRegionTargetSchema),
1011 strokes: Schema.Array(PreviewAnnotationStrokeTargetSchema),
1012 styleChanges: Schema.Array(PreviewAnnotationStyleChangeSchema),
1013 screenshot: Schema.NullOr(PreviewAnnotationScreenshotSchema),
1014 createdAt: Schema.String,
1015 },
1016);
1017
1018export type PreviewAnnotationSubmission = "attach" | "send";
1019export const PreviewAnnotationSubmissionSchema: Schema.Codec<PreviewAnnotationSubmission> =
1020 Schema.Literals(["attach", "send"]);
1021
1022export interface PreviewAnnotationSubmissionResult {
1023 annotation: PreviewAnnotationPayload;
1024 submission: PreviewAnnotationSubmission;
1025 /** The crop was requested but failed or timed out, so `annotation.screenshot` is null. */
1026 screenshotFailed?: boolean;
1027}
1028export const PreviewAnnotationSubmissionResultSchema: Schema.Codec<PreviewAnnotationSubmissionResult> =
1029 Schema.Struct({
1030 annotation: PreviewAnnotationPayloadSchema,
1031 submission: PreviewAnnotationSubmissionSchema,
1032 screenshotFailed: Schema.optionalKey(Schema.Boolean),
1033 });
1034
1035export const DesktopPreviewTabInputSchema = Schema.Struct({
1036 tabId: DesktopPreviewTabIdSchema,
1037});
1038
1039/**
1040 * Tab creation carries the client's configured browser defaults so the guest
1041 * is born already zoomed and color-scheme-emulated. Applying them after
1042 * creation instead would paint one frame at 100%/system first, which reads as
1043 * a flash on every tab open. Both fields are optional so an older renderer
1044 * still gets the historical defaults.
1045 */
1046export const DesktopPreviewCreateTabInputSchema = Schema.Struct({
1047 tabId: DesktopPreviewTabIdSchema,
1048 zoomFactor: Schema.optional(Schema.Number.check(Schema.isGreaterThan(0))),
1049 colorScheme: Schema.optional(DesktopPreviewColorSchemeSchema),
1050 serverTab: Schema.optional(
1051 Schema.Struct({ threadId: TrimmedNonEmptyString, tabId: TrimmedNonEmptyString }),
1052 ),
1053});
1054
1055export interface DesktopPreviewTabDefaults {
1056 readonly zoomFactor?: number | undefined;
1057 readonly colorScheme?: DesktopPreviewColorScheme | undefined;
1058 /** A tab of the desktop's own server: the server drives it through the desktop browser channel. */
1059 readonly serverTab?: { readonly threadId: string; readonly tabId: string } | undefined;
1060}
1061
1062export const DesktopPreviewRegisterWebviewInputSchema = Schema.Struct({
1063 tabId: DesktopPreviewTabIdSchema,
1064 webContentsId: Schema.Int.check(Schema.isGreaterThan(0)),
1065});
1066
1067export const DesktopPreviewNavigateInputSchema = Schema.Struct({
1068 tabId: DesktopPreviewTabIdSchema,
1069 url: Schema.String,
1070});
1071
1072export const DesktopPreviewConfigInputSchema = Schema.Struct({
1073 environmentId: EnvironmentId,
1074 /**
1075 * Browser profile the partition is derived from. Derivation stays in main:
1076 * `will-attach-webview` only prefix-checks the partition string, so a
1077 * renderer-supplied partition could attach to a session that never had the
1078 * UA rewrite or permission handlers installed.
1079 */
1080 profileId: Schema.optional(BrowserProfileId),
1081});
1082
1083export const DesktopPreviewClearDataInputSchema = Schema.Struct({
1084 environmentId: EnvironmentId,
1085 /** Omit to clear every profile; otherwise only this profile's partition. */
1086 profileId: Schema.optional(BrowserProfileId),
1087});
1088
1089export const DesktopPreviewSetColorSchemeInputSchema = Schema.Struct({
1090 tabId: DesktopPreviewTabIdSchema,
1091 colorScheme: DesktopPreviewColorSchemeSchema,
1092});
1093
1094export const DesktopPreviewSetZoomFactorInputSchema = Schema.Struct({
1095 tabId: DesktopPreviewTabIdSchema,
1096 zoomFactor: Schema.Number.check(Schema.isGreaterThan(0)),
1097});
1098
1099export const DesktopPreviewSetAudioMutedInputSchema = Schema.Struct({
1100 tabId: DesktopPreviewTabIdSchema,
1101 audioMuted: Schema.Boolean,
1102});
1103
1104export const DesktopPreviewAnnotationThemeInputSchema = Schema.Struct({
1105 theme: DesktopPreviewAnnotationThemeSchema,
1106});
1107
1108export const DesktopPreviewAnnotationSendEnabledInputSchema = Schema.Struct({
1109 tabId: DesktopPreviewTabIdSchema,
1110 enabled: Schema.Boolean,
1111});
1112
1113export const DesktopPreviewArtifactInputSchema = Schema.Struct({
1114 path: Schema.String.check(Schema.isTrimmed()).check(Schema.isNonEmpty()),
1115});
1116
1117export const DesktopPreviewRecordingSaveInputSchema = Schema.Struct({
1118 tabId: DesktopPreviewTabIdSchema,
1119 mimeType: Schema.String.check(Schema.isTrimmed()).check(Schema.isNonEmpty()),
1120 data: Schema.Uint8Array,
1121});
1122
1123/**
1124 * A System Settings pane the app can deep-link to. The identifier crosses IPC
1125 * rather than a URL, so the renderer can only reach these known destinations.
1126 */
1127export const SystemSettingsPaneSchema = Schema.Literals(["full-disk-access"]);
1128export type SystemSettingsPane = typeof SystemSettingsPaneSchema.Type;
1129
1130export interface DesktopBridge {
1131 beginHostedAuth?: (server: string) => Promise<void>;
1132 getHostedAuthState?: () => Promise<DesktopHostedAuthState>;
1133 getHostedAccessToken?: (server: string) => Promise<string>;
1134 cancelHostedAuth?: () => Promise<void>;
1135 disconnectHostedAuth?: () => Promise<void>;
1136 supportsLocalTerminal?: (input: {
1137 readonly httpBaseUrl: string;
1138 readonly environmentId: string;
1139 }) => boolean;
1140 openLocalTerminal?: (
1141 input: { readonly environmentId: string; readonly url: string },
1142 onMessage: (data: string) => void,
1143 onClose: () => void,
1144 ) => import("./desktopTerminal.ts").DesktopTerminalConnection;
1145 getAppBranding: () => DesktopAppBranding | null;
1146 /** Absolute path of a dropped or picked file; absent on desktop builds predating it. */
1147 getPathForFile?: (file: File) => string;
1148 /** The desktop client's OS platform, read from Electron's preload process. */
1149 getClientPlatform?: () => string;
1150 setNotificationBadge?: (badge: { count: number; image: string | null }) => Promise<void>;
1151 onNotificationBadgeClear?: (listener: () => void) => () => void;
1152 onTrackpadScrollEnd?: (listener: () => void) => () => void;
1153 /**
1154 * The OS locale as a BCP-47 tag, which the renderer cannot read for itself:
1155 * the packaged app ships only the `en-US` Chromium locale pak, so
1156 * `navigator.language` and the default `Intl` locale are pinned to `en-US`
1157 * regardless of OS settings.
1158 */
1159 getSystemLocale?: () => string | null;
1160 // One bootstrap per pool instance currently registered with bootstrap
1161 // info (omits instances whose backend hasn't produced a config yet).
1162 // The primary backend is identified by id === PRIMARY_LOCAL_ENVIRONMENT_ID.
1163 getLocalEnvironmentBootstraps: () => readonly DesktopEnvironmentBootstrap[];
1164 getLocalEnvironmentEnabled?: () => boolean;
1165 setLocalEnvironmentEnabled?: (enabled: boolean) => Promise<void>;
1166 getLocalEnvironmentBearerToken: () => Promise<string>;
1167 getClientSettings: () => Promise<ClientSettings | null>;
1168 setClientSettings: (settings: ClientSettings) => Promise<void>;
1169 getConnectionCatalog?: () => Promise<string | null>;
1170 setConnectionCatalog?: (catalog: string) => Promise<boolean>;
1171 clearConnectionCatalog?: () => Promise<void>;
1172 discoverSshHosts: () => Promise<readonly DesktopDiscoveredSshHost[]>;
1173 /** Resolves a suggested SSH alias before populating the connection form. */
1174 resolveSshHost: (alias: string) => Promise<DesktopSshEnvironmentTarget>;
1175 requestSnapShotPermissions?: (includeAccessibility: boolean) => Promise<void>;
1176 getSnapShotState?: () => Promise<DesktopSnapShotState>;
1177 setupSnapShot?: (action: DesktopSnapShotSetupAction) => Promise<void>;
1178 previewSnapShotConfig?: (
1179 request: DesktopCaptureConfigRequest,
1180 ) => Promise<DesktopCaptureConfigPreview | null>;
1181 applySnapShotConfig?: (previewId: string) => Promise<DesktopCaptureConfigApplied>;
1182 checkSnapShotShortcut?: (
1183 shortcut: SnapShotShortcut,
1184 ) => Promise<DesktopSnapShotShortcutAvailability>;
1185 setSnapShotShortcutSuppressed?: (suppressed: boolean) => Promise<void>;
1186 listPendingSnapShots?: () => Promise<readonly DesktopPendingSnapShot[]>;
1187 readSnapShot?: (id: string) => Promise<DesktopSnapShot>;
1188 setSnapShotAnimationDestination?: (
1189 destination: DesktopSnapShotAnimationDestination,
1190 ) => Promise<void>;
1191 dismissSnapShotAnimation?: (id: DesktopSnapShotId) => Promise<void>;
1192 acknowledgeSnapShot?: (id: string) => Promise<void>;
1193 ensureSshEnvironment: (
1194 target: DesktopSshEnvironmentTarget,
1195 options?: { issuePairingToken?: boolean },
1196 ) => Promise<DesktopSshEnvironmentBootstrap>;
1197 disconnectSshEnvironment: (target: DesktopSshEnvironmentTarget) => Promise<void>;
1198 fetchSshEnvironmentDescriptor: (httpBaseUrl: string) => Promise<ExecutionEnvironmentDescriptor>;
1199 bootstrapSshBearerSession: (
1200 httpBaseUrl: string,
1201 credential: string,
1202 ) => Promise<AuthAccessTokenResult>;
1203 fetchSshSessionState: (httpBaseUrl: string, bearerToken: string) => Promise<AuthSessionState>;
1204 issueSshWebSocketTicket: (
1205 httpBaseUrl: string,
1206 bearerToken: string,
1207 ) => Promise<AuthWebSocketTicketResult>;
1208 onSshPasswordPrompt: (listener: (request: DesktopSshPasswordPromptRequest) => void) => () => void;
1209 resolveSshPasswordPrompt: (requestId: string, password: string | null) => Promise<void>;
1210 getServerExposureState: () => Promise<DesktopServerExposureState>;
1211 setServerExposureMode: (mode: DesktopServerExposureMode) => Promise<DesktopServerExposureState>;
1212 setTailscaleServeEnabled: (input: {
1213 readonly enabled: boolean;
1214 readonly port?: number;
1215 }) => Promise<DesktopServerExposureState>;
1216 getAdvertisedEndpoints: () => Promise<readonly AdvertisedEndpoint[]>;
1217 getWslState: () => Promise<DesktopWslState>;
1218 setWslBackendEnabled: (enabled: boolean) => Promise<DesktopWslState>;
1219 setWslDistro: (distro: string | null) => Promise<DesktopWslState>;
1220 setWslOnly: (enabled: boolean) => Promise<DesktopWslState>;
1221 pickFolder: (options?: PickFolderOptions) => Promise<string | null>;
1222 /** Optional while older desktop shells can host a newer web client. */
1223 pickProjectFavicon?: (initialPath?: string) => Promise<string | null>;
1224 /**
1225 * Multi-select JSON file picker that opens in the VS Code extensions
1226 * directory when one exists. Optional: older desktop builds lack it, and
1227 * web callers fall back to a plain file input.
1228 */
1229 pickThemeFiles?: () => Promise<readonly PickedThemeFile[] | null>;
1230 setTheme: (theme: DesktopTheme) => Promise<void>;
1231 showContextMenu: <T extends string>(
1232 items: readonly ContextMenuItem<T>[],
1233 position?: { x: number; y: number },
1234 ) => Promise<T | null>;
1235 /** Receives a local OAuth code for a sign-in owned by a remote environment. */
1236 receiveProviderAuthCallback?: (authorizationUrl: string) => Promise<string>;
1237 cancelProviderAuthCallback?: (authorizationUrl: string) => Promise<void>;
1238 openExternal: (url: string) => Promise<boolean>;
1239 openVideo?: (input: { readonly url: string; readonly name: string }) => Promise<void>;
1240 /**
1241 * Open a System Settings pane by identifier. Optional: older desktop builds
1242 * lack it, and callers no-op when it is missing.
1243 */
1244 openSystemSettings?: (pane: SystemSettingsPane) => Promise<boolean>;
1245 checkSystemPermission?: (pane: SystemSettingsPane) => Promise<boolean>;
1246 /**
1247 * Probe this desktop machine for installed remote-capable editor CLIs
1248 * (used for remote open-in-editor deep links). Optional: older desktop
1249 * builds lack it; callers fall back to VS Code only.
1250 */
1251 probeRemoteEditors?: () => Promise<readonly EditorId[]>;
1252 /** Present when the desktop shell can perform an ordered plain-text paste. */
1253 pasteAsText?: () => Promise<void>;
1254 onMenuAction: (listener: (action: string) => void) => () => void;
1255 onSnapShotEvent?: (listener: (event: DesktopSnapShotEvent) => void) => () => void;
1256 /**
1257 * Quit-confirmation hint pushes. Optional: older desktop builds never emit
1258 * them.
1259 */
1260 onQuitShortcut?: (listener: (event: QuitShortcutHintEvent) => void) => () => void;
1261 getWindowFullscreenState: () => boolean;
1262 onWindowFullscreenStateChange: (listener: (fullscreen: boolean) => void) => () => void;
1263 getUpdateState: () => Promise<DesktopUpdateState>;
1264 setUpdateChannel: (channel: DesktopUpdateChannel) => Promise<DesktopUpdateState>;
1265 checkForUpdate: () => Promise<DesktopUpdateCheckResult>;
1266 downloadUpdate: () => Promise<DesktopUpdateActionResult>;
1267 installUpdate: () => Promise<DesktopUpdateActionResult>;
1268 onUpdateState: (listener: (state: DesktopUpdateState) => void) => () => void;
1269 /** Settings → `t3` command. Optional: older desktop builds lack it. */
1270 cliCommand?: {
1271 getState: () => Promise<DesktopCliCommandState>;
1272 install: () => Promise<DesktopCliCommandState>;
1273 uninstall: () => Promise<DesktopCliCommandState>;
1274 };
1275 /** Present when the desktop shell accepts `t3 app` activation requests. */
1276 /**
1277 * Web links the OS opens with T3 Code as the default browser (macOS). The
1278 * renderer reports when it listens; links queue in the main process until then.
1279 */
1280 webLinks?: {
1281 setReady: (ready: boolean) => Promise<void>;
1282 onOpen: (listener: (url: string) => void) => () => void;
1283 };
1284 appActivation?: {
1285 setReady: (ready: boolean) => Promise<void>;
1286 complete: (response: DesktopAppActivationResponse) => Promise<void>;
1287 onRequest: (listener: (request: DesktopAppActivationRequest) => void) => () => void;
1288 };
1289 /**
1290 * Desktop-only preview surface. Present iff the renderer is hosted by the
1291 * Electron desktop build; web builds have `preview === undefined`.
1292 */
1293 preview?: DesktopPreviewBridge;
1294}
1295
1296/** Renderer callback invoked by Electron with a fresh user gesture before display-media capture. */
1297export const DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER = "__t3DesktopPreviewRecordingCapture";
1298
1299export interface DesktopPreviewBridge {
1300 setForwardedShortcuts?: (shortcuts: ReadonlyArray<PreviewForwardedShortcut>) => Promise<void>;
1301 createTab: (tabId: string, defaults?: DesktopPreviewTabDefaults) => Promise<void>;
1302 closeTab: (tabId: string) => Promise<void>;
1303 registerWebview: (tabId: string, webContentsId: number) => Promise<void>;
1304 navigate: (tabId: string, url: string) => Promise<void>;
1305 goBack: (tabId: string) => Promise<void>;
1306 goForward: (tabId: string) => Promise<void>;
1307 refresh: (tabId: string) => Promise<void>;
1308 zoomIn: (tabId: string) => Promise<void>;
1309 zoomOut: (tabId: string) => Promise<void>;
1310 resetZoom: (tabId: string) => Promise<void>;
1311 /** Sets a tab's zoom to the factor its environment published, for tabs the server drives. */
1312 setZoomFactor: (tabId: string, zoomFactor: number) => Promise<void>;
1313 /** Reload bypassing the HTTP cache. */
1314 hardReload: (tabId: string) => Promise<void>;
1315 /**
1316 * Emulate `prefers-color-scheme` on the guest page ("system" clears the
1317 * override). Persists per tab and is re-applied across webview swaps.
1318 */
1319 setColorScheme: (tabId: string, colorScheme: DesktopPreviewColorScheme) => Promise<void>;
1320 /**
1321 * Silence the tab's audio output. Persists per tab and is re-applied across
1322 * webview swaps, but is dropped when the tab closes. Muting a silent tab is
1323 * allowed; it simply takes effect once the page plays something.
1324 */
1325 setAudioMuted: (tabId: string, audioMuted: boolean) => Promise<void>;
1326 /** Open the guest webview's DevTools (detached). */
1327 openDevTools: (tabId: string) => Promise<void>;
1328 /** Drop cookies + storage data for the preview partition (all tabs). */
1329 clearCookies: (environmentId: EnvironmentId, profileId?: string) => Promise<void>;
1330 /** Drop the HTTP cache for the preview partition (all tabs). */
1331 clearCache: (environmentId: EnvironmentId, profileId?: string) => Promise<void>;
1332 /**
1333 * One-shot config for mounting a preview `<webview>`. Replaces three
1334 * earlier round-trip calls (`getBrowserPartition`, `getWebviewPreferences`,
1335 * `getPickPreloadPath`) so adding a new field here only requires touching
1336 * the contract + main, not the renderer's mount logic.
1337 */
1338 getPreviewConfig: (
1339 environmentId: EnvironmentId,
1340 profileId?: string,
1341 ) => Promise<DesktopPreviewWebviewConfig>;
1342 /** Browsers on this machine whose cookies can be imported. */
1343 listBrowserImportSources: () => Promise<ReadonlyArray<BrowserImportSource>>;
1344 importBrowserCookies: (input: {
1345 readonly environmentId: EnvironmentId;
1346 readonly sourceId: BrowserImportSourceId;
1347 readonly sourceProfileDirectory: string;
1348 readonly targetProfileId: string;
1349 }) => Promise<BrowserImportResult>;
1350 setAnnotationTheme: (theme: DesktopPreviewAnnotationTheme) => Promise<void>;
1351 /** Keep an open annotation picker's send shortcut in sync with its thread grant. */
1352 setAnnotationSendEnabled: (tabId: string, enabled: boolean) => Promise<void>;
1353 /**
1354 * Activate the in-page element picker for the given tab. Resolves with
1355 * the picked annotation and its attach/send intent, or `null` when the
1356 * user cancels (Escape / nav). The promise rejects if the picker can't be
1357 * activated (no webview, etc.).
1358 */
1359 pickElement: (tabId: string) => Promise<PreviewAnnotationSubmissionResult | null>;
1360 /** Cancel an in-flight preview annotation session. */
1361 cancelPickElement: (tabId: string) => Promise<void>;
1362 captureScreenshot: (tabId: string) => Promise<DesktopPreviewScreenshotArtifact>;
1363 revealArtifact: (path: string) => Promise<void>;
1364 copyArtifactToClipboard: (path: string) => Promise<void>;
1365 pictureInPicture: {
1366 open: (tabId: string) => Promise<void>;
1367 close: (tabId: string) => Promise<void>;
1368 };
1369 recording: {
1370 onInput: (listener: (event: DesktopPreviewRecordingInputEvent) => void) => () => void;
1371 startScreencast: (tabId: string) => Promise<void>;
1372 stopScreencast: (tabId: string) => Promise<void>;
1373 save: (
1374 tabId: string,
1375 mimeType: string,
1376 data: Uint8Array,
1377 ) => Promise<DesktopPreviewRecordingArtifact>;
1378 onFrame: (listener: (frame: DesktopPreviewRecordingFrame) => void) => () => void;
1379 };
1380 onStateChange: (listener: (tabId: string, state: DesktopPreviewTabState) => void) => () => void;
1381 onPointerEvent: (listener: (event: DesktopPreviewPointerEvent) => void) => () => void;
1382 onOpenLink: (listener: (event: DesktopPreviewOpenLinkEvent) => void) => () => void;
1383}
1384
1385export type ConfirmDialogVariant = "default" | "destructive";
1386
1387export interface ConfirmDialogOptions {
1388 readonly variant?: ConfirmDialogVariant;
1389}
1390
1391/**
1392 * APIs bound to the local app shell, not to any particular backend environment.
1393 *
1394 * These capabilities describe the desktop/browser host that the user is
1395 * currently running: dialogs, external-link opening, context menus, and
1396 * app-level settings/config access. They must not be used as a proxy for
1397 * "whatever environment the user is targeting", because in a multi-environment
1398 * world the local shell and a selected backend environment are distinct
1399 * concepts.
1400 */
1401export interface LocalApi {
1402 dialogs: {
1403 pickFolder: (options?: PickFolderOptions) => Promise<string | null>;
1404 confirm: (message: string, options?: ConfirmDialogOptions) => Promise<boolean>;
1405 };
1406 shell: {
1407 openExternal: (url: string) => Promise<void>;
1408 /** Opens a known System Settings pane; no-ops outside the desktop app. */
1409 openSystemSettings: (pane: SystemSettingsPane) => Promise<void>;
1410 };
1411 contextMenu: {
1412 show: <T extends string>(
1413 items: readonly ContextMenuItem<T>[],
1414 position?: { x: number; y: number },
1415 ) => Promise<T | null>;
1416 close: () => Promise<void>;
1417 };
1418 persistence: {
1419 getClientSettings: () => Promise<ClientSettings | null>;
1420 setClientSettings: (settings: ClientSettings) => Promise<void>;
1421 };
1422}