From 9b43123ba2a90ecaaf2eac1f75b6aebcf181f253 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 11 Jun 2026 15:51:47 -0700 Subject: [PATCH] fix(preview): port browser preview to current main Co-authored-by: codex --- apps/desktop/src/ipc/methods/preview.ts | 56 ++++-- apps/desktop/src/main.ts | 2 - .../src/picked-element-payload.test.ts | 2 +- apps/desktop/src/preload.ts | 10 +- apps/desktop/src/preview-pick-preload.test.ts | 2 +- apps/desktop/src/preview-pick-preload.ts | 1 + apps/desktop/src/preview-view-manager.ts | 1 + .../src/preview-webview-preferences.test.ts | 2 +- .../server/src/preview/Layers/Manager.test.ts | 2 +- apps/server/src/preview/Layers/Manager.ts | 46 ++--- .../src/preview/Layers/PortScanner.test.ts | 88 +++++----- apps/server/src/preview/Layers/PortScanner.ts | 35 ++-- .../src/preview/Services/PortScanner.ts | 2 +- apps/server/src/server.test.ts | 38 ++--- apps/server/src/server.ts | 9 +- apps/server/src/ws.ts | 9 +- apps/web/src/components/chat/ChatComposer.tsx | 14 +- apps/web/src/components/chat/ChatHeader.tsx | 1 - .../src/components/chat/MessagesTimeline.tsx | 14 +- .../preview/useDiscoveredLocalServers.test.ts | 2 +- .../service.threadSubscriptions.test.ts | 10 ++ apps/web/src/lib/elementContext.test.ts | 2 +- apps/web/src/lib/elementContext.ts | 7 +- apps/web/src/previewStateStore.test.ts | 2 +- apps/web/src/rightPanelStore.test.ts | 2 +- packages/contracts/src/preview.test.ts | 2 +- packages/contracts/src/rpc.ts | 13 +- packages/shared/src/preview.test.ts | 2 +- packages/shared/src/preview.ts | 7 +- pnpm-lock.yaml | 161 ++++++++++++++++++ 30 files changed, 381 insertions(+), 163 deletions(-) diff --git a/apps/desktop/src/ipc/methods/preview.ts b/apps/desktop/src/ipc/methods/preview.ts index 9b80339a2e2c9a2b9fb951b7057f27382348a85a..b4476fe6fc1b58227dbc29f2ea10f1a5632b1e2f 100644 --- a/apps/desktop/src/ipc/methods/preview.ts +++ b/apps/desktop/src/ipc/methods/preview.ts @@ -1,7 +1,7 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import { BrowserWindow } from "electron"; -import * as NodeFileSystem from "node:fs"; -import * as NodePath from "node:path"; import { pathToFileURL } from "node:url"; import { previewViewManager } from "../../preview-view-manager.ts"; @@ -29,18 +29,38 @@ const tabIdFrom = (raw: unknown): string => { return tabId; }; -const method = (channel: string, handler: (raw: unknown) => unknown | Promise): DesktopIpcMethod => ({ +class PreviewIpcError extends Data.TaggedError("PreviewIpcError")<{ + readonly cause: unknown; +}> {} + +const method = ( + channel: string, + handler: (raw: unknown) => unknown | Promise, +): DesktopIpcMethod => ({ channel, - handler: (raw) => Effect.tryPromise({ try: () => Promise.resolve(handler(raw)), catch: (error) => error }), + handler: (raw) => + Effect.tryPromise({ + try: () => Promise.resolve(handler(raw)), + catch: (cause) => new PreviewIpcError({ cause }), + }), }); export const previewMethods = [ - method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) => previewViewManager.createTab(tabIdFrom(raw))), - method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) => previewViewManager.closeTab(tabIdFrom(raw))), + method(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, (raw) => + previewViewManager.createTab(tabIdFrom(raw)), + ), + method(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, (raw) => + previewViewManager.closeTab(tabIdFrom(raw)), + ), method(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, (raw) => { const tabId = tabIdFrom(raw); - const webContentsId = typeof raw === "object" && raw !== null && "webContentsId" in raw ? raw.webContentsId : null; - if (typeof webContentsId !== "number" || !Number.isInteger(webContentsId) || webContentsId <= 0) { + const webContentsId = + typeof raw === "object" && raw !== null && "webContentsId" in raw ? raw.webContentsId : null; + if ( + typeof webContentsId !== "number" || + !Number.isInteger(webContentsId) || + webContentsId <= 0 + ) { throw new Error("preview webContentsId must be a positive integer"); } return previewViewManager.registerWebview(tabId, webContentsId); @@ -52,21 +72,29 @@ export const previewMethods = [ return previewViewManager.navigate(tabId, url); }), method(IpcChannels.PREVIEW_GO_BACK_CHANNEL, (raw) => previewViewManager.goBack(tabIdFrom(raw))), - method(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, (raw) => previewViewManager.goForward(tabIdFrom(raw))), + method(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, (raw) => + previewViewManager.goForward(tabIdFrom(raw)), + ), method(IpcChannels.PREVIEW_REFRESH_CHANNEL, (raw) => previewViewManager.refresh(tabIdFrom(raw))), method(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, (raw) => previewViewManager.zoomIn(tabIdFrom(raw))), method(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, (raw) => previewViewManager.zoomOut(tabIdFrom(raw))), - method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) => previewViewManager.resetZoom(tabIdFrom(raw))), - method(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, (raw) => previewViewManager.hardReload(tabIdFrom(raw))), - method(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, (raw) => previewViewManager.openDevTools(tabIdFrom(raw))), + method(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, (raw) => + previewViewManager.resetZoom(tabIdFrom(raw)), + ), + method(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, (raw) => + previewViewManager.hardReload(tabIdFrom(raw)), + ), + method(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, (raw) => + previewViewManager.openDevTools(tabIdFrom(raw)), + ), method(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, () => previewViewManager.clearCookies()), method(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, () => previewViewManager.clearCache()), method(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, () => { - const preloadPath = NodePath.join(__dirname, "preview-pick-preload.cjs"); + const preloadPath = `${__dirname}/preview-pick-preload.cjs`; return { partition: previewViewManager.getBrowserPartition(), webPreferences: PREVIEW_WEBVIEW_PREFERENCES, - preloadUrl: NodeFileSystem.existsSync(preloadPath) ? pathToFileURL(preloadPath).href : null, + preloadUrl: pathToFileURL(preloadPath).href, }; }), method(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, (raw) => diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 74ffcba1a165fc677e8774209b3ce8de2d0b1725..9356eef441b642ae97be47447025cf9292cc382b 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -28,7 +28,6 @@ import * as DesktopApp from "./app/DesktopApp.ts"; import * as DesktopAppIdentity from "./app/DesktopAppIdentity.ts"; import * as DesktopCloudAuth from "./app/DesktopCloudAuth.ts"; import * as DesktopCloudAuthTokenStore from "./app/DesktopCloudAuthTokenStore.ts"; -import * as DesktopConnectionCatalogStore from "./app/DesktopConnectionCatalogStore.ts"; import * as DesktopApplicationMenu from "./window/DesktopApplicationMenu.ts"; import * as DesktopAssets from "./app/DesktopAssets.ts"; import * as DesktopBackendConfiguration from "./backend/DesktopBackendConfiguration.ts"; @@ -115,7 +114,6 @@ const desktopFoundationLayer = Layer.mergeAll( DesktopClientSettings.layer, DesktopSavedEnvironments.layer, DesktopCloudAuthTokenStore.layer, - DesktopConnectionCatalogStore.layer, DesktopAssets.layer, DesktopObservability.layer, ).pipe(Layer.provideMerge(desktopEnvironmentLayer)); diff --git a/apps/desktop/src/picked-element-payload.test.ts b/apps/desktop/src/picked-element-payload.test.ts index 0b6d23b6acd7a03278a03e751b12006b62fec675..8e5c3ad54bac9676467a1fb5cf0fdb395039a385 100644 --- a/apps/desktop/src/picked-element-payload.test.ts +++ b/apps/desktop/src/picked-element-payload.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { isPickedElementPayload } from "./picked-element-payload.ts"; diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index b9deced7ffb4b007d1d2ade620fee763537056f8..a66a168040190246576e3d80f1559ced34af67c4 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -47,10 +47,6 @@ contextBridge.exposeInMainWorld("desktopBridge", { ipcRenderer.invoke(IpcChannels.SET_SAVED_ENVIRONMENT_SECRET_CHANNEL, { environmentId, secret }), removeSavedEnvironmentSecret: (environmentId) => ipcRenderer.invoke(IpcChannels.REMOVE_SAVED_ENVIRONMENT_SECRET_CHANNEL, environmentId), - getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL), - setConnectionCatalog: (catalog) => - ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog), - clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL), discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL), ensureSshEnvironment: async (target, options) => unwrapEnsureSshEnvironmentResult( @@ -164,8 +160,7 @@ contextBridge.exposeInMainWorld("desktopBridge", { clearCookies: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL), clearCache: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL), getPreviewConfig: () => ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL), - pickElement: (tabId) => - ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }), + pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }), cancelPickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }), onStateChange: (listener) => { @@ -178,7 +173,8 @@ contextBridge.exposeInMainWorld("desktopBridge", { listener(tabId, state as DesktopPreviewTabState); }; ipcRenderer.on(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener); - return () => ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener); + return () => + ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener); }, }, } satisfies DesktopBridge); diff --git a/apps/desktop/src/preview-pick-preload.test.ts b/apps/desktop/src/preview-pick-preload.test.ts index dbb6eb964aa922eeccfac19ff70fc9178e427cab..9f239e1d2e86a0cbecd1125bd99af4391d811c12 100644 --- a/apps/desktop/src/preview-pick-preload.test.ts +++ b/apps/desktop/src/preview-pick-preload.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { computeLabelPosition } from "./preview-pick-label-position.ts"; diff --git a/apps/desktop/src/preview-pick-preload.ts b/apps/desktop/src/preview-pick-preload.ts index 9f1fee9da15020a5bea023b440c06ab8bf102ce6..f5dacf1edb9807187c65c71bf2cf6d72105a5697 100644 --- a/apps/desktop/src/preview-pick-preload.ts +++ b/apps/desktop/src/preview-pick-preload.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off /** * Preview pick preload — runs in the isolated world of the Chromium * `` that hosts the in-app browser. Loaded via the diff --git a/apps/desktop/src/preview-view-manager.ts b/apps/desktop/src/preview-view-manager.ts index 127fb216b0055d432630e9de03f2e13f560adc0b..a49695430cae33db937140843675c03611bc44a1 100644 --- a/apps/desktop/src/preview-view-manager.ts +++ b/apps/desktop/src/preview-view-manager.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off /** * PreviewViewManager — desktop side of the in-app browser preview. * diff --git a/apps/desktop/src/preview-webview-preferences.test.ts b/apps/desktop/src/preview-webview-preferences.test.ts index 82e8a88ce025d8e8f95b45c1a05942388eada90c..dc88714a51dc02b20f40e6aeb250e6a3418bae48 100644 --- a/apps/desktop/src/preview-webview-preferences.test.ts +++ b/apps/desktop/src/preview-webview-preferences.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { PREVIEW_WEBVIEW_PREFERENCES } from "./preview-webview-preferences.ts"; diff --git a/apps/server/src/preview/Layers/Manager.test.ts b/apps/server/src/preview/Layers/Manager.test.ts index 197b79d565141e3c26c1e783fb18922381653264..97a64ec844f8f3442c0806eaa6c041f8836ca631 100644 --- a/apps/server/src/preview/Layers/Manager.test.ts +++ b/apps/server/src/preview/Layers/Manager.test.ts @@ -1,7 +1,7 @@ import { it } from "@effect/vitest"; import { type PreviewEvent, ThreadId } from "@t3tools/contracts"; import { Effect, PubSub } from "effect"; -import { expect } from "vitest"; +import { expect } from "vite-plus/test"; import { PreviewManager } from "../Services/Manager.ts"; import { PreviewManagerLive } from "./Manager.ts"; diff --git a/apps/server/src/preview/Layers/Manager.ts b/apps/server/src/preview/Layers/Manager.ts index 3a70f0dceaff763cb48492cf67a43e9f332b4f2c..dc58b3f9f13d511a59bdf51f12da768f2521bafe 100644 --- a/apps/server/src/preview/Layers/Manager.ts +++ b/apps/server/src/preview/Layers/Manager.ts @@ -21,7 +21,7 @@ import { normalizePreviewUrl, PreviewUrlNormalizationError, } from "@t3tools/shared/preview"; -import { Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect"; +import { DateTime, Effect, Layer, PubSub, Stream, SynchronizedRef } from "effect"; import { PreviewManager, type PreviewManagerShape } from "../Services/Manager.ts"; @@ -66,30 +66,34 @@ const normalizeUrl = (rawUrl: string): Effect.Effect ({ threadId: input.threadId, tabId: input.tabId, navStatus: { _tag: "Loading", url: input.url, title: input.title }, canGoBack: false, canGoForward: false, - updatedAt: new Date().toISOString(), + updatedAt: input.updatedAt, }); const buildIdleSnapshot = (input: { readonly threadId: string; readonly tabId: string; + readonly updatedAt: string; }): PreviewSessionSnapshot => ({ threadId: input.threadId, tabId: input.tabId, navStatus: { _tag: "Idle" }, canGoBack: false, canGoForward: false, - updatedAt: new Date().toISOString(), + updatedAt: input.updatedAt, }); export const makePreviewManager = Effect.gen(function* () { @@ -152,14 +156,16 @@ export const makePreviewManager = Effect.gen(function* () { const open: PreviewManagerShape["open"] = (input) => Effect.gen(function* () { const tabId = newPreviewTabId(); + const updatedAt = yield* currentIsoTimestamp; const snapshot = input.url ? buildLoadingSnapshot({ threadId: input.threadId, tabId, url: yield* normalizeUrl(input.url), title: "", + updatedAt, }) - : buildIdleSnapshot({ threadId: input.threadId, tabId }); + : buildIdleSnapshot({ threadId: input.threadId, tabId, updatedAt }); yield* SynchronizedRef.update(stateRef, (state) => { const sessions = new Map(state.sessions); sessions.set(compositeKey(input.threadId, tabId), { @@ -183,7 +189,8 @@ export const makePreviewManager = Effect.gen(function* () { Effect.gen(function* () { const url = yield* normalizeUrl(input.url); return yield* mutateExistingSession(input.threadId, input.tabId, (session) => - Effect.sync(() => { + Effect.gen(function* () { + const updatedAt = yield* currentIsoTimestamp; const previousTitle = session.snapshot.navStatus._tag === "Idle" ? "" : session.snapshot.navStatus.title; const resolvedTitle = input.resolvedTitle ?? previousTitle; @@ -193,7 +200,7 @@ export const makePreviewManager = Effect.gen(function* () { navStatus: { _tag: "Success", url, title: resolvedTitle }, canGoBack: session.snapshot.canGoBack, canGoForward: session.snapshot.canGoForward, - updatedAt: new Date().toISOString(), + updatedAt, }; return { next: { ...session, snapshot }, @@ -212,14 +219,15 @@ export const makePreviewManager = Effect.gen(function* () { const reportStatus: PreviewManagerShape["reportStatus"] = (input) => mutateExistingSession(input.threadId, input.tabId, (session) => - Effect.sync(() => { + Effect.gen(function* () { + const updatedAt = yield* currentIsoTimestamp; const snapshot: PreviewSessionSnapshot = { threadId: session.threadId, tabId: session.tabId, navStatus: input.navStatus, canGoBack: input.canGoBack, canGoForward: input.canGoForward, - updatedAt: new Date().toISOString(), + updatedAt, }; const emit: PreviewEvent = input.navStatus._tag === "LoadFailed" @@ -256,8 +264,9 @@ export const makePreviewManager = Effect.gen(function* () { ); const close: PreviewManagerShape["close"] = (input) => - Effect.flatMap( - SynchronizedRef.modify(stateRef, (state) => { + Effect.gen(function* () { + const createdAt = yield* currentIsoTimestamp; + const events = yield* SynchronizedRef.modify(stateRef, (state) => { const eventsToEmit: PreviewEvent[] = []; const sessions = new Map(state.sessions); const targets = input.tabId @@ -271,21 +280,20 @@ export const makePreviewManager = Effect.gen(function* () { type: "closed", threadId: target.threadId, tabId: target.tabId, - createdAt: new Date().toISOString(), + createdAt, }); } if (eventsToEmit.length === 0) { return [eventsToEmit, state] as const; } return [eventsToEmit, { sessions }] as const; - }), - (events) => - events.length === 0 - ? Effect.void - : Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), { - discard: true, - }), - ); + }); + if (events.length > 0) { + yield* Effect.forEach(events, (event) => PubSub.publish(eventsPubSub, event), { + discard: true, + }); + } + }); const list: PreviewManagerShape["list"] = (input) => SynchronizedRef.get(stateRef).pipe( diff --git a/apps/server/src/preview/Layers/PortScanner.test.ts b/apps/server/src/preview/Layers/PortScanner.test.ts index b81103c151260e76a0c19ffea30b150cd91b0358..e0bec8cd4d3a4e53da29de5580e0f19eaf24c921 100644 --- a/apps/server/src/preview/Layers/PortScanner.test.ts +++ b/apps/server/src/preview/Layers/PortScanner.test.ts @@ -1,12 +1,18 @@ import * as net from "node:net"; -import { Effect } from "effect"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { it as effectIt } from "@effect/vitest"; +import { Effect, Layer } from "effect"; +import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; -import { PreviewPortScanner } from "../Services/PortScanner.ts"; +import { COMMON_DEV_PORTS, PreviewPortScanner } from "../Services/PortScanner.ts"; +import { ProcessRunner } from "../../processRunner.ts"; import { __testing, PreviewPortScannerLive } from "./PortScanner.ts"; const { parseLsofOutput, parsePortFromLsofName, serversEqual } = __testing; +const TestProcessRunner = Layer.succeed(ProcessRunner, { + run: () => Effect.die("ProcessRunner should not be used by Windows TCP probe tests"), +}); +const TestPreviewPortScannerLive = PreviewPortScannerLive.pipe(Layer.provide(TestProcessRunner)); describe("parsePortFromLsofName", () => { it("parses *:port", () => { @@ -121,16 +127,25 @@ describe("serversEqual", () => { describe("PreviewPortScanner integration (TCP probe path)", () => { let originalPlatform: NodeJS.Platform; let server: net.Server; + let port: number; beforeEach(async () => { originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "win32", configurable: true }); - // 3001 is in COMMON_DEV_PORTS so the TCP-probe pass will check it. - server = net.createServer(); - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(3001, "127.0.0.1", () => resolve()); - }); + for (const candidate of COMMON_DEV_PORTS) { + const candidateServer = net.createServer(); + const listening = await new Promise((resolve) => { + candidateServer.once("error", () => resolve(false)); + candidateServer.listen(candidate, "127.0.0.1", () => resolve(true)); + }); + if (listening) { + server = candidateServer; + port = candidate; + return; + } + candidateServer.close(); + } + throw new Error("No common development port was available for the preview scanner test"); }); afterEach(async () => { @@ -141,40 +156,29 @@ describe("PreviewPortScanner integration (TCP probe path)", () => { await new Promise((resolve) => server.close(() => resolve())); }); - it("scan() returns a server we just opened on a curated dev port", async () => { - const result = await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - const scanner = yield* PreviewPortScanner; - return yield* scanner.scan(); - }).pipe(Effect.provide(PreviewPortScannerLive)), - ), - ); - const found = result.find((s) => s.port === 3001); - expect(found).toBeDefined(); - expect(found?.host).toBe("localhost"); - }); + effectIt.effect("scan() returns a server we just opened on a curated dev port", () => + Effect.gen(function* () { + const scanner = yield* PreviewPortScanner; + const result = yield* scanner.scan(); + const found = result.find((server) => server.port === port); + expect(found).toBeDefined(); + expect(found?.host).toBe("localhost"); + }).pipe(Effect.provide(TestPreviewPortScannerLive)), + ); - it("retain() drives an immediate broadcast to subscribers", async () => { + effectIt.effect("retain() drives an immediate broadcast to subscribers", () => { const received: number[] = []; - await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - const scanner = yield* PreviewPortScanner; - const unsubscribe = yield* scanner.subscribe((servers) => - Effect.sync(() => { - for (const server of servers) received.push(server.port); - }), - ); - const release = yield* scanner.retain(); - // The retain() pollTick is fire-and-forget within Effect, so wait - // a frame for the broadcast to land. - yield* Effect.sleep("100 millis"); - unsubscribe(); - release(); - }).pipe(Effect.provide(PreviewPortScannerLive)), - ), - ); - expect(received).toContain(3001); + return Effect.gen(function* () { + const scanner = yield* PreviewPortScanner; + const unsubscribe = yield* scanner.subscribe((servers) => + Effect.sync(() => { + for (const server of servers) received.push(server.port); + }), + ); + const release = yield* scanner.retain(); + unsubscribe(); + release(); + expect(received).toContain(port); + }).pipe(Effect.provide(TestPreviewPortScannerLive)); }); }); diff --git a/apps/server/src/preview/Layers/PortScanner.ts b/apps/server/src/preview/Layers/PortScanner.ts index a7333015896f9f7529273c383b8ed1f7b7bdf009..fa0bd2b05201349913f9bbcbb21f13d9299798f2 100644 --- a/apps/server/src/preview/Layers/PortScanner.ts +++ b/apps/server/src/preview/Layers/PortScanner.ts @@ -17,7 +17,7 @@ import type { DiscoveredLocalServer } from "@t3tools/contracts"; import { LSOF_LOCAL_HOST_TOKENS } from "@t3tools/shared/preview"; import { Cause, Data, Duration, Effect, Layer, Ref, Schedule } from "effect"; -import { runProcess } from "../../processRunner.ts"; +import { ProcessRunner, type ProcessRunnerShape } from "../../processRunner.ts"; import { COMMON_DEV_PORTS, PreviewPortScanner, @@ -91,20 +91,22 @@ const parsePortFromLsofName = (name: string): number | null => { return port; }; -const probeLsof = (): Effect.Effect | null> => - Effect.tryPromise({ - try: () => - runProcess("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"], { - timeoutMs: LSOF_TIMEOUT_MS, - allowNonZeroExit: true, - maxBufferBytes: 1024 * 1024, - outputMode: "truncate", - }), - catch: (cause) => new LsofProbeError({ cause }), - }).pipe( - Effect.map((result) => parseLsofOutput(result.stdout)), - Effect.catch(() => Effect.succeed(null)), - ); +const probeLsof = ( + processRunner: ProcessRunnerShape, +): Effect.Effect | null> => + processRunner + .run({ + command: "lsof", + args: ["-iTCP", "-sTCP:LISTEN", "-P", "-n", "-F", "pcn"], + timeout: Duration.millis(LSOF_TIMEOUT_MS), + maxOutputBytes: 1024 * 1024, + outputMode: "truncate", + }) + .pipe( + Effect.mapError((cause) => new LsofProbeError({ cause })), + Effect.map((result) => parseLsofOutput(result.stdout)), + Effect.orElseSucceed(() => null), + ); const probeTcpPort = (port: number): Promise => new Promise((resolve) => { @@ -162,6 +164,7 @@ const serversEqual = ( }; export const makePreviewPortScanner = Effect.gen(function* () { + const processRunner = yield* ProcessRunner; const stateRef = yield* Ref.make({ lastSnapshot: [], }); @@ -176,7 +179,7 @@ export const makePreviewPortScanner = Effect.gen(function* () { if (process.platform === "win32") { return yield* probeCommonPorts(); } - const lsof = yield* probeLsof(); + const lsof = yield* probeLsof(processRunner); if (lsof !== null) return lsof; return yield* probeCommonPorts(); }); diff --git a/apps/server/src/preview/Services/PortScanner.ts b/apps/server/src/preview/Services/PortScanner.ts index 19a83c51c20bb00bc0d7b078eaefc0ce8d9aca2b..4e8751bb6699b4a71b249738449d2b9c01800f45 100644 --- a/apps/server/src/preview/Services/PortScanner.ts +++ b/apps/server/src/preview/Services/PortScanner.ts @@ -28,7 +28,7 @@ export interface PreviewPortScannerShape { export class PreviewPortScanner extends Context.Service< PreviewPortScanner, PreviewPortScannerShape ->()("t3/preview/Services/PortScanner") {} +>()("t3/preview/Services/PortScanner/PreviewPortScanner") {} /** Curated list of common dev-server ports for the Windows TCP-probe fallback. */ export const COMMON_DEV_PORTS: ReadonlyArray = Object.freeze([ diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 46e06085ed13825483bf020198139ae0f3d78635..c69771181f1020d8311a45178323f53577617ca7 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -668,25 +668,25 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(PreviewManager)({ - open: () => Effect.die("PreviewManager not stubbed in this test"), - navigate: () => Effect.die("PreviewManager not stubbed in this test"), - reportStatus: () => Effect.void, - refresh: () => Effect.void, - close: () => Effect.void, - list: () => Effect.succeed({ sessions: [] }), - events: Stream.empty, - subscribeEvents: Effect.flatMap(PubSub.unbounded(), (pubsub) => - PubSub.subscribe(pubsub), - ), - }), - ), - Layer.provide( - Layer.mock(PreviewPortScanner)({ - scan: () => Effect.succeed([]), - subscribe: () => Effect.succeed(() => {}), - retain: () => Effect.succeed(() => {}), - }), + Layer.mergeAll( + Layer.mock(PreviewManager)({ + open: () => Effect.die("PreviewManager not stubbed in this test"), + navigate: () => Effect.die("PreviewManager not stubbed in this test"), + reportStatus: () => Effect.void, + refresh: () => Effect.void, + close: () => Effect.void, + list: () => Effect.succeed({ sessions: [] }), + events: Stream.empty, + subscribeEvents: Effect.flatMap(PubSub.unbounded(), (pubsub) => + PubSub.subscribe(pubsub), + ), + }), + Layer.mock(PreviewPortScanner)({ + scan: () => Effect.succeed([]), + subscribe: () => Effect.succeed(() => {}), + retain: () => Effect.succeed(() => {}), + }), + ), ), Layer.provide( Layer.mock(OrchestrationEngineService)({ diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index aaea59ab62f6ed48113daf72cc57428c79a1fb01..378e4c6060b062cde16f29638d519b303523296c 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -37,6 +37,7 @@ import { ProviderInstanceRegistryHydrationLive } from "./provider/Layers/Provide import { TerminalManagerLive } from "./terminal/Layers/Manager.ts"; import { PreviewManagerLive } from "./preview/Layers/Manager.ts"; import { PreviewPortScannerLive } from "./preview/Layers/PortScanner.ts"; +import * as ProcessRunner from "./processRunner.ts"; import * as GitManager from "./git/GitManager.ts"; import { KeybindingsLive } from "./keybindings.ts"; import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts"; @@ -235,7 +236,10 @@ const CheckpointingLayerLive = Layer.empty.pipe( const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive)); -const PreviewLayerLive = Layer.mergeAll(PreviewManagerLive, PreviewPortScannerLive); +const PreviewLayerLive = Layer.mergeAll( + PreviewManagerLive, + PreviewPortScannerLive.pipe(Layer.provide(ProcessRunner.layer)), +); const WorkspaceEntriesLayerLive = WorkspaceEntriesLive.pipe( Layer.provide(WorkspacePathsLive), @@ -278,8 +282,7 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), - Layer.provideMerge(TerminalLayerLive), - Layer.provideMerge(PreviewLayerLive), + Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(KeybindingsLive), Layer.provideMerge(ProviderRegistryLive), diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 39187075fdb54a593bcc29c6b20df1244c0ed0df..83bb1641680c219821bfc88cfce9fb773a6f6d0e 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1391,14 +1391,15 @@ const makeWsRpcLayer = (currentSession: AuthenticatedSession) => Effect.gen(function* () { const release = yield* previewPortScanner.retain(); const initial = yield* previewPortScanner.scan(); + const initialScannedAt = DateTime.formatIso(yield* DateTime.now); yield* Queue.offer(queue, { servers: initial, - scannedAt: new Date().toISOString(), + scannedAt: initialScannedAt, }); const unsubscribe = yield* previewPortScanner.subscribe((servers) => - Queue.offer(queue, { - servers, - scannedAt: new Date().toISOString(), + Effect.gen(function* () { + const scannedAt = DateTime.formatIso(yield* DateTime.now); + yield* Queue.offer(queue, { servers, scannedAt }); }), ); return { unsubscribe, release }; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 5696c6f069c96e11941fed575a4aec2f0ea50131..445c4eda2dff30f534a9efab9ed9fe2ed526a2ff 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -17,10 +17,6 @@ import { PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, } from "@t3tools/contracts"; -import { - connectionStatusText, - type EnvironmentConnectionPresentation, -} from "@t3tools/client-runtime/connection"; import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model"; import { @@ -441,7 +437,7 @@ export interface ChatComposerProps { isPreparingWorktree: boolean; environmentUnavailable: { readonly label: string; - readonly connection: EnvironmentConnectionPresentation; + readonly connectionState: "connecting" | "disconnected" | "error"; } | null; // Pending approvals / inputs @@ -2354,9 +2350,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : showPlanFollowUpPrompt && activeProposedPlan ? "Add feedback to refine the plan, or leave this blank to implement it" : environmentUnavailable - ? `${environmentUnavailable.label}: ${connectionStatusText( - environmentUnavailable.connection, - )}` + ? `${environmentUnavailable.label} is ${ + environmentUnavailable.connectionState === "connecting" + ? "connecting" + : "disconnected" + }` : phase === "disconnected" ? "Ask for follow-up changes or attach images" : "Ask anything, @tag files/folders, $use skills, or / for commands" diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 41b213f2b21a411d08acc1cd6a9851d2ad06d909..2110b53d604e7b234a16fb8d0d739623ca369368 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -10,7 +10,6 @@ import { memo } from "react"; import GitActionsControl from "../GitActionsControl"; import { type DraftId } from "~/composerDraftStore"; import { DiffIcon, Globe, TerminalSquareIcon } from "lucide-react"; -import { Badge } from "../ui/badge"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl"; import { Toggle } from "../ui/toggle"; diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 1267e935bb2fd838ec56f8464357be2c8d43092f..6629e036866d7e1879f51bcc3ed1079ceacedb12 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -470,9 +470,9 @@ function UserTimelineRow({ row }: { row: Extract 0 ? (
- {elementContextState.contexts.map((context, index) => ( + {elementContextState.contexts.map((context) => ( ))} @@ -579,10 +579,16 @@ function AssistantTimelineRow({ row }: { row: Extract} > - {formatShortTimestamp(row.message.updatedAt, ctx.timestampFormat)} + {formatShortTimestamp( + row.message.completedAt ?? row.message.createdAt, + ctx.timestampFormat, + )} - {formatChatTimestampTooltip(row.message.updatedAt, ctx.timestampFormat)} + {formatChatTimestampTooltip( + row.message.completedAt ?? row.message.createdAt, + ctx.timestampFormat, + )} )} diff --git a/apps/web/src/components/preview/useDiscoveredLocalServers.test.ts b/apps/web/src/components/preview/useDiscoveredLocalServers.test.ts index 43bd879d03293a39cdc71492906dd84337e80631..ada9153b4b613fd9e4a94cf227c4a3eaa0454239 100644 --- a/apps/web/src/components/preview/useDiscoveredLocalServers.test.ts +++ b/apps/web/src/components/preview/useDiscoveredLocalServers.test.ts @@ -1,5 +1,5 @@ import type { DiscoveredLocalServer } from "@t3tools/contracts"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { mergeServers, type PreviewableServer } from "./useDiscoveredLocalServers"; diff --git a/apps/web/src/environments/runtime/service.threadSubscriptions.test.ts b/apps/web/src/environments/runtime/service.threadSubscriptions.test.ts index 675a48680328d0610311075906c8dd3cc4f90547..b1fe715f491862ddf5d87c5a78bfcaf47bf8c843 100644 --- a/apps/web/src/environments/runtime/service.threadSubscriptions.test.ts +++ b/apps/web/src/environments/runtime/service.threadSubscriptions.test.ts @@ -104,6 +104,16 @@ vi.mock("@t3tools/client-runtime", async (importOriginal) => { onEvent: vi.fn(() => () => undefined), onMetadata: vi.fn(() => () => undefined), }, + preview: { + open: vi.fn(), + navigate: vi.fn(), + refresh: vi.fn(), + close: vi.fn(), + list: vi.fn(), + reportStatus: vi.fn(), + onEvent: vi.fn(() => () => undefined), + subscribePorts: vi.fn(() => () => undefined), + }, projects: { searchEntries: vi.fn(), writeFile: vi.fn(), diff --git a/apps/web/src/lib/elementContext.test.ts b/apps/web/src/lib/elementContext.test.ts index d3fa3f2215d8d48093b31be052dbcf5952ff8217..a8c740741eb25570bf3eec28a0a6a2aa773268af 100644 --- a/apps/web/src/lib/elementContext.test.ts +++ b/apps/web/src/lib/elementContext.test.ts @@ -1,5 +1,5 @@ import type { PickedElementPayload } from "@t3tools/contracts"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { appendElementContextsToPrompt, diff --git a/apps/web/src/lib/elementContext.ts b/apps/web/src/lib/elementContext.ts index b5f7d9342f7859beaa0fb47a044ceafc01c4859a..8064db710794fe15e5a5ca08523eaafc4a621972 100644 --- a/apps/web/src/lib/elementContext.ts +++ b/apps/web/src/lib/elementContext.ts @@ -198,12 +198,11 @@ export function appendElementContextsToPrompt( } const ELEMENT_CONTEXT_ID_PREFIX = "el_"; +let nextElementContextSequence = 0; export function newElementContextId(): string { - if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { - return `${ELEMENT_CONTEXT_ID_PREFIX}${crypto.randomUUID()}`; - } - return `${ELEMENT_CONTEXT_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`; + nextElementContextSequence += 1; + return `${ELEMENT_CONTEXT_ID_PREFIX}${nextElementContextSequence.toString(36)}`; } /** diff --git a/apps/web/src/previewStateStore.test.ts b/apps/web/src/previewStateStore.test.ts index 39dc7adee16215d0d36c30aaf6fc80f2e9e27daa..eb7c998109b5233676005808d165c24a2afc0971 100644 --- a/apps/web/src/previewStateStore.test.ts +++ b/apps/web/src/previewStateStore.test.ts @@ -1,6 +1,6 @@ import { scopeThreadRef } from "@t3tools/client-runtime"; import { type EnvironmentId, type PreviewSessionSnapshot, ThreadId } from "@t3tools/contracts"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vite-plus/test"; import { __testing, selectThreadPreviewState, usePreviewStateStore } from "./previewStateStore"; diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index 9074ff49d02a88ad9f909156a1fb56178392eaab..7865c57f8e71d7965f4bdb9982dccf18f6126bb0 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -1,6 +1,6 @@ import { scopeThreadRef } from "@t3tools/client-runtime"; import { type EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vite-plus/test"; import { selectActiveRightPanel, diff --git a/packages/contracts/src/preview.test.ts b/packages/contracts/src/preview.test.ts index d1f108ab4e9fcc8a0f7e511475f6a68bafc7b538..c11af1c89e1fba04823311f4fb2af56c1e3776ea 100644 --- a/packages/contracts/src/preview.test.ts +++ b/packages/contracts/src/preview.test.ts @@ -1,5 +1,5 @@ import { Schema } from "effect"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { DiscoveredLocalServer, diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index e8cba6ebe78e8375263e9251859ab6b15280ee75..e3d462bb82a9e803a4e7bbbc54f8462839fffe16 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -480,38 +480,40 @@ export const WsTerminalCloseRpc = Rpc.make(WS_METHODS.terminalClose, { export const WsPreviewOpenRpc = Rpc.make(WS_METHODS.previewOpen, { payload: PreviewOpenInput, success: PreviewSessionSnapshot, - error: PreviewError, + error: Schema.Union([PreviewError, EnvironmentAuthorizationError]), }); export const WsPreviewNavigateRpc = Rpc.make(WS_METHODS.previewNavigate, { payload: PreviewNavigateInput, success: PreviewSessionSnapshot, - error: PreviewError, + error: Schema.Union([PreviewError, EnvironmentAuthorizationError]), }); export const WsPreviewRefreshRpc = Rpc.make(WS_METHODS.previewRefresh, { payload: PreviewRefreshInput, - error: PreviewError, + error: Schema.Union([PreviewError, EnvironmentAuthorizationError]), }); export const WsPreviewCloseRpc = Rpc.make(WS_METHODS.previewClose, { payload: PreviewCloseInput, - error: PreviewError, + error: Schema.Union([PreviewError, EnvironmentAuthorizationError]), }); export const WsPreviewListRpc = Rpc.make(WS_METHODS.previewList, { payload: PreviewListInput, success: PreviewListResult, + error: EnvironmentAuthorizationError, }); export const WsPreviewReportStatusRpc = Rpc.make(WS_METHODS.previewReportStatus, { payload: PreviewReportStatusInput, - error: PreviewError, + error: Schema.Union([PreviewError, EnvironmentAuthorizationError]), }); export const WsSubscribePreviewEventsRpc = Rpc.make(WS_METHODS.subscribePreviewEvents, { payload: Schema.Struct({}), success: PreviewEvent, + error: EnvironmentAuthorizationError, stream: true, }); @@ -520,6 +522,7 @@ export const WsSubscribeDiscoveredLocalServersRpc = Rpc.make( { payload: Schema.Struct({}), success: DiscoveredLocalServerList, + error: EnvironmentAuthorizationError, stream: true, }, ); diff --git a/packages/shared/src/preview.test.ts b/packages/shared/src/preview.test.ts index e9b7217247781e8a03e39ba189bc507d6f8e186b..6030686d3ed90b5f905b8985464ce17179bb6517 100644 --- a/packages/shared/src/preview.test.ts +++ b/packages/shared/src/preview.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vite-plus/test"; import { isLoopbackHost, diff --git a/packages/shared/src/preview.ts b/packages/shared/src/preview.ts index 963023f0ee3f37c8e02193195fe99e0b9dbf35ef..cc5a765ddcb7249db5ef42128d39ac0a5dfa1805 100644 --- a/packages/shared/src/preview.ts +++ b/packages/shared/src/preview.ts @@ -5,16 +5,15 @@ */ const TAB_ID_PREFIX = "tab_"; +let nextPreviewTabSequence = 0; /** * Generate a fresh preview tab id. Lives in shared (not contracts) because * the contracts package is schema-only — runtime helpers belong here. */ export function newPreviewTabId(): string { - if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { - return `${TAB_ID_PREFIX}${crypto.randomUUID()}`; - } - return `${TAB_ID_PREFIX}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`; + nextPreviewTabSequence += 1; + return `${TAB_ID_PREFIX}${nextPreviewTabSequence.toString(36)}`; } const LOOPBACK_HOSTS: ReadonlySet = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96d563a2fd53fc0651ee18ee1cbb99e25c1b9d24..d7644d97d0da14618032a09518366436cc7745df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: electron-updater: specifier: ^6.6.2 version: 6.8.3 + react-grab: + specifier: ^0.1.32 + version: 0.1.44(react@19.2.6) devDependencies: '@effect/vitest': specifier: 4.0.0-beta.78 @@ -580,6 +583,9 @@ importers: playwright: specifier: ^1.58.2 version: 1.60.0 + react-grab: + specifier: ^0.1.32 + version: 0.1.44(react@19.2.6) tailwindcss: specifier: ^4.0.0 version: 4.3.0 @@ -2395,6 +2401,9 @@ packages: peerDependencies: hono: ^4 + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -3432,6 +3441,10 @@ packages: '@types/react': optional: true + '@react-grab/cli@0.1.44': + resolution: {integrity: sha512-gMDYY2rw6OWajCcDlXSIgs2LC432YJXSb3Lm5yM187uhRgBYddoEVULi36h+IolX3r7jSb3ew7vn9FfI8NSo0A==} + hasBin: true + '@react-native-masked-view/masked-view@0.3.2': resolution: {integrity: sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==} peerDependencies: @@ -4787,6 +4800,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-install@0.0.5: + resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==} + hasBin: true + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -5054,6 +5071,11 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + bippy@0.5.41: + resolution: {integrity: sha512-jCP2pXXLhXqPrAN+iSEFZmLI4uUM4fjSqajh0K+TmM062VehfDT3ZJNkrTGyN701Z5XMejs9qAudSqkMGhSMKg==} + peerDependencies: + react: '>=17.0.1' + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -5244,10 +5266,18 @@ packages: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -5321,6 +5351,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -6604,6 +6638,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@1.2.1: resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} engines: {node: '>=16.x'} @@ -6725,6 +6763,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} @@ -6742,6 +6784,10 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -7154,6 +7200,10 @@ packages: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} engines: {node: '>=4'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -7484,6 +7534,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -7729,6 +7783,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -7743,6 +7801,10 @@ packages: resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} engines: {node: '>=6'} + ora@9.4.0: + resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} + engines: {node: '>=20'} + os-paths@7.4.0: resolution: {integrity: sha512-Ux1J4NUqC6tZayBqLN1kUlDAEvLiQlli/53sSddU4IN+h+3xxnv2HmRSMpVSvr1hvJzotfMs3ERvETGK+f4OwA==} engines: {node: '>= 4.0'} @@ -8136,6 +8198,15 @@ packages: peerDependencies: react: '>=17.0.0' + react-grab@0.1.44: + resolution: {integrity: sha512-bDEwBdI90ljq2lhUtPqmWis/HwYB/CvfT0m5i+P9F83Pt0Ot8o9XL8v00s9jcWzdQUlsFDzmq2FO2CHUe8JY8A==} + hasBin: true + peerDependencies: + react: '>=17.0.0' + peerDependenciesMeta: + react: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -8454,6 +8525,10 @@ packages: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -8757,6 +8832,10 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + stream-buffers@2.2.0: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} @@ -9631,6 +9710,10 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -11621,6 +11704,8 @@ snapshots: dependencies: hono: 4.12.25 + '@iarna/toml@2.2.5': {} + '@img/colour@1.1.0': optional: true @@ -12577,6 +12662,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 + '@react-grab/cli@0.1.44': + dependencies: + agent-install: 0.0.5 + commander: 14.0.3 + ignore: 7.0.5 + ora: 9.4.0 + package-manager-detector: 1.6.0 + picocolors: 1.1.1 + prompts: 2.4.2 + tinyexec: 1.2.4 + '@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)': dependencies: react: 19.2.3 @@ -13796,6 +13892,15 @@ snapshots: agent-base@7.1.4: {} + agent-install@0.0.5: + dependencies: + '@iarna/toml': 2.2.5 + commander: 14.0.3 + jsonc-parser: 3.3.1 + picocolors: 1.1.1 + prompts: 2.4.2 + yaml: 2.9.0 + ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -14229,6 +14334,10 @@ snapshots: big-integer@1.6.52: {} + bippy@0.5.41(react@19.2.6): + dependencies: + react: 19.2.6 + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -14451,8 +14560,14 @@ snapshots: dependencies: restore-cursor: 4.0.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -14520,6 +14635,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@5.1.0: {} @@ -15980,6 +16097,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-size@1.2.1: dependencies: queue: 6.0.2 @@ -16099,6 +16218,8 @@ snapshots: dependencies: is-docker: 3.0.0 + is-interactive@2.0.0: {} + is-node-process@1.2.0: {} is-number@7.0.0: {} @@ -16109,6 +16230,8 @@ snapshots: is-property@1.0.2: {} + is-unicode-supported@2.1.0: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -16442,6 +16565,11 @@ snapshots: dependencies: chalk: 2.4.2 + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + long@5.3.2: {} longest-streak@3.1.0: {} @@ -17071,6 +17199,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@1.0.1: {} mimic-response@3.1.0: {} @@ -17308,6 +17438,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -17330,6 +17464,17 @@ snapshots: strip-ansi: 5.2.0 wcwidth: 1.0.1 + ora@9.4.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.1 + os-paths@7.4.0: optionalDependencies: fsevents: 2.3.3 @@ -17721,6 +17866,13 @@ snapshots: dependencies: react: 19.2.3 + react-grab@0.1.44(react@19.2.6): + dependencies: + '@react-grab/cli': 0.1.44 + bippy: 0.5.41(react@19.2.6) + optionalDependencies: + react: 19.2.6 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -18122,6 +18274,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -18563,6 +18720,8 @@ snapshots: std-env@4.1.0: {} + stdin-discarder@0.3.2: {} + stream-buffers@2.2.0: {} strict-event-emitter@0.5.1: {} @@ -19377,6 +19536,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} zod-to-json-schema@3.25.2(zod@4.4.3): -- 2.54.0