big simplifications to the preview mocks for rendering blocks (#3082)

This commit is contained in:
Mike Sawka 2026-03-18 18:49:36 -07:00 committed by GitHub
parent c126306da1
commit 4ee003aeeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 338 additions and 440 deletions

View file

@ -110,7 +110,8 @@ Create a new file for your view model (e.g., `frontend/app/view/myview/myview-mo
```typescript ```typescript
import { BlockNodeModel } from "@/app/block/blocktypes"; 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 * as jotai from "jotai";
import { MyView } from "./myview"; import { MyView } from "./myview";

View file

@ -104,7 +104,8 @@ Create a new file for your view model (e.g., `frontend/app/view/myview/myview-mo
```typescript ```typescript
import { BlockNodeModel } from "@/app/block/blocktypes"; 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 * as jotai from "jotai";
import { MyView } from "./myview"; import { MyView } from "./myview";

View file

@ -9,13 +9,14 @@ import {
} from "@/app/store/badge"; } from "@/app/store/badge";
import { ClientModel } from "@/app/store/client-model"; import { ClientModel } from "@/app/store/client-model";
import { GlobalModel } from "@/app/store/global-model"; import { GlobalModel } from "@/app/store/global-model";
import { globalStore } from "@/app/store/jotaiStore";
import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model";
import { WaveEnvContext } from "@/app/waveenv/waveenv"; import { WaveEnvContext } from "@/app/waveenv/waveenv";
import { makeWaveEnvImpl } from "@/app/waveenv/waveenvimpl"; import { makeWaveEnvImpl } from "@/app/waveenv/waveenvimpl";
import { Workspace } from "@/app/workspace/workspace"; import { Workspace } from "@/app/workspace/workspace";
import { getLayoutModelForStaticTab } from "@/layout/index"; import { getLayoutModelForStaticTab } from "@/layout/index";
import { ContextMenuModel } from "@/store/contextmenu"; 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 { appHandleKeyDown, keyboardMouseDownHandler } from "@/store/keymodel";
import { getElemAsStr } from "@/util/focusutil"; import { getElemAsStr } from "@/util/focusutil";
import * as keyutil from "@/util/keyutil"; import * as keyutil from "@/util/keyutil";

View file

@ -5,7 +5,8 @@ import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common";
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade"; import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
import { ClientModel } from "@/app/store/client-model"; 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 { modalsModel } from "@/store/modalmodel";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { useEffect } from "react"; import { useEffect } from "react";

View file

@ -6,7 +6,7 @@ import type { TabModel } from "@/app/store/tab-model";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import { DiffViewer } from "@/app/view/codeeditor/diffviewer"; import { DiffViewer } from "@/app/view/codeeditor/diffviewer";
import type { WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; 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 { base64ToString } from "@/util/util";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { useEffect } from "react"; import { useEffect } from "react";

View file

@ -1,14 +1,14 @@
// Copyright 2025, Command Line Inc. // Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { globalStore } from "@/app/store/jotaiStore";
import { tryReinjectKey } from "@/app/store/keymodel"; import { tryReinjectKey } from "@/app/store/keymodel";
import { CodeEditor } from "@/app/view/codeeditor/codeeditor"; import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
import { globalStore } from "@/store/global";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget } from "@/util/util"; import { fireAndForget } from "@/util/util";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import * as monaco from "monaco-editor";
import type * as MonacoTypes from "monaco-editor"; import type * as MonacoTypes from "monaco-editor";
import * as monaco from "monaco-editor";
import { useEffect } from "react"; import { useEffect } from "react";
import type { SpecializedViewProps } from "./preview"; import type { SpecializedViewProps } from "./preview";

View file

@ -1,8 +1,9 @@
// Copyright 2025, Command Line Inc. // Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { globalStore } from "@/app/store/jotaiStore";
import { Markdown } from "@/element/markdown"; import { Markdown } from "@/element/markdown";
import { getOverrideConfigAtom, globalStore } from "@/store/global"; import { getOverrideConfigAtom } from "@/store/global";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import type { SpecializedViewProps } from "./preview"; import type { SpecializedViewProps } from "./preview";

View file

@ -3,9 +3,10 @@
import { BlockNodeModel } from "@/app/block/blocktypes"; import { BlockNodeModel } from "@/app/block/blocktypes";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { globalStore } from "@/app/store/jotaiStore";
import type { TabModel } from "@/app/store/tab-model"; import type { TabModel } from "@/app/store/tab-model";
import { TabRpcClient } from "@/app/store/wshrpcutil"; 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 * as WOS from "@/store/wos";
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
import { checkKeyPressed } from "@/util/keyutil"; import { checkKeyPressed } from "@/util/keyutil";

View file

@ -3,7 +3,7 @@
import { Button } from "@/app/element/button"; import { Button } from "@/app/element/button";
import { CenteredDiv } from "@/app/element/quickelems"; import { CenteredDiv } from "@/app/element/quickelems";
import { globalStore } from "@/store/global"; import { globalStore } from "@/app/store/jotaiStore";
import { getWebServerEndpoint } from "@/util/endpoints"; import { getWebServerEndpoint } from "@/util/endpoints";
import { formatRemoteUri } from "@/util/waveutil"; import { formatRemoteUri } from "@/util/waveutil";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";

View file

@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { CenteredDiv } from "@/app/element/quickelems"; import { CenteredDiv } from "@/app/element/quickelems";
import { globalStore } from "@/app/store/jotaiStore";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion"; import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion";
import { useWaveEnv } from "@/app/waveenv/waveenv"; import { useWaveEnv } from "@/app/waveenv/waveenv";
import { globalStore } from "@/store/global";
import { isBlank, makeConnRoute } from "@/util/util"; import { isBlank, makeConnRoute } from "@/util/util";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";

View file

@ -6,12 +6,13 @@ import type { BlockNodeModel } from "@/app/block/blocktypes";
import { NullErrorBoundary } from "@/app/element/errorboundary"; import { NullErrorBoundary } from "@/app/element/errorboundary";
import { Search, useSearch } from "@/app/element/search"; import { Search, useSearch } from "@/app/element/search";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { globalStore } from "@/app/store/jotaiStore";
import { useTabModel } from "@/app/store/tab-model"; import { useTabModel } from "@/app/store/tab-model";
import { waveEventSubscribeSingle } from "@/app/store/wps"; import { waveEventSubscribeSingle } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; import { TabRpcClient } from "@/app/store/wshrpcutil";
import type { TermViewModel } from "@/app/view/term/term-model"; 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 { fireAndForget, useAtomValueSafe } from "@/util/util";
import { computeBgStyleFromMeta } from "@/util/waveutil"; import { computeBgStyleFromMeta } from "@/util/waveutil";
import { ISearchOptions } from "@xterm/addon-search"; import { ISearchOptions } from "@xterm/addon-search";

View file

@ -6,13 +6,14 @@ import { Button } from "@/app/element/button";
import { Markdown } from "@/app/element/markdown"; import { Markdown } from "@/app/element/markdown";
import { TypingIndicator } from "@/app/element/typingindicator"; import { TypingIndicator } from "@/app/element/typingindicator";
import { ClientModel } from "@/app/store/client-model"; import { ClientModel } from "@/app/store/client-model";
import { globalStore } from "@/app/store/jotaiStore";
import type { TabModel } from "@/app/store/tab-model"; import type { TabModel } from "@/app/store/tab-model";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; 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 { BlockService, ObjectService } from "@/store/services";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget, isBlank, makeIconClass, mergeMeta } from "@/util/util"; import { fireAndForget, isBlank, makeIconClass, mergeMeta } from "@/util/util";

