diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 19a8529b11..37453473c9 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { @@ -9,12 +9,15 @@ import { FullSubBlockProps, SubBlockProps, } from "@/app/block/blocktypes"; +import type { TabModel } from "@/app/store/tab-model"; +import { useTabModel } from "@/app/store/tab-model"; import { AiFileDiffViewModel } from "@/app/view/aifilediff/aifilediff"; import { LauncherViewModel } from "@/app/view/launcher/launcher"; import { PreviewModel } from "@/app/view/preview/preview-model"; import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo"; import { TsunamiViewModel } from "@/app/view/tsunami/tsunami"; import { VDomModel } from "@/app/view/vdom/vdom-model"; +import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; import { useDebouncedNodeInnerRect } from "@/layout/index"; @@ -26,8 +29,6 @@ import { registerBlockComponentModel, unregisterBlockComponentModel, } from "@/store/global"; -import type { TabModel } from "@/app/store/tab-model"; -import { useTabModel } from "@/app/store/tab-model"; import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { isBlank, useAtomValueSafe } from "@/util/util"; @@ -59,10 +60,16 @@ BlockRegistry.set("tsunami", TsunamiViewModel); BlockRegistry.set("aifilediff", AiFileDiffViewModel); BlockRegistry.set("waveconfig", WaveConfigViewModel); -function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel, tabModel: TabModel): ViewModel { +function makeViewModel( + blockId: string, + blockView: string, + nodeModel: BlockNodeModel, + tabModel: TabModel, + waveEnv: WaveEnv +): ViewModel { const ctor = BlockRegistry.get(blockView); if (ctor != null) { - return new ctor(blockId, nodeModel, tabModel); + return new ctor({ blockId, nodeModel, tabModel, waveEnv }); } return makeDefaultViewModel(blockId, blockView); } @@ -86,7 +93,7 @@ function getViewElem( function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { const blockDataAtom = getWaveObjectAtom(makeORef("block", blockId)); - let viewModel: ViewModel = { + const viewModel: ViewModel = { viewType: viewType, viewIcon: atom((get) => { const blockData = get(blockDataAtom); @@ -308,11 +315,12 @@ const Block = memo((props: BlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const tabModel = useTabModel(); + const waveEnv = useWaveEnv(); const [blockData, loading] = useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { - viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel); + viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { @@ -334,11 +342,12 @@ const SubBlock = memo((props: SubBlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const tabModel = useTabModel(); + const waveEnv = useWaveEnv(); const [blockData, loading] = useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { - viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel); + viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 88b679fb57..2ae7cb47c6 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -132,10 +132,6 @@ function getBlockMetaKeyAtom(blockId: string, key: T): return metaAtom; } -function useBlockMetaKeyAtom(blockId: string, key: T): MetaType[T] { - return useAtomValue(getBlockMetaKeyAtom(blockId, key)); -} - function getOrefMetaKeyAtom(oref: string, key: T): Atom { const orefCache = getSingleOrefAtomCache(oref); const metaAtomName = "#meta-" + key; @@ -614,33 +610,34 @@ function subscribeToConnEvents() { }); } +function makeDefaultConnStatus(conn: string): ConnStatus { + if (isLocalConnName(conn)) { + return { + connection: conn, + connected: true, + error: null, + status: "connected", + hasconnected: true, + activeconnnum: 0, + wshenabled: false, + }; + } + return { + connection: conn, + connected: false, + error: null, + status: "disconnected", + hasconnected: false, + activeconnnum: 0, + wshenabled: false, + }; +} + function getConnStatusAtom(conn: string): PrimitiveAtom { const connStatusMap = globalStore.get(ConnStatusMapAtom); let rtn = connStatusMap.get(conn); if (rtn == null) { - if (isLocalConnName(conn)) { - const connStatus: ConnStatus = { - connection: conn, - connected: true, - error: null, - status: "connected", - hasconnected: true, - activeconnnum: 0, - wshenabled: false, - }; - rtn = atom(connStatus); - } else { - const connStatus: ConnStatus = { - connection: conn, - connected: false, - error: null, - status: "disconnected", - hasconnected: false, - activeconnnum: 0, - wshenabled: false, - }; - rtn = atom(connStatus); - } + rtn = atom(makeDefaultConnStatus(conn)); const newConnStatusMap = new Map(connStatusMap); newConnStatusMap.set(conn, rtn); globalStore.set(ConnStatusMapAtom, newConnStatusMap); @@ -692,6 +689,7 @@ export { initGlobalWaveEventSubs, isDev, loadConnStatus, + makeDefaultConnStatus, openLink, readAtom, recordTEvent, @@ -706,7 +704,6 @@ export { useBlockAtom, useBlockCache, useBlockDataLoaded, - useBlockMetaKeyAtom, useOrefMetaKeyAtom, useOverrideConfigAtom, useSettingsKeyAtom, diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 1d3bdeabdb..f2395e12d0 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -3,8 +3,8 @@ // WaveObjectStore -import { waveEventSubscribeSingle } from "@/app/store/wps"; import { isPreviewWindow } from "@/app/store/windowtype"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { getWebServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; import { fireAndForget } from "@/util/util"; @@ -218,14 +218,9 @@ function loadAndPinWaveObject(oref: string): Promise { return wov.pendingPromise; } -function getWaveObjectAtom(oref: string): WritableWaveObjectAtom { +function getWaveObjectAtom(oref: string): Atom { const wov = getWaveObjectValue(oref); - return atom( - (get) => get(wov.dataAtom).value, - (_get, set, value: T) => { - setObjectValue(value, set, true); - } - ); + return atom((get) => get(wov.dataAtom).value); } function getWaveObjectLoadingAtom(oref: string): Atom { diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index c28809a22e..31711c354c 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { Button } from "@/app/element/button"; @@ -149,24 +149,6 @@ function strArrayIsEqual(a: string[], b: string[]) { return true; } -function setIsEqual(a: Set | null, b: Set | null): boolean { - if (a == null && b == null) { - return true; - } - if (a == null || b == null) { - return false; - } - if (a.size !== b.size) { - return false; - } - for (const item of a) { - if (!b.has(item)) { - return false; - } - } - return true; -} - const TabBar = memo(({ workspace }: TabBarProps) => { const [tabIds, setTabIds] = useState([]); const [dragStartPositions, setDragStartPositions] = useState([]); @@ -506,7 +488,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { [] ); - const handleMouseUp = (event: MouseEvent) => { + const handleMouseUp = (_event: MouseEvent) => { const { tabIndex, dragged } = draggingTabDataRef.current; // Update the final position of the dragged tab diff --git a/frontend/app/view/aifilediff/aifilediff.tsx b/frontend/app/view/aifilediff/aifilediff.tsx index dfd85f2917..3b853a6eb6 100644 --- a/frontend/app/view/aifilediff/aifilediff.tsx +++ b/frontend/app/view/aifilediff/aifilediff.tsx @@ -1,13 +1,13 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import type { BlockNodeModel } from "@/app/block/blocktypes"; import type { TabModel } from "@/app/store/tab-model"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { base64ToString } from "@/util/util"; import { DiffViewer } from "@/app/view/codeeditor/diffviewer"; import { globalStore, WOS } from "@/store/global"; +import { base64ToString } from "@/util/util"; import * as jotai from "jotai"; import { useEffect } from "react"; @@ -30,7 +30,7 @@ export class AiFileDiffViewModel implements ViewModel { viewName: jotai.Atom; viewText: jotai.Atom; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index 7b9f675bf4..02f4db48b0 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -1,8 +1,6 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { globalStore, WOS } from "@/app/store/global"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -16,8 +14,8 @@ class HelpViewModel extends WebViewModel { return HelpView; } - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { - super(blockId, nodeModel, tabModel); + constructor(initOpts: ViewModelInitType) { + super(initOpts); this.viewText = atom((get) => { // force a dependency on meta.url so we re-render the buttons when the url changes void (get(this.blockAtom)?.meta?.url || get(this.homepageUrl)); diff --git a/frontend/app/view/launcher/launcher.tsx b/frontend/app/view/launcher/launcher.tsx index f71f272aa4..7b5a935f19 100644 --- a/frontend/app/view/launcher/launcher.tsx +++ b/frontend/app/view/launcher/launcher.tsx @@ -1,10 +1,10 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import type { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import logoUrl from "@/app/asset/logo.svg?url"; +import type { BlockNodeModel } from "@/app/block/blocktypes"; import { atoms, globalStore, replaceBlock } from "@/app/store/global"; +import type { TabModel } from "@/app/store/tab-model"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isBlank, makeIconClass } from "@/util/util"; import clsx from "clsx"; @@ -35,7 +35,7 @@ export class LauncherViewModel implements ViewModel { containerSize = atom({ width: 0, height: 0 }); gridLayout: GridLayoutType = null; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index ca85bba96e..2bfa643031 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -1,9 +1,9 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { ContextMenuModel } from "@/app/store/contextmenu"; +import type { TabModel } from "@/app/store/tab-model"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { getConnStatusAtom, getOverrideConfigAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global"; @@ -168,7 +168,7 @@ export class PreviewModel implements ViewModel { directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.viewType = "preview"; this.blockId = blockId; this.nodeModel = nodeModel; diff --git a/frontend/app/view/quicktipsview/quicktipsview.tsx b/frontend/app/view/quicktipsview/quicktipsview.tsx index ec79e4e3f7..c018fdca2a 100644 --- a/frontend/app/view/quicktipsview/quicktipsview.tsx +++ b/frontend/app/view/quicktipsview/quicktipsview.tsx @@ -1,10 +1,10 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import type { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { QuickTips } from "@/app/element/quicktips"; import { globalStore } from "@/app/store/global"; +import type { TabModel } from "@/app/store/tab-model"; import { Atom, atom, PrimitiveAtom } from "jotai"; class QuickTipsViewModel implements ViewModel { @@ -15,7 +15,7 @@ class QuickTipsViewModel implements ViewModel { showTocAtom: PrimitiveAtom; endIconButtons: Atom; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; diff --git a/frontend/app/view/sysinfo/sysinfo.tsx b/frontend/app/view/sysinfo/sysinfo.tsx index 2869f8ac1a..dca9d6d09f 100644 --- a/frontend/app/view/sysinfo/sysinfo.tsx +++ b/frontend/app/view/sysinfo/sysinfo.tsx @@ -1,9 +1,8 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import type { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; -import { getConnStatusAtom, globalStore, WOS } from "@/store/global"; +import { globalStore } from "@/app/store/jotaiStore"; +import { makeORef } from "@/app/store/wos"; import * as util from "@/util/util"; import * as Plot from "@observablehq/plot"; import clsx from "clsx"; @@ -14,11 +13,22 @@ import * as React from "react"; import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; import { waveEventSubscribeSingle } from "@/app/store/wps"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms } from "@/store/global"; +import type { BlockMetaKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; +export type SysinfoEnv = { + rpc: { + EventReadHistoryCommand: WaveEnv["rpc"]["EventReadHistoryCommand"]; + SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; + }; + atoms: { + fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + }; + getConnStatusAtom: WaveEnv["getConnStatusAtom"]; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"graph:numpoints" | "sysinfo:type" | "connection" | "count">; +}; + const DefaultNumPoints = 120; type DataItem = { @@ -49,13 +59,13 @@ function defaultMemMeta(name: string, maxY: string): TimeSeriesMeta { } const PlotTypes: object = { - CPU: function (dataItem: DataItem): Array { + CPU: function (_dataItem: DataItem): Array { return ["cpu"]; }, - Mem: function (dataItem: DataItem): Array { + Mem: function (_dataItem: DataItem): Array { return ["mem:used"]; }, - "CPU + Mem": function (dataItem: DataItem): Array { + "CPU + Mem": function (_dataItem: DataItem): Array { return ["cpu", "mem:used"]; }, "All CPU": function (dataItem: DataItem): Array { @@ -94,9 +104,6 @@ function convertWaveEventToDataItem(event: Extract; termMode: jotai.Atom; htmlElemFocusRef: React.RefObject; blockId: string; @@ -117,13 +124,12 @@ class SysinfoViewModel implements ViewModel { plotMetaAtom: jotai.PrimitiveAtom>; endIconButtons: jotai.Atom; plotTypeSelectedAtom: jotai.Atom; + env: SysinfoEnv; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { - this.nodeModel = nodeModel; - this.tabModel = tabModel; + constructor({ blockId, waveEnv }: ViewModelInitType) { this.viewType = "sysinfo"; this.blockId = blockId; - this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); + this.env = waveEnv; this.addInitialDataAtom = jotai.atom(null, (get, set, points) => { const targetLen = get(this.numPoints) + 1; try { @@ -169,7 +175,7 @@ class SysinfoViewModel implements ViewModel { }); this.addContinuousDataAtom = jotai.atom(null, (get, set, newPoint) => { const targetLen = get(this.numPoints) + 1; - let data = get(this.dataAtom); + const data = get(this.dataAtom); try { const latestItemTs = newPoint?.ts ?? 0; const cutoffTs = latestItemTs - 1000 * targetLen; @@ -185,15 +191,14 @@ class SysinfoViewModel implements ViewModel { this.filterOutNowsh = jotai.atom(true); this.loadingAtom = jotai.atom(true); this.numPoints = jotai.atom((get) => { - const blockData = get(this.blockAtom); - const metaNumPoints = blockData?.meta?.["graph:numpoints"]; + const metaNumPoints = get(this.env.getBlockMetaKeyAtom(blockId, "graph:numpoints")); if (metaNumPoints == null || metaNumPoints <= 0) { return DefaultNumPoints; } return metaNumPoints; }); this.metrics = jotai.atom((get) => { - let plotType = get(this.plotTypeSelectedAtom); + const plotType = get(this.plotTypeSelectedAtom); const plotData = get(this.dataAtom); try { const metrics = PlotTypes[plotType](plotData[plotData.length - 1]); @@ -206,8 +211,7 @@ class SysinfoViewModel implements ViewModel { } }); this.plotTypeSelectedAtom = jotai.atom((get) => { - const blockData = get(this.blockAtom); - const plotType = blockData?.meta?.["sysinfo:type"]; + const plotType = get(this.env.getBlockMetaKeyAtom(blockId, "sysinfo:type")); if (plotType == null || typeof plotType != "string") { return "CPU"; } @@ -219,17 +223,15 @@ class SysinfoViewModel implements ViewModel { this.viewName = jotai.atom((get) => { return get(this.plotTypeSelectedAtom); }); - this.incrementCount = jotai.atom(null, async (get, set) => { - const meta = get(this.blockAtom).meta; - const count = meta.count ?? 0; - await RpcApi.SetMetaCommand(TabRpcClient, { - oref: WOS.makeORef("block", this.blockId), + this.incrementCount = jotai.atom(null, async (get, _set) => { + const count = get(this.env.getBlockMetaKeyAtom(blockId, "count")) ?? 0; + await this.env.rpc.SetMetaCommand(TabRpcClient, { + oref: makeORef("block", this.blockId), meta: { count: count + 1 }, }); }); this.connection = jotai.atom((get) => { - const blockData = get(this.blockAtom); - const connValue = blockData?.meta?.connection; + const connValue = get(this.env.getBlockMetaKeyAtom(blockId, "connection")); if (util.isBlank(connValue)) { return "local"; } @@ -238,9 +240,8 @@ class SysinfoViewModel implements ViewModel { this.dataAtom = jotai.atom([]); this.loadInitialData(); this.connStatus = jotai.atom((get) => { - const blockData = get(this.blockAtom); - const connName = blockData?.meta?.connection; - const connAtom = getConnStatusAtom(connName); + const connName = get(this.env.getBlockMetaKeyAtom(blockId, "connection")); + const connAtom = this.env.getConnStatusAtom(connName); return get(connAtom); }); } @@ -254,7 +255,7 @@ class SysinfoViewModel implements ViewModel { try { const numPoints = globalStore.get(this.numPoints); const connName = globalStore.get(this.connection); - const initialData = await RpcApi.EventReadHistoryCommand(TabRpcClient, { + const initialData = await this.env.rpc.EventReadHistoryCommand(TabRpcClient, { event: "sysinfo", scope: connName, maxitems: numPoints, @@ -262,7 +263,7 @@ class SysinfoViewModel implements ViewModel { if (initialData == null) { return; } - const newData = this.getDefaultData(); + this.getDefaultData(); const initialDataItems: DataItem[] = initialData.map(convertWaveEventToDataItem); // splice the initial data into the default data (replacing the newest points) //newData.splice(newData.length - initialDataItems.length, initialDataItems.length, ...initialDataItems); @@ -275,7 +276,7 @@ class SysinfoViewModel implements ViewModel { } getSettingsMenuItems(): ContextMenuItem[] { - const fullConfig = globalStore.get(atoms.fullConfigAtom); + const fullConfig = globalStore.get(this.env.atoms.fullConfigAtom); const termThemes = fullConfig?.termthemes ?? {}; const termThemeKeys = Object.keys(termThemes); const plotData = globalStore.get(this.dataAtom); @@ -296,8 +297,8 @@ class SysinfoViewModel implements ViewModel { type: "radio", checked: currentlySelected == plotType, click: async () => { - await RpcApi.SetMetaCommand(TabRpcClient, { - oref: WOS.makeORef("block", this.blockId), + await this.env.rpc.SetMetaCommand(TabRpcClient, { + oref: makeORef("block", this.blockId), meta: { "graph:metrics": dataTypes, "sysinfo:type": plotType }, }); }, @@ -326,7 +327,7 @@ class SysinfoViewModel implements ViewModel { } } -const plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"]; +const _plotColors = ["#58C142", "#FFC107", "#FF5722", "#2196F3", "#9C27B0", "#00BCD4", "#FFEB3B", "#795548"]; type SysinfoViewProps = { blockId: string; @@ -418,7 +419,7 @@ function SingleLinePlot({ const plotHeight = domRect?.height ?? 0; const plotWidth = domRect?.width ?? 0; const marks: Plot.Markish[] = []; - let decimalPlaces = yvalMeta?.decimalPlaces ?? 0; + const decimalPlaces = yvalMeta?.decimalPlaces ?? 0; let color = yvalMeta?.color; if (!color) { color = defaultColor; @@ -492,10 +493,10 @@ function SingleLinePlot({ Plot.pointerX({ x: "ts", y: yval, fill: color, r: 3, stroke: "var(--main-text-color)", strokeWidth: 1 }) ) ); - let maxY = resolveDomainBound(yvalMeta?.maxy, plotData[plotData.length - 1]) ?? 100; - let minY = resolveDomainBound(yvalMeta?.miny, plotData[plotData.length - 1]) ?? 0; - let maxX = plotData[plotData.length - 1].ts; - let minX = maxX - targetLen * 1000; + const maxY = resolveDomainBound(yvalMeta?.maxy, plotData[plotData.length - 1]) ?? 100; + const minY = resolveDomainBound(yvalMeta?.miny, plotData[plotData.length - 1]) ?? 0; + const maxX = plotData[plotData.length - 1].ts; + const minX = maxX - targetLen * 1000; const plot = Plot.plot({ axis: !sparkline, x: { @@ -549,7 +550,7 @@ const SysinfoViewInner = React.memo(({ model }: SysinfoViewProps) => { > {plotData && plotData.length > 0 && - yvals.map((yval, idx) => { + yvals.map((yval, _idx) => { return ( ; searchAtoms?: SearchAtoms; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.viewType = "term"; this.blockId = blockId; this.tabModel = tabModel; diff --git a/frontend/app/view/tsunami/tsunami.tsx b/frontend/app/view/tsunami/tsunami.tsx index 14ca08574f..c23cd76035 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -1,9 +1,7 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockNodeModel } from "@/app/block/blocktypes"; import { getApi, globalStore, WOS } from "@/app/store/global"; -import type { TabModel } from "@/app/store/tab-model"; import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -21,8 +19,8 @@ class TsunamiViewModel extends WebViewModel { viewIcon: jotai.Atom; viewName: jotai.Atom; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { - super(blockId, nodeModel, tabModel); + constructor(initOpts: ViewModelInitType) { + super(initOpts); this.viewType = "tsunami"; this.isRestarting = jotai.atom(false); @@ -30,16 +28,16 @@ class TsunamiViewModel extends WebViewModel { this.hideNav = jotai.atom(true); // Set custom partition for tsunami WebView isolation - this.partitionOverride = jotai.atom(`tsunami:${blockId}`); + this.partitionOverride = jotai.atom(`tsunami:${this.blockId}`); this.shellProcFullStatus = jotai.atom(null) as jotai.PrimitiveAtom; - const initialShellProcStatus = services.BlockService.GetControllerStatus(blockId); + const initialShellProcStatus = services.BlockService.GetControllerStatus(this.blockId); initialShellProcStatus.then((rts) => { this.updateShellProcStatus(rts); }); this.shellProcStatusUnsubFn = waveEventSubscribeSingle({ eventType: "controllerstatus", - scope: WOS.makeORef("block", blockId), + scope: WOS.makeORef("block", this.blockId), handler: (event) => { this.updateShellProcStatus(event.data); }, @@ -61,7 +59,7 @@ class TsunamiViewModel extends WebViewModel { return meta?.title || "WaveApp"; }); const initialRTInfo = RpcApi.GetRTInfoCommand(TabRpcClient, { - oref: WOS.makeORef("block", blockId), + oref: WOS.makeORef("block", this.blockId), }); initialRTInfo.then((rtInfo) => { if (rtInfo && rtInfo["tsunami:appmeta"]) { @@ -70,7 +68,7 @@ class TsunamiViewModel extends WebViewModel { }); this.appMetaUnsubFn = waveEventSubscribeSingle({ eventType: "tsunami:updatemeta", - scope: WOS.makeORef("block", blockId), + scope: WOS.makeORef("block", this.blockId), handler: (event) => { globalStore.set(this.appMeta, event.data); }, diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index 4751ed1d24..77b01495e2 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -1,9 +1,9 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global"; +import type { TabModel } from "@/app/store/tab-model"; import { makeORef } from "@/app/store/wos"; import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; @@ -140,7 +140,7 @@ export class VDomModel { hasBackendWork: boolean = false; noPadding: jotai.PrimitiveAtom; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.viewType = "vdom"; this.blockId = blockId; this.nodeModel = nodeModel; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 6d2dd8fc85..630f047265 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; @@ -92,7 +92,7 @@ export class WaveAiModel implements ViewModel { cancel: boolean; aiWshClient: AiWshClient; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; diff --git a/frontend/app/view/waveconfig/waveconfig-model.ts b/frontend/app/view/waveconfig/waveconfig-model.ts index cd7d4e45e3..f41a39eccd 100644 --- a/frontend/app/view/waveconfig/waveconfig-model.ts +++ b/frontend/app/view/waveconfig/waveconfig-model.ts @@ -1,10 +1,10 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { getApi, getBlockMetaKeyAtom, WOS } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; +import type { TabModel } from "@/app/store/tab-model"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { SecretsContent } from "@/app/view/waveconfig/secretscontent"; @@ -170,7 +170,7 @@ export class WaveConfigViewModel implements ViewModel { storageBackendErrorAtom: PrimitiveAtom; secretValueRef: HTMLTextAreaElement | null = null; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 7fa1671b26..df50221764 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -1,12 +1,12 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { Search, useSearch } from "@/app/element/search"; import { createBlock, getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global"; import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; import { ObjectService } from "@/app/store/services"; +import type { TabModel } from "@/app/store/tab-model"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { @@ -72,7 +72,7 @@ export class WebViewModel implements ViewModel { partitionOverride: PrimitiveAtom | null; userAgentType: Atom; - constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { this.nodeModel = nodeModel; this.tabModel = tabModel; this.viewType = "web"; diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index e6fa73a36e..365bf74be9 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -2,11 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { RpcApiType } from "@/app/store/wshclientapi"; -import { Atom } from "jotai"; +import { Atom, PrimitiveAtom } from "jotai"; import React from "react"; type ConfigAtoms = { [K in keyof SettingsType]: Atom }; +export type BlockMetaKeyAtomFnType = ( + blockId: string, + key: T +) => Atom; + // default implementation for production is in ./waveenvimpl.ts export type WaveEnv = { electron: ElectronApi; @@ -16,6 +21,9 @@ export type WaveEnv = { atoms: GlobalAtomsType; createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => Promise; showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => void; + getConnStatusAtom: (conn: string) => PrimitiveAtom; + getWaveObjectAtom: (oref: string) => Atom; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; }; export const WaveEnvContext = React.createContext(null); diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 85c7bab1f8..50aa4ef7ea 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -1,8 +1,16 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { atoms, createBlock, getSettingsKeyAtom, isDev } from "@/app/store/global"; import { ContextMenuModel } from "@/app/store/contextmenu"; +import { + atoms, + createBlock, + getBlockMetaKeyAtom, + getConnStatusAtom, + getSettingsKeyAtom, + isDev, + WOS, +} from "@/app/store/global"; import { RpcApi } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; @@ -23,5 +31,8 @@ export function makeWaveEnvImpl(): WaveEnv { showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => { ContextMenuModel.getInstance().showContextMenu(menu, e); }, + getConnStatusAtom, + getWaveObjectAtom: WOS.getWaveObjectAtom, + getBlockMetaKeyAtom, }; } diff --git a/frontend/layout/lib/layoutAtom.ts b/frontend/layout/lib/layoutAtom.ts index 62890446da..e34cd3e200 100644 --- a/frontend/layout/lib/layoutAtom.ts +++ b/frontend/layout/lib/layoutAtom.ts @@ -4,7 +4,7 @@ import { WOS } from "@/app/store/global"; import { Atom, Getter } from "jotai"; -export function getLayoutStateAtomFromTab(tabAtom: Atom, get: Getter): WritableWaveObjectAtom { +export function getLayoutStateAtomFromTab(tabAtom: Atom, get: Getter): Atom { const tabData = get(tabAtom); if (!tabData) return; const layoutStateOref = WOS.makeORef("layout", tabData.layoutstate); diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 14b0476c90..0741df9bcb 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -4,6 +4,7 @@ import { FocusManager } from "@/app/store/focusManager"; import { getSettingsKeyAtom } from "@/app/store/global"; import { BlockService } from "@/app/store/services"; +import * as WOS from "@/app/store/wos"; import { atomWithThrottle, boundNumber, fireAndForget } from "@/util/util"; import { Atom, atom, Getter, PrimitiveAtom, Setter } from "jotai"; import { splitAtom } from "jotai/utils"; @@ -88,7 +89,7 @@ export class LayoutModel { /** * WaveObject atom for persistence */ - private waveObjectAtom: WritableWaveObjectAtom; + private waveObjectAtom: Atom; /** * Debounce timer for persistence */ @@ -587,7 +588,7 @@ export class LayoutModel { waveObj.leaforder = this.treeState.leafOrder; waveObj.pendingbackendactions = this.treeState.pendingBackendActions; - this.setter(this.waveObjectAtom, waveObj); + WOS.setObjectValue(waveObj, this.setter, true); this.persistDebounceTimer = null; }, 100); } diff --git a/frontend/preview/index.html b/frontend/preview/index.html index cf9e957e37..4c5e76af8a 100644 --- a/frontend/preview/index.html +++ b/frontend/preview/index.html @@ -5,6 +5,7 @@ Wave Preview Server + diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 0718433798..9ed61e2b58 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -1,12 +1,55 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getSettingsKeyAtom } from "@/app/store/global"; +import { getSettingsKeyAtom, makeDefaultConnStatus } from "@/app/store/global"; import { RpcApiType } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; -import { atom } from "jotai"; +import { Atom, atom, PrimitiveAtom } from "jotai"; import { previewElectronApi } from "./preview-electron-api"; +type RpcOverrides = { + [K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: (...args: any[]) => any; +}; + +export type MockEnv = { + isDev?: boolean; + config?: Partial; + rpc?: RpcOverrides; + atoms?: Partial; + electron?: Partial; + createBlock?: WaveEnv["createBlock"]; + showContextMenu?: WaveEnv["showContextMenu"]; + connStatus?: Record; + mockWaveObjs?: Record; +}; + +export type MockWaveEnv = WaveEnv & { mockEnv: MockEnv }; + +function mergeRecords(base: Record, overrides: Record): Record { + if (base == null && overrides == null) { + return undefined; + } + return { ...(base ?? {}), ...(overrides ?? {}) }; +} + +export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv { + return { + isDev: overrides.isDev ?? base.isDev, + config: mergeRecords(base.config, overrides.config), + rpc: mergeRecords(base.rpc as any, overrides.rpc as any) as RpcOverrides, + atoms: + overrides.atoms != null || base.atoms != null ? { ...base.atoms, ...overrides.atoms } : undefined, + electron: + overrides.electron != null || base.electron != null + ? { ...(base.electron ?? {}), ...(overrides.electron ?? {}) } + : undefined, + createBlock: overrides.createBlock ?? base.createBlock, + showContextMenu: overrides.showContextMenu ?? base.showContextMenu, + connStatus: mergeRecords(base.connStatus, overrides.connStatus), + mockWaveObjs: mergeRecords(base.mockWaveObjs, overrides.mockWaveObjs), + }; +} + function makeMockConfigAtoms(overrides?: Partial): WaveEnv["configAtoms"] { const overrideAtoms = new Map>(); if (overrides) { @@ -24,23 +67,17 @@ function makeMockConfigAtoms(overrides?: Partial): WaveEnv["config }); } -type MockIds = { - tabId?: string; - windowId?: string; - clientId?: string; -}; - -function makeMockGlobalAtoms(ids?: MockIds): GlobalAtomsType { - return { +function makeMockGlobalAtoms(atomOverrides?: Partial): GlobalAtomsType { + const defaults: GlobalAtomsType = { builderId: atom(""), builderAppId: atom("") as any, - uiContext: atom({ windowid: ids?.windowId ?? "", activetabid: ids?.tabId ?? "" } as UIContext), + uiContext: atom({} as UIContext), workspace: atom(null as Workspace), fullConfigAtom: atom(null) as any, waveaiModeConfigAtom: atom({}) as any, settingsAtom: atom({} as SettingsType), hasCustomAIPresetsAtom: atom(false), - staticTabId: atom(ids?.tabId ?? ""), + staticTabId: atom(""), isFullScreen: atom(false) as any, zoomFactorAtom: atom(1.0) as any, controlShiftDelayAtom: atom(false) as any, @@ -52,12 +89,12 @@ function makeMockGlobalAtoms(ids?: MockIds): GlobalAtomsType { reinitVersion: atom(0) as any, waveAIRateLimitInfoAtom: atom(null) as any, }; + if (!atomOverrides) { + return defaults; + } + return { ...defaults, ...atomOverrides }; } -type RpcOverrides = { - [K in keyof RpcApiType as K extends `${string}Command` ? K : never]?: (...args: any[]) => any; -}; - export function makeMockRpc(overrides?: RpcOverrides): RpcApiType { const dispatchMap = new Map any>(); if (overrides) { @@ -89,19 +126,62 @@ export function makeMockRpc(overrides?: RpcOverrides): RpcApiType { return rpc; } -export function makeMockWaveEnv(ids?: MockIds): WaveEnv { - return { - electron: previewElectronApi, - rpc: makeMockRpc(), - configAtoms: makeMockConfigAtoms(), - isDev: () => true, - atoms: makeMockGlobalAtoms(ids), - createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => { - console.log("[mock createBlock]", blockDef, { magnified, ephemeral }); - return Promise.resolve(crypto.randomUUID()); +export function applyMockEnvOverrides(env: WaveEnv, newOverrides: MockEnv): MockWaveEnv { + const existing = (env as MockWaveEnv).mockEnv; + const merged = existing != null ? mergeMockEnv(existing, newOverrides) : newOverrides; + return makeMockWaveEnv(merged); +} + +export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { + const overrides: MockEnv = mockEnv ?? {}; + const connStatusAtomCache = new Map>(); + const waveObjectAtomCache = new Map>(); + const blockMetaKeyAtomCache = new Map>(); + const env = { + mockEnv: overrides, + electron: overrides.electron ? { ...previewElectronApi, ...overrides.electron } : previewElectronApi, + rpc: makeMockRpc(overrides.rpc), + configAtoms: makeMockConfigAtoms(overrides.config), + atoms: makeMockGlobalAtoms(overrides.atoms), + isDev: () => overrides.isDev ?? true, + createBlock: + overrides.createBlock ?? + ((blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => { + console.log("[mock createBlock]", blockDef, { magnified, ephemeral }); + return Promise.resolve(crypto.randomUUID()); + }), + showContextMenu: + overrides.showContextMenu ?? + ((menu, e) => { + console.log("[mock showContextMenu]", menu, e); + }), + getConnStatusAtom: (conn: string) => { + if (!connStatusAtomCache.has(conn)) { + const connStatus = overrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn); + connStatusAtomCache.set(conn, atom(connStatus)); + } + return connStatusAtomCache.get(conn); }, - showContextMenu: (menu, e) => { - console.log("[mock showContextMenu]", menu, e); + getWaveObjectAtom: (oref: string) => { + if (!waveObjectAtomCache.has(oref)) { + const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; + waveObjectAtomCache.set(oref, atom(obj)); + } + return waveObjectAtomCache.get(oref) as PrimitiveAtom; + }, + getBlockMetaKeyAtom: (blockId: string, key: T) => { + const cacheKey = blockId + "#meta-" + key; + if (!blockMetaKeyAtomCache.has(cacheKey)) { + const metaAtom = atom((get) => { + const blockORef = "block:" + blockId; + const blockAtom = env.getWaveObjectAtom(blockORef); + const blockData = get(blockAtom); + return blockData?.meta?.[key] as MetaType[T]; + }); + blockMetaKeyAtomCache.set(cacheKey, metaAtom); + } + return blockMetaKeyAtomCache.get(cacheKey) as Atom; }, }; + return env; } diff --git a/frontend/preview/preview.tsx b/frontend/preview/preview.tsx index a461f137dc..9cb03c0014 100644 --- a/frontend/preview/preview.tsx +++ b/frontend/preview/preview.tsx @@ -7,6 +7,7 @@ import { GlobalModel } from "@/app/store/global-model"; import { globalStore } from "@/app/store/jotaiStore"; import { WaveEnvContext } from "@/app/waveenv/waveenv"; import { loadFonts } from "@/util/fontutil"; +import { atom, Provider } from "jotai"; import React, { lazy, Suspense, useRef } from "react"; import { createRoot } from "react-dom/client"; import { makeMockWaveEnv } from "./mock/mockwaveenv"; @@ -91,15 +92,18 @@ function PreviewHeader({ previewName }: { previewName: string }) { function PreviewRoot() { const waveEnvRef = useRef( makeMockWaveEnv({ - tabId: PreviewTabId, - windowId: PreviewWindowId, - clientId: PreviewClientId, + atoms: { + uiContext: atom({ windowid: PreviewWindowId, activetabid: PreviewTabId } as UIContext), + staticTabId: atom(PreviewTabId), + }, }) ); return ( - - - + + + + + ); } diff --git a/frontend/preview/previews/widgets.preview.tsx b/frontend/preview/previews/widgets.preview.tsx index c81afb1bc4..b6970da902 100644 --- a/frontend/preview/previews/widgets.preview.tsx +++ b/frontend/preview/previews/widgets.preview.tsx @@ -5,7 +5,7 @@ import { useWaveEnv, WaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; import { Widgets } from "@/app/workspace/widgets"; import { atom, useAtom } from "jotai"; import { useRef } from "react"; -import { makeMockRpc } from "../mock/mockwaveenv"; +import { applyMockEnvOverrides } from "../mock/mockwaveenv"; const workspaceAtom = atom(null as Workspace); const resizableHeightAtom = atom(250); @@ -14,7 +14,12 @@ function makeMockApp(name: string, icon: string, iconcolor: string): AppInfo { return { appid: `local/${name.toLowerCase().replace(/\s+/g, "-")}`, modtime: 0, - manifest: { appmeta: { title: name, shortdesc: "", icon, iconcolor }, configschema: {}, dataschema: {}, secrets: {} }, + manifest: { + appmeta: { title: name, shortdesc: "", icon, iconcolor }, + configschema: {}, + dataschema: {}, + secrets: {}, + }, }; } @@ -81,17 +86,15 @@ const mockWidgets: { [key: string]: WidgetConfigType } = { const fullConfigAtom = atom({ settings: {}, widgets: mockWidgets } as unknown as FullConfigType); function makeWidgetsEnv(baseEnv: WaveEnv, isDev: boolean, hasCustomAIPresets: boolean, apps?: AppInfo[]) { - return { - ...baseEnv, - rpc: makeMockRpc({ ListAllAppsCommand: () => Promise.resolve(apps ?? []) }), - isDev: () => isDev, + return applyMockEnvOverrides(baseEnv, { + isDev, + rpc: { ListAllAppsCommand: () => Promise.resolve(apps ?? []) }, atoms: { - ...baseEnv.atoms, fullConfigAtom, workspace: workspaceAtom, hasCustomAIPresetsAtom: atom(hasCustomAIPresets), }, - }; + }); } function WidgetsScenario({ @@ -108,7 +111,10 @@ function WidgetsScenario({ apps?: AppInfo[]; }) { const baseEnv = useWaveEnv(); - const envRef = useRef(makeWidgetsEnv(baseEnv, isDev, hasCustomAIPresets, apps)); + const envRef = useRef(null); + if (envRef.current == null) { + envRef.current = makeWidgetsEnv(baseEnv, isDev, hasCustomAIPresets, apps); + } return (
@@ -131,7 +137,10 @@ function WidgetsScenario({ function WidgetsResizable() { const [height, setHeight] = useAtom(resizableHeightAtom); const baseEnv = useWaveEnv(); - const envRef = useRef(makeWidgetsEnv(baseEnv, true, true, mockApps)); + const envRef = useRef(null); + if (envRef.current == null) { + envRef.current = makeWidgetsEnv(baseEnv, true, true, mockApps); + } return (
@@ -174,4 +183,3 @@ export function WidgetsPreview() {
); } - diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 6fbe95a0ea..d0b5f0e3b0 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -4,6 +4,7 @@ import { type Placement } from "@floating-ui/react"; import type * as jotai from "jotai"; import type * as rxjs from "rxjs"; +import type { WaveEnv } from "@/app/waveenv/waveenv"; declare global { type GlobalAtomsType = { @@ -28,8 +29,6 @@ declare global { waveAIRateLimitInfoAtom: jotai.PrimitiveAtom; }; - type WritableWaveObjectAtom = jotai.WritableAtom; - type ThrottledValueAtom = jotai.WritableAtom], void>; type AtomWithThrottle = { @@ -291,7 +290,14 @@ declare global { declare type ViewComponent = React.FC; - type ViewModelClass = new (blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) => ViewModel; + type ViewModelInitType = { + blockId: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; + waveEnv: WaveEnv; + }; + + type ViewModelClass = new (initOpts: ViewModelInitType) => ViewModel; interface ViewModel { // The type of view, used for identifying and rendering the appropriate component.