authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-11 15:51:47-07:00
committergravatar for me@theo.liTheo Browne <me@theo.li> 2026-06-13 22:18:37-07:00
log9b43123ba2a90ecaaf2eac1f75b6aebcf181f253
tree40fd2cc58603d6ca9504bf3f36136e8bebb53e39
parent17fb0e4a9a068f3ba6c55871dd44ed3c5d70aefe

fix(preview): port browser preview to current main

Co-authored-by: codex <codex@users.noreply.github.com>

30 files changed, 381 insertions(+), 163 deletions(-)

apps/desktop/src/ipc/methods/preview.ts+42-14
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as Data from "effect/Data";
1import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
2import { BrowserWindow } from "electron";4import { BrowserWindow } from "electron";
3import * as NodeFileSystem from "node:fs";
4import * as NodePath from "node:path";
5import { pathToFileURL } from "node:url";5import { pathToFileURL } from "node:url";
66
7import { previewViewManager } from "../../preview-view-manager.ts";7import { previewViewManager } from "../../preview-view-manager.ts";
...@@ -29,18 +29,38 @@ const tabIdFrom = (raw: unknown): string => {...@@ -29,18 +29,38 @@ const tabIdFrom = (raw: unknown): string => {
29 return tabId;29 return tabId;
30};30};
3131
32const method = (channel: string, handler: (raw: unknown) => unknown | Promise<unknown>): DesktopIpcMethod<unknown, never> => ({32class PreviewIpcError extends Data.TaggedError("PreviewIpcError")<{
33 readonly cause: unknown;
34}> {}
35
36const method = (
37 channel: string,
38 handler: (raw: unknown) => unknown | Promise<unknown>,
39): DesktopIpcMethod<PreviewIpcError, never> => ({
33 channel,40 channel,
34 handler: (raw) => Effect.tryPromise({ try: () => Promise.resolve(handler(raw)), catch: (error) => error }),41 handler: (raw) =>
42 Effect.tryPromise({
43 try: () => Promise.resolve(handler(raw)),
44 catch: (cause) => new PreviewIpcError({ cause }),
45 }),
35});46});
3647
37export const previewMethods = [48export const previewMethods = [
38 method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) => previewViewManager.createTab(tabIdFrom(raw))),49 method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) =>
39 method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) => previewViewManager.closeTab(tabIdFrom(raw))),50 previewViewManager.createTab(tabIdFrom(raw)),
51 ),
52 method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) =>
53 previewViewManager.closeTab(tabIdFrom(raw)),
54 ),
40 method(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, (raw) => {55 method(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, (raw) => {
41 const tabId = tabIdFrom(raw);56 const tabId = tabIdFrom(raw);
42 const webContentsId = typeof raw === "object" && raw !== null && "webContentsId" in raw ? raw.webContentsId : null;57 const webContentsId =
43 if (typeof webContentsId !== "number" || !Number.isInteger(webContentsId) || webContentsId <= 0) {58 typeof raw === "object" && raw !== null && "webContentsId" in raw ? raw.webContentsId : null;
59 if (
60 typeof webContentsId !== "number" ||
61 !Number.isInteger(webContentsId) ||
62 webContentsId <= 0
63 ) {
44 throw new Error("preview webContentsId must be a positive integer");64 throw new Error("preview webContentsId must be a positive integer");
45 }65 }
46 return previewViewManager.registerWebview(tabId, webContentsId);66 return previewViewManager.registerWebview(tabId, webContentsId);
...@@ -52,21 +72,29 @@ export const previewMethods = [...@@ -52,21 +72,29 @@ export const previewMethods = [
52 return previewViewManager.navigate(tabId, url);72 return previewViewManager.navigate(tabId, url);
53 }),73 }),
54 method(IpcChannels.PREVIEW_GO_BACK_CHANNEL, (raw) => previewViewManager.goBack(tabIdFrom(raw))),74 method(IpcChannels.PREVIEW_GO_BACK_CHANNEL, (raw) => previewViewManager.goBack(tabIdFrom(raw))),
55 method(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, (raw) => previewViewManager.goForward(tabIdFrom(raw))),75 method(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, (raw) =>
76 previewViewManager.goForward(tabIdFrom(raw)),
77 ),
56 method(IpcChannels.PREVIEW_REFRESH_CHANNEL, (raw) => previewViewManager.refresh(tabIdFrom(raw))),78 method(IpcChannels.PREVIEW_REFRESH_CHANNEL, (raw) => previewViewManager.refresh(tabIdFrom(raw))),
57 method(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, (raw) => previewViewManager.zoomIn(tabIdFrom(raw))),79 method(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, (raw) => previewViewManager.zoomIn(tabIdFrom(raw))),
58 method(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, (raw) => previewViewManager.zoomOut(tabIdFrom(raw))),80 method(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, (raw) => previewViewManager.zoomOut(tabIdFrom(raw))),
59 method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) => previewViewManager.resetZoom(tabIdFrom(raw))),81 method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) =>
60 method(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, (raw) => previewViewManager.hardReload(tabIdFrom(raw))),82 previewViewManager.resetZoom(tabIdFrom(raw)),
61 method(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, (raw) => previewViewManager.openDevTools(tabIdFrom(raw))),83 ),
84 method(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, (raw) =>
85 previewViewManager.hardReload(tabIdFrom(raw)),
86 ),
87 method(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, (raw) =>
88 previewViewManager.openDevTools(tabIdFrom(raw)),
89 ),
62 method(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, () => previewViewManager.clearCookies()),90 method(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, () => previewViewManager.clearCookies()),
63 method(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, () => previewViewManager.clearCache()),91 method(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, () => previewViewManager.clearCache()),
64 method(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, () => {92 method(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, () => {
65 const preloadPath = NodePath.join(__dirname, "preview-pick-preload.cjs");93 const preloadPath = `${__dirname}/preview-pick-preload.cjs`;
66 return {94 return {
67 partition: previewViewManager.getBrowserPartition(),95 partition: previewViewManager.getBrowserPartition(),
68 webPreferences: PREVIEW_WEBVIEW_PREFERENCES,96 webPreferences: PREVIEW_WEBVIEW_PREFERENCES,
69 preloadUrl: NodeFileSystem.existsSync(preloadPath) ? pathToFileURL(preloadPath).href : null,97 preloadUrl: pathToFileURL(preloadPath).href,
70 };98 };
71 }),99 }),
72 method(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, (raw) =>100 method(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, (raw) =>
apps/desktop/src/main.ts-2
...@@ -28,7 +28,6 @@ import * as DesktopApp from "./app/DesktopApp.ts";...@@ -28,7 +28,6 @@ import * as DesktopApp from "./app/DesktopApp.ts";
28import * as DesktopAppIdentity from "./app/DesktopAppIdentity.ts";28import * as DesktopAppIdentity from "./app/DesktopAppIdentity.ts";
29import * as DesktopCloudAuth from "./app/DesktopCloudAuth.ts";29import * as DesktopCloudAuth from "./app/DesktopCloudAuth.ts";
30import * as DesktopCloudAuthTokenStore from "./app/DesktopCloudAuthTokenStore.ts";30import * as DesktopCloudAuthTokenStore from "./app/DesktopCloudAuthTokenStore.ts";
31import * as DesktopConnectionCatalogStore from "./app/DesktopConnectionCatalogStore.ts";
32import * as DesktopApplicationMenu from "./window/DesktopApplicationMenu.ts";31import * as DesktopApplicationMenu from "./window/DesktopApplicationMenu.ts";
33import * as DesktopAssets from "./app/DesktopAssets.ts";32import * as DesktopAssets from "./app/DesktopAssets.ts";
34import * as DesktopBackendConfiguration from "./backend/DesktopBackendConfiguration.ts";33import * as DesktopBackendConfiguration from "./backend/DesktopBackendConfiguration.ts";
...@@ -115,7 +114,6 @@ const desktopFoundationLayer = Layer.mergeAll(...@@ -115,7 +114,6 @@ const desktopFoundationLayer = Layer.mergeAll(
115 DesktopClientSettings.layer,114 DesktopClientSettings.layer,
116 DesktopSavedEnvironments.layer,115 DesktopSavedEnvironments.layer,
117 DesktopCloudAuthTokenStore.layer,116 DesktopCloudAuthTokenStore.layer,
118 DesktopConnectionCatalogStore.layer,
119 DesktopAssets.layer,117 DesktopAssets.layer,
120 DesktopObservability.layer,118 DesktopObservability.layer,
121).pipe(Layer.provideMerge(desktopEnvironmentLayer));119).pipe(Layer.provideMerge(desktopEnvironmentLayer));
apps/desktop/src/picked-element-payload.test.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vite-plus/test";
22
3import { isPickedElementPayload } from "./picked-element-payload.ts";3import { isPickedElementPayload } from "./picked-element-payload.ts";
44
apps/desktop/src/preload.ts+3-7
...@@ -47,10 +47,6 @@ contextBridge.exposeInMainWorld("desktopBridge", {...@@ -47,10 +47,6 @@ contextBridge.exposeInMainWorld("desktopBridge", {
47 ipcRenderer.invoke(IpcChannels.SET_SAVED_ENVIRONMENT_SECRET_CHANNEL, { environmentId, secret }),47 ipcRenderer.invoke(IpcChannels.SET_SAVED_ENVIRONMENT_SECRET_CHANNEL, { environmentId, secret }),
48 removeSavedEnvironmentSecret: (environmentId) =>48 removeSavedEnvironmentSecret: (environmentId) =>
49 ipcRenderer.invoke(IpcChannels.REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL, environmentId),49 ipcRenderer.invoke(IpcChannels.REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL, environmentId),
50 getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL),
51 setConnectionCatalog: (catalog) =>
52 ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog),
53 clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL),
54 discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),50 discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),
55 ensureSshEnvironment: async (target, options) =>51 ensureSshEnvironment: async (target, options) =>
56 unwrapEnsureSshEnvironmentResult(52 unwrapEnsureSshEnvironmentResult(
...@@ -164,8 +160,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {...@@ -164,8 +160,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
164 clearCookies: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL),160 clearCookies: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL),
165 clearCache: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL),161 clearCache: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL),
166 getPreviewConfig: () => ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL),162 getPreviewConfig: () => ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL),
167 pickElement: (tabId) =>163 pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
168 ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
169 cancelPickElement: (tabId) =>164 cancelPickElement: (tabId) =>
170 ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),165 ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),
171 onStateChange: (listener) => {166 onStateChange: (listener) => {
...@@ -178,7 +173,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {...@@ -178,7 +173,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
178 listener(tabId, state as DesktopPreviewTabState);173 listener(tabId, state as DesktopPreviewTabState);
179 };174 };
180 ipcRenderer.on(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);175 ipcRenderer.on(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);
181 return () => ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);176 return () =>
177 ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);
182 },178 },
183 },179 },
184} satisfies DesktopBridge);180} satisfies DesktopBridge);
apps/desktop/src/preview-pick-preload.test.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vite-plus/test";
22
3import { computeLabelPosition } from "./preview-pick-label-position.ts";3import { computeLabelPosition } from "./preview-pick-label-position.ts";
44
apps/desktop/src/preview-pick-preload.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics globalDate:off
1/**2/**
2 * Preview pick preload — runs in the isolated world of the Chromium3 * Preview pick preload — runs in the isolated world of the Chromium
3 * `<webview>` that hosts the in-app browser. Loaded via the4 * `<webview>` that hosts the in-app browser. Loaded via the
apps/desktop/src/preview-view-manager.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics globalDate:off
1/**2/**
2 * PreviewViewManager — desktop side of the in-app browser preview.3 * PreviewViewManager — desktop side of the in-app browser preview.
3 *4 *
apps/desktop/src/preview-webview-preferences.test.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vite-plus/test";
22
3import { PREVIEW_WEBVIEW_PREFERENCES } from "./preview-webview-preferences.ts";3import { PREVIEW_WEBVIEW_PREFERENCES } from "./preview-webview-preferences.ts";
44
apps/server/src/preview/Layers/Manager.test.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { it } from "@effect/vitest";1import { it } from "@effect/vitest";
2import { type PreviewEvent, ThreadId } from "@t3tools/contracts";2import { type PreviewEvent, ThreadId } from "@t3tools/contracts";
3import { Effect, PubSub } from "effect";3import { Effect, PubSub } from "effect";
4import { expect } from "vitest";4import { expect } from "vite-plus/test";
55
6import { PreviewManager } from "../Services/Manager.ts";6import { PreviewManager } from "../Services/Manager.ts";
7import { PreviewManagerLive } from "./Manager.ts";7import { PreviewManagerLive } from "./Manager.ts";
apps/server/src/preview/Layers/Manager.ts+27-19
...@@ -21,7 +21,7 @@ import {...@@ -21,7 +21,7 @@ import {
21 normalizePreviewUrl,21 normalizePreviewUrl,
22 PreviewUrlNormalizationError,22 PreviewUrlNormalizationError,
23} from "@t3tools/shared/preview";23} from "@t3tools/shared/preview";
24import { Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect";24import { DateTime, Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect";
2525
26import { PreviewManager, type PreviewManagerShape } from "../Services/Manager.ts";26import { PreviewManager, type PreviewManagerShape } from "../Services/Manager.ts";
2727
...@@ -66,30 +66,34 @@ const normalizeUrl = (rawUrl: string): Effect.Effect<string, PreviewInvalidUrlEr...@@ -66,30 +66,34 @@ const normalizeUrl = (rawUrl: string): Effect.Effect<string, PreviewInvalidUrlEr
66 }),66 }),
67 });67 });
6868
69const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));
70
69const buildLoadingSnapshot = (input: {71const buildLoadingSnapshot = (input: {
70 readonly threadId: string;72 readonly threadId: string;
71 readonly tabId: string;73 readonly tabId: string;
72 readonly url: string;74 readonly url: string;
73 readonly title: string;75 readonly title: string;
76 readonly updatedAt: string;
74}): PreviewSessionSnapshot => ({77}): PreviewSessionSnapshot => ({
75 threadId: input.threadId,78 threadId: input.threadId,
76 tabId: input.tabId,79 tabId: input.tabId,
77 navStatus: { _tag: "Loading", url: input.url, title: input.title },80 navStatus: { _tag: "Loading", url: input.url, title: input.title },
78 canGoBack: false,81 canGoBack: false,
79 canGoForward: false,82 canGoForward: false,
80 updatedAt: new Date().toISOString(),83 updatedAt: input.updatedAt,
81});84});
8285
83const buildIdleSnapshot = (input: {86const buildIdleSnapshot = (input: {
84 readonly threadId: string;87 readonly threadId: string;
85 readonly tabId: string;88 readonly tabId: string;
89 readonly updatedAt: string;
86}): PreviewSessionSnapshot => ({90}): PreviewSessionSnapshot => ({
87 threadId: input.threadId,91 threadId: input.threadId,
88 tabId: input.tabId,92 tabId: input.tabId,
89 navStatus: { _tag: "Idle" },93 navStatus: { _tag: "Idle" },
90 canGoBack: false,94 canGoBack: false,
91 canGoForward: false,95 canGoForward: false,
92 updatedAt: new Date().toISOString(),96 updatedAt: input.updatedAt,
93});97});
9498
95export const makePreviewManager = Effect.gen(function* () {99export const makePreviewManager = Effect.gen(function* () {
...@@ -152,14 +156,16 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -152,14 +156,16 @@ export const makePreviewManager = Effect.gen(function* () {
152 const open: PreviewManagerShape["open"] = (input) =>156 const open: PreviewManagerShape["open"] = (input) =>
153 Effect.gen(function* () {157 Effect.gen(function* () {
154 const tabId = newPreviewTabId();158 const tabId = newPreviewTabId();
159 const updatedAt = yield* currentIsoTimestamp;
155 const snapshot = input.url160 const snapshot = input.url
156 ? buildLoadingSnapshot({161 ? buildLoadingSnapshot({
157 threadId: input.threadId,162 threadId: input.threadId,
158 tabId,163 tabId,
159 url: yield* normalizeUrl(input.url),164 url: yield* normalizeUrl(input.url),
160 title: "",165 title: "",
166 updatedAt,
161 })167 })
162 : buildIdleSnapshot({ threadId: input.threadId, tabId });168 : buildIdleSnapshot({ threadId: input.threadId, tabId, updatedAt });
163 yield* SynchronizedRef.update(stateRef, (state) => {169 yield* SynchronizedRef.update(stateRef, (state) => {
164 const sessions = new Map(state.sessions);170 const sessions = new Map(state.sessions);
165 sessions.set(compositeKey(input.threadId, tabId), {171 sessions.set(compositeKey(input.threadId, tabId), {
...@@ -183,7 +189,8 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -183,7 +189,8 @@ export const makePreviewManager = Effect.gen(function* () {
183 Effect.gen(function* () {189 Effect.gen(function* () {
184 const url = yield* normalizeUrl(input.url);190 const url = yield* normalizeUrl(input.url);
185 return yield* mutateExistingSession(input.threadId, input.tabId, (session) =>191 return yield* mutateExistingSession(input.threadId, input.tabId, (session) =>
186 Effect.sync(() => {192 Effect.gen(function* () {
193 const updatedAt = yield* currentIsoTimestamp;
187 const previousTitle =194 const previousTitle =
188 session.snapshot.navStatus._tag === "Idle" ? "" : session.snapshot.navStatus.title;195 session.snapshot.navStatus._tag === "Idle" ? "" : session.snapshot.navStatus.title;
189 const resolvedTitle = input.resolvedTitle ?? previousTitle;196 const resolvedTitle = input.resolvedTitle ?? previousTitle;
...@@ -193,7 +200,7 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -193,7 +200,7 @@ export const makePreviewManager = Effect.gen(function* () {
193 navStatus: { _tag: "Success", url, title: resolvedTitle },200 navStatus: { _tag: "Success", url, title: resolvedTitle },
194 canGoBack: session.snapshot.canGoBack,201 canGoBack: session.snapshot.canGoBack,
195 canGoForward: session.snapshot.canGoForward,202 canGoForward: session.snapshot.canGoForward,
196 updatedAt: new Date().toISOString(),203 updatedAt,
197 };204 };
198 return {205 return {
199 next: { ...session, snapshot },206 next: { ...session, snapshot },
...@@ -212,14 +219,15 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -212,14 +219,15 @@ export const makePreviewManager = Effect.gen(function* () {
212219
213 const reportStatus: PreviewManagerShape["reportStatus"] = (input) =>220 const reportStatus: PreviewManagerShape["reportStatus"] = (input) =>
214 mutateExistingSession(input.threadId, input.tabId, (session) =>221 mutateExistingSession(input.threadId, input.tabId, (session) =>
215 Effect.sync(() => {222 Effect.gen(function* () {
223 const updatedAt = yield* currentIsoTimestamp;
216 const snapshot: PreviewSessionSnapshot = {224 const snapshot: PreviewSessionSnapshot = {
217 threadId: session.threadId,225 threadId: session.threadId,
218 tabId: session.tabId,226 tabId: session.tabId,
219 navStatus: input.navStatus,227 navStatus: input.navStatus,
220 canGoBack: input.canGoBack,228 canGoBack: input.canGoBack,
221 canGoForward: input.canGoForward,229 canGoForward: input.canGoForward,
222 updatedAt: new Date().toISOString(),230 updatedAt,
223 };231 };
224 const emit: PreviewEvent =232 const emit: PreviewEvent =
225 input.navStatus._tag === "LoadFailed"233 input.navStatus._tag === "LoadFailed"
...@@ -256,8 +264,9 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -256,8 +264,9 @@ export const makePreviewManager = Effect.gen(function* () {
256 );264 );
257265
258 const close: PreviewManagerShape["close"] = (input) =>266 const close: PreviewManagerShape["close"] = (input) =>
259 Effect.flatMap(267 Effect.gen(function* () {
260 SynchronizedRef.modify(stateRef, (state) => {268 const createdAt = yield* currentIsoTimestamp;
269 const events = yield* SynchronizedRef.modify(stateRef, (state) => {
261 const eventsToEmit: PreviewEvent[] = [];270 const eventsToEmit: PreviewEvent[] = [];
262 const sessions = new Map(state.sessions);271 const sessions = new Map(state.sessions);
263 const targets = input.tabId272 const targets = input.tabId
...@@ -271,21 +280,20 @@ export const makePreviewManager = Effect.gen(function* () {...@@ -271,21 +280,20 @@ export const makePreviewManager = Effect.gen(function* () {
271 type: "closed",280 type: "closed",
272 threadId: target.threadId,281 threadId: target.threadId,
273 tabId: target.tabId,282 tabId: target.tabId,
274 createdAt: new Date().toISOString(),283 createdAt,
275 });284 });
276 }285 }
277 if (eventsToEmit.length === 0) {286 if (eventsToEmit.length === 0) {
278 return [eventsToEmit, state] as const;287 return [eventsToEmit, state] as const;
279 }288 }
280 return [eventsToEmit, { sessions }] as const;289 return [eventsToEmit, { sessions }] as const;
281 }),290 });
282 (events) =>291 if (events.length > 0) {
283 events.length === 0292 yield* Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), {
284 ? Effect.void293 discard: true,
285 : Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), {294 });
286 discard: true,295 }
287 }),296 });
288 );
289297
290 const list: PreviewManagerShape["list"] = (input) =>298 const list: PreviewManagerShape["list"] = (input) =>
291 SynchronizedRef.get(stateRef).pipe(299 SynchronizedRef.get(stateRef).pipe(
apps/server/src/preview/Layers/PortScanner.test.ts+46-42
...@@ -1,12 +1,18 @@...@@ -1,12 +1,18 @@
1import * as net from "node:net";1import * as net from "node:net";
22
3import { Effect } from "effect";3import { it as effectIt } from "@effect/vitest";
4import { afterEach, beforeEach, describe, expect, it } from "vitest";4import { Effect, Layer } from "effect";
5import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
56
6import { PreviewPortScanner } from "../Services/PortScanner.ts";7import { COMMON_DEV_PORTS, PreviewPortScanner } from "../Services/PortScanner.ts";
8import { ProcessRunner } from "../../processRunner.ts";
7import { __testing, PreviewPortScannerLive } from "./PortScanner.ts";9import { __testing, PreviewPortScannerLive } from "./PortScanner.ts";
810
9const { parseLsofOutput, parsePortFromLsofName, serversEqual } = __testing;11const { parseLsofOutput, parsePortFromLsofName, serversEqual } = __testing;
12const TestProcessRunner = Layer.succeed(ProcessRunner, {
13 run: () => Effect.die("ProcessRunner should not be used by Windows TCP probe tests"),
14});
15const TestPreviewPortScannerLive = PreviewPortScannerLive.pipe(Layer.provide(TestProcessRunner));
1016
11describe("parsePortFromLsofName", () => {17describe("parsePortFromLsofName", () => {
12 it("parses *:port", () => {18 it("parses *:port", () => {
...@@ -121,16 +127,25 @@ describe("serversEqual", () => {...@@ -121,16 +127,25 @@ describe("serversEqual", () => {
121describe("PreviewPortScanner integration (TCP probe path)", () => {127describe("PreviewPortScanner integration (TCP probe path)", () => {
122 let originalPlatform: NodeJS.Platform;128 let originalPlatform: NodeJS.Platform;
123 let server: net.Server;129 let server: net.Server;
130 let port: number;
124131
125 beforeEach(async () => {132 beforeEach(async () => {
126 originalPlatform = process.platform;133 originalPlatform = process.platform;
127 Object.defineProperty(process, "platform", { value: "win32", configurable: true });134 Object.defineProperty(process, "platform", { value: "win32", configurable: true });
128 // 3001 is in COMMON_DEV_PORTS so the TCP-probe pass will check it.135 for (const candidate of COMMON_DEV_PORTS) {
129 server = net.createServer();136 const candidateServer = net.createServer();
130 await new Promise<void>((resolve, reject) => {137 const listening = await new Promise<boolean>((resolve) => {
131 server.once("error", reject);138 candidateServer.once("error", () => resolve(false));
132 server.listen(3001, "127.0.0.1", () => resolve());139 candidateServer.listen(candidate, "127.0.0.1", () => resolve(true));
133 });140 });
141 if (listening) {
142 server = candidateServer;
143 port = candidate;
144 return;
145 }
146 candidateServer.close();
147 }
148 throw new Error("No common development port was available for the preview scanner test");
134 });149 });
135150
136 afterEach(async () => {151 afterEach(async () => {
...@@ -141,40 +156,29 @@ describe("PreviewPortScanner integration (TCP probe path)", () => {...@@ -141,40 +156,29 @@ describe("PreviewPortScanner integration (TCP probe path)", () => {
141 await new Promise<void>((resolve) => server.close(() => resolve()));156 await new Promise<void>((resolve) => server.close(() => resolve()));
142 });157 });
143158
144 it("scan() returns a server we just opened on a curated dev port", async () => {159 effectIt.effect("scan() returns a server we just opened on a curated dev port", () =>
145 const result = await Effect.runPromise(160 Effect.gen(function* () {
146 Effect.scoped(161 const scanner = yield* PreviewPortScanner;
147 Effect.gen(function* () {162 const result = yield* scanner.scan();
148 const scanner = yield* PreviewPortScanner;163 const found = result.find((server) => server.port === port);
149 return yield* scanner.scan();164 expect(found).toBeDefined();
150 }).pipe(Effect.provide(PreviewPortScannerLive)),165 expect(found?.host).toBe("localhost");
151 ),166 }).pipe(Effect.provide(TestPreviewPortScannerLive)),
152 );167 );
153 const found = result.find((s) => s.port === 3001);
154 expect(found).toBeDefined();
155 expect(found?.host).toBe("localhost");
156 });
157168
158 it("retain() drives an immediate broadcast to subscribers", async () => {169 effectIt.effect("retain() drives an immediate broadcast to subscribers", () => {
159 const received: number[] = [];170 const received: number[] = [];
160 await Effect.runPromise(171 return Effect.gen(function* () {
161 Effect.scoped(172 const scanner = yield* PreviewPortScanner;
162 Effect.gen(function* () {173 const unsubscribe = yield* scanner.subscribe((servers) =>
163 const scanner = yield* PreviewPortScanner;174 Effect.sync(() => {
164 const unsubscribe = yield* scanner.subscribe((servers) =>175 for (const server of servers) received.push(server.port);
165 Effect.sync(() => {176 }),
166 for (const server of servers) received.push(server.port);177 );
167 }),178 const release = yield* scanner.retain();
168 );179 unsubscribe();
169 const release = yield* scanner.retain();180 release();
170 // The retain() pollTick is fire-and-forget within Effect, so wait181 expect(received).toContain(port);
171 // a frame for the broadcast to land.182 }).pipe(Effect.provide(TestPreviewPortScannerLive));
172 yield* Effect.sleep("100 millis");
173 unsubscribe();
174 release();
175 }).pipe(Effect.provide(PreviewPortScannerLive)),
176 ),
177 );
178 expect(received).toContain(3001);
179 });183 });
180});184});
apps/server/src/preview/Layers/PortScanner.ts+19-16
...@@ -17,7 +17,7 @@ import type { DiscoveredLocalServer } from "@t3tools/contracts";...@@ -17,7 +17,7 @@ import type { DiscoveredLocalServer } from "@t3tools/contracts";
17import { LSOF_LOCAL_HOST_TOKENS } from "@t3tools/shared/preview";17import { LSOF_LOCAL_HOST_TOKENS } from "@t3tools/shared/preview";
18import { Cause, Data, Duration, Effect, Layer, Ref, Schedule } from "effect";18import { Cause, Data, Duration, Effect, Layer, Ref, Schedule } from "effect";
1919
20import { runProcess } from "../../processRunner.ts";20import { ProcessRunner, type ProcessRunnerShape } from "../../processRunner.ts";
21import {21import {
22 COMMON_DEV_PORTS,22 COMMON_DEV_PORTS,
23 PreviewPortScanner,23 PreviewPortScanner,
...@@ -91,20 +91,22 @@ const parsePortFromLsofName = (name: string): number | null => {...@@ -91,20 +91,22 @@ const parsePortFromLsofName = (name: string): number | null => {
91 return port;91 return port;
92};92};
9393
94const probeLsof = (): Effect.Effect<ReadonlyArray<DiscoveredLocalServer> | null> =>94const probeLsof = (
95 Effect.tryPromise({95 processRunner: ProcessRunnerShape,
96 try: () =>96): Effect.Effect<ReadonlyArray<DiscoveredLocalServer> | null> =>
97 runProcess("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"], {97 processRunner
98 timeoutMs: LSOF_TIMEOUT_MS,98 .run({
99 allowNonZeroExit: true,99 command: "lsof",
100 maxBufferBytes: 1024 * 1024,100 args: ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"],
101 outputMode: "truncate",101 timeout: Duration.millis(LSOF_TIMEOUT_MS),
102 }),102 maxOutputBytes: 1024 * 1024,
103 catch: (cause) => new LsofProbeError({ cause }),103 outputMode: "truncate",
104 }).pipe(104 })
105 Effect.map((result) => parseLsofOutput(result.stdout)),105 .pipe(
106 Effect.catch(() => Effect.succeed(null)),106 Effect.mapError((cause) => new LsofProbeError({ cause })),
107 );107 Effect.map((result) => parseLsofOutput(result.stdout)),
108 Effect.orElseSucceed(() => null),
109 );
108110
109const probeTcpPort = (port: number): Promise<boolean> =>111const probeTcpPort = (port: number): Promise<boolean> =>
110 new Promise((resolve) => {112 new Promise((resolve) => {
...@@ -162,6 +164,7 @@ const serversEqual = (...@@ -162,6 +164,7 @@ const serversEqual = (
162};164};
163165
164export const makePreviewPortScanner = Effect.gen(function* () {166export const makePreviewPortScanner = Effect.gen(function* () {
167 const processRunner = yield* ProcessRunner;
165 const stateRef = yield* Ref.make<ScannerState>({168 const stateRef = yield* Ref.make<ScannerState>({
166 lastSnapshot: [],169 lastSnapshot: [],
167 });170 });
...@@ -176,7 +179,7 @@ export const makePreviewPortScanner = Effect.gen(function* () {...@@ -176,7 +179,7 @@ export const makePreviewPortScanner = Effect.gen(function* () {
176 if (process.platform === "win32") {179 if (process.platform === "win32") {
177 return yield* probeCommonPorts();180 return yield* probeCommonPorts();
178 }181 }
179 const lsof = yield* probeLsof();182 const lsof = yield* probeLsof(processRunner);
180 if (lsof !== null) return lsof;183 if (lsof !== null) return lsof;
181 return yield* probeCommonPorts();184 return yield* probeCommonPorts();
182 });185 });
apps/server/src/preview/Services/PortScanner.ts+1-1
...@@ -28,7 +28,7 @@ export interface PreviewPortScannerShape {...@@ -28,7 +28,7 @@ export interface PreviewPortScannerShape {
28export class PreviewPortScanner extends Context.Service<28export class PreviewPortScanner extends Context.Service<
29 PreviewPortScanner,29 PreviewPortScanner,
30 PreviewPortScannerShape30 PreviewPortScannerShape
31>()("t3/preview/Services/PortScanner") {}31>()("t3/preview/Services/PortScanner/PreviewPortScanner") {}
3232
33/** Curated list of common dev-server ports for the Windows TCP-probe fallback. */33/** Curated list of common dev-server ports for the Windows TCP-probe fallback. */
34export const COMMON_DEV_PORTS: ReadonlyArray<number> = Object.freeze([34export const COMMON_DEV_PORTS: ReadonlyArray<number> = Object.freeze([
apps/server/src/server.test.ts+19-19
...@@ -668,25 +668,25 @@ const buildAppUnderTest = (options?: {...@@ -668,25 +668,25 @@ const buildAppUnderTest = (options?: {
668 }),668 }),
669 ),669 ),
670 Layer.provide(670 Layer.provide(
671 Layer.mock(PreviewManager)({671 Layer.mergeAll(
672 open: () => Effect.die("PreviewManager not stubbed in this test"),672 Layer.mock(PreviewManager)({
673 navigate: () => Effect.die("PreviewManager not stubbed in this test"),673 open: () => Effect.die("PreviewManager not stubbed in this test"),
674 reportStatus: () => Effect.void,674 navigate: () => Effect.die("PreviewManager not stubbed in this test"),
675 refresh: () => Effect.void,675 reportStatus: () => Effect.void,
676 close: () => Effect.void,676 refresh: () => Effect.void,
677 list: () => Effect.succeed({ sessions: [] }),677 close: () => Effect.void,
678 events: Stream.empty,678 list: () => Effect.succeed({ sessions: [] }),
679 subscribeEvents: Effect.flatMap(PubSub.unbounded<PreviewEvent>(), (pubsub) =>679 events: Stream.empty,
680 PubSub.subscribe(pubsub),680 subscribeEvents: Effect.flatMap(PubSub.unbounded<PreviewEvent>(), (pubsub) =>
681 ),681 PubSub.subscribe(pubsub),
682 }),682 ),
683 ),683 }),
684 Layer.provide(684 Layer.mock(PreviewPortScanner)({
685 Layer.mock(PreviewPortScanner)({685 scan: () => Effect.succeed([]),
686 scan: () => Effect.succeed([]),686 subscribe: () => Effect.succeed(() => {}),
687 subscribe: () => Effect.succeed(() => {}),687 retain: () => Effect.succeed(() => {}),
688 retain: () => Effect.succeed(() => {}),688 }),
689 }),689 ),
690 ),690 ),
691 Layer.provide(691 Layer.provide(
692 Layer.mock(OrchestrationEngineService)({692 Layer.mock(OrchestrationEngineService)({
apps/server/src/server.ts+6-3
...@@ -37,6 +37,7 @@ import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/Provide...@@ -37,6 +37,7 @@ import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/Provide
37import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";37import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
38import { PreviewManagerLive } from "./preview/Layers/Manager.ts";38import { PreviewManagerLive } from "./preview/Layers/Manager.ts";
39import { PreviewPortScannerLive } from "./preview/Layers/PortScanner.ts";39import { PreviewPortScannerLive } from "./preview/Layers/PortScanner.ts";
40import * as ProcessRunner from "./processRunner.ts";
40import * as GitManager from "./git/GitManager.ts";41import * as GitManager from "./git/GitManager.ts";
41import { KeybindingsLive } from "./keybindings.ts";42import { KeybindingsLive } from "./keybindings.ts";
42import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";43import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
...@@ -235,7 +236,10 @@ const CheckpointingLayerLive = Layer.empty.pipe(...@@ -235,7 +236,10 @@ const CheckpointingLayerLive = Layer.empty.pipe(
235236
236const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive));237const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive));
237238
238const PreviewLayerLive = Layer.mergeAll(PreviewManagerLive, PreviewPortScannerLive);239const PreviewLayerLive = Layer.mergeAll(
240 PreviewManagerLive,
241 PreviewPortScannerLive.pipe(Layer.provide(ProcessRunner.layer)),
242);
239243
240const WorkspaceEntriesLayerLive = WorkspaceEntriesLive.pipe(244const WorkspaceEntriesLayerLive = WorkspaceEntriesLive.pipe(
241 Layer.provide(WorkspacePathsLive),245 Layer.provide(WorkspacePathsLive),
...@@ -278,8 +282,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(...@@ -278,8 +282,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
278 Layer.provideMerge(GitLayerLive),282 Layer.provideMerge(GitLayerLive),
279 Layer.provideMerge(VcsLayerLive),283 Layer.provideMerge(VcsLayerLive),
280 Layer.provideMerge(ProviderRuntimeLayerLive),284 Layer.provideMerge(ProviderRuntimeLayerLive),
281 Layer.provideMerge(TerminalLayerLive),285 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),
282 Layer.provideMerge(PreviewLayerLive),
283 Layer.provideMerge(PersistenceLayerLive),286 Layer.provideMerge(PersistenceLayerLive),
284 Layer.provideMerge(KeybindingsLive),287 Layer.provideMerge(KeybindingsLive),
285 Layer.provideMerge(ProviderRegistryLive),288 Layer.provideMerge(ProviderRegistryLive),
apps/server/src/ws.ts+5-4
...@@ -1391,14 +1391,15 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) =>...@@ -1391,14 +1391,15 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) =>
1391 Effect.gen(function* () {1391 Effect.gen(function* () {
1392 const release = yield* previewPortScanner.retain();1392 const release = yield* previewPortScanner.retain();
1393 const initial = yield* previewPortScanner.scan();1393 const initial = yield* previewPortScanner.scan();
1394 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
1394 yield* Queue.offer(queue, {1395 yield* Queue.offer(queue, {
1395 servers: initial,1396 servers: initial,
1396 scannedAt: new Date().toISOString(),1397 scannedAt: initialScannedAt,
1397 });1398 });
1398 const unsubscribe = yield* previewPortScanner.subscribe((servers) =>1399 const unsubscribe = yield* previewPortScanner.subscribe((servers) =>
1399 Queue.offer(queue, {1400 Effect.gen(function* () {
1400 servers,1401 const scannedAt = DateTime.formatIso(yield* DateTime.now);
1401 scannedAt: new Date().toISOString(),1402 yield* Queue.offer(queue, { servers, scannedAt });
1402 }),1403 }),
1403 );1404 );
1404 return { unsubscribe, release };1405 return { unsubscribe, release };
apps/web/src/components/chat/ChatComposer.tsx+6-8
...@@ -17,10 +17,6 @@ import {...@@ -17,10 +17,6 @@ import {
17 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,17 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
18 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,18 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
19} from "@t3tools/contracts";19} from "@t3tools/contracts";
20import {
21 connectionStatusText,
22 type EnvironmentConnectionPresentation,
23} from "@t3tools/client-runtime/connection";
24import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";20import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
25import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";21import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
26import {22import {
...@@ -441,7 +437,7 @@ export interface ChatComposerProps {...@@ -441,7 +437,7 @@ export interface ChatComposerProps {
441 isPreparingWorktree: boolean;437 isPreparingWorktree: boolean;
442 environmentUnavailable: {438 environmentUnavailable: {
443 readonly label: string;439 readonly label: string;
444 readonly connection: EnvironmentConnectionPresentation;440 readonly connectionState: "connecting" | "disconnected" | "error";
445 } | null;441 } | null;
446442
447 // Pending approvals / inputs443 // Pending approvals / inputs
...@@ -2354,9 +2350,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2354,9 +2350,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2354 : showPlanFollowUpPrompt && activeProposedPlan2350 : showPlanFollowUpPrompt && activeProposedPlan
2355 ? "Add feedback to refine the plan, or leave this blank to implement it"2351 ? "Add feedback to refine the plan, or leave this blank to implement it"
2356 : environmentUnavailable2352 : environmentUnavailable
2357 ? `${environmentUnavailable.label}: ${connectionStatusText(2353 ? `${environmentUnavailable.label} is ${
2358 environmentUnavailable.connection,2354 environmentUnavailable.connectionState === "connecting"
2359 )}`2355 ? "connecting"
2356 : "disconnected"
2357 }`
2360 : phase === "disconnected"2358 : phase === "disconnected"
2361 ? "Ask for follow-up changes or attach images"2359 ? "Ask for follow-up changes or attach images"
2362 : "Ask anything, @tag files/folders, $use skills, or / for commands"2360 : "Ask anything, @tag files/folders, $use skills, or / for commands"
apps/web/src/components/chat/ChatHeader.tsx-1
...@@ -10,7 +10,6 @@ import { memo } from "react";...@@ -10,7 +10,6 @@ import { memo } from "react";
10import GitActionsControl from "../GitActionsControl";10import GitActionsControl from "../GitActionsControl";
11import { type DraftId } from "~/composerDraftStore";11import { type DraftId } from "~/composerDraftStore";
12import { DiffIcon, Globe, TerminalSquareIcon } from "lucide-react";12import { DiffIcon, Globe, TerminalSquareIcon } from "lucide-react";
13import { Badge } from "../ui/badge";
14import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";13import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
15import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";14import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";
16import { Toggle } from "../ui/toggle";15import { Toggle } from "../ui/toggle";
apps/web/src/components/chat/MessagesTimeline.tsx+10-4
...@@ -470,9 +470,9 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"...@@ -470,9 +470,9 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
470 )}470 )}
471 {elementContextState.contexts.length > 0 ? (471 {elementContextState.contexts.length > 0 ? (
472 <div className="mb-2 flex flex-wrap gap-1.5">472 <div className="mb-2 flex flex-wrap gap-1.5">
473 {elementContextState.contexts.map((context, index) => (473 {elementContextState.contexts.map((context) => (
474 <UserMessageElementContextChip474 <UserMessageElementContextChip
475 key={`${context.header}:${index}`}475 key={`${context.header}:${context.body}`}
476 context={context}476 context={context}
477 />477 />
478 ))}478 ))}
...@@ -579,10 +579,16 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess...@@ -579,10 +579,16 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
579 <TooltipTrigger579 <TooltipTrigger
580 render={<p className="text-muted-foreground text-xs tabular-nums" />}580 render={<p className="text-muted-foreground text-xs tabular-nums" />}
581 >581 >
582 {formatShortTimestamp(row.message.updatedAt, ctx.timestampFormat)}582 {formatShortTimestamp(
583 row.message.completedAt ?? row.message.createdAt,
584 ctx.timestampFormat,
585 )}
583 </TooltipTrigger>586 </TooltipTrigger>
584 <TooltipPopup>587 <TooltipPopup>
585 {formatChatTimestampTooltip(row.message.updatedAt, ctx.timestampFormat)}588 {formatChatTimestampTooltip(
589 row.message.completedAt ?? row.message.createdAt,
590 ctx.timestampFormat,
591 )}
586 </TooltipPopup>592 </TooltipPopup>
587 </Tooltip>593 </Tooltip>
588 )}594 )}
apps/web/src/components/preview/useDiscoveredLocalServers.test.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import type { DiscoveredLocalServer } from "@t3tools/contracts";1import type { DiscoveredLocalServer } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vite-plus/test";
33
4import { mergeServers, type PreviewableServer } from "./useDiscoveredLocalServers";4import { mergeServers, type PreviewableServer } from "./useDiscoveredLocalServers";
55
apps/web/src/environments/runtime/service.threadSubscriptions.test.ts+10
...@@ -104,6 +104,16 @@ vi.mock("@t3tools/client-runtime", async (importOriginal) => {...@@ -104,6 +104,16 @@ vi.mock("@t3tools/client-runtime", async (importOriginal) => {
104 onEvent: vi.fn(() => () => undefined),104 onEvent: vi.fn(() => () => undefined),
105 onMetadata: vi.fn(() => () => undefined),105 onMetadata: vi.fn(() => () => undefined),
106 },106 },
107 preview: {
108 open: vi.fn(),
109 navigate: vi.fn(),
110 refresh: vi.fn(),
111 close: vi.fn(),
112 list: vi.fn(),
113 reportStatus: vi.fn(),
114 onEvent: vi.fn(() => () => undefined),
115 subscribePorts: vi.fn(() => () => undefined),
116 },
107 projects: {117 projects: {
108 searchEntries: vi.fn(),118 searchEntries: vi.fn(),
109 writeFile: vi.fn(),119 writeFile: vi.fn(),
apps/web/src/lib/elementContext.test.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import type { PickedElementPayload } from "@t3tools/contracts";1import type { PickedElementPayload } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vite-plus/test";
33
4import {4import {
5 appendElementContextsToPrompt,5 appendElementContextsToPrompt,
apps/web/src/lib/elementContext.ts+3-4
...@@ -198,12 +198,11 @@ export function appendElementContextsToPrompt(...@@ -198,12 +198,11 @@ export function appendElementContextsToPrompt(
198}198}
199199
200const ELEMENT_CONTEXT_ID_PREFIX = "el_";200const ELEMENT_CONTEXT_ID_PREFIX = "el_";
201let nextElementContextSequence = 0;
201202
202export function newElementContextId(): string {203export function newElementContextId(): string {
203 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {204 nextElementContextSequence += 1;
204 return `${ELEMENT_CONTEXT_ID_PREFIX}${crypto.randomUUID()}`;205 return `${ELEMENT_CONTEXT_ID_PREFIX}${nextElementContextSequence.toString(36)}`;
205 }
206 return `${ELEMENT_CONTEXT_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
207}206}
208207
209/**208/**
apps/web/src/previewStateStore.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { scopeThreadRef } from "@t3tools/client-runtime";1import { scopeThreadRef } from "@t3tools/client-runtime";
2import { type EnvironmentId, type PreviewSessionSnapshot, ThreadId } from "@t3tools/contracts";2import { type EnvironmentId, type PreviewSessionSnapshot, ThreadId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vitest";3import { beforeEach, describe, expect, it } from "vite-plus/test";
44
5import { __testing, selectThreadPreviewState, usePreviewStateStore } from "./previewStateStore";5import { __testing, selectThreadPreviewState, usePreviewStateStore } from "./previewStateStore";
66
apps/web/src/rightPanelStore.test.ts+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { scopeThreadRef } from "@t3tools/client-runtime";1import { scopeThreadRef } from "@t3tools/client-runtime";
2import { type EnvironmentId, ThreadId } from "@t3tools/contracts";2import { type EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vitest";3import { beforeEach, describe, expect, it } from "vite-plus/test";
44
5import {5import {
6 selectActiveRightPanel,6 selectActiveRightPanel,
packages/contracts/src/preview.test.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vite-plus/test";
33
4import {4import {
5 DiscoveredLocalServer,5 DiscoveredLocalServer,
packages/contracts/src/rpc.ts+8-5
...@@ -480,38 +480,40 @@ export const WsTerminalCloseRpc = Rpc.make(WS_METHODS.terminalClose, {...@@ -480,38 +480,40 @@ export const WsTerminalCloseRpc = Rpc.make(WS_METHODS.terminalClose, {
480export const WsPreviewOpenRpc = Rpc.make(WS_METHODS.previewOpen, {480export const WsPreviewOpenRpc = Rpc.make(WS_METHODS.previewOpen, {
481 payload: PreviewOpenInput,481 payload: PreviewOpenInput,
482 success: PreviewSessionSnapshot,482 success: PreviewSessionSnapshot,
483 error: PreviewError,483 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
484});484});
485485
486export const WsPreviewNavigateRpc = Rpc.make(WS_METHODS.previewNavigate, {486export const WsPreviewNavigateRpc = Rpc.make(WS_METHODS.previewNavigate, {
487 payload: PreviewNavigateInput,487 payload: PreviewNavigateInput,
488 success: PreviewSessionSnapshot,488 success: PreviewSessionSnapshot,
489 error: PreviewError,489 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
490});490});
491491
492export const WsPreviewRefreshRpc = Rpc.make(WS_METHODS.previewRefresh, {492export const WsPreviewRefreshRpc = Rpc.make(WS_METHODS.previewRefresh, {
493 payload: PreviewRefreshInput,493 payload: PreviewRefreshInput,
494 error: PreviewError,494 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
495});495});
496496
497export const WsPreviewCloseRpc = Rpc.make(WS_METHODS.previewClose, {497export const WsPreviewCloseRpc = Rpc.make(WS_METHODS.previewClose, {
498 payload: PreviewCloseInput,498 payload: PreviewCloseInput,
499 error: PreviewError,499 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
500});500});
501501
502export const WsPreviewListRpc = Rpc.make(WS_METHODS.previewList, {502export const WsPreviewListRpc = Rpc.make(WS_METHODS.previewList, {
503 payload: PreviewListInput,503 payload: PreviewListInput,
504 success: PreviewListResult,504 success: PreviewListResult,
505 error: EnvironmentAuthorizationError,
505});506});
506507
507export const WsPreviewReportStatusRpc = Rpc.make(WS_METHODS.previewReportStatus, {508export const WsPreviewReportStatusRpc = Rpc.make(WS_METHODS.previewReportStatus, {
508 payload: PreviewReportStatusInput,509 payload: PreviewReportStatusInput,
509 error: PreviewError,510 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
510});511});
511512
512export const WsSubscribePreviewEventsRpc = Rpc.make(WS_METHODS.subscribePreviewEvents, {513export const WsSubscribePreviewEventsRpc = Rpc.make(WS_METHODS.subscribePreviewEvents, {
513 payload: Schema.Struct({}),514 payload: Schema.Struct({}),
514 success: PreviewEvent,515 success: PreviewEvent,
516 error: EnvironmentAuthorizationError,
515 stream: true,517 stream: true,
516});518});
517519
...@@ -520,6 +522,7 @@ export const WsSubscribeDiscoveredLocalServersRpc = Rpc.make(...@@ -520,6 +522,7 @@ export const WsSubscribeDiscoveredLocalServersRpc = Rpc.make(
520 {522 {
521 payload: Schema.Struct({}),523 payload: Schema.Struct({}),
522 success: DiscoveredLocalServerList,524 success: DiscoveredLocalServerList,
525 error: EnvironmentAuthorizationError,
523 stream: true,526 stream: true,
524 },527 },
525);528);
packages/shared/src/preview.test.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vite-plus/test";
22
3import {3import {
4 isLoopbackHost,4 isLoopbackHost,
packages/shared/src/preview.ts+3-4
...@@ -5,16 +5,15 @@...@@ -5,16 +5,15 @@
5 */5 */
66
7const TAB_ID_PREFIX = "tab_";7const TAB_ID_PREFIX = "tab_";
8let nextPreviewTabSequence = 0;
89
9/**10/**
10 * Generate a fresh preview tab id. Lives in shared (not contracts) because11 * Generate a fresh preview tab id. Lives in shared (not contracts) because
11 * the contracts package is schema-only — runtime helpers belong here.12 * the contracts package is schema-only — runtime helpers belong here.
12 */13 */
13export function newPreviewTabId(): string {14export function newPreviewTabId(): string {
14 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {15 nextPreviewTabSequence += 1;
15 return `${TAB_ID_PREFIX}${crypto.randomUUID()}`;16 return `${TAB_ID_PREFIX}${nextPreviewTabSequence.toString(36)}`;
16 }
17 return `${TAB_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
18}17}
1918
20const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);19const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
pnpm-lock.yaml+161
...@@ -128,6 +128,9 @@ importers:...@@ -128,6 +128,9 @@ importers:
128 electron-updater:128 electron-updater:
129 specifier: ^6.6.2129 specifier: ^6.6.2
130 version: 6.8.3130 version: 6.8.3
131 react-grab:
132 specifier: ^0.1.32
133 version: 0.1.44(react@19.2.6)
131 devDependencies:134 devDependencies:
132 '@effect/vitest':135 '@effect/vitest':
133 specifier: 4.0.0-beta.78136 specifier: 4.0.0-beta.78
...@@ -580,6 +583,9 @@ importers:...@@ -580,6 +583,9 @@ importers:
580 playwright:583 playwright:
581 specifier: ^1.58.2584 specifier: ^1.58.2
582 version: 1.60.0585 version: 1.60.0
586 react-grab:
587 specifier: ^0.1.32
588 version: 0.1.44(react@19.2.6)
583 tailwindcss:589 tailwindcss:
584 specifier: ^4.0.0590 specifier: ^4.0.0
585 version: 4.3.0591 version: 4.3.0
...@@ -2395,6 +2401,9 @@ packages:...@@ -2395,6 +2401,9 @@ packages:
2395 peerDependencies:2401 peerDependencies:
2396 hono: ^42402 hono: ^4
23972403
2404 '@iarna/toml@2.2.5':
2405 resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
2406
2398 '@img/colour@1.1.0':2407 '@img/colour@1.1.0':
2399 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}2408 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
2400 engines: {node: '>=18'}2409 engines: {node: '>=18'}
...@@ -3432,6 +3441,10 @@ packages:...@@ -3432,6 +3441,10 @@ packages:
3432 '@types/react':3441 '@types/react':
3433 optional: true3442 optional: true
34343443
3444 '@react-grab/cli@0.1.44':
3445 resolution: {integrity: sha512-gMDYY2rw6OWajCcDlXSIgs2LC432YJXSb3Lm5yM187uhRgBYddoEVULi36h+IolX3r7jSb3ew7vn9FfI8NSo0A==}
3446 hasBin: true
3447
3435 '@react-native-masked-view/masked-view@0.3.2':3448 '@react-native-masked-view/masked-view@0.3.2':
3436 resolution: {integrity: sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==}3449 resolution: {integrity: sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==}
3437 peerDependencies:3450 peerDependencies:
...@@ -4787,6 +4800,10 @@ packages:...@@ -4787,6 +4800,10 @@ packages:
4787 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}4800 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
4788 engines: {node: '>= 14'}4801 engines: {node: '>= 14'}
47894802
4803 agent-install@0.0.5:
4804 resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==}
4805 hasBin: true
4806
4790 ajv-draft-04@1.0.0:4807 ajv-draft-04@1.0.0:
4791 resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}4808 resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
4792 peerDependencies:4809 peerDependencies:
...@@ -5054,6 +5071,11 @@ packages:...@@ -5054,6 +5071,11 @@ packages:
5054 resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}5071 resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
5055 engines: {node: '>=0.6'}5072 engines: {node: '>=0.6'}
50565073
5074 bippy@0.5.41:
5075 resolution: {integrity: sha512-jCP2pXXLhXqPrAN+iSEFZmLI4uUM4fjSqajh0K+TmM062VehfDT3ZJNkrTGyN701Z5XMejs9qAudSqkMGhSMKg==}
5076 peerDependencies:
5077 react: '>=17.0.1'
5078
5057 body-parser@2.2.2:5079 body-parser@2.2.2:
5058 resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}5080 resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
5059 engines: {node: '>=18'}5081 engines: {node: '>=18'}
...@@ -5244,10 +5266,18 @@ packages:...@@ -5244,10 +5266,18 @@ packages:
5244 resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}5266 resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
5245 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}5267 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
52465268
5269 cli-cursor@5.0.0:
5270 resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
5271 engines: {node: '>=18'}
5272
5247 cli-spinners@2.9.2:5273 cli-spinners@2.9.2:
5248 resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}5274 resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
5249 engines: {node: '>=6'}5275 engines: {node: '>=6'}
52505276
5277 cli-spinners@3.4.0:
5278 resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
5279 engines: {node: '>=18.20'}
5280
5251 cli-truncate@2.1.0:5281 cli-truncate@2.1.0:
5252 resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}5282 resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
5253 engines: {node: '>=8'}5283 engines: {node: '>=8'}
...@@ -5321,6 +5351,10 @@ packages:...@@ -5321,6 +5351,10 @@ packages:
5321 resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}5351 resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
5322 engines: {node: '>=18'}5352 engines: {node: '>=18'}
53235353
5354 commander@14.0.3:
5355 resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
5356 engines: {node: '>=20'}
5357
5324 commander@2.20.3:5358 commander@2.20.3:
5325 resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}5359 resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
53265360
...@@ -6604,6 +6638,10 @@ packages:...@@ -6604,6 +6638,10 @@ packages:
6604 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}6638 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
6605 engines: {node: '>= 4'}6639 engines: {node: '>= 4'}
66066640
6641 ignore@7.0.5:
6642 resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
6643 engines: {node: '>= 4'}
6644
6607 image-size@1.2.1:6645 image-size@1.2.1:
6608 resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}6646 resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
6609 engines: {node: '>=16.x'}6647 engines: {node: '>=16.x'}
...@@ -6725,6 +6763,10 @@ packages:...@@ -6725,6 +6763,10 @@ packages:
6725 engines: {node: '>=14.16'}6763 engines: {node: '>=14.16'}
6726 hasBin: true6764 hasBin: true
67276765
6766 is-interactive@2.0.0:
6767 resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
6768 engines: {node: '>=12'}
6769
6728 is-node-process@1.2.0:6770 is-node-process@1.2.0:
6729 resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}6771 resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
67306772
...@@ -6742,6 +6784,10 @@ packages:...@@ -6742,6 +6784,10 @@ packages:
6742 is-property@1.0.2:6784 is-property@1.0.2:
6743 resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}6785 resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
67446786
6787 is-unicode-supported@2.1.0:
6788 resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
6789 engines: {node: '>=18'}
6790
6745 is-wsl@2.2.0:6791 is-wsl@2.2.0:
6746 resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}6792 resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
6747 engines: {node: '>=8'}6793 engines: {node: '>=8'}
...@@ -7154,6 +7200,10 @@ packages:...@@ -7154,6 +7200,10 @@ packages:
7154 resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}7200 resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
7155 engines: {node: '>=4'}7201 engines: {node: '>=4'}
71567202
7203 log-symbols@7.0.1:
7204 resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
7205 engines: {node: '>=18'}
7206
7157 long@5.3.2:7207 long@5.3.2:
7158 resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}7208 resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
71597209
...@@ -7484,6 +7534,10 @@ packages:...@@ -7484,6 +7534,10 @@ packages:
7484 resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}7534 resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
7485 engines: {node: '>=6'}7535 engines: {node: '>=6'}
74867536
7537 mimic-function@5.0.1:
7538 resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
7539 engines: {node: '>=18'}
7540
7487 mimic-response@1.0.1:7541 mimic-response@1.0.1:
7488 resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}7542 resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
7489 engines: {node: '>=4'}7543 engines: {node: '>=4'}
...@@ -7729,6 +7783,10 @@ packages:...@@ -7729,6 +7783,10 @@ packages:
7729 resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}7783 resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
7730 engines: {node: '>=6'}7784 engines: {node: '>=6'}
77317785
7786 onetime@7.0.0:
7787 resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
7788 engines: {node: '>=18'}
7789
7732 oniguruma-parser@0.12.2:7790 oniguruma-parser@0.12.2:
7733 resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}7791 resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
77347792
...@@ -7743,6 +7801,10 @@ packages:...@@ -7743,6 +7801,10 @@ packages:
7743 resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}7801 resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
7744 engines: {node: '>=6'}7802 engines: {node: '>=6'}
77457803
7804 ora@9.4.0:
7805 resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==}
7806 engines: {node: '>=20'}
7807
7746 os-paths@7.4.0:7808 os-paths@7.4.0:
7747 resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==}7809 resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==}
7748 engines: {node: '>= 4.0'}7810 engines: {node: '>= 4.0'}
...@@ -8136,6 +8198,15 @@ packages:...@@ -8136,6 +8198,15 @@ packages:
8136 peerDependencies:8198 peerDependencies:
8137 react: '>=17.0.0'8199 react: '>=17.0.0'
81388200
8201 react-grab@0.1.44:
8202 resolution: {integrity: sha512-bDEwBdI90ljq2lhUtPqmWis/HwYB/CvfT0m5i+P9F83Pt0Ot8o9XL8v00s9jcWzdQUlsFDzmq2FO2CHUe8JY8A==}
8203 hasBin: true
8204 peerDependencies:
8205 react: '>=17.0.0'
8206 peerDependenciesMeta:
8207 react:
8208 optional: true
8209
8139 react-is@16.13.1:8210 react-is@16.13.1:
8140 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}8211 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
81418212
...@@ -8454,6 +8525,10 @@ packages:...@@ -8454,6 +8525,10 @@ packages:
8454 resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}8525 resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
8455 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}8526 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
84568527
8528 restore-cursor@5.1.0:
8529 resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
8530 engines: {node: '>=18'}
8531
8457 retext-latin@4.0.0:8532 retext-latin@4.0.0:
8458 resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}8533 resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}
84598534
...@@ -8757,6 +8832,10 @@ packages:...@@ -8757,6 +8832,10 @@ packages:
8757 std-env@4.1.0:8832 std-env@4.1.0:
8758 resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}8833 resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
87598834
8835 stdin-discarder@0.3.2:
8836 resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
8837 engines: {node: '>=18'}
8838
8760 stream-buffers@2.2.0:8839 stream-buffers@2.2.0:
8761 resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}8840 resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
8762 engines: {node: '>= 0.10.0'}8841 engines: {node: '>= 0.10.0'}
...@@ -9631,6 +9710,10 @@ packages:...@@ -9631,6 +9710,10 @@ packages:
9631 resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}9710 resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
9632 engines: {node: '>=18'}9711 engines: {node: '>=18'}
96339712
9713 yoctocolors@2.1.2:
9714 resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
9715 engines: {node: '>=18'}
9716
9634 yoga-layout@3.2.1:9717 yoga-layout@3.2.1:
9635 resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}9718 resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
96369719
...@@ -11621,6 +11704,8 @@ snapshots:...@@ -11621,6 +11704,8 @@ snapshots:
11621 dependencies:11704 dependencies:
11622 hono: 4.12.2511705 hono: 4.12.25
1162311706
11707 '@iarna/toml@2.2.5': {}
11708
11624 '@img/colour@1.1.0':11709 '@img/colour@1.1.0':
11625 optional: true11710 optional: true
1162611711
...@@ -12577,6 +12662,17 @@ snapshots:...@@ -12577,6 +12662,17 @@ snapshots:
12577 optionalDependencies:12662 optionalDependencies:
12578 '@types/react': 19.2.1612663 '@types/react': 19.2.16
1257912664
12665 '@react-grab/cli@0.1.44':
12666 dependencies:
12667 agent-install: 0.0.5
12668 commander: 14.0.3
12669 ignore: 7.0.5
12670 ora: 9.4.0
12671 package-manager-detector: 1.6.0
12672 picocolors: 1.1.1
12673 prompts: 2.4.2
12674 tinyexec: 1.2.4
12675
12580 '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':12676 '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
12581 dependencies:12677 dependencies:
12582 react: 19.2.312678 react: 19.2.3
...@@ -13796,6 +13892,15 @@ snapshots:...@@ -13796,6 +13892,15 @@ snapshots:
1379613892
13797 agent-base@7.1.4: {}13893 agent-base@7.1.4: {}
1379813894
13895 agent-install@0.0.5:
13896 dependencies:
13897 '@iarna/toml': 2.2.5
13898 commander: 14.0.3
13899 jsonc-parser: 3.3.1
13900 picocolors: 1.1.1
13901 prompts: 2.4.2
13902 yaml: 2.9.0
13903
13799 ajv-draft-04@1.0.0(ajv@8.20.0):13904 ajv-draft-04@1.0.0(ajv@8.20.0):
13800 optionalDependencies:13905 optionalDependencies:
13801 ajv: 8.20.013906 ajv: 8.20.0
...@@ -14229,6 +14334,10 @@ snapshots:...@@ -14229,6 +14334,10 @@ snapshots:
1422914334
14230 big-integer@1.6.52: {}14335 big-integer@1.6.52: {}
1423114336
14337 bippy@0.5.41(react@19.2.6):
14338 dependencies:
14339 react: 19.2.6
14340
14232 body-parser@2.2.2:14341 body-parser@2.2.2:
14233 dependencies:14342 dependencies:
14234 bytes: 3.1.214343 bytes: 3.1.2
...@@ -14451,8 +14560,14 @@ snapshots:...@@ -14451,8 +14560,14 @@ snapshots:
14451 dependencies:14560 dependencies:
14452 restore-cursor: 4.0.014561 restore-cursor: 4.0.0
1445314562
14563 cli-cursor@5.0.0:
14564 dependencies:
14565 restore-cursor: 5.1.0
14566
14454 cli-spinners@2.9.2: {}14567 cli-spinners@2.9.2: {}
1445514568
14569 cli-spinners@3.4.0: {}
14570
14456 cli-truncate@2.1.0:14571 cli-truncate@2.1.0:
14457 dependencies:14572 dependencies:
14458 slice-ansi: 3.0.014573 slice-ansi: 3.0.0
...@@ -14520,6 +14635,8 @@ snapshots:...@@ -14520,6 +14635,8 @@ snapshots:
1452014635
14521 commander@12.1.0: {}14636 commander@12.1.0: {}
1452214637
14638 commander@14.0.3: {}
14639
14523 commander@2.20.3: {}14640 commander@2.20.3: {}
1452414641
14525 commander@5.1.0: {}14642 commander@5.1.0: {}
...@@ -15980,6 +16097,8 @@ snapshots:...@@ -15980,6 +16097,8 @@ snapshots:
1598016097
15981 ignore@5.3.2: {}16098 ignore@5.3.2: {}
1598216099
16100 ignore@7.0.5: {}
16101
15983 image-size@1.2.1:16102 image-size@1.2.1:
15984 dependencies:16103 dependencies:
15985 queue: 6.0.216104 queue: 6.0.2
...@@ -16099,6 +16218,8 @@ snapshots:...@@ -16099,6 +16218,8 @@ snapshots:
16099 dependencies:16218 dependencies:
16100 is-docker: 3.0.016219 is-docker: 3.0.0
1610116220
16221 is-interactive@2.0.0: {}
16222
16102 is-node-process@1.2.0: {}16223 is-node-process@1.2.0: {}
1610316224
16104 is-number@7.0.0: {}16225 is-number@7.0.0: {}
...@@ -16109,6 +16230,8 @@ snapshots:...@@ -16109,6 +16230,8 @@ snapshots:
1610916230
16110 is-property@1.0.2: {}16231 is-property@1.0.2: {}
1611116232
16233 is-unicode-supported@2.1.0: {}
16234
16112 is-wsl@2.2.0:16235 is-wsl@2.2.0:
16113 dependencies:16236 dependencies:
16114 is-docker: 2.2.116237 is-docker: 2.2.1
...@@ -16442,6 +16565,11 @@ snapshots:...@@ -16442,6 +16565,11 @@ snapshots:
16442 dependencies:16565 dependencies:
16443 chalk: 2.4.216566 chalk: 2.4.2
1644416567
16568 log-symbols@7.0.1:
16569 dependencies:
16570 is-unicode-supported: 2.1.0
16571 yoctocolors: 2.1.2
16572
16445 long@5.3.2: {}16573 long@5.3.2: {}
1644616574
16447 longest-streak@3.1.0: {}16575 longest-streak@3.1.0: {}
...@@ -17071,6 +17199,8 @@ snapshots:...@@ -17071,6 +17199,8 @@ snapshots:
1707117199
17072 mimic-fn@2.1.0: {}17200 mimic-fn@2.1.0: {}
1707317201
17202 mimic-function@5.0.1: {}
17203
17074 mimic-response@1.0.1: {}17204 mimic-response@1.0.1: {}
1707517205
17076 mimic-response@3.1.0: {}17206 mimic-response@3.1.0: {}
...@@ -17308,6 +17438,10 @@ snapshots:...@@ -17308,6 +17438,10 @@ snapshots:
17308 dependencies:17438 dependencies:
17309 mimic-fn: 2.1.017439 mimic-fn: 2.1.0
1731017440
17441 onetime@7.0.0:
17442 dependencies:
17443 mimic-function: 5.0.1
17444
17311 oniguruma-parser@0.12.2: {}17445 oniguruma-parser@0.12.2: {}
1731217446
17313 oniguruma-to-es@4.3.6:17447 oniguruma-to-es@4.3.6:
...@@ -17330,6 +17464,17 @@ snapshots:...@@ -17330,6 +17464,17 @@ snapshots:
17330 strip-ansi: 5.2.017464 strip-ansi: 5.2.0
17331 wcwidth: 1.0.117465 wcwidth: 1.0.1
1733217466
17467 ora@9.4.0:
17468 dependencies:
17469 chalk: 5.6.2
17470 cli-cursor: 5.0.0
17471 cli-spinners: 3.4.0
17472 is-interactive: 2.0.0
17473 is-unicode-supported: 2.1.0
17474 log-symbols: 7.0.1
17475 stdin-discarder: 0.3.2
17476 string-width: 8.2.1
17477
17333 os-paths@7.4.0:17478 os-paths@7.4.0:
17334 optionalDependencies:17479 optionalDependencies:
17335 fsevents: 2.3.317480 fsevents: 2.3.3
...@@ -17721,6 +17866,13 @@ snapshots:...@@ -17721,6 +17866,13 @@ snapshots:
17721 dependencies:17866 dependencies:
17722 react: 19.2.317867 react: 19.2.3
1772317868
17869 react-grab@0.1.44(react@19.2.6):
17870 dependencies:
17871 '@react-grab/cli': 0.1.44
17872 bippy: 0.5.41(react@19.2.6)
17873 optionalDependencies:
17874 react: 19.2.6
17875
17724 react-is@16.13.1: {}17876 react-is@16.13.1: {}
1772517877
17726 react-is@17.0.2: {}17878 react-is@17.0.2: {}
...@@ -18122,6 +18274,11 @@ snapshots:...@@ -18122,6 +18274,11 @@ snapshots:
18122 onetime: 5.1.218274 onetime: 5.1.2
18123 signal-exit: 3.0.718275 signal-exit: 3.0.7
1812418276
18277 restore-cursor@5.1.0:
18278 dependencies:
18279 onetime: 7.0.0
18280 signal-exit: 4.1.0
18281
18125 retext-latin@4.0.0:18282 retext-latin@4.0.0:
18126 dependencies:18283 dependencies:
18127 '@types/nlcst': 2.0.318284 '@types/nlcst': 2.0.3
...@@ -18563,6 +18720,8 @@ snapshots:...@@ -18563,6 +18720,8 @@ snapshots:
1856318720
18564 std-env@4.1.0: {}18721 std-env@4.1.0: {}
1856518722
18723 stdin-discarder@0.3.2: {}
18724
18566 stream-buffers@2.2.0: {}18725 stream-buffers@2.2.0: {}
1856718726
18568 strict-event-emitter@0.5.1: {}18727 strict-event-emitter@0.5.1: {}
...@@ -19377,6 +19536,8 @@ snapshots:...@@ -19377,6 +19536,8 @@ snapshots:
1937719536
19378 yoctocolors-cjs@2.1.3: {}19537 yoctocolors-cjs@2.1.3: {}
1937919538
19539 yoctocolors@2.1.2: {}
19540
19380 yoga-layout@3.2.1: {}19541 yoga-layout@3.2.1: {}
1938119542
19382 zod-to-json-schema@3.25.2(zod@4.4.3):19543 zod-to-json-schema@3.25.2(zod@4.4.3):