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// @effect-diagnostics nodeBuiltinImport:off
2import * as Data from "effect/Data";
13import * as Effect from "effect/Effect";
24import { BrowserWindow } from "electron";
3import * as NodeFileSystem from "node:fs";
4import * as NodePath from "node:path";
55import { pathToFileURL } from "node:url";
66
77import { previewViewManager } from "../../preview-view-manager.ts";
......@@ -29,18 +29,38 @@ const tabIdFrom = (raw: unknown): string => {
2929 return tabId;
3030};
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> => ({
3340 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 }),
3546});
3647
3748export const previewMethods = [
38 method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) => previewViewManager.createTab(tabIdFrom(raw))),
39 method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) => previewViewManager.closeTab(tabIdFrom(raw))),
49 method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) =>
50 previewViewManager.createTab(tabIdFrom(raw)),
51 ),
52 method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) =>
53 previewViewManager.closeTab(tabIdFrom(raw)),
54 ),
4055 method(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, (raw) => {
4156 const tabId = tabIdFrom(raw);
42 const webContentsId = typeof raw === "object" && raw !== null && "webContentsId" in raw ? raw.webContentsId : null;
43 if (typeof webContentsId !== "number" || !Number.isInteger(webContentsId) || webContentsId <= 0) {
57 const webContentsId =
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 ) {
4464 throw new Error("preview webContentsId must be a positive integer");
4565 }
4666 return previewViewManager.registerWebview(tabId, webContentsId);
......@@ -52,21 +72,29 @@ export const previewMethods = [
5272 return previewViewManager.navigate(tabId, url);
5373 }),
5474 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 ),
5678 method(IpcChannels.PREVIEW_REFRESH_CHANNEL, (raw) => previewViewManager.refresh(tabIdFrom(raw))),
5779 method(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, (raw) => previewViewManager.zoomIn(tabIdFrom(raw))),
5880 method(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, (raw) => previewViewManager.zoomOut(tabIdFrom(raw))),
59 method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) => previewViewManager.resetZoom(tabIdFrom(raw))),
60 method(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, (raw) => previewViewManager.hardReload(tabIdFrom(raw))),
61 method(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, (raw) => previewViewManager.openDevTools(tabIdFrom(raw))),
81 method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) =>
82 previewViewManager.resetZoom(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 ),
6290 method(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, () => previewViewManager.clearCookies()),
6391 method(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, () => previewViewManager.clearCache()),
6492 method(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, () => {
65 const preloadPath = NodePath.join(__dirname, "preview-pick-preload.cjs");
93 const preloadPath = `${__dirname}/preview-pick-preload.cjs`;
6694 return {
6795 partition: previewViewManager.getBrowserPartition(),
6896 webPreferences: PREVIEW_WEBVIEW_PREFERENCES,
69 preloadUrl: NodeFileSystem.existsSync(preloadPath) ? pathToFileURL(preloadPath).href : null,
97 preloadUrl: pathToFileURL(preloadPath).href,
7098 };
7199 }),
72100 method(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, (raw) =>
apps/desktop/src/main.ts-2
......@@ -28,7 +28,6 @@ import * as DesktopApp from "./app/DesktopApp.ts";
2828import * as DesktopAppIdentity from "./app/DesktopAppIdentity.ts";
2929import * as DesktopCloudAuth from "./app/DesktopCloudAuth.ts";
3030import * as DesktopCloudAuthTokenStore from "./app/DesktopCloudAuthTokenStore.ts";
31import * as DesktopConnectionCatalogStore from "./app/DesktopConnectionCatalogStore.ts";
3231import * as DesktopApplicationMenu from "./window/DesktopApplicationMenu.ts";
3332import * as DesktopAssets from "./app/DesktopAssets.ts";
3433import * as DesktopBackendConfiguration from "./backend/DesktopBackendConfiguration.ts";
......@@ -115,7 +114,6 @@ const desktopFoundationLayer = Layer.mergeAll(
115114 DesktopClientSettings.layer,
116115 DesktopSavedEnvironments.layer,
117116 DesktopCloudAuthTokenStore.layer,
118 DesktopConnectionCatalogStore.layer,
119117 DesktopAssets.layer,
120118 DesktopObservability.layer,
121119).pipe(Layer.provideMerge(desktopEnvironmentLayer));
apps/desktop/src/picked-element-payload.test.ts+1-1
......@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";
1import { describe, expect, it } from "vite-plus/test";
22
33import { isPickedElementPayload } from "./picked-element-payload.ts";
44
apps/desktop/src/preload.ts+3-7
......@@ -47,10 +47,6 @@ contextBridge.exposeInMainWorld("desktopBridge", {
4747 ipcRenderer.invoke(IpcChannels.SET_SAVED_ENVIRONMENT_SECRET_CHANNEL, { environmentId, secret }),
4848 removeSavedEnvironmentSecret: (environmentId) =>
4949 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),
5450 discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),
5551 ensureSshEnvironment: async (target, options) =>
5652 unwrapEnsureSshEnvironmentResult(
......@@ -164,8 +160,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
164160 clearCookies: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL),
165161 clearCache: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL),
166162 getPreviewConfig: () => ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL),
167 pickElement: (tabId) =>
168 ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
163 pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
169164 cancelPickElement: (tabId) =>
170165 ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),
171166 onStateChange: (listener) => {
......@@ -178,7 +173,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
178173 listener(tabId, state as DesktopPreviewTabState);
179174 };
180175 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);
182178 },
183179 },
184180} satisfies DesktopBridge);
apps/desktop/src/preview-pick-preload.test.ts+1-1
......@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";
1import { describe, expect, it } from "vite-plus/test";
22
33import { computeLabelPosition } from "./preview-pick-label-position.ts";
44
apps/desktop/src/preview-pick-preload.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics globalDate:off
12/**
23 * Preview pick preload — runs in the isolated world of the Chromium
34 * `<webview>` that hosts the in-app browser. Loaded via the
apps/desktop/src/preview-view-manager.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics globalDate:off
12/**
23 * PreviewViewManager — desktop side of the in-app browser preview.
34 *
apps/desktop/src/preview-webview-preferences.test.ts+1-1
......@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";
1import { describe, expect, it } from "vite-plus/test";
22
33import { PREVIEW_WEBVIEW_PREFERENCES } from "./preview-webview-preferences.ts";
44
apps/server/src/preview/Layers/Manager.test.ts+1-1
......@@ -1,7 +1,7 @@
11import { it } from "@effect/vitest";
22import { type PreviewEvent, ThreadId } from "@t3tools/contracts";
33import { Effect, PubSub } from "effect";
4import { expect } from "vitest";
4import { expect } from "vite-plus/test";
55
66import { PreviewManager } from "../Services/Manager.ts";
77import { PreviewManagerLive } from "./Manager.ts";
apps/server/src/preview/Layers/Manager.ts+27-19
......@@ -21,7 +21,7 @@ import {
2121 normalizePreviewUrl,
2222 PreviewUrlNormalizationError,
2323} from "@t3tools/shared/preview";
24import { Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect";
24import { DateTime, Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect";
2525
2626import { PreviewManager, type PreviewManagerShape } from "../Services/Manager.ts";
2727
......@@ -66,30 +66,34 @@ const normalizeUrl = (rawUrl: string): Effect.Effect<string, PreviewInvalidUrlEr
6666 }),
6767 });
6868
69const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));
70
6971const buildLoadingSnapshot = (input: {
7072 readonly threadId: string;
7173 readonly tabId: string;
7274 readonly url: string;
7375 readonly title: string;
76 readonly updatedAt: string;
7477}): PreviewSessionSnapshot => ({
7578 threadId: input.threadId,
7679 tabId: input.tabId,
7780 navStatus: { _tag: "Loading", url: input.url, title: input.title },
7881 canGoBack: false,
7982 canGoForward: false,
80 updatedAt: new Date().toISOString(),
83 updatedAt: input.updatedAt,
8184});
8285
8386const buildIdleSnapshot = (input: {
8487 readonly threadId: string;
8588 readonly tabId: string;
89 readonly updatedAt: string;
8690}): PreviewSessionSnapshot => ({
8791 threadId: input.threadId,
8892 tabId: input.tabId,
8993 navStatus: { _tag: "Idle" },
9094 canGoBack: false,
9195 canGoForward: false,
92 updatedAt: new Date().toISOString(),
96 updatedAt: input.updatedAt,
9397});
9498
9599export const makePreviewManager = Effect.gen(function* () {
......@@ -152,14 +156,16 @@ export const makePreviewManager = Effect.gen(function* () {
152156 const open: PreviewManagerShape["open"] = (input) =>
153157 Effect.gen(function* () {
154158 const tabId = newPreviewTabId();
159 const updatedAt = yield* currentIsoTimestamp;
155160 const snapshot = input.url
156161 ? buildLoadingSnapshot({
157162 threadId: input.threadId,
158163 tabId,
159164 url: yield* normalizeUrl(input.url),
160165 title: "",
166 updatedAt,
161167 })
162 : buildIdleSnapshot({ threadId: input.threadId, tabId });
168 : buildIdleSnapshot({ threadId: input.threadId, tabId, updatedAt });
163169 yield* SynchronizedRef.update(stateRef, (state) => {
164170 const sessions = new Map(state.sessions);
165171 sessions.set(compositeKey(input.threadId, tabId), {
......@@ -183,7 +189,8 @@ export const makePreviewManager = Effect.gen(function* () {
183189 Effect.gen(function* () {
184190 const url = yield* normalizeUrl(input.url);
185191 return yield* mutateExistingSession(input.threadId, input.tabId, (session) =>
186 Effect.sync(() => {
192 Effect.gen(function* () {
193 const updatedAt = yield* currentIsoTimestamp;
187194 const previousTitle =
188195 session.snapshot.navStatus._tag === "Idle" ? "" : session.snapshot.navStatus.title;
189196 const resolvedTitle = input.resolvedTitle ?? previousTitle;
......@@ -193,7 +200,7 @@ export const makePreviewManager = Effect.gen(function* () {
193200 navStatus: { _tag: "Success", url, title: resolvedTitle },
194201 canGoBack: session.snapshot.canGoBack,
195202 canGoForward: session.snapshot.canGoForward,
196 updatedAt: new Date().toISOString(),
203 updatedAt,
197204 };
198205 return {
199206 next: { ...session, snapshot },
......@@ -212,14 +219,15 @@ export const makePreviewManager = Effect.gen(function* () {
212219
213220 const reportStatus: PreviewManagerShape["reportStatus"] = (input) =>
214221 mutateExistingSession(input.threadId, input.tabId, (session) =>
215 Effect.sync(() => {
222 Effect.gen(function* () {
223 const updatedAt = yield* currentIsoTimestamp;
216224 const snapshot: PreviewSessionSnapshot = {
217225 threadId: session.threadId,
218226 tabId: session.tabId,
219227 navStatus: input.navStatus,
220228 canGoBack: input.canGoBack,
221229 canGoForward: input.canGoForward,
222 updatedAt: new Date().toISOString(),
230 updatedAt,
223231 };
224232 const emit: PreviewEvent =
225233 input.navStatus._tag === "LoadFailed"
......@@ -256,8 +264,9 @@ export const makePreviewManager = Effect.gen(function* () {
256264 );
257265
258266 const close: PreviewManagerShape["close"] = (input) =>
259 Effect.flatMap(
260 SynchronizedRef.modify(stateRef, (state) => {
267 Effect.gen(function* () {
268 const createdAt = yield* currentIsoTimestamp;
269 const events = yield* SynchronizedRef.modify(stateRef, (state) => {
261270 const eventsToEmit: PreviewEvent[] = [];
262271 const sessions = new Map(state.sessions);
263272 const targets = input.tabId
......@@ -271,21 +280,20 @@ export const makePreviewManager = Effect.gen(function* () {
271280 type: "closed",
272281 threadId: target.threadId,
273282 tabId: target.tabId,
274 createdAt: new Date().toISOString(),
283 createdAt,
275284 });
276285 }
277286 if (eventsToEmit.length === 0) {
278287 return [eventsToEmit, state] as const;
279288 }
280289 return [eventsToEmit, { sessions }] as const;
281 }),
282 (events) =>
283 events.length === 0
284 ? Effect.void
285 : Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), {
286 discard: true,
287 }),
288 );
290 });
291 if (events.length > 0) {
292 yield* Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), {
293 discard: true,
294 });
295 }
296 });
289297
290298 const list: PreviewManagerShape["list"] = (input) =>
291299 SynchronizedRef.get(stateRef).pipe(
apps/server/src/preview/Layers/PortScanner.test.ts+46-42
......@@ -1,12 +1,18 @@
11import * as net from "node:net";
22
3import { Effect } from "effect";
4import { afterEach, beforeEach, describe, expect, it } from "vitest";
3import { it as effectIt } from "@effect/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";
79import { __testing, PreviewPortScannerLive } from "./PortScanner.ts";
810
911const { 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
1117describe("parsePortFromLsofName", () => {
1218 it("parses *:port", () => {
......@@ -121,16 +127,25 @@ describe("serversEqual", () => {
121127describe("PreviewPortScanner integration (TCP probe path)", () => {
122128 let originalPlatform: NodeJS.Platform;
123129 let server: net.Server;
130 let port: number;
124131
125132 beforeEach(async () => {
126133 originalPlatform = process.platform;
127134 Object.defineProperty(process, "platform", { value: "win32", configurable: true });
128 // 3001 is in COMMON_DEV_PORTS so the TCP-probe pass will check it.
129 server = net.createServer();
130 await new Promise<void>((resolve, reject) => {
131 server.once("error", reject);
132 server.listen(3001, "127.0.0.1", () => resolve());
133 });
135 for (const candidate of COMMON_DEV_PORTS) {
136 const candidateServer = net.createServer();
137 const listening = await new Promise<boolean>((resolve) => {
138 candidateServer.once("error", () => resolve(false));
139 candidateServer.listen(candidate, "127.0.0.1", () => resolve(true));
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");
134149 });
135150
136151 afterEach(async () => {
......@@ -141,40 +156,29 @@ describe("PreviewPortScanner integration (TCP probe path)", () => {
141156 await new Promise<void>((resolve) => server.close(() => resolve()));
142157 });
143158
144 it("scan() returns a server we just opened on a curated dev port", async () => {
145 const result = await Effect.runPromise(
146 Effect.scoped(
147 Effect.gen(function* () {
148 const scanner = yield* PreviewPortScanner;
149 return yield* scanner.scan();
150 }).pipe(Effect.provide(PreviewPortScannerLive)),
151 ),
152 );
153 const found = result.find((s) => s.port === 3001);
154 expect(found).toBeDefined();
155 expect(found?.host).toBe("localhost");
156 });
159 effectIt.effect("scan() returns a server we just opened on a curated dev port", () =>
160 Effect.gen(function* () {
161 const scanner = yield* PreviewPortScanner;
162 const result = yield* scanner.scan();
163 const found = result.find((server) => server.port === port);
164 expect(found).toBeDefined();
165 expect(found?.host).toBe("localhost");
166 }).pipe(Effect.provide(TestPreviewPortScannerLive)),
167 );
157168
158 it("retain() drives an immediate broadcast to subscribers", async () => {
169 effectIt.effect("retain() drives an immediate broadcast to subscribers", () => {
159170 const received: number[] = [];
160 await Effect.runPromise(
161 Effect.scoped(
162 Effect.gen(function* () {
163 const scanner = yield* PreviewPortScanner;
164 const unsubscribe = yield* scanner.subscribe((servers) =>
165 Effect.sync(() => {
166 for (const server of servers) received.push(server.port);
167 }),
168 );
169 const release = yield* scanner.retain();
170 // The retain() pollTick is fire-and-forget within Effect, so wait
171 // a frame for the broadcast to land.
172 yield* Effect.sleep("100 millis");
173 unsubscribe();
174 release();
175 }).pipe(Effect.provide(PreviewPortScannerLive)),
176 ),
177 );
178 expect(received).toContain(3001);
171 return Effect.gen(function* () {
172 const scanner = yield* PreviewPortScanner;
173 const unsubscribe = yield* scanner.subscribe((servers) =>
174 Effect.sync(() => {
175 for (const server of servers) received.push(server.port);
176 }),
177 );
178 const release = yield* scanner.retain();
179 unsubscribe();
180 release();
181 expect(received).toContain(port);
182 }).pipe(Effect.provide(TestPreviewPortScannerLive));
179183 });
180184});
apps/server/src/preview/Layers/PortScanner.ts+19-16
......@@ -17,7 +17,7 @@ import type { DiscoveredLocalServer } from "@t3tools/contracts";
1717import { LSOF_LOCAL_HOST_TOKENS } from "@t3tools/shared/preview";
1818import { Cause, Data, Duration, Effect, Layer, Ref, Schedule } from "effect";
1919
20import { runProcess } from "../../processRunner.ts";
20import { ProcessRunner, type ProcessRunnerShape } from "../../processRunner.ts";
2121import {
2222 COMMON_DEV_PORTS,
2323 PreviewPortScanner,
......@@ -91,20 +91,22 @@ const parsePortFromLsofName = (name: string): number | null => {
9191 return port;
9292};
9393
94const probeLsof = (): Effect.Effect<ReadonlyArray<DiscoveredLocalServer> | null> =>
95 Effect.tryPromise({
96 try: () =>
97 runProcess("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"], {
98 timeoutMs: LSOF_TIMEOUT_MS,
99 allowNonZeroExit: true,
100 maxBufferBytes: 1024 * 1024,
101 outputMode: "truncate",
102 }),
103 catch: (cause) => new LsofProbeError({ cause }),
104 }).pipe(
105 Effect.map((result) => parseLsofOutput(result.stdout)),
106 Effect.catch(() => Effect.succeed(null)),
107 );
94const probeLsof = (
95 processRunner: ProcessRunnerShape,
96): Effect.Effect<ReadonlyArray<DiscoveredLocalServer> | null> =>
97 processRunner
98 .run({
99 command: "lsof",
100 args: ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"],
101 timeout: Duration.millis(LSOF_TIMEOUT_MS),
102 maxOutputBytes: 1024 * 1024,
103 outputMode: "truncate",
104 })
105 .pipe(
106 Effect.mapError((cause) => new LsofProbeError({ cause })),
107 Effect.map((result) => parseLsofOutput(result.stdout)),
108 Effect.orElseSucceed(() => null),
109 );
108110
109111const probeTcpPort = (port: number): Promise<boolean> =>
110112 new Promise((resolve) => {
......@@ -162,6 +164,7 @@ const serversEqual = (
162164};
163165
164166export const makePreviewPortScanner = Effect.gen(function* () {
167 const processRunner = yield* ProcessRunner;
165168 const stateRef = yield* Ref.make<ScannerState>({
166169 lastSnapshot: [],
167170 });
......@@ -176,7 +179,7 @@ export const makePreviewPortScanner = Effect.gen(function* () {
176179 if (process.platform === "win32") {
177180 return yield* probeCommonPorts();
178181 }
179 const lsof = yield* probeLsof();
182 const lsof = yield* probeLsof(processRunner);
180183 if (lsof !== null) return lsof;
181184 return yield* probeCommonPorts();
182185 });
apps/server/src/preview/Services/PortScanner.ts+1-1
......@@ -28,7 +28,7 @@ export interface PreviewPortScannerShape {
2828export class PreviewPortScanner extends Context.Service<
2929 PreviewPortScanner,
3030 PreviewPortScannerShape
31>()("t3/preview/Services/PortScanner") {}
31>()("t3/preview/Services/PortScanner/PreviewPortScanner") {}
3232
3333/** Curated list of common dev-server ports for the Windows TCP-probe fallback. */
3434export const COMMON_DEV_PORTS: ReadonlyArray<number> = Object.freeze([
apps/server/src/server.test.ts+19-19
......@@ -668,25 +668,25 @@ const buildAppUnderTest = (options?: {
668668 }),
669669 ),
670670 Layer.provide(
671 Layer.mock(PreviewManager)({
672 open: () => Effect.die("PreviewManager not stubbed in this test"),
673 navigate: () => Effect.die("PreviewManager not stubbed in this test"),
674 reportStatus: () => Effect.void,
675 refresh: () => Effect.void,
676 close: () => Effect.void,
677 list: () => Effect.succeed({ sessions: [] }),
678 events: Stream.empty,
679 subscribeEvents: Effect.flatMap(PubSub.unbounded<PreviewEvent>(), (pubsub) =>
680 PubSub.subscribe(pubsub),
681 ),
682 }),
683 ),
684 Layer.provide(
685 Layer.mock(PreviewPortScanner)({
686 scan: () => Effect.succeed([]),
687 subscribe: () => Effect.succeed(() => {}),
688 retain: () => Effect.succeed(() => {}),
689 }),
671 Layer.mergeAll(
672 Layer.mock(PreviewManager)({
673 open: () => Effect.die("PreviewManager not stubbed in this test"),
674 navigate: () => Effect.die("PreviewManager not stubbed in this test"),
675 reportStatus: () => Effect.void,
676 refresh: () => Effect.void,
677 close: () => Effect.void,
678 list: () => Effect.succeed({ sessions: [] }),
679 events: Stream.empty,
680 subscribeEvents: Effect.flatMap(PubSub.unbounded<PreviewEvent>(), (pubsub) =>
681 PubSub.subscribe(pubsub),
682 ),
683 }),
684 Layer.mock(PreviewPortScanner)({
685 scan: () => Effect.succeed([]),
686 subscribe: () => Effect.succeed(() => {}),
687 retain: () => Effect.succeed(() => {}),
688 }),
689 ),
690690 ),
691691 Layer.provide(
692692 Layer.mock(OrchestrationEngineService)({
apps/server/src/server.ts+6-3
......@@ -37,6 +37,7 @@ import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/Provide
3737import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
3838import { PreviewManagerLive } from "./preview/Layers/Manager.ts";
3939import { PreviewPortScannerLive } from "./preview/Layers/PortScanner.ts";
40import * as ProcessRunner from "./processRunner.ts";
4041import * as GitManager from "./git/GitManager.ts";
4142import { KeybindingsLive } from "./keybindings.ts";
4243import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
......@@ -235,7 +236,10 @@ const CheckpointingLayerLive = Layer.empty.pipe(
235236
236237const 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
240244const WorkspaceEntriesLayerLive = WorkspaceEntriesLive.pipe(
241245 Layer.provide(WorkspacePathsLive),
......@@ -278,8 +282,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(
278282 Layer.provideMerge(GitLayerLive),
279283 Layer.provideMerge(VcsLayerLive),
280284 Layer.provideMerge(ProviderRuntimeLayerLive),
281 Layer.provideMerge(TerminalLayerLive),
282 Layer.provideMerge(PreviewLayerLive),
285 Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),
283286 Layer.provideMerge(PersistenceLayerLive),
284287 Layer.provideMerge(KeybindingsLive),
285288 Layer.provideMerge(ProviderRegistryLive),
apps/server/src/ws.ts+5-4
......@@ -1391,14 +1391,15 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) =>
13911391 Effect.gen(function* () {
13921392 const release = yield* previewPortScanner.retain();
13931393 const initial = yield* previewPortScanner.scan();
1394 const initialScannedAt = DateTime.formatIso(yield* DateTime.now);
13941395 yield* Queue.offer(queue, {
13951396 servers: initial,
1396 scannedAt: new Date().toISOString(),
1397 scannedAt: initialScannedAt,
13971398 });
13981399 const unsubscribe = yield* previewPortScanner.subscribe((servers) =>
1399 Queue.offer(queue, {
1400 servers,
1401 scannedAt: new Date().toISOString(),
1400 Effect.gen(function* () {
1401 const scannedAt = DateTime.formatIso(yield* DateTime.now);
1402 yield* Queue.offer(queue, { servers, scannedAt });
14021403 }),
14031404 );
14041405 return { unsubscribe, release };
apps/web/src/components/chat/ChatComposer.tsx+6-8
......@@ -17,10 +17,6 @@ import {
1717 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
1818 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
1919} from "@t3tools/contracts";
20import {
21 connectionStatusText,
22 type EnvironmentConnectionPresentation,
23} from "@t3tools/client-runtime/connection";
2420import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
2521import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
2622import {
......@@ -441,7 +437,7 @@ export interface ChatComposerProps {
441437 isPreparingWorktree: boolean;
442438 environmentUnavailable: {
443439 readonly label: string;
444 readonly connection: EnvironmentConnectionPresentation;
440 readonly connectionState: "connecting" | "disconnected" | "error";
445441 } | null;
446442
447443 // Pending approvals / inputs
......@@ -2354,9 +2350,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
23542350 : showPlanFollowUpPrompt && activeProposedPlan
23552351 ? "Add feedback to refine the plan, or leave this blank to implement it"
23562352 : environmentUnavailable
2357 ? `${environmentUnavailable.label}: ${connectionStatusText(
2358 environmentUnavailable.connection,
2359 )}`
2353 ? `${environmentUnavailable.label} is ${
2354 environmentUnavailable.connectionState === "connecting"
2355 ? "connecting"
2356 : "disconnected"
2357 }`
23602358 : phase === "disconnected"
23612359 ? "Ask for follow-up changes or attach images"
23622360 : "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";
1010import GitActionsControl from "../GitActionsControl";
1111import { type DraftId } from "~/composerDraftStore";
1212import { DiffIcon, Globe, TerminalSquareIcon } from "lucide-react";
13import { Badge } from "../ui/badge";
1413import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1514import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";
1615import { 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"
470470 )}
471471 {elementContextState.contexts.length > 0 ? (
472472 <div className="mb-2 flex flex-wrap gap-1.5">
473 {elementContextState.contexts.map((context, index) => (
473 {elementContextState.contexts.map((context) => (
474474 <UserMessageElementContextChip
475 key={`${context.header}:${index}`}
475 key={`${context.header}:${context.body}`}
476476 context={context}
477477 />
478478 ))}
......@@ -579,10 +579,16 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
579579 <TooltipTrigger
580580 render={<p className="text-muted-foreground text-xs tabular-nums" />}
581581 >
582 {formatShortTimestamp(row.message.updatedAt, ctx.timestampFormat)}
582 {formatShortTimestamp(
583 row.message.completedAt ?? row.message.createdAt,
584 ctx.timestampFormat,
585 )}
583586 </TooltipTrigger>
584587 <TooltipPopup>
585 {formatChatTimestampTooltip(row.message.updatedAt, ctx.timestampFormat)}
588 {formatChatTimestampTooltip(
589 row.message.completedAt ?? row.message.createdAt,
590 ctx.timestampFormat,
591 )}
586592 </TooltipPopup>
587593 </Tooltip>
588594 )}
apps/web/src/components/preview/useDiscoveredLocalServers.test.ts+1-1
......@@ -1,5 +1,5 @@
11import type { DiscoveredLocalServer } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";
2import { describe, expect, it } from "vite-plus/test";
33
44import { 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) => {
104104 onEvent: vi.fn(() => () => undefined),
105105 onMetadata: vi.fn(() => () => undefined),
106106 },
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 },
107117 projects: {
108118 searchEntries: vi.fn(),
109119 writeFile: vi.fn(),
apps/web/src/lib/elementContext.test.ts+1-1
......@@ -1,5 +1,5 @@
11import type { PickedElementPayload } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";
2import { describe, expect, it } from "vite-plus/test";
33
44import {
55 appendElementContextsToPrompt,
apps/web/src/lib/elementContext.ts+3-4
......@@ -198,12 +198,11 @@ export function appendElementContextsToPrompt(
198198}
199199
200200const ELEMENT_CONTEXT_ID_PREFIX = "el_";
201let nextElementContextSequence = 0;
201202
202203export function newElementContextId(): string {
203 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
204 return `${ELEMENT_CONTEXT_ID_PREFIX}${crypto.randomUUID()}`;
205 }
206 return `${ELEMENT_CONTEXT_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
204 nextElementContextSequence += 1;
205 return `${ELEMENT_CONTEXT_ID_PREFIX}${nextElementContextSequence.toString(36)}`;
207206}
208207
209208/**
apps/web/src/previewStateStore.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { scopeThreadRef } from "@t3tools/client-runtime";
22import { 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
55import { __testing, selectThreadPreviewState, usePreviewStateStore } from "./previewStateStore";
66
apps/web/src/rightPanelStore.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { scopeThreadRef } from "@t3tools/client-runtime";
22import { type EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { beforeEach, describe, expect, it } from "vitest";
3import { beforeEach, describe, expect, it } from "vite-plus/test";
44
55import {
66 selectActiveRightPanel,
packages/contracts/src/preview.test.ts+1-1
......@@ -1,5 +1,5 @@
11import { Schema } from "effect";
2import { describe, expect, it } from "vitest";
2import { describe, expect, it } from "vite-plus/test";
33
44import {
55 DiscoveredLocalServer,
packages/contracts/src/rpc.ts+8-5
......@@ -480,38 +480,40 @@ export const WsTerminalCloseRpc = Rpc.make(WS_METHODS.terminalClose, {
480480export const WsPreviewOpenRpc = Rpc.make(WS_METHODS.previewOpen, {
481481 payload: PreviewOpenInput,
482482 success: PreviewSessionSnapshot,
483 error: PreviewError,
483 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
484484});
485485
486486export const WsPreviewNavigateRpc = Rpc.make(WS_METHODS.previewNavigate, {
487487 payload: PreviewNavigateInput,
488488 success: PreviewSessionSnapshot,
489 error: PreviewError,
489 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
490490});
491491
492492export const WsPreviewRefreshRpc = Rpc.make(WS_METHODS.previewRefresh, {
493493 payload: PreviewRefreshInput,
494 error: PreviewError,
494 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
495495});
496496
497497export const WsPreviewCloseRpc = Rpc.make(WS_METHODS.previewClose, {
498498 payload: PreviewCloseInput,
499 error: PreviewError,
499 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
500500});
501501
502502export const WsPreviewListRpc = Rpc.make(WS_METHODS.previewList, {
503503 payload: PreviewListInput,
504504 success: PreviewListResult,
505 error: EnvironmentAuthorizationError,
505506});
506507
507508export const WsPreviewReportStatusRpc = Rpc.make(WS_METHODS.previewReportStatus, {
508509 payload: PreviewReportStatusInput,
509 error: PreviewError,
510 error: Schema.Union([PreviewError, EnvironmentAuthorizationError]),
510511});
511512
512513export const WsSubscribePreviewEventsRpc = Rpc.make(WS_METHODS.subscribePreviewEvents, {
513514 payload: Schema.Struct({}),
514515 success: PreviewEvent,
516 error: EnvironmentAuthorizationError,
515517 stream: true,
516518});
517519
......@@ -520,6 +522,7 @@ export const WsSubscribeDiscoveredLocalServersRpc = Rpc.make(
520522 {
521523 payload: Schema.Struct({}),
522524 success: DiscoveredLocalServerList,
525 error: EnvironmentAuthorizationError,
523526 stream: true,
524527 },
525528);
packages/shared/src/preview.test.ts+1-1
......@@ -1,4 +1,4 @@
1import { describe, expect, it } from "vitest";
1import { describe, expect, it } from "vite-plus/test";
22
33import {
44 isLoopbackHost,
packages/shared/src/preview.ts+3-4
......@@ -5,16 +5,15 @@
55 */
66
77const TAB_ID_PREFIX = "tab_";
8let nextPreviewTabSequence = 0;
89
910/**
1011 * Generate a fresh preview tab id. Lives in shared (not contracts) because
1112 * the contracts package is schema-only — runtime helpers belong here.
1213 */
1314export function newPreviewTabId(): string {
14 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
15 return `${TAB_ID_PREFIX}${crypto.randomUUID()}`;
16 }
17 return `${TAB_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
15 nextPreviewTabSequence += 1;
16 return `${TAB_ID_PREFIX}${nextPreviewTabSequence.toString(36)}`;
1817}
1918
2019const 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:
128128 electron-updater:
129129 specifier: ^6.6.2
130130 version: 6.8.3
131 react-grab:
132 specifier: ^0.1.32
133 version: 0.1.44(react@19.2.6)
131134 devDependencies:
132135 '@effect/vitest':
133136 specifier: 4.0.0-beta.78
......@@ -580,6 +583,9 @@ importers:
580583 playwright:
581584 specifier: ^1.58.2
582585 version: 1.60.0
586 react-grab:
587 specifier: ^0.1.32
588 version: 0.1.44(react@19.2.6)
583589 tailwindcss:
584590 specifier: ^4.0.0
585591 version: 4.3.0
......@@ -2395,6 +2401,9 @@ packages:
23952401 peerDependencies:
23962402 hono: ^4
23972403
2404 '@iarna/toml@2.2.5':
2405 resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
2406
23982407 '@img/colour@1.1.0':
23992408 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
24002409 engines: {node: '>=18'}
......@@ -3432,6 +3441,10 @@ packages:
34323441 '@types/react':
34333442 optional: true
34343443
3444 '@react-grab/cli@0.1.44':
3445 resolution: {integrity: sha512-gMDYY2rw6OWajCcDlXSIgs2LC432YJXSb3Lm5yM187uhRgBYddoEVULi36h+IolX3r7jSb3ew7vn9FfI8NSo0A==}
3446 hasBin: true
3447
34353448 '@react-native-masked-view/masked-view@0.3.2':
34363449 resolution: {integrity: sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==}
34373450 peerDependencies:
......@@ -4787,6 +4800,10 @@ packages:
47874800 resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
47884801 engines: {node: '>= 14'}
47894802
4803 agent-install@0.0.5:
4804 resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==}
4805 hasBin: true
4806
47904807 ajv-draft-04@1.0.0:
47914808 resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
47924809 peerDependencies:
......@@ -5054,6 +5071,11 @@ packages:
50545071 resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
50555072 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
50575079 body-parser@2.2.2:
50585080 resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
50595081 engines: {node: '>=18'}
......@@ -5244,10 +5266,18 @@ packages:
52445266 resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
52455267 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
52475273 cli-spinners@2.9.2:
52485274 resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
52495275 engines: {node: '>=6'}
52505276
5277 cli-spinners@3.4.0:
5278 resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
5279 engines: {node: '>=18.20'}
5280
52515281 cli-truncate@2.1.0:
52525282 resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
52535283 engines: {node: '>=8'}
......@@ -5321,6 +5351,10 @@ packages:
53215351 resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
53225352 engines: {node: '>=18'}
53235353
5354 commander@14.0.3:
5355 resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
5356 engines: {node: '>=20'}
5357
53245358 commander@2.20.3:
53255359 resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
53265360
......@@ -6604,6 +6638,10 @@ packages:
66046638 resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
66056639 engines: {node: '>= 4'}
66066640
6641 ignore@7.0.5:
6642 resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
6643 engines: {node: '>= 4'}
6644
66076645 image-size@1.2.1:
66086646 resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
66096647 engines: {node: '>=16.x'}
......@@ -6725,6 +6763,10 @@ packages:
67256763 engines: {node: '>=14.16'}
67266764 hasBin: true
67276765
6766 is-interactive@2.0.0:
6767 resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
6768 engines: {node: '>=12'}
6769
67286770 is-node-process@1.2.0:
67296771 resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
67306772
......@@ -6742,6 +6784,10 @@ packages:
67426784 is-property@1.0.2:
67436785 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
67456791 is-wsl@2.2.0:
67466792 resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
67476793 engines: {node: '>=8'}
......@@ -7154,6 +7200,10 @@ packages:
71547200 resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
71557201 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
71577207 long@5.3.2:
71587208 resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
71597209
......@@ -7484,6 +7534,10 @@ packages:
74847534 resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
74857535 engines: {node: '>=6'}
74867536
7537 mimic-function@5.0.1:
7538 resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
7539 engines: {node: '>=18'}
7540
74877541 mimic-response@1.0.1:
74887542 resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
74897543 engines: {node: '>=4'}
......@@ -7729,6 +7783,10 @@ packages:
77297783 resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
77307784 engines: {node: '>=6'}
77317785
7786 onetime@7.0.0:
7787 resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
7788 engines: {node: '>=18'}
7789
77327790 oniguruma-parser@0.12.2:
77337791 resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
77347792
......@@ -7743,6 +7801,10 @@ packages:
77437801 resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
77447802 engines: {node: '>=6'}
77457803
7804 ora@9.4.0:
7805 resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==}
7806 engines: {node: '>=20'}
7807
77467808 os-paths@7.4.0:
77477809 resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==}
77487810 engines: {node: '>= 4.0'}
......@@ -8136,6 +8198,15 @@ packages:
81368198 peerDependencies:
81378199 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
81398210 react-is@16.13.1:
81408211 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
81418212
......@@ -8454,6 +8525,10 @@ packages:
84548525 resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
84558526 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
84578532 retext-latin@4.0.0:
84588533 resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}
84598534
......@@ -8757,6 +8832,10 @@ packages:
87578832 std-env@4.1.0:
87588833 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
87608839 stream-buffers@2.2.0:
87618840 resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
87628841 engines: {node: '>= 0.10.0'}
......@@ -9631,6 +9710,10 @@ packages:
96319710 resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
96329711 engines: {node: '>=18'}
96339712
9713 yoctocolors@2.1.2:
9714 resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
9715 engines: {node: '>=18'}
9716
96349717 yoga-layout@3.2.1:
96359718 resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
96369719
......@@ -11621,6 +11704,8 @@ snapshots:
1162111704 dependencies:
1162211705 hono: 4.12.25
1162311706
11707 '@iarna/toml@2.2.5': {}
11708
1162411709 '@img/colour@1.1.0':
1162511710 optional: true
1162611711
......@@ -12577,6 +12662,17 @@ snapshots:
1257712662 optionalDependencies:
1257812663 '@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
1258012676 '@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)':
1258112677 dependencies:
1258212678 react: 19.2.3
......@@ -13796,6 +13892,15 @@ snapshots:
1379613892
1379713893 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
1379913904 ajv-draft-04@1.0.0(ajv@8.20.0):
1380013905 optionalDependencies:
1380113906 ajv: 8.20.0
......@@ -14229,6 +14334,10 @@ snapshots:
1422914334
1423014335 big-integer@1.6.52: {}
1423114336
14337 bippy@0.5.41(react@19.2.6):
14338 dependencies:
14339 react: 19.2.6
14340
1423214341 body-parser@2.2.2:
1423314342 dependencies:
1423414343 bytes: 3.1.2
......@@ -14451,8 +14560,14 @@ snapshots:
1445114560 dependencies:
1445214561 restore-cursor: 4.0.0
1445314562
14563 cli-cursor@5.0.0:
14564 dependencies:
14565 restore-cursor: 5.1.0
14566
1445414567 cli-spinners@2.9.2: {}
1445514568
14569 cli-spinners@3.4.0: {}
14570
1445614571 cli-truncate@2.1.0:
1445714572 dependencies:
1445814573 slice-ansi: 3.0.0
......@@ -14520,6 +14635,8 @@ snapshots:
1452014635
1452114636 commander@12.1.0: {}
1452214637
14638 commander@14.0.3: {}
14639
1452314640 commander@2.20.3: {}
1452414641
1452514642 commander@5.1.0: {}
......@@ -15980,6 +16097,8 @@ snapshots:
1598016097
1598116098 ignore@5.3.2: {}
1598216099
16100 ignore@7.0.5: {}
16101
1598316102 image-size@1.2.1:
1598416103 dependencies:
1598516104 queue: 6.0.2
......@@ -16099,6 +16218,8 @@ snapshots:
1609916218 dependencies:
1610016219 is-docker: 3.0.0
1610116220
16221 is-interactive@2.0.0: {}
16222
1610216223 is-node-process@1.2.0: {}
1610316224
1610416225 is-number@7.0.0: {}
......@@ -16109,6 +16230,8 @@ snapshots:
1610916230
1611016231 is-property@1.0.2: {}
1611116232
16233 is-unicode-supported@2.1.0: {}
16234
1611216235 is-wsl@2.2.0:
1611316236 dependencies:
1611416237 is-docker: 2.2.1
......@@ -16442,6 +16565,11 @@ snapshots:
1644216565 dependencies:
1644316566 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
1644516573 long@5.3.2: {}
1644616574
1644716575 longest-streak@3.1.0: {}
......@@ -17071,6 +17199,8 @@ snapshots:
1707117199
1707217200 mimic-fn@2.1.0: {}
1707317201
17202 mimic-function@5.0.1: {}
17203
1707417204 mimic-response@1.0.1: {}
1707517205
1707617206 mimic-response@3.1.0: {}
......@@ -17308,6 +17438,10 @@ snapshots:
1730817438 dependencies:
1730917439 mimic-fn: 2.1.0
1731017440
17441 onetime@7.0.0:
17442 dependencies:
17443 mimic-function: 5.0.1
17444
1731117445 oniguruma-parser@0.12.2: {}
1731217446
1731317447 oniguruma-to-es@4.3.6:
......@@ -17330,6 +17464,17 @@ snapshots:
1733017464 strip-ansi: 5.2.0
1733117465 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
1733317478 os-paths@7.4.0:
1733417479 optionalDependencies:
1733517480 fsevents: 2.3.3
......@@ -17721,6 +17866,13 @@ snapshots:
1772117866 dependencies:
1772217867 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
1772417876 react-is@16.13.1: {}
1772517877
1772617878 react-is@17.0.2: {}
......@@ -18122,6 +18274,11 @@ snapshots:
1812218274 onetime: 5.1.2
1812318275 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
1812518282 retext-latin@4.0.0:
1812618283 dependencies:
1812718284 '@types/nlcst': 2.0.3
......@@ -18563,6 +18720,8 @@ snapshots:
1856318720
1856418721 std-env@4.1.0: {}
1856518722
18723 stdin-discarder@0.3.2: {}
18724
1856618725 stream-buffers@2.2.0: {}
1856718726
1856818727 strict-event-emitter@0.5.1: {}
......@@ -19377,6 +19536,8 @@ snapshots:
1937719536
1937819537 yoctocolors-cjs@2.1.3: {}
1937919538
19539 yoctocolors@2.1.2: {}
19540
1938019541 yoga-layout@3.2.1: {}
1938119542
1938219543 zod-to-json-schema@3.25.2(zod@4.4.3):