1import type { ScopedThreadRef } from "@t3tools/contracts";
2import { isAtomCommandInterrupted } from "@t3tools/client-runtime/state/runtime";
3import * as Schema from "effect/Schema";
4
5import {
6 browserDefaultOpenProfileId,
7 browserDefaultOpenViewport,
8 resolveBrowserDefaults,
9} from "~/browser/browserDefaults";
10import { isWebUrl, resolveBrowserLinkTargetPreference } from "~/browser/browserLinkTarget";
11import type { OpenPreviewMutation } from "~/browser/openFileInPreview";
12import { isPreviewAvailableFor, previewRuntimeFor } from "~/browser/previewRuntime";
13import { recordVisitForThread } from "~/browserHistoryStore";
14import { applyPreviewServerSnapshot } from "~/previewStateStore";
15import { useRightPanelStore } from "~/rightPanelStore";
16
17const terminalLinkErrorContext = {
18 environmentId: Schema.String,
19 threadId: Schema.String,
20 targetOrigin: Schema.String,
21 cause: Schema.Defect(),
22};
23
24export class TerminalLinkPreviewOpenError extends Schema.TaggedError<TerminalLinkPreviewOpenError>()(
25 "TerminalLinkPreviewOpenError",
26 terminalLinkErrorContext,
27) {
28 override get message(): string {
29 return `Failed to open terminal link ${this.targetOrigin} in preview for thread ${this.threadId}.`;
30 }
31}
32
33interface OpenTerminalLinkInPreviewInput<E> {
34 readonly url: string;
35 readonly threadRef: ScopedThreadRef;
36 readonly openPreview: OpenPreviewMutation<E>;
37 readonly fallbackToBrowser: () => void;
38 /** Cmd/Ctrl-click bypasses the preference and opens in the system browser. */
39 readonly forceBrowser: boolean;
40}
41
42/**
43 * Opens a terminal hyperlink where the "Open links in" setting says, unless a
44 * Cmd/Ctrl-click explicitly requests the system browser.
45 */
46export async function openTerminalLinkInPreview<E>(
47 input: OpenTerminalLinkInPreviewInput<E>,
48): Promise<void> {
49 const supportsPreview =
50 !input.forceBrowser &&
51 isWebUrl(input.url) &&
52 isPreviewAvailableFor(input.threadRef.environmentId) &&
53 input.threadRef.threadId.length > 0 &&
54 (await resolveBrowserLinkTargetPreference()) === "app";
55
56 if (!supportsPreview) {
57 input.fallbackToBrowser();
58 return;
59 }
60
61 const errorContext = {
62 environmentId: input.threadRef.environmentId,
63 threadId: input.threadRef.threadId,
64 targetOrigin: new URL(input.url).origin,
65 };
66
67 const defaults = await resolveBrowserDefaults();
68 const runtime = previewRuntimeFor(input.threadRef.environmentId);
69 const result = await input.openPreview({
70 environmentId: input.threadRef.environmentId,
71 input: {
72 threadId: input.threadRef.threadId,
73 url: input.url,
74 // Same reason as `openUrlInPreview`: this path handles its own result
75 // mapping, so the configured defaults are applied explicitly.
76 viewport: browserDefaultOpenViewport(defaults),
77 profileId: browserDefaultOpenProfileId(defaults),
78 ...(runtime === undefined ? {} : { runtime }),
79 },
80 });
81 if (result._tag === "Failure") {
82 if (isAtomCommandInterrupted(result)) {
83 return;
84 }
85 console.error(
86 new TerminalLinkPreviewOpenError({
87 ...errorContext,
88 cause: result.cause,
89 }),
90 );
91 input.fallbackToBrowser();
92 return;
93 }
94 recordVisitForThread(input.threadRef, input.url);
95 applyPreviewServerSnapshot(input.threadRef, result.value);
96 useRightPanelStore.getState().openBrowser(input.threadRef, result.value.tabId);
97}