View file

@ -3,6 +3,7 @@
import { BlockNodeModel } from "@/app/block/blocktypes"; import { BlockNodeModel } from "@/app/block/blocktypes";
import { Search, useSearch } from "@/app/element/search"; import { Search, useSearch } from "@/app/element/search";
import { globalStore } from "@/app/store/jotaiStore";
import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
import type { TabModel } from "@/app/store/tab-model"; import type { TabModel } from "@/app/store/tab-model";
import { makeORef } from "@/app/store/wos"; import { makeORef } from "@/app/store/wos";
@ -14,7 +15,7 @@ import {
} from "@/app/suggestion/suggestion"; } from "@/app/suggestion/suggestion";
import { MockBoundary } from "@/app/waveenv/mockboundary"; import { MockBoundary } from "@/app/waveenv/mockboundary";
import { useWaveEnv } from "@/app/waveenv/waveenv"; import { useWaveEnv } from "@/app/waveenv/waveenv";
import { globalStore, openLink } from "@/store/global"; import { openLink } from "@/store/global";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { fireAndForget, useAtomValueSafe } from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";

View file

@ -2,9 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { FlexiModal } from "@/app/modals/modal"; import { FlexiModal } from "@/app/modals/modal";
import { globalStore } from "@/app/store/jotaiStore";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil"; 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 * as WOS from "@/store/wos";
import { formatRelativeTime } from "@/util/util"; import { formatRelativeTime } from "@/util/util";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";

