Repository navigation
Expand file tree
/
Copy pathopenFileInPreview.ts
More file actions
162 lines (153 loc) · 5.57 KB
/
Copy pathopenFileInPreview.ts
File metadata and controls
162 lines (153 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import type {
AssetCreateUrlResult,
AssetResource,
EnvironmentId,
PreviewOpenInput,
PreviewSessionSnapshot,
ScopedThreadRef,
} from "@t3tools/contracts";
import { mediaFileReference } from "@t3tools/client-runtime/media-reference";
import {
type AtomCommandResult,
mapAtomCommandResult,
} from "@t3tools/client-runtime/state/runtime";
import * as Cause from "effect/Cause";
import * as Schema from "effect/Schema";
import { AsyncResult } from "effect/reactivity";
import { resolveAssetUrl } from "~/assets/assetUrls";
import { isPreviewAvailableFor, previewRuntimeFor } from "~/browser/previewRuntime";
import {
applyPreviewServerSnapshot,
readThreadPreviewState,
rememberPreviewUrl,
setActivePreviewTab,
updatePreviewServerSnapshot,
} from "~/previewStateStore";
import {
rightPanelHostRef,
selectSelectedRightPanelSurface,
useRightPanelStore,
} from "~/rightPanelStore";
import { resolveBrowserDefaults } from "./browserDefaults";
export const isBrowserPreviewFile = (path: string): boolean =>
/\.(?:html?|pdf)$/i.test(path.split(/[?#]/, 1)[0] ?? "");
export class BrowserPreviewUnavailableError extends Schema.TaggedError<BrowserPreviewUnavailableError>()(
"BrowserPreviewUnavailableError",
{ message: Schema.String },
) {}
export class BrowserSettingsReadError extends Schema.TaggedError<BrowserSettingsReadError>()(
"BrowserSettingsReadError",
{ cause: Schema.Defect() },
) {
override get message(): string {
return "Saved browser settings could not be loaded.";
}
}
export const isBrowserSettingsReadError = Schema.is(BrowserSettingsReadError);
export type OpenPreviewMutation<E = unknown> = (input: {
readonly environmentId: EnvironmentId;
readonly input: PreviewOpenInput;
}) => Promise<AtomCommandResult<PreviewSessionSnapshot, E>>;
export async function openUrlInPreview<E>(input: {
readonly threadRef: ScopedThreadRef;
readonly url: string;
readonly openPreview: OpenPreviewMutation<E>;
readonly profileId?: PreviewOpenInput["profileId"];
readonly background?: boolean;
}): Promise<AtomCommandResult<void, E | BrowserSettingsReadError>> {
// Read before any await: a side chat pane that unmounts meanwhile drops its host mapping.
const threadRef = rightPanelHostRef(input.threadRef);
const defaults = await resolveBrowserDefaults().catch(
(cause: unknown) => new BrowserSettingsReadError({ cause }),
);
if (isBrowserSettingsReadError(defaults)) {
return AsyncResult.failure(Cause.fail(defaults));
}
const runtime = previewRuntimeFor(threadRef.environmentId);
const previousActiveTabId = readThreadPreviewState(threadRef).activeTabId;
// The server changes the active tab, not panel selection; selection changes come from the user.
const selectedSurface = () =>
selectSelectedRightPanelSurface(useRightPanelStore.getState().byThreadKey, threadRef)?.id ??
null;
const surfaceBeforeOpen = selectedSurface();
const result = await input.openPreview({
environmentId: threadRef.environmentId,
input: {
threadId: threadRef.threadId,
url: input.url,
viewport: defaults.viewport,
profileId: input.profileId ?? defaults.profileId,
...(runtime === undefined ? {} : { runtime }),
},
});
return mapAtomCommandResult(result, (snapshot) => {
rememberPreviewUrl(threadRef, input.url);
if (input.background) {
updatePreviewServerSnapshot(threadRef, snapshot);
// Restore focus unless the user picked a tab while opening this one.
if (
previousActiveTabId &&
readThreadPreviewState(threadRef).activeTabId === snapshot.tabId &&
selectedSurface() === surfaceBeforeOpen
) {
setActivePreviewTab(threadRef, previousActiveTabId);
}
return;
}
applyPreviewServerSnapshot(threadRef, snapshot);
useRightPanelStore.getState().openBrowser(threadRef, snapshot.tabId);
});
}
export async function openFileInPreview<AssetError, PreviewError>(input: {
readonly threadRef: ScopedThreadRef;
readonly filePath: string;
readonly workspaceRoot: string | undefined;
readonly httpBaseUrl: string;
readonly createAssetUrl: (input: {
readonly environmentId: EnvironmentId;
readonly input: { readonly resource: AssetResource };
}) => Promise<AtomCommandResult<AssetCreateUrlResult, AssetError>>;
readonly openPreview: OpenPreviewMutation<PreviewError>;
}): Promise<
AtomCommandResult<
void,
AssetError | PreviewError | BrowserPreviewUnavailableError | BrowserSettingsReadError
>
> {
if (!isPreviewAvailableFor(input.threadRef.environmentId)) {
return AsyncResult.failure(
Cause.fail(
new BrowserPreviewUnavailableError({
message: "The integrated browser is unavailable in this runtime.",
}),
),
);
}
const hostRef = rightPanelHostRef(input.threadRef);
const insideWorkspace =
mediaFileReference(input.filePath, input.workspaceRoot).relativePath !== undefined;
const assetResult = await input.createAssetUrl({
environmentId: input.threadRef.environmentId,
input: {
resource: {
_tag: insideWorkspace ? "workspace-file" : "media-file",
threadId: input.threadRef.threadId,
path: input.filePath,
},
},
});
if (assetResult._tag === "Failure") {
return AsyncResult.failure(assetResult.cause);
}
const assetUrl = resolveAssetUrl(input.httpBaseUrl, assetResult.value.relativeUrl);
if (assetUrl === null) {
return AsyncResult.failure(
Cause.die(new Error("The environment returned an invalid asset URL.")),
);
}
return openUrlInPreview({
threadRef: hostRef,
url: assetUrl,
openPreview: input.openPreview,
});
}