diff --git a/.kilocode/skills/create-view/SKILL.md b/.kilocode/skills/create-view/SKILL.md index 70506e1ac6..f39b1ce0d8 100644 --- a/.kilocode/skills/create-view/SKILL.md +++ b/.kilocode/skills/create-view/SKILL.md @@ -110,7 +110,8 @@ Create a new file for your view model (e.g., `frontend/app/view/myview/myview-mo ```typescript import { BlockNodeModel } from "@/app/block/blocktypes"; -import { WOS, globalStore, useBlockAtom } from "@/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; +import { WOS, useBlockAtom } from "@/store/global"; import * as jotai from "jotai"; import { MyView } from "./myview"; diff --git a/aiprompts/newview.md b/aiprompts/newview.md index d12bfca937..ddb2da57fc 100644 --- a/aiprompts/newview.md +++ b/aiprompts/newview.md @@ -104,7 +104,8 @@ Create a new file for your view model (e.g., `frontend/app/view/myview/myview-mo ```typescript import { BlockNodeModel } from "@/app/block/blocktypes"; -import { WOS, globalStore, useBlockAtom } from "@/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; +import { WOS, useBlockAtom } from "@/store/global"; import * as jotai from "jotai"; import { MyView } from "./myview"; diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 15b09785ef..581b8c1a9f 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -9,13 +9,14 @@ import { } from "@/app/store/badge"; import { ClientModel } from "@/app/store/client-model"; import { GlobalModel } from "@/app/store/global-model"; +import { globalStore } from "@/app/store/jotaiStore"; import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; import { WaveEnvContext } from "@/app/waveenv/waveenv"; import { makeWaveEnvImpl } from "@/app/waveenv/waveenvimpl"; import { Workspace } from "@/app/workspace/workspace"; import { getLayoutModelForStaticTab } from "@/layout/index"; import { ContextMenuModel } from "@/store/contextmenu"; -import { atoms, createBlock, getSettingsPrefixAtom, globalStore } from "@/store/global"; +import { atoms, createBlock, getSettingsPrefixAtom } from "@/store/global"; import { appHandleKeyDown, keyboardMouseDownHandler } from "@/store/keymodel"; import { getElemAsStr } from "@/util/focusutil"; import * as keyutil from "@/util/keyutil"; diff --git a/frontend/app/modals/modalsrenderer.tsx b/frontend/app/modals/modalsrenderer.tsx index 218d777831..ebb67899b8 100644 --- a/frontend/app/modals/modalsrenderer.tsx +++ b/frontend/app/modals/modalsrenderer.tsx @@ -5,7 +5,8 @@ import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade"; import { ClientModel } from "@/app/store/client-model"; -import { atoms, globalPrimaryTabStartup, globalStore } from "@/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; +import { atoms, globalPrimaryTabStartup } from "@/store/global"; import { modalsModel } from "@/store/modalmodel"; import * as jotai from "jotai"; import { useEffect } from "react"; diff --git a/frontend/app/view/aifilediff/aifilediff.tsx b/frontend/app/view/aifilediff/aifilediff.tsx index f7ca817068..9d96d290c2 100644 --- a/frontend/app/view/aifilediff/aifilediff.tsx +++ b/frontend/app/view/aifilediff/aifilediff.tsx @@ -6,7 +6,7 @@ import type { TabModel } from "@/app/store/tab-model"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { DiffViewer } from "@/app/view/codeeditor/diffviewer"; import type { WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; -import { globalStore, WOS } from "@/store/global"; +import { globalStore } from "@/store/jotaiStore"; import { base64ToString } from "@/util/util"; import * as jotai from "jotai"; import { useEffect } from "react"; diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index f63e399bba..2961771fa3 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -1,14 +1,14 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { globalStore } from "@/app/store/jotaiStore"; import { tryReinjectKey } from "@/app/store/keymodel"; import { CodeEditor } from "@/app/view/codeeditor/codeeditor"; -import { globalStore } from "@/store/global"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget } from "@/util/util"; import { useAtomValue, useSetAtom } from "jotai"; -import * as monaco from "monaco-editor"; import type * as MonacoTypes from "monaco-editor"; +import * as monaco from "monaco-editor"; import { useEffect } from "react"; import type { SpecializedViewProps } from "./preview"; diff --git a/frontend/app/view/preview/preview-markdown.tsx b/frontend/app/view/preview/preview-markdown.tsx index 22bba88880..d47248d457 100644 --- a/frontend/app/view/preview/preview-markdown.tsx +++ b/frontend/app/view/preview/preview-markdown.tsx @@ -1,8 +1,9 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { globalStore } from "@/app/store/jotaiStore"; import { Markdown } from "@/element/markdown"; -import { getOverrideConfigAtom, globalStore } from "@/store/global"; +import { getOverrideConfigAtom } from "@/store/global"; import { useAtomValue } from "jotai"; import { useEffect, useMemo } from "react"; import type { SpecializedViewProps } from "./preview"; diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index c523560db8..8315e48b2a 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -3,9 +3,10 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import { ContextMenuModel } from "@/app/store/contextmenu"; +import { globalStore } from "@/app/store/jotaiStore"; import type { TabModel } from "@/app/store/tab-model"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { getOverrideConfigAtom, globalStore, refocusNode } from "@/store/global"; +import { getOverrideConfigAtom, refocusNode } from "@/store/global"; import * as WOS from "@/store/wos"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil"; import { checkKeyPressed } from "@/util/keyutil"; diff --git a/frontend/app/view/preview/preview-streaming.tsx b/frontend/app/view/preview/preview-streaming.tsx index 408da54de4..55e0ee07f3 100644 --- a/frontend/app/view/preview/preview-streaming.tsx +++ b/frontend/app/view/preview/preview-streaming.tsx @@ -3,7 +3,7 @@ import { Button } from "@/app/element/button"; import { CenteredDiv } from "@/app/element/quickelems"; -import { globalStore } from "@/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; import { getWebServerEndpoint } from "@/util/endpoints"; import { formatRemoteUri } from "@/util/waveutil"; import { useAtomValue } from "jotai"; diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 33188ae5b5..87cf44678a 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { CenteredDiv } from "@/app/element/quickelems"; +import { globalStore } from "@/app/store/jotaiStore"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion"; import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { globalStore } from "@/store/global"; import { isBlank, makeConnRoute } from "@/util/util"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { memo, useEffect } from "react"; diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index b167688907..33e4759134 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -6,12 +6,13 @@ import type { BlockNodeModel } from "@/app/block/blocktypes"; import { NullErrorBoundary } from "@/app/element/errorboundary"; import { Search, useSearch } from "@/app/element/search"; import { ContextMenuModel } from "@/app/store/contextmenu"; +import { globalStore } from "@/app/store/jotaiStore"; import { useTabModel } from "@/app/store/tab-model"; import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; -import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; +import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, WOS } from "@/store/global"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import { ISearchOptions } from "@xterm/addon-search"; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 630f047265..c71d012a61 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -6,13 +6,14 @@ import { Button } from "@/app/element/button"; import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; import { ClientModel } from "@/app/store/client-model"; +import { globalStore } from "@/app/store/jotaiStore"; import type { TabModel } from "@/app/store/tab-model"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; -import { atoms, createBlock, fetchWaveFile, getApi, globalStore, WOS } from "@/store/global"; +import { atoms, createBlock, fetchWaveFile, getApi, WOS } from "@/store/global"; import { BlockService, ObjectService } from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget, isBlank, makeIconClass, mergeMeta } from "@/util/util"; diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 116d0ef0b3..39369e4daf 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -3,6 +3,7 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import { Search, useSearch } from "@/app/element/search"; +import { globalStore } from "@/app/store/jotaiStore"; import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; import type { TabModel } from "@/app/store/tab-model"; import { makeORef } from "@/app/store/wos"; @@ -14,7 +15,7 @@ import { } from "@/app/suggestion/suggestion"; import { MockBoundary } from "@/app/waveenv/mockboundary"; import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { globalStore, openLink } from "@/store/global"; +import { openLink } from "@/store/global"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import clsx from "clsx"; diff --git a/frontend/builder/app-selection-modal.tsx b/frontend/builder/app-selection-modal.tsx index 4e49b2f35d..c98133dc43 100644 --- a/frontend/builder/app-selection-modal.tsx +++ b/frontend/builder/app-selection-modal.tsx @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { FlexiModal } from "@/app/modals/modal"; +import { globalStore } from "@/app/store/jotaiStore"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms, getApi, globalStore } from "@/store/global"; +import { atoms, getApi } from "@/store/global"; import * as WOS from "@/store/wos"; import { formatRelativeTime } from "@/util/util"; import { useEffect, useState } from "react"; diff --git a/frontend/builder/builder-app.tsx b/frontend/builder/builder-app.tsx index e72a0578be..5f78a6b9a7 100644 --- a/frontend/builder/builder-app.tsx +++ b/frontend/builder/builder-app.tsx @@ -1,10 +1,11 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { ModalsRenderer } from "@/app/modals/modalsrenderer"; +import { globalStore } from "@/app/store/jotaiStore"; import { AppSelectionModal } from "@/builder/app-selection-modal"; import { BuilderWorkspace } from "@/builder/builder-workspace"; -import { ModalsRenderer } from "@/app/modals/modalsrenderer"; -import { atoms, globalStore, isDev } from "@/store/global"; +import { atoms, isDev } from "@/store/global"; import { appHandleKeyDown } from "@/store/keymodel"; import * as keyutil from "@/util/keyutil"; import { isBlank } from "@/util/util"; diff --git a/frontend/preview/mock/mock-node-model.ts b/frontend/preview/mock/mock-node-model.ts new file mode 100644 index 0000000000..009d25ea2b --- /dev/null +++ b/frontend/preview/mock/mock-node-model.ts @@ -0,0 +1,45 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { globalStore } from "@/app/store/jotaiStore"; +import type { NodeModel } from "@/layout/index"; +import { atom } from "jotai"; + +export type MockNodeModelOpts = { + nodeId: string; + blockId: string; + innerRect?: { width: string; height: string }; + numLeafs?: number; +}; + +export function makeMockNodeModel(opts: MockNodeModelOpts): NodeModel { + const isFocusedAtom = atom(true); + const isMagnifiedAtom = atom(false); + + return { + additionalProps: atom({} as any), + innerRect: atom(opts.innerRect ?? { width: "1000px", height: "640px" }), + blockNum: atom(1), + numLeafs: atom(opts.numLeafs ?? 1), + nodeId: opts.nodeId, + blockId: opts.blockId, + addEphemeralNodeToLayout: () => {}, + animationTimeS: atom(0), + isResizing: atom(false), + isFocused: isFocusedAtom, + isMagnified: isMagnifiedAtom, + anyMagnified: atom((get) => get(isMagnifiedAtom)), + isEphemeral: atom(false), + ready: atom(true), + disablePointerEvents: atom(false), + toggleMagnify: () => { + globalStore.set(isMagnifiedAtom, !globalStore.get(isMagnifiedAtom)); + }, + focusNode: () => { + globalStore.set(isFocusedAtom, true); + }, + onClose: () => {}, + dragHandleRef: { current: null }, + displayContainerRef: { current: null }, + }; +} diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 9cffa943e2..faaf6cdde7 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -9,11 +9,19 @@ import { RpcApiType } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil"; import { Atom, atom, PrimitiveAtom, useAtomValue } from "jotai"; +import { showPreviewContextMenu } from "../preview-contextmenu"; +import { MockSysinfoConnection } from "../previews/sysinfo.preview-util"; import { DefaultFullConfig } from "./defaultconfig"; import { DefaultMockFilesystem } from "./mockfilesystem"; -import { showPreviewContextMenu } from "../preview-contextmenu"; import { previewElectronApi } from "./preview-electron-api"; +export const PreviewTabId = crypto.randomUUID(); +export const PreviewWindowId = crypto.randomUUID(); +export const PreviewWorkspaceId = crypto.randomUUID(); +export const PreviewClientId = crypto.randomUUID(); +export const WebBlockId = crypto.randomUUID(); +export const SysinfoBlockId = crypto.randomUUID(); + // What works "out of the box" in the mock environment (no MockEnv overrides needed): // // RPC calls (handled in makeMockRpc): @@ -31,17 +39,23 @@ import { previewElectronApi } from "./preview-electron-api"; // - rpc.UpdateWorkspaceTabIdsCommand -- updates .tabids on the Workspace WaveObj in the mock WOS // // Any other RPC call falls through to a console.log and resolves null. -// Override specific calls via MockEnv.rpc (keys are the Command method names, e.g. "GetMetaCommand"). +// Override specific calls via MockEnv.rpc (keys are Command method names, e.g. "GetMetaCommand"). +// Override specific streaming calls via MockEnv.rpcStreaming (same key names, handler returns AsyncGenerator). // // Backend service calls (handled in callBackendService): // Any call falls through to a console.log and resolves null. // Override specific calls via MockEnv.services: { Service: { Method: impl } } // e.g. { "block": { "GetControllerStatus": (blockId) => myStatus } } -type RpcOverrides = { - [K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: ( - ...args: any[] - ) => Promise | AsyncGenerator; +export type RpcHandlerType = (...args: any[]) => Promise; +export type RpcStreamHandlerType = (...args: any[]) => AsyncGenerator; + +export type RpcOverrides = { + [K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: RpcHandlerType; +}; + +export type RpcStreamOverrides = { + [K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: RpcStreamHandlerType; }; type ServiceOverrides = { @@ -56,6 +70,7 @@ export type MockEnv = { platform?: NodeJS.Platform; settings?: Partial; rpc?: RpcOverrides; + rpcStreaming?: RpcStreamOverrides; services?: ServiceOverrides; atoms?: Partial; electron?: Partial; @@ -65,7 +80,11 @@ export type MockEnv = { mockWaveObjs?: Record; }; -export type MockWaveEnv = WaveEnv & { mockEnv: MockEnv }; +export type MockWaveEnv = WaveEnv & { + mockEnv: MockEnv; + addRpcOverride: (command: K, handler: RpcHandlerType) => void; + addRpcStreamOverride: (command: K, handler: RpcStreamHandlerType) => void; +}; function mergeRecords(base: Record, overrides: Record): Record { if (base == null && overrides == null) { @@ -91,6 +110,7 @@ export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv { platform: overrides.platform ?? base.platform, settings: mergeRecords(base.settings, overrides.settings), rpc: mergeRecords(base.rpc as any, overrides.rpc as any) as RpcOverrides, + rpcStreaming: mergeRecords(base.rpcStreaming as any, overrides.rpcStreaming as any) as RpcStreamOverrides, services: mergedServices, atoms: overrides.atoms != null || base.atoms != null ? { ...base.atoms, ...overrides.atoms } : undefined, electron: @@ -108,7 +128,10 @@ function makeMockSettingsKeyAtom(settingsAtom: Atom): WaveEnv["get const keyAtomCache = new Map>(); return (key: T) => { if (!keyAtomCache.has(key)) { - keyAtomCache.set(key, atom((get) => get(settingsAtom)?.[key])); + keyAtomCache.set( + key, + atom((get) => get(settingsAtom)?.[key]) + ); } return keyAtomCache.get(key) as Atom; }; @@ -180,7 +203,15 @@ type MockWosFns = { platform: NodeJS.Platform; }; -export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiType { +export function makeMockRpc( + overrides: RpcOverrides, + streamOverrides: RpcStreamOverrides, + wos: MockWosFns +): { + rpc: RpcApiType; + setRpcHandler: (command: string, fn: RpcHandlerType) => void; + setRpcStreamHandler: (command: string, fn: RpcStreamHandlerType) => void; +} { const callDispatchMap = new Map Promise>(); const streamDispatchMap = new Map AsyncGenerator>(); const secrets = new Map(); @@ -288,11 +319,13 @@ export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiTyp if (overrides) { for (const key of Object.keys(overrides) as (keyof RpcOverrides)[]) { const cmdName = key.slice(0, -"Command".length).toLowerCase(); - if (cmdName === "filereadstream" || cmdName === "fileliststream") { - setStreamHandler(cmdName, overrides[key] as (...args: any[]) => AsyncGenerator); - } else { - setCallHandler(cmdName, overrides[key] as (...args: any[]) => Promise); - } + setCallHandler(cmdName, overrides[key] as RpcHandlerType); + } + } + if (streamOverrides) { + for (const key of Object.keys(streamOverrides) as (keyof RpcStreamOverrides)[]) { + const cmdName = key.slice(0, -"Command".length).toLowerCase(); + setStreamHandler(cmdName, streamOverrides[key] as RpcStreamHandlerType); } } const rpc = new RpcApiType(); @@ -320,7 +353,17 @@ export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiTyp yield null; }, }); - return rpc; + return { + rpc, + setRpcHandler: (command: string, fn: RpcHandlerType) => { + const cmdName = command.endsWith("Command") ? command.slice(0, -"Command".length).toLowerCase() : command; + setCallHandler(cmdName, fn); + }, + setRpcStreamHandler: (command: string, fn: RpcStreamHandlerType) => { + const cmdName = command.endsWith("Command") ? command.slice(0, -"Command".length).toLowerCase() : command; + setStreamHandler(cmdName, fn); + }, + }; } export function applyMockEnvOverrides(env: WaveEnv, newOverrides: MockEnv): MockWaveEnv { @@ -331,7 +374,57 @@ export function applyMockEnvOverrides(env: WaveEnv, newOverrides: MockEnv): Mock export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const overrides: MockEnv = mockEnv ?? {}; - const platform = overrides.platform ?? PlatformMacOS; + const tabId = overrides.tabId ?? PreviewTabId; + const defaultMockWaveObjs: Record = { + [`workspace:${PreviewWorkspaceId}`]: { + otype: "workspace", + oid: PreviewWorkspaceId, + version: 1, + name: "Preview Workspace", + tabids: [PreviewTabId], + activetabid: PreviewTabId, + meta: {}, + } as Workspace, + [`tab:${PreviewTabId}`]: { + otype: "tab", + oid: PreviewTabId, + version: 1, + name: "Preview Tab", + blockids: [WebBlockId, SysinfoBlockId], + meta: {}, + } as Tab, + [`block:${WebBlockId}`]: { + otype: "block", + oid: WebBlockId, + version: 1, + meta: { + view: "web", + }, + } as Block, + [`block:${SysinfoBlockId}`]: { + otype: "block", + oid: SysinfoBlockId, + version: 1, + meta: { + view: "sysinfo", + connection: MockSysinfoConnection, + "sysinfo:type": "CPU + Mem", + "graph:numpoints": 90, + }, + } as Block, + }; + const defaultAtoms: Partial = { + uiContext: atom({ windowid: PreviewWindowId, activetabid: PreviewTabId } as UIContext), + staticTabId: atom(PreviewTabId), + workspaceId: atom(PreviewWorkspaceId), + }; + const mergedOverrides: MockEnv = { + ...overrides, + tabId, + mockWaveObjs: { ...defaultMockWaveObjs, ...(overrides.mockWaveObjs ?? {}) }, + atoms: { ...defaultAtoms, ...(overrides.atoms ?? {}) }, + }; + const platform = mergedOverrides.platform ?? PlatformMacOS; const connStatusAtomCache = new Map>(); const waveObjectValueAtomCache = new Map>(); const waveObjectDerivedAtomCache = new Map>(); @@ -339,12 +432,17 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const connConfigKeyAtomCache = new Map>(); const getWaveObjectAtom = (oref: string): PrimitiveAtom => { if (!waveObjectValueAtomCache.has(oref)) { - const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; + const obj = (mergedOverrides.mockWaveObjs?.[oref] ?? null) as T; waveObjectValueAtomCache.set(oref, atom(obj) as PrimitiveAtom); } return waveObjectValueAtomCache.get(oref) as PrimitiveAtom; }; - const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId, getWaveObjectAtom); + const atoms = makeMockGlobalAtoms( + mergedOverrides.settings, + mergedOverrides.atoms, + mergedOverrides.tabId, + getWaveObjectAtom + ); const localHostDisplayNameAtom = atom((get) => { const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; if (configValue != null) { @@ -363,38 +461,55 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { globalStore.set(waveObjectValueAtomCache.get(oref), obj); }, }; + const { rpc, setRpcHandler, setRpcStreamHandler } = makeMockRpc(mergedOverrides.rpc, mergedOverrides.rpcStreaming, mockWosFns); const env = { isMock: true, - mockEnv: overrides, + mockEnv: mergedOverrides, electron: { ...previewElectronApi, getPlatform: () => platform, openExternal: (url: string) => { window.open(url, "_blank"); }, - ...overrides.electron, + ...mergedOverrides.electron, }, - rpc: makeMockRpc(overrides.rpc, mockWosFns), + rpc, atoms, getSettingsKeyAtom: makeMockSettingsKeyAtom(atoms.settingsAtom), platform, - isDev: () => overrides.isDev ?? true, + isDev: () => mergedOverrides.isDev ?? true, isWindows: () => platform === PlatformWindows, isMacOS: () => platform === PlatformMacOS, createBlock: - overrides.createBlock ?? + mergedOverrides.createBlock ?? ((blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => { console.log("[mock createBlock]", blockDef, { magnified, ephemeral }); - return Promise.resolve(crypto.randomUUID()); + const newBlockId = crypto.randomUUID(); + const newBlock: Block = { + otype: "block", + oid: newBlockId, + version: 1, + meta: blockDef.meta ?? {}, + }; + mockWosFns.mockSetWaveObj(`block:${newBlockId}`, newBlock); + const tabORef = `tab:${tabId}`; + const tabAtom = getWaveObjectAtom(tabORef); + const currentTab = globalStore.get(tabAtom); + if (currentTab != null) { + mockWosFns.mockSetWaveObj(tabORef, { + ...currentTab, + blockids: [...(currentTab.blockids ?? []), newBlockId], + }); + } + return Promise.resolve(newBlockId); }), - showContextMenu: - overrides.showContextMenu ?? showPreviewContextMenu, + showContextMenu: mergedOverrides.showContextMenu ?? showPreviewContextMenu, getLocalHostDisplayNameAtom: () => { return localHostDisplayNameAtom; }, getConnStatusAtom: (conn: string) => { if (!connStatusAtomCache.has(conn)) { - const connStatus = overrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn); + const connStatus = mergedOverrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn); connStatusAtomCache.set(conn, atom(connStatus)); } return connStatusAtomCache.get(conn); @@ -449,7 +564,7 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { }, services: null as any, callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => { - const fn = overrides.services?.[service]?.[method]; + const fn = mergedOverrides.services?.[service]?.[method]; if (fn) { return fn(...args); } @@ -458,6 +573,12 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { }, mockSetWaveObj: mockWosFns.mockSetWaveObj, mockModels: new Map(), + addRpcOverride: (command: K, handler: RpcHandlerType) => { + setRpcHandler(command as string, handler); + }, + addRpcStreamOverride: (command: K, handler: RpcStreamHandlerType) => { + setRpcStreamHandler(command as string, handler); + }, } as MockWaveEnv; env.services = Object.fromEntries( Object.entries(AllServiceTypes).map(([key, ServiceClass]) => [key, new ServiceClass(env)]) diff --git a/frontend/preview/mock/use-rpc-override.ts b/frontend/preview/mock/use-rpc-override.ts new file mode 100644 index 0000000000..78dd4c8c01 --- /dev/null +++ b/frontend/preview/mock/use-rpc-override.ts @@ -0,0 +1,24 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import * as React from "react"; +import { MockWaveEnv, RpcHandlerType, RpcOverrides, RpcStreamHandlerType, RpcStreamOverrides } from "./mockwaveenv"; + +export function useRpcOverride(command: K, handler: RpcHandlerType): void { + const mockEnv = useWaveEnv() as MockWaveEnv; + const registeredRef = React.useRef(false); + if (!registeredRef.current) { + registeredRef.current = true; + mockEnv.addRpcOverride(command, handler); + } +} + +export function useRpcStreamOverride(command: K, handler: RpcStreamHandlerType): void { + const mockEnv = useWaveEnv() as MockWaveEnv; + const registeredRef = React.useRef(false); + if (!registeredRef.current) { + registeredRef.current = true; + mockEnv.addRpcStreamOverride(command, handler); + } +} diff --git a/frontend/preview/preview.tsx b/frontend/preview/preview.tsx index 9ec47366a0..032fa2a531 100644 --- a/frontend/preview/preview.tsx +++ b/frontend/preview/preview.tsx @@ -6,12 +6,13 @@ import { ErrorBoundary } from "@/app/element/errorboundary"; import { getAtoms, initGlobalAtoms } from "@/app/store/global-atoms"; import { GlobalModel } from "@/app/store/global-model"; import { globalStore } from "@/app/store/jotaiStore"; +import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; import { WaveEnvContext } from "@/app/waveenv/waveenv"; import { loadFonts } from "@/util/fontutil"; -import { atom, Provider } from "jotai"; +import { Provider } from "jotai"; import React, { lazy, Suspense, useRef } from "react"; import { createRoot } from "react-dom/client"; -import { makeMockWaveEnv } from "./mock/mockwaveenv"; +import { makeMockWaveEnv, PreviewClientId, PreviewTabId, PreviewWindowId } from "./mock/mockwaveenv"; import { installPreviewElectronApi } from "./mock/preview-electron-api"; import { PreviewContextMenu } from "./preview-contextmenu"; @@ -93,22 +94,14 @@ function PreviewHeader({ previewName }: { previewName: string }) { } function PreviewRoot() { - const waveEnvRef = useRef( - makeMockWaveEnv({ - atoms: { - uiContext: atom({ windowid: PreviewWindowId, activetabid: PreviewTabId } as UIContext), - staticTabId: atom(PreviewTabId), - workspaceId: atom(PreviewWorkspaceId), - }, - }) - ); + const waveEnvRef = useRef(makeMockWaveEnv()); return ( - <> + - + ); @@ -150,11 +143,6 @@ function PreviewApp() { return ; } -const PreviewTabId = crypto.randomUUID(); -const PreviewWindowId = crypto.randomUUID(); -const PreviewWorkspaceId = crypto.randomUUID(); -const PreviewClientId = crypto.randomUUID(); - function initPreview() { installPreviewElectronApi(); const initOpts = { diff --git a/frontend/preview/previews/aifilediff.preview.tsx b/frontend/preview/previews/aifilediff.preview.tsx index 3cd0125765..12654cb7cc 100644 --- a/frontend/preview/previews/aifilediff.preview.tsx +++ b/frontend/preview/previews/aifilediff.preview.tsx @@ -2,13 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Block } from "@/app/block/block"; -import { globalStore } from "@/app/store/jotaiStore"; -import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; -import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; -import type { NodeModel } from "@/layout/index"; -import { atom } from "jotai"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import * as React from "react"; -import { applyMockEnvOverrides, MockWaveEnv } from "../mock/mockwaveenv"; +import { makeMockNodeModel } from "../mock/mock-node-model"; +import { useRpcOverride } from "../mock/use-rpc-override"; import { DefaultAiFileDiffChatId, DefaultAiFileDiffFileName, @@ -16,130 +13,51 @@ import { makeMockAiFileDiffResponse, } from "./aifilediff.preview-util"; -const PreviewWorkspaceId = "preview-aifilediff-workspace"; -const PreviewTabId = "preview-aifilediff-tab"; const PreviewNodeId = "preview-aifilediff-node"; -const PreviewBlockId = "preview-aifilediff-block"; -function makeMockWorkspace(): Workspace { - return { - otype: "workspace", - oid: PreviewWorkspaceId, - version: 1, - name: "Preview Workspace", - tabids: [PreviewTabId], - activetabid: PreviewTabId, - meta: {}, - } as Workspace; -} - -function makeMockTab(): Tab { - return { - otype: "tab", - oid: PreviewTabId, - version: 1, - name: "AI File Diff Preview", - blockids: [PreviewBlockId], - meta: {}, - } as Tab; -} - -function makeMockBlock(): Block { - return { - otype: "block", - oid: PreviewBlockId, - version: 1, - meta: { - view: "aifilediff", - file: DefaultAiFileDiffFileName, - "aifilediff:chatid": DefaultAiFileDiffChatId, - "aifilediff:toolcallid": DefaultAiFileDiffToolCallId, - }, - } as Block; -} - -function makePreviewNodeModel(): NodeModel { - const isFocusedAtom = atom(true); - const isMagnifiedAtom = atom(false); - - return { - additionalProps: atom({} as any), - innerRect: atom({ width: "1000px", height: "640px" }), - blockNum: atom(1), - numLeafs: atom(1), - nodeId: PreviewNodeId, - blockId: PreviewBlockId, - addEphemeralNodeToLayout: () => {}, - animationTimeS: atom(0), - isResizing: atom(false), - isFocused: isFocusedAtom, - isMagnified: isMagnifiedAtom, - anyMagnified: atom(false), - isEphemeral: atom(false), - ready: atom(true), - disablePointerEvents: atom(false), - toggleMagnify: () => { - globalStore.set(isMagnifiedAtom, !globalStore.get(isMagnifiedAtom)); - }, - focusNode: () => { - globalStore.set(isFocusedAtom, true); - }, - onClose: () => {}, - dragHandleRef: { current: null }, - displayContainerRef: { current: null }, - }; -} - -function AiFileDiffPreviewInner() { - const baseEnv = useWaveEnv(); - const nodeModel = React.useMemo(() => makePreviewNodeModel(), []); - - const env = React.useMemo(() => { - const mockWaveObjs: Record = { - [`workspace:${PreviewWorkspaceId}`]: makeMockWorkspace(), - [`tab:${PreviewTabId}`]: makeMockTab(), - [`block:${PreviewBlockId}`]: makeMockBlock(), - }; - - return applyMockEnvOverrides(baseEnv, { - tabId: PreviewTabId, - mockWaveObjs, - atoms: { - workspaceId: atom(PreviewWorkspaceId), - staticTabId: atom(PreviewTabId), - }, - rpc: { - WaveAIGetToolDiffCommand: async (_client, data) => { - if ( - data.chatid !== DefaultAiFileDiffChatId || - data.toolcallid !== DefaultAiFileDiffToolCallId - ) { - return null; - } - return makeMockAiFileDiffResponse(); +export function AiFileDiffPreview() { + const env = useWaveEnv(); + const [blockId, setBlockId] = React.useState(null); + + useRpcOverride("WaveAIGetToolDiffCommand", async (_client, data) => { + if (data.chatid !== DefaultAiFileDiffChatId || data.toolcallid !== DefaultAiFileDiffToolCallId) { + return null; + } + return makeMockAiFileDiffResponse(); + }); + + React.useEffect(() => { + env.createBlock( + { + meta: { + view: "aifilediff", + file: DefaultAiFileDiffFileName, + "aifilediff:chatid": DefaultAiFileDiffChatId, + "aifilediff:toolcallid": DefaultAiFileDiffToolCallId, }, }, - }); - }, [baseEnv]); + false, + false + ).then((id) => setBlockId(id)); + }, []); + + const nodeModel = React.useMemo( + () => (blockId != null ? makeMockNodeModel({ nodeId: PreviewNodeId, blockId }) : null), + [blockId] + ); - const tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]); + if (blockId == null || nodeModel == null) { + return null; + } return ( - - -
-
full aifilediff block (mock WOS + mock WaveAI diff RPC)
-
-
- -
-
+
+
full aifilediff block (mock WOS + mock WaveAI diff RPC)
+
+
+
- - +
+
); } - -export function AiFileDiffPreview() { - return ; -} diff --git a/frontend/preview/previews/sysinfo.preview.tsx b/frontend/preview/previews/sysinfo.preview.tsx index ee4fadb9e1..3d0657ece0 100644 --- a/frontend/preview/previews/sysinfo.preview.tsx +++ b/frontend/preview/previews/sysinfo.preview.tsx @@ -2,14 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { Block } from "@/app/block/block"; -import { globalStore } from "@/app/store/jotaiStore"; -import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; import { handleWaveEvent } from "@/app/store/wps"; -import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; -import type { NodeModel } from "@/layout/index"; -import { atom } from "jotai"; import * as React from "react"; -import { applyMockEnvOverrides, MockWaveEnv } from "../mock/mockwaveenv"; +import { makeMockNodeModel } from "../mock/mock-node-model"; +import { SysinfoBlockId } from "../mock/mockwaveenv"; +import { useRpcOverride } from "../mock/use-rpc-override"; import { DefaultSysinfoHistoryPoints, makeMockSysinfoEvent, @@ -17,112 +14,22 @@ import { MockSysinfoConnection, } from "./sysinfo.preview-util"; -const PreviewWorkspaceId = "preview-sysinfo-workspace"; -const PreviewTabId = "preview-sysinfo-tab"; const PreviewNodeId = "preview-sysinfo-node"; -const PreviewBlockId = "preview-sysinfo-block"; -function makeMockWorkspace(): Workspace { - return { - otype: "workspace", - oid: PreviewWorkspaceId, - version: 1, - name: "Preview Workspace", - tabids: [PreviewTabId], - activetabid: PreviewTabId, - meta: {}, - } as Workspace; -} - -function makeMockTab(): Tab { - return { - otype: "tab", - oid: PreviewTabId, - version: 1, - name: "Sysinfo Preview", - blockids: [PreviewBlockId], - meta: {}, - } as Tab; -} - -function makeMockBlock(): Block { - return { - otype: "block", - oid: PreviewBlockId, - version: 1, - meta: { - view: "sysinfo", - connection: MockSysinfoConnection, - "sysinfo:type": "CPU + Mem", - "graph:numpoints": 90, - }, - } as Block; -} - -function makePreviewNodeModel(): NodeModel { - const isFocusedAtom = atom(true); - const isMagnifiedAtom = atom(false); - - return { - additionalProps: atom({} as any), - innerRect: atom({ width: "920px", height: "560px" }), - blockNum: atom(1), - numLeafs: atom(2), - nodeId: PreviewNodeId, - blockId: PreviewBlockId, - addEphemeralNodeToLayout: () => {}, - animationTimeS: atom(0), - isResizing: atom(false), - isFocused: isFocusedAtom, - isMagnified: isMagnifiedAtom, - anyMagnified: atom(false), - isEphemeral: atom(false), - ready: atom(true), - disablePointerEvents: atom(false), - toggleMagnify: () => { - globalStore.set(isMagnifiedAtom, !globalStore.get(isMagnifiedAtom)); - }, - focusNode: () => { - globalStore.set(isFocusedAtom, true); - }, - onClose: () => {}, - dragHandleRef: { current: null }, - displayContainerRef: { current: null }, - }; -} - -function SysinfoPreviewInner() { - const baseEnv = useWaveEnv(); +export default function SysinfoPreview() { const historyRef = React.useRef(makeMockSysinfoHistory()); - const nodeModel = React.useMemo(() => makePreviewNodeModel(), []); - - const env = React.useMemo(() => { - const mockWaveObjs: Record = { - [`workspace:${PreviewWorkspaceId}`]: makeMockWorkspace(), - [`tab:${PreviewTabId}`]: makeMockTab(), - [`block:${PreviewBlockId}`]: makeMockBlock(), - }; - - return applyMockEnvOverrides(baseEnv, { - tabId: PreviewTabId, - mockWaveObjs, - atoms: { - workspaceId: atom(PreviewWorkspaceId), - staticTabId: atom(PreviewTabId), - }, - rpc: { - EventReadHistoryCommand: async (_client, data) => { - if (data.event !== "sysinfo" || data.scope !== MockSysinfoConnection) { - return []; - } - const maxItems = data.maxitems ?? historyRef.current.length; - return historyRef.current.slice(-maxItems); - }, - }, - }); - }, [baseEnv]); + const nodeModel = React.useMemo( + () => makeMockNodeModel({ nodeId: PreviewNodeId, blockId: SysinfoBlockId, innerRect: { width: "920px", height: "560px" }, numLeafs: 2 }), + [] + ); - const tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]); + useRpcOverride("EventReadHistoryCommand", async (_client, data) => { + if (data.event !== "sysinfo" || data.scope !== MockSysinfoConnection) { + return []; + } + const maxItems = data.maxitems ?? historyRef.current.length; + return historyRef.current.slice(-maxItems); + }); React.useEffect(() => { let nextStep = historyRef.current.length; @@ -141,21 +48,13 @@ function SysinfoPreviewInner() { }, []); return ( - - -
-
full sysinfo block (mock WOS + FE-only WPS events)
-
-
- -
-
+
+
full sysinfo block (mock WOS + FE-only WPS events)
+
+
+
- - +
+
); } - -export default function SysinfoPreview() { - return ; -} diff --git a/frontend/preview/previews/web.preview.tsx b/frontend/preview/previews/web.preview.tsx index 27cd91bb6b..c63331a224 100644 --- a/frontend/preview/previews/web.preview.tsx +++ b/frontend/preview/previews/web.preview.tsx @@ -2,134 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 import { Block } from "@/app/block/block"; -import { globalStore } from "@/app/store/jotaiStore"; -import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; -import { mockObjectForPreview } from "@/app/store/wos"; -import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; -import type { NodeModel } from "@/layout/index"; -import { atom } from "jotai"; import * as React from "react"; -import { applyMockEnvOverrides, MockWaveEnv } from "../mock/mockwaveenv"; +import { makeMockNodeModel } from "../mock/mock-node-model"; +import { WebBlockId } from "../mock/mockwaveenv"; -const PreviewWorkspaceId = "preview-web-workspace"; -const PreviewTabId = "preview-web-tab"; const PreviewNodeId = "preview-web-node"; -const PreviewBlockId = "preview-web-block"; -const PreviewUrl = "https://waveterm.dev"; -function makeMockWorkspace(): Workspace { - return { - otype: "workspace", - oid: PreviewWorkspaceId, - version: 1, - name: "Preview Workspace", - tabids: [PreviewTabId], - activetabid: PreviewTabId, - meta: {}, - } as Workspace; -} - -function makeMockTab(): Tab { - return { - otype: "tab", - oid: PreviewTabId, - version: 1, - name: "Web Preview", - blockids: [PreviewBlockId], - meta: {}, - } as Tab; -} - -function makeMockBlock(): Block { - return { - otype: "block", - oid: PreviewBlockId, - version: 1, - meta: { - view: "web", - url: PreviewUrl, - }, - } as Block; -} - -const previewWaveObjs: Record = { - [`workspace:${PreviewWorkspaceId}`]: makeMockWorkspace(), - [`tab:${PreviewTabId}`]: makeMockTab(), - [`block:${PreviewBlockId}`]: makeMockBlock(), -}; - -for (const [oref, obj] of Object.entries(previewWaveObjs)) { - mockObjectForPreview(oref, obj); -} - -function makePreviewNodeModel(): NodeModel { - const isFocusedAtom = atom(true); - const isMagnifiedAtom = atom(false); - - return { - additionalProps: atom({} as any), - innerRect: atom({ width: "1040px", height: "620px" }), - blockNum: atom(1), - numLeafs: atom(1), - nodeId: PreviewNodeId, - blockId: PreviewBlockId, - addEphemeralNodeToLayout: () => {}, - animationTimeS: atom(0), - isResizing: atom(false), - isFocused: isFocusedAtom, - isMagnified: isMagnifiedAtom, - anyMagnified: atom(false), - isEphemeral: atom(false), - ready: atom(true), - disablePointerEvents: atom(false), - toggleMagnify: () => { - globalStore.set(isMagnifiedAtom, !globalStore.get(isMagnifiedAtom)); - }, - focusNode: () => { - globalStore.set(isFocusedAtom, true); - }, - onClose: () => {}, - dragHandleRef: { current: null }, - displayContainerRef: { current: null }, - }; -} - -function WebPreviewInner() { - const baseEnv = useWaveEnv(); - const nodeModel = React.useMemo(() => makePreviewNodeModel(), []); - - const env = React.useMemo(() => { - return applyMockEnvOverrides(baseEnv, { - tabId: PreviewTabId, - mockWaveObjs: previewWaveObjs, - atoms: { - workspaceId: atom(PreviewWorkspaceId), - staticTabId: atom(PreviewTabId), - }, - settings: { - "web:defaultsearch": "https://www.google.com/search?q={query}", - }, - }); - }, [baseEnv]); - - const tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]); +export function WebPreview() { + const nodeModel = React.useMemo( + () => makeMockNodeModel({ nodeId: PreviewNodeId, blockId: WebBlockId, innerRect: { width: "1040px", height: "620px" } }), + [] + ); return ( - - -
-
full web block using preview mock fallback
-
-
- -
-
+
+
full web block using preview mock fallback
+
+
+
- - +
+
); } - -export function WebPreview() { - return ; -}