View file

@ -1,10 +1,11 @@
// Copyright 2025, Command Line Inc. // Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // 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 { AppSelectionModal } from "@/builder/app-selection-modal";
import { BuilderWorkspace } from "@/builder/builder-workspace"; import { BuilderWorkspace } from "@/builder/builder-workspace";
import { ModalsRenderer } from "@/app/modals/modalsrenderer"; import { atoms, isDev } from "@/store/global";
import { atoms, globalStore, isDev } from "@/store/global";
import { appHandleKeyDown } from "@/store/keymodel"; import { appHandleKeyDown } from "@/store/keymodel";
import * as keyutil from "@/util/keyutil"; import * as keyutil from "@/util/keyutil";
import { isBlank } from "@/util/util"; import { isBlank } from "@/util/util";

View file

@ -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 },
};
}

View file

@ -9,11 +9,19 @@ import { RpcApiType } from "@/app/store/wshclientapi";
import { WaveEnv } from "@/app/waveenv/waveenv"; import { WaveEnv } from "@/app/waveenv/waveenv";
import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil"; import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil";
import { Atom, atom, PrimitiveAtom, useAtomValue } from "jotai"; import { Atom, atom, PrimitiveAtom, useAtomValue } from "jotai";
import { showPreviewContextMenu } from "../preview-contextmenu";
import { MockSysinfoConnection } from "../previews/sysinfo.preview-util";
import { DefaultFullConfig } from "./defaultconfig"; import { DefaultFullConfig } from "./defaultconfig";
import { DefaultMockFilesystem } from "./mockfilesystem"; import { DefaultMockFilesystem } from "./mockfilesystem";
import { showPreviewContextMenu } from "../preview-contextmenu";
import { previewElectronApi } from "./preview-electron-api"; 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): // What works "out of the box" in the mock environment (no MockEnv overrides needed):
// //
// RPC calls (handled in makeMockRpc): // 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 // - 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. // 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): // Backend service calls (handled in callBackendService):
// Any call falls through to a console.log and resolves null. // Any call falls through to a console.log and resolves null.
// Override specific calls via MockEnv.services: { Service: { Method: impl } } // Override specific calls via MockEnv.services: { Service: { Method: impl } }
// e.g. { "block": { "GetControllerStatus": (blockId) => myStatus } } // e.g. { "block": { "GetControllerStatus": (blockId) => myStatus } }
type RpcOverrides = { export type RpcHandlerType = (...args: any[]) => Promise<any>;
[K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: ( export type RpcStreamHandlerType = (...args: any[]) => AsyncGenerator<any, void, boolean>;
...args: any[]
) => Promise<any> | AsyncGenerator<any, void, boolean>; 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 = { type ServiceOverrides = {
@ -56,6 +70,7 @@ export type MockEnv = {
platform?: NodeJS.Platform; platform?: NodeJS.Platform;
settings?: Partial<SettingsType>; settings?: Partial<SettingsType>;
rpc?: RpcOverrides; rpc?: RpcOverrides;
rpcStreaming?: RpcStreamOverrides;
services?: ServiceOverrides; services?: ServiceOverrides;
atoms?: Partial<GlobalAtomsType>; atoms?: Partial<GlobalAtomsType>;
electron?: Partial<ElectronApi>; electron?: Partial<ElectronApi>;
@ -65,7 +80,11 @@ export type MockEnv = {
mockWaveObjs?: Record<string, WaveObj>; mockWaveObjs?: Record<string, WaveObj>;
}; };
export type MockWaveEnv = WaveEnv & { mockEnv: MockEnv }; export type MockWaveEnv = WaveEnv & {
mockEnv: MockEnv;
addRpcOverride: <K extends keyof RpcOverrides>(command: K, handler: RpcHandlerType) => void;
addRpcStreamOverride: <K extends keyof RpcStreamOverrides>(command: K, handler: RpcStreamHandlerType) => void;
};
function mergeRecords<T>(base: Record<string, T>, overrides: Record<string, T>): Record<string, T> { function mergeRecords<T>(base: Record<string, T>, overrides: Record<string, T>): Record<string, T> {
if (base == null && overrides == null) { if (base == null && overrides == null) {
@ -91,6 +110,7 @@ export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv {
platform: overrides.platform ?? base.platform, platform: overrides.platform ?? base.platform,
settings: mergeRecords(base.settings, overrides.settings), settings: mergeRecords(base.settings, overrides.settings),
rpc: mergeRecords(base.rpc as any, overrides.rpc as any) as RpcOverrides, 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, services: mergedServices,
atoms: overrides.atoms != null || base.atoms != null ? { ...base.atoms, ...overrides.atoms } : undefined, atoms: overrides.atoms != null || base.atoms != null ? { ...base.atoms, ...overrides.atoms } : undefined,
electron: electron:
@ -108,7 +128,10 @@ function makeMockSettingsKeyAtom(settingsAtom: Atom<SettingsType>): WaveEnv["get
const keyAtomCache = new Map<keyof SettingsType, Atom<any>>(); const keyAtomCache = new Map<keyof SettingsType, Atom<any>>();
return <T extends keyof SettingsType>(key: T) => { return <T extends keyof SettingsType>(key: T) => {
if (!keyAtomCache.has(key)) { 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<SettingsType[T]>; return keyAtomCache.get(key) as Atom<SettingsType[T]>;
}; };
@ -180,7 +203,15 @@ type MockWosFns = {
platform: NodeJS.Platform; 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<string, (...args: any[]) => Promise<any>>(); const callDispatchMap = new Map<string, (...args: any[]) => Promise<any>>();
const streamDispatchMap = new Map<string, (...args: any[]) => AsyncGenerator<any, void, boolean>>(); const streamDispatchMap = new Map<string, (...args: any[]) => AsyncGenerator<any, void, boolean>>();
const secrets = new Map<string, string>(); const secrets = new Map<string, string>();
@ -288,11 +319,13 @@ export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiTyp
if (overrides) { if (overrides) {
for (const key of Object.keys(overrides) as (keyof RpcOverrides)[]) { for (const key of Object.keys(overrides) as (keyof RpcOverrides)[]) {
const cmdName = key.slice(0, -"Command".length).toLowerCase(); const cmdName = key.slice(0, -"Command".length).toLowerCase();
if (cmdName === "filereadstream" || cmdName === "fileliststream") { setCallHandler(cmdName, overrides[key] as RpcHandlerType);
setStreamHandler(cmdName, overrides[key] as (...args: any[]) => AsyncGenerator<any, void, boolean>); }
} else { }
setCallHandler(cmdName, overrides[key] as (...args: any[]) => Promise<any>); 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(); const rpc = new RpcApiType();
@ -320,7 +353,17 @@ export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiTyp
yield null; 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 { 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 { export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
const overrides: MockEnv = mockEnv ?? {}; const overrides: MockEnv = mockEnv ?? {};
const platform = overrides.platform ?? PlatformMacOS; const tabId = overrides.tabId ?? PreviewTabId;
const defaultMockWaveObjs: Record<string, WaveObj> = {
[`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<GlobalAtomsType> = {
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<string, PrimitiveAtom<ConnStatus>>(); const connStatusAtomCache = new Map<string, PrimitiveAtom<ConnStatus>>();
const waveObjectValueAtomCache = new Map<string, PrimitiveAtom<any>>(); const waveObjectValueAtomCache = new Map<string, PrimitiveAtom<any>>();
const waveObjectDerivedAtomCache = new Map<string, Atom<any>>(); const waveObjectDerivedAtomCache = new Map<string, Atom<any>>();
@ -339,12 +432,17 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
const connConfigKeyAtomCache = new Map<string, Atom<any>>(); const connConfigKeyAtomCache = new Map<string, Atom<any>>();
const getWaveObjectAtom = <T extends WaveObj>(oref: string): PrimitiveAtom<T> => { const getWaveObjectAtom = <T extends WaveObj>(oref: string): PrimitiveAtom<T> => {
if (!waveObjectValueAtomCache.has(oref)) { 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<T>); waveObjectValueAtomCache.set(oref, atom(obj) as PrimitiveAtom<T>);
} }
return waveObjectValueAtomCache.get(oref) as PrimitiveAtom<T>; return waveObjectValueAtomCache.get(oref) as PrimitiveAtom<T>;
}; };
const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId, getWaveObjectAtom); const atoms = makeMockGlobalAtoms(
mergedOverrides.settings,
mergedOverrides.atoms,
mergedOverrides.tabId,
getWaveObjectAtom
);
const localHostDisplayNameAtom = atom<string>((get) => { const localHostDisplayNameAtom = atom<string>((get) => {
const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"];
if (configValue != null) { if (configValue != null) {
@ -363,38 +461,55 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
globalStore.set(waveObjectValueAtomCache.get(oref), obj); globalStore.set(waveObjectValueAtomCache.get(oref), obj);
}, },
}; };
const { rpc, setRpcHandler, setRpcStreamHandler } = makeMockRpc(mergedOverrides.rpc, mergedOverrides.rpcStreaming, mockWosFns);
const env = { const env = {
isMock: true, isMock: true,
mockEnv: overrides, mockEnv: mergedOverrides,
electron: { electron: {
...previewElectronApi, ...previewElectronApi,
getPlatform: () => platform, getPlatform: () => platform,
openExternal: (url: string) => { openExternal: (url: string) => {
window.open(url, "_blank"); window.open(url, "_blank");
}, },
...overrides.electron, ...mergedOverrides.electron,
}, },
rpc: makeMockRpc(overrides.rpc, mockWosFns), rpc,
atoms, atoms,
getSettingsKeyAtom: makeMockSettingsKeyAtom(atoms.settingsAtom), getSettingsKeyAtom: makeMockSettingsKeyAtom(atoms.settingsAtom),
platform, platform,
isDev: () => overrides.isDev ?? true, isDev: () => mergedOverrides.isDev ?? true,
isWindows: () => platform === PlatformWindows, isWindows: () => platform === PlatformWindows,
isMacOS: () => platform === PlatformMacOS, isMacOS: () => platform === PlatformMacOS,
createBlock: createBlock:
overrides.createBlock ?? mergedOverrides.createBlock ??
((blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => { ((blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => {
console.log("[mock createBlock]", blockDef, { magnified, ephemeral }); 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<Tab>(tabORef);
const currentTab = globalStore.get(tabAtom);
if (currentTab != null) {
mockWosFns.mockSetWaveObj(tabORef, {
...currentTab,
blockids: [...(currentTab.blockids ?? []), newBlockId],
});
}
return Promise.resolve(newBlockId);
}), }),
showContextMenu: showContextMenu: mergedOverrides.showContextMenu ?? showPreviewContextMenu,
overrides.showContextMenu ?? showPreviewContextMenu,
getLocalHostDisplayNameAtom: () => { getLocalHostDisplayNameAtom: () => {
return localHostDisplayNameAtom; return localHostDisplayNameAtom;
}, },
getConnStatusAtom: (conn: string) => { getConnStatusAtom: (conn: string) => {
if (!connStatusAtomCache.has(conn)) { if (!connStatusAtomCache.has(conn)) {
const connStatus = overrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn); const connStatus = mergedOverrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn);
connStatusAtomCache.set(conn, atom(connStatus)); connStatusAtomCache.set(conn, atom(connStatus));
} }
return connStatusAtomCache.get(conn); return connStatusAtomCache.get(conn);
@ -449,7 +564,7 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
}, },
services: null as any, services: null as any,
callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => { callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => {
const fn = overrides.services?.[service]?.[method]; const fn = mergedOverrides.services?.[service]?.[method];
if (fn) { if (fn) {
return fn(...args); return fn(...args);
} }
@ -458,6 +573,12 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
}, },
mockSetWaveObj: mockWosFns.mockSetWaveObj, mockSetWaveObj: mockWosFns.mockSetWaveObj,
mockModels: new Map<any, any>(), mockModels: new Map<any, any>(),
addRpcOverride: <K extends keyof RpcOverrides>(command: K, handler: RpcHandlerType) => {
setRpcHandler(command as string, handler);
},
addRpcStreamOverride: <K extends keyof RpcStreamOverrides>(command: K, handler: RpcStreamHandlerType) => {
setRpcStreamHandler(command as string, handler);
},
} as MockWaveEnv; } as MockWaveEnv;
env.services = Object.fromEntries( env.services = Object.fromEntries(
Object.entries(AllServiceTypes).map(([key, ServiceClass]) => [key, new ServiceClass(env)]) Object.entries(AllServiceTypes).map(([key, ServiceClass]) => [key, new ServiceClass(env)])

View file

@ -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<K extends keyof RpcOverrides>(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<K extends keyof RpcStreamOverrides>(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);
}
}

View file

@ -6,12 +6,13 @@ import { ErrorBoundary } from "@/app/element/errorboundary";
import { getAtoms, initGlobalAtoms } from "@/app/store/global-atoms"; import { getAtoms, initGlobalAtoms } from "@/app/store/global-atoms";
import { GlobalModel } from "@/app/store/global-model"; import { GlobalModel } from "@/app/store/global-model";
import { globalStore } from "@/app/store/jotaiStore"; import { globalStore } from "@/app/store/jotaiStore";
import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model";
import { WaveEnvContext } from "@/app/waveenv/waveenv"; import { WaveEnvContext } from "@/app/waveenv/waveenv";
import { loadFonts } from "@/util/fontutil"; import { loadFonts } from "@/util/fontutil";
import { atom, Provider } from "jotai"; import { Provider } from "jotai";
import React, { lazy, Suspense, useRef } from "react"; import React, { lazy, Suspense, useRef } from "react";
import { createRoot } from "react-dom/client"; 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 { installPreviewElectronApi } from "./mock/preview-electron-api";
import { PreviewContextMenu } from "./preview-contextmenu"; import { PreviewContextMenu } from "./preview-contextmenu";
@ -93,22 +94,14 @@ function PreviewHeader({ previewName }: { previewName: string }) {
} }
function PreviewRoot() { function PreviewRoot() {
const waveEnvRef = useRef( const waveEnvRef = useRef(makeMockWaveEnv());
makeMockWaveEnv({
atoms: {
uiContext: atom({ windowid: PreviewWindowId, activetabid: PreviewTabId } as UIContext),
staticTabId: atom(PreviewTabId),
workspaceId: atom(PreviewWorkspaceId),
},
})
);
return ( return (
<Provider store={globalStore}> <Provider store={globalStore}>
<WaveEnvContext.Provider value={waveEnvRef.current}> <WaveEnvContext.Provider value={waveEnvRef.current}>
<> <TabModelContext.Provider value={getTabModelByTabId(PreviewTabId, waveEnvRef.current)}>
<PreviewApp /> <PreviewApp />
<PreviewContextMenu /> <PreviewContextMenu />
</> </TabModelContext.Provider>
</WaveEnvContext.Provider> </WaveEnvContext.Provider>
</Provider> </Provider>
); );
@ -150,11 +143,6 @@ function PreviewApp() {
return <PreviewIndex />; return <PreviewIndex />;
} }
const PreviewTabId = crypto.randomUUID();
const PreviewWindowId = crypto.randomUUID();
const PreviewWorkspaceId = crypto.randomUUID();
const PreviewClientId = crypto.randomUUID();
function initPreview() { function initPreview() {
installPreviewElectronApi(); installPreviewElectronApi();
const initOpts = { const initOpts = {

View file

@ -2,13 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block"; import { Block } from "@/app/block/block";
import { globalStore } from "@/app/store/jotaiStore"; import { useWaveEnv } from "@/app/waveenv/waveenv";
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 * as React from "react"; 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 { import {
DefaultAiFileDiffChatId, DefaultAiFileDiffChatId,
DefaultAiFileDiffFileName, DefaultAiFileDiffFileName,
@ -16,130 +13,51 @@ import {
makeMockAiFileDiffResponse, makeMockAiFileDiffResponse,
} from "./aifilediff.preview-util"; } from "./aifilediff.preview-util";
const PreviewWorkspaceId = "preview-aifilediff-workspace";
const PreviewTabId = "preview-aifilediff-tab";
const PreviewNodeId = "preview-aifilediff-node"; 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<MockWaveEnv>(() => {
const mockWaveObjs: Record<string, WaveObj> = {
[`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();
},
},
});
}, [baseEnv]);
const tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]);
return (
<WaveEnvContext.Provider value={env}>
<TabModelContext.Provider value={tabModel}>
<div className="flex w-full max-w-[1120px] flex-col gap-2 px-6 py-6">
<div className="text-xs text-muted font-mono">full aifilediff block (mock WOS + mock WaveAI diff RPC)</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="h-[720px]">
<Block preview={false} nodeModel={nodeModel} />
</div>
</div>
</div>
</TabModelContext.Provider>
</WaveEnvContext.Provider>
);
}
export function AiFileDiffPreview() { export function AiFileDiffPreview() {
return <AiFileDiffPreviewInner />; const env = useWaveEnv();
const [blockId, setBlockId] = React.useState<string>(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,
},
},
false,
false
).then((id) => setBlockId(id));
}, []);
const nodeModel = React.useMemo(
() => (blockId != null ? makeMockNodeModel({ nodeId: PreviewNodeId, blockId }) : null),
[blockId]
);
if (blockId == null || nodeModel == null) {
return null;
}
return (
<div className="flex w-full max-w-[1120px] flex-col gap-2 px-6 py-6">
<div className="text-xs text-muted font-mono">full aifilediff block (mock WOS + mock WaveAI diff RPC)</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="h-[720px]">
<Block preview={false} nodeModel={nodeModel} />
</div>
</div>
</div>
);
} }

View file

@ -2,14 +2,11 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block"; 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 { 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 * 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 { import {
DefaultSysinfoHistoryPoints, DefaultSysinfoHistoryPoints,
makeMockSysinfoEvent, makeMockSysinfoEvent,
@ -17,112 +14,22 @@ import {
MockSysinfoConnection, MockSysinfoConnection,
} from "./sysinfo.preview-util"; } from "./sysinfo.preview-util";
const PreviewWorkspaceId = "preview-sysinfo-workspace";
const PreviewTabId = "preview-sysinfo-tab";
const PreviewNodeId = "preview-sysinfo-node"; const PreviewNodeId = "preview-sysinfo-node";
const PreviewBlockId = "preview-sysinfo-block";
function makeMockWorkspace(): Workspace { export default function SysinfoPreview() {
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();
const historyRef = React.useRef(makeMockSysinfoHistory()); const historyRef = React.useRef(makeMockSysinfoHistory());
const nodeModel = React.useMemo(() => makePreviewNodeModel(), []); const nodeModel = React.useMemo(
() => makeMockNodeModel({ nodeId: PreviewNodeId, blockId: SysinfoBlockId, innerRect: { width: "920px", height: "560px" }, numLeafs: 2 }),
[]
);
const env = React.useMemo<MockWaveEnv>(() => { useRpcOverride("EventReadHistoryCommand", async (_client, data) => {
const mockWaveObjs: Record<string, WaveObj> = { if (data.event !== "sysinfo" || data.scope !== MockSysinfoConnection) {
[`workspace:${PreviewWorkspaceId}`]: makeMockWorkspace(), return [];
[`tab:${PreviewTabId}`]: makeMockTab(), }
[`block:${PreviewBlockId}`]: makeMockBlock(), const maxItems = data.maxitems ?? historyRef.current.length;
}; return historyRef.current.slice(-maxItems);
});
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 tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]);
React.useEffect(() => { React.useEffect(() => {
let nextStep = historyRef.current.length; let nextStep = historyRef.current.length;
@ -141,21 +48,13 @@ function SysinfoPreviewInner() {
}, []); }, []);
return ( return (
<WaveEnvContext.Provider value={env}> <div className="flex w-full max-w-[980px] flex-col gap-2 px-6 py-6">
<TabModelContext.Provider value={tabModel}> <div className="text-xs text-muted font-mono">full sysinfo block (mock WOS + FE-only WPS events)</div>
<div className="flex w-full max-w-[980px] flex-col gap-2 px-6 py-6"> <div className="rounded-md border border-border bg-panel p-4">
<div className="text-xs text-muted font-mono">full sysinfo block (mock WOS + FE-only WPS events)</div> <div className="h-[620px]">
<div className="rounded-md border border-border bg-panel p-4"> <Block preview={false} nodeModel={nodeModel} />
<div className="h-[620px]">
<Block preview={false} nodeModel={nodeModel} />
</div>
</div>
</div> </div>
</TabModelContext.Provider> </div>
</WaveEnvContext.Provider> </div>
); );
} }
export default function SysinfoPreview() {
return <SysinfoPreviewInner />;
}

View file

@ -2,134 +2,26 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block"; 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 * 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 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<string, WaveObj> = {
[`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<MockWaveEnv>(() => {
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]);
return (
<WaveEnvContext.Provider value={env}>
<TabModelContext.Provider value={tabModel}>
<div className="flex w-full max-w-[1100px] flex-col gap-2 px-6 py-6">
<div className="text-xs text-muted font-mono">full web block using preview mock fallback</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="h-[680px]">
<Block preview={false} nodeModel={nodeModel} />
</div>
</div>
</div>
</TabModelContext.Provider>
</WaveEnvContext.Provider>
);
}
export function WebPreview() { export function WebPreview() {
return <WebPreviewInner />; const nodeModel = React.useMemo(
() => makeMockNodeModel({ nodeId: PreviewNodeId, blockId: WebBlockId, innerRect: { width: "1040px", height: "620px" } }),
[]
);
return (
<div className="flex w-full max-w-[1100px] flex-col gap-2 px-6 py-6">
<div className="text-xs text-muted font-mono">full web block using preview mock fallback</div>
<div className="rounded-md border border-border bg-panel p-4">
<div className="h-[680px]">
<Block preview={false} nodeModel={nodeModel} />
</div>
</div>
</div>
);
} }