From 5d7781db495690035701d171c011ae09de5c65b5 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 12:45:40 -0800 Subject: [PATCH 01/11] tab model and provider --- frontend/app/store/tab-model.ts | 35 ++++++++++++++++++++++++++++ frontend/app/workspace/workspace.tsx | 5 +++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 frontend/app/store/tab-model.ts diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts new file mode 100644 index 0000000000..b1114a23b9 --- /dev/null +++ b/frontend/app/store/tab-model.ts @@ -0,0 +1,35 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { createContext, useContext } from "react"; + +class TabModel { + tabId: string; + + constructor(tabId: string) { + this.tabId = tabId; + } +} + +const tabModelCache = new Map(); + +function getTabModelByTabId(tabId: string): TabModel { + let model = tabModelCache.get(tabId); + if (model == null) { + model = new TabModel(tabId); + tabModelCache.set(tabId, model); + } + return model; +} + +const TabModelContext = createContext(undefined); + +function useTabModel(): TabModel { + const model = useContext(TabModelContext); + if (model == null) { + throw new Error("useTabModel must be used within a TabModelProvider"); + } + return model; +} + +export { getTabModelByTabId, TabModel, TabModelContext, useTabModel }; \ No newline at end of file diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 43af38de4c..9a0ec1431e 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -7,6 +7,7 @@ import { CenteredDiv } from "@/app/element/quickelems"; import { ModalsRenderer } from "@/app/modals/modalsrenderer"; import { TabBar } from "@/app/tab/tabbar"; import { TabContent } from "@/app/tab/tabcontent"; +import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model"; import { Widgets } from "@/app/workspace/widgets"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { atoms, getApi } from "@/store/global"; @@ -78,7 +79,9 @@ const WorkspaceElem = memo(() => { No Active Tab ) : (
- + + +
)} From d2b681e569a580549a5edefcc1ef52f5abf5f93c Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 12:57:00 -0800 Subject: [PATCH 02/11] new atoms to use in tab-model --- frontend/app/block/blockframe.tsx | 12 ++++++------ frontend/app/store/global.ts | 4 ---- frontend/app/store/tab-model.ts | 24 ++++++++++++++++++++++++ frontend/types/custom.d.ts | 1 - 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 9da0d6340b..656a15e032 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -17,6 +17,7 @@ import { useBlockAtom, WOS, } from "@/app/store/global"; +import { useTabModel } from "@/app/store/tab-model"; import { uxCloseBlock } from "@/app/store/keymodel"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -492,6 +493,7 @@ const ConnStatusOverlay = React.memo( ); const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { + const tabModel = useTabModel(); const isFocused = jotai.useAtomValue(nodeModel.isFocused); const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral); const blockNum = jotai.useAtomValue(nodeModel.blockNum); @@ -499,12 +501,12 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true; const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); + const tabActiveBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:activebordercolor")); + const tabBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:bordercolor")); const style: React.CSSProperties = {}; let showBlockMask = false; if (isFocused) { - const tabData = jotai.useAtomValue(atoms.tabAtom); - const tabActiveBorderColor = tabData?.meta?.["bg:activebordercolor"]; if (tabActiveBorderColor) { style.borderColor = tabActiveBorderColor; } @@ -512,8 +514,6 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { style.borderColor = blockData.meta["frame:activebordercolor"]; } } else { - const tabData = jotai.useAtomValue(atoms.tabAtom); - const tabBorderColor = tabData?.meta?.["bg:bordercolor"]; if (tabBorderColor) { style.borderColor = tabBorderColor; } @@ -674,13 +674,13 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component; const BlockFrame = React.memo((props: BlockFrameProps) => { + const tabModel = useTabModel(); const blockId = props.nodeModel.blockId; const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); - const tabData = jotai.useAtomValue(atoms.tabAtom); + const numBlocks = jotai.useAtomValue(tabModel.tabNumBlocksAtom); if (!blockId || !blockData) { return null; } - const numBlocks = tabData?.blockids?.length ?? 0; return ; }); diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 35eb9f1585..7a68591d20 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -140,9 +140,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } return false; }) as Atom; - const tabAtom: Atom = atom((get) => { - return WOS.getObjectValue(WOS.makeORef("tab", initOpts.tabId), get); - }); // this is *the* tab that this tabview represents. it should never change. const staticTabIdAtom: Atom = atom(initOpts.tabId); const controlShiftDelayAtom = atom(false); @@ -201,7 +198,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { waveaiModeConfigAtom, settingsAtom, hasCustomAIPresetsAtom, - tabAtom, staticTabId: staticTabIdAtom, isFullScreen: isFullScreenAtom, zoomFactorAtom, diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index b1114a23b9..772c22a836 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -1,13 +1,37 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { atom, Atom } from "jotai"; import { createContext, useContext } from "react"; +import * as WOS from "./wos"; class TabModel { tabId: string; + tabAtom: Atom; + tabNumBlocksAtom: Atom; + metaCache: Map> = new Map(); constructor(tabId: string) { this.tabId = tabId; + this.tabAtom = atom((get) => { + return WOS.getObjectValue(WOS.makeORef("tab", this.tabId), get); + }); + this.tabNumBlocksAtom = atom((get) => { + const tabData = get(this.tabAtom); + return tabData?.blockids?.length ?? 0; + }); + } + + getTabMetaAtom(metaKey: T): Atom { + let metaAtom = this.metaCache.get(metaKey); + if (metaAtom == null) { + metaAtom = atom((get) => { + const tabData = get(this.tabAtom); + return tabData?.meta?.[metaKey]; + }); + this.metaCache.set(metaKey, metaAtom); + } + return metaAtom; } } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe5392..cfd16db97e 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -19,7 +19,6 @@ declare global { waveaiModeConfigAtom: jotai.PrimitiveAtom>; // resolved AI mode configs -- updated via WebSocket settingsAtom: jotai.Atom; // derrived from fullConfig hasCustomAIPresetsAtom: jotai.Atom; // derived from fullConfig - tabAtom: jotai.Atom; // driven from WOS staticTabId: jotai.Atom; isFullScreen: jotai.PrimitiveAtom; zoomFactorAtom: jotai.PrimitiveAtom; From 9d19f3be99a68ba3e436efc77bf2e8296c605ced Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 13:33:02 -0800 Subject: [PATCH 03/11] push tab model through view models. move multiinput atom --- frontend/app/block/block.tsx | 12 ++++++---- frontend/app/store/global.ts | 1 - frontend/app/store/keymodel.ts | 9 ++++++-- frontend/app/store/tab-model.ts | 16 +++++++++++-- frontend/app/view/aifilediff/aifilediff.tsx | 8 ++++++- frontend/app/view/helpview/helpview.tsx | 5 ++-- frontend/app/view/launcher/launcher.tsx | 8 ++++++- frontend/app/view/preview/preview-model.tsx | 5 +++- .../app/view/quicktipsview/quicktipsview.tsx | 10 +++++++- frontend/app/view/sysinfo/sysinfo.tsx | 16 ++++++++++--- frontend/app/view/term/term-model.ts | 23 +++++++++---------- frontend/app/view/term/term.tsx | 4 +++- frontend/app/view/tsunami/tsunami.tsx | 5 ++-- frontend/app/view/vdom/vdom-model.tsx | 5 +++- frontend/app/view/waveai/waveai.tsx | 10 ++++++-- .../app/view/waveconfig/waveconfig-model.ts | 5 +++- frontend/app/view/webview/webview.tsx | 5 +++- frontend/types/custom.d.ts | 3 +-- frontend/wave.ts | 2 ++ 19 files changed, 112 insertions(+), 40 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index ea2527bb2a..1d6da87248 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -24,6 +24,8 @@ 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"; @@ -55,10 +57,10 @@ BlockRegistry.set("tsunami", TsunamiViewModel); BlockRegistry.set("aifilediff", AiFileDiffViewModel); BlockRegistry.set("waveconfig", WaveConfigViewModel); -function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel { +function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel, tabModel: TabModel): ViewModel { const ctor = BlockRegistry.get(blockView); if (ctor != null) { - return new ctor(blockId, nodeModel); + return new ctor(blockId, nodeModel, tabModel); } return makeDefaultViewModel(blockId, blockView); } @@ -261,11 +263,12 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { const Block = memo((props: BlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); + const tabModel = useTabModel(); 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); + viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { @@ -286,11 +289,12 @@ const Block = memo((props: BlockProps) => { const SubBlock = memo((props: SubBlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); + const tabModel = useTabModel(); 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); + viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 7a68591d20..32342776bc 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -211,7 +211,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { notifications: notificationsAtom, notificationPopoverMode: notificationPopoverModeAtom, reinitVersion, - isTermMultiInput: atom(false), waveAIRateLimitInfoAtom: rateLimitInfoAtom, }; } diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 32b33abcf4..6f023b1ed9 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -19,6 +19,7 @@ import { replaceBlock, WOS, } from "@/app/store/global"; +import { getActiveTabModel } from "@/app/store/tab-model"; import { TabBarModel } from "@/app/tab/tabbar-model"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { deleteLayoutModelForTab, getLayoutModelForStaticTab, NavigateDirection } from "@/layout/index"; @@ -585,12 +586,16 @@ function registerGlobalKeys() { } }); globalKeyMap.set("Ctrl:Shift:i", () => { - const curMI = globalStore.get(atoms.isTermMultiInput); + const tabModel = getActiveTabModel(); + if (tabModel == null) { + return true; + } + const curMI = globalStore.get(tabModel.isTermMultiInput); if (!curMI && countTermBlocks() <= 1) { // don't turn on multi-input unless there are 2 or more basic term blocks return true; } - globalStore.set(atoms.isTermMultiInput, !curMI); + globalStore.set(tabModel.isTermMultiInput, !curMI); return true; }); for (let idx = 1; idx <= 9; idx++) { diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index 772c22a836..5d46a1fb61 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -1,14 +1,16 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { atom, Atom } from "jotai"; +import { atom, Atom, PrimitiveAtom } from "jotai"; import { createContext, useContext } from "react"; +import { globalStore } from "./jotaiStore"; import * as WOS from "./wos"; class TabModel { tabId: string; tabAtom: Atom; tabNumBlocksAtom: Atom; + isTermMultiInput = atom(false) as PrimitiveAtom; metaCache: Map> = new Map(); constructor(tabId: string) { @@ -37,6 +39,8 @@ class TabModel { const tabModelCache = new Map(); +const activeTabIdAtom = atom(null) as PrimitiveAtom; + function getTabModelByTabId(tabId: string): TabModel { let model = tabModelCache.get(tabId); if (model == null) { @@ -46,6 +50,14 @@ function getTabModelByTabId(tabId: string): TabModel { return model; } +function getActiveTabModel(): TabModel | null { + const activeTabId = globalStore.get(activeTabIdAtom); + if (activeTabId == null) { + return null; + } + return getTabModelByTabId(activeTabId); +} + const TabModelContext = createContext(undefined); function useTabModel(): TabModel { @@ -56,4 +68,4 @@ function useTabModel(): TabModel { return model; } -export { getTabModelByTabId, TabModel, TabModelContext, useTabModel }; \ No newline at end of file +export { activeTabIdAtom, getActiveTabModel, getTabModelByTabId, TabModel, TabModelContext, useTabModel }; diff --git a/frontend/app/view/aifilediff/aifilediff.tsx b/frontend/app/view/aifilediff/aifilediff.tsx index ce56c1b0f7..0d84b52fb3 100644 --- a/frontend/app/view/aifilediff/aifilediff.tsx +++ b/frontend/app/view/aifilediff/aifilediff.tsx @@ -1,6 +1,8 @@ // Copyright 2025, 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"; @@ -17,6 +19,8 @@ type DiffData = { export class AiFileDiffViewModel implements ViewModel { blockId: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; viewType = "aifilediff"; blockAtom: jotai.Atom; diffDataAtom: jotai.PrimitiveAtom; @@ -26,8 +30,10 @@ export class AiFileDiffViewModel implements ViewModel { viewName: jotai.Atom; viewText: jotai.Atom; - constructor(blockId: string) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.blockId = blockId; + this.nodeModel = nodeModel; + this.tabModel = tabModel; this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); this.diffDataAtom = jotai.atom(null) as jotai.PrimitiveAtom; this.errorAtom = jotai.atom(null) as jotai.PrimitiveAtom; diff --git a/frontend/app/view/helpview/helpview.tsx b/frontend/app/view/helpview/helpview.tsx index b683a49762..ef7cc89072 100644 --- a/frontend/app/view/helpview/helpview.tsx +++ b/frontend/app/view/helpview/helpview.tsx @@ -2,6 +2,7 @@ // 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"; @@ -15,8 +16,8 @@ class HelpViewModel extends WebViewModel { return HelpView; } - constructor(blockId: string, nodeModel: BlockNodeModel) { - super(blockId, nodeModel); + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + super(blockId, nodeModel, tabModel); this.viewText = atom((get) => { // force a dependency on meta.url so we re-render the buttons when the url changes 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 cdde41e19d..6bcea92424 100644 --- a/frontend/app/view/launcher/launcher.tsx +++ b/frontend/app/view/launcher/launcher.tsx @@ -1,6 +1,8 @@ // Copyright 2025, 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 { atoms, globalStore, replaceBlock } from "@/app/store/global"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; @@ -20,6 +22,8 @@ type GridLayoutType = { columns: number; tileWidth: number; tileHeight: number; export class LauncherViewModel implements ViewModel { blockId: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; viewType = "launcher"; viewIcon = atom("shapes"); viewName = atom("Widget Launcher"); @@ -31,8 +35,10 @@ export class LauncherViewModel implements ViewModel { containerSize = atom({ width: 0, height: 0 }); gridLayout: GridLayoutType = null; - constructor(blockId: string) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.blockId = blockId; + this.nodeModel = nodeModel; + this.tabModel = tabModel; } filteredWidgetsAtom = atom((get) => { diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index b6aa8313ba..7a817fbccd 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -2,6 +2,7 @@ // 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 { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -118,6 +119,7 @@ export class PreviewModel implements ViewModel { viewType: string; blockId: string; nodeModel: BlockNodeModel; + tabModel: TabModel; noPadding?: Atom; blockAtom: Atom; viewIcon: Atom; @@ -167,10 +169,11 @@ export class PreviewModel implements ViewModel { showS3 = atom(true); - constructor(blockId: string, nodeModel: BlockNodeModel) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.viewType = "preview"; this.blockId = blockId; this.nodeModel = nodeModel; + this.tabModel = tabModel; let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true; this.showHiddenFiles = atom(showHiddenFiles); this.refreshVersion = atom(0); diff --git a/frontend/app/view/quicktipsview/quicktipsview.tsx b/frontend/app/view/quicktipsview/quicktipsview.tsx index edbf8e02cb..ec79e4e3f7 100644 --- a/frontend/app/view/quicktipsview/quicktipsview.tsx +++ b/frontend/app/view/quicktipsview/quicktipsview.tsx @@ -1,16 +1,24 @@ // Copyright 2025, 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 { Atom, atom, PrimitiveAtom } from "jotai"; class QuickTipsViewModel implements ViewModel { viewType: string; + blockId: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; showTocAtom: PrimitiveAtom; endIconButtons: Atom; - constructor() { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + this.blockId = blockId; + this.nodeModel = nodeModel; + this.tabModel = tabModel; this.viewType = "tips"; this.showTocAtom = atom(false); } diff --git a/frontend/app/view/sysinfo/sysinfo.tsx b/frontend/app/view/sysinfo/sysinfo.tsx index e1296dc503..645861a6e9 100644 --- a/frontend/app/view/sysinfo/sysinfo.tsx +++ b/frontend/app/view/sysinfo/sysinfo.tsx @@ -1,6 +1,8 @@ // Copyright 2025, 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 * as util from "@/util/util"; import * as Plot from "@observablehq/plot"; @@ -92,6 +94,8 @@ function convertWaveEventToDataItem(event: WaveEvent): DataItem { class SysinfoViewModel implements ViewModel { viewType: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; blockAtom: jotai.Atom; termMode: jotai.Atom; htmlElemFocusRef: React.RefObject; @@ -114,8 +118,10 @@ class SysinfoViewModel implements ViewModel { endIconButtons: jotai.Atom; plotTypeSelectedAtom: jotai.Atom; - constructor(blockId: string, viewType: string) { - this.viewType = viewType; + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + this.nodeModel = nodeModel; + this.tabModel = tabModel; + this.viewType = "sysinfo"; this.blockId = blockId; this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); this.addInitialDataAtom = jotai.atom(null, (get, set, points) => { @@ -536,7 +542,11 @@ const SysinfoViewInner = React.memo(({ model }: SysinfoViewProps) => { className="flex flex-col flex-grow mb-0 overflow-y-auto" options={{ scrollbars: { autoHide: "leave" } }} > -
+
{yvals.map((yval, idx) => { return ( = { current: null }; blockAtom: jotai.Atom; @@ -67,9 +69,10 @@ export class TermViewModel implements ViewModel { isRestarting: jotai.PrimitiveAtom; searchAtoms?: SearchAtoms; - constructor(blockId: string, nodeModel: BlockNodeModel) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.viewType = "term"; this.blockId = blockId; + this.tabModel = tabModel; this.termWshClient = new TermWshClient(blockId, this); DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient); this.nodeModel = nodeModel; @@ -181,7 +184,7 @@ export class TermViewModel implements ViewModel { } } } - const isMI = get(atoms.isTermMultiInput); + const isMI = get(this.tabModel.isTermMultiInput); if (isMI && this.isBasicTerm(get)) { rtn.push({ elemtype: "textbutton", @@ -189,7 +192,7 @@ export class TermViewModel implements ViewModel { className: "yellow !py-[2px] !px-[10px] text-[11px] font-[500]", title: "Input will be sent to all connected terminals (click to disable)", onClick: () => { - globalStore.set(atoms.isTermMultiInput, false); + globalStore.set(this.tabModel.isTermMultiInput, false); }, }); } @@ -502,16 +505,16 @@ export class TermViewModel implements ViewModel { if (isMacOS()) { return false; } - + // Get the app:ctrlvpaste setting const ctrlVPasteAtom = getSettingsKeyAtom("app:ctrlvpaste"); const ctrlVPasteSetting = globalStore.get(ctrlVPasteAtom); - + // If setting is explicitly set, use it if (ctrlVPasteSetting != null) { return ctrlVPasteSetting; } - + // Default behavior: Windows=true, Linux/other=false return isWindows(); } @@ -545,7 +548,7 @@ export class TermViewModel implements ViewModel { return false; } } - + // Check for Ctrl-V paste (platform-dependent) if (this.shouldHandleCtrlVPaste() && keyutil.checkKeyPressed(waveEvent, "Ctrl:v")) { event.preventDefault(); @@ -553,7 +556,7 @@ export class TermViewModel implements ViewModel { getApi().nativePaste(); return false; } - + if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) { event.preventDefault(); event.stopPropagation(); @@ -881,7 +884,3 @@ export function getAllBasicTermModels(): TermViewModel[] { } return termModels; } - -export function makeTerminalModel(blockId: string, nodeModel: BlockNodeModel): TermViewModel { - return new TermViewModel(blockId, nodeModel); -} diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752d..9b0b5a7bd7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -3,6 +3,7 @@ import { Block, SubBlock } from "@/app/block/block"; import { Search, useSearch } from "@/app/element/search"; +import { useTabModel } from "@/app/store/tab-model"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -164,11 +165,12 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => } const termModeRef = React.useRef(termMode); + const tabModel = useTabModel(); const termFontSize = jotai.useAtomValue(model.fontSizeAtom); const fullConfig = globalStore.get(atoms.fullConfigAtom); const connFontFamily = fullConfig.connections?.[blockData?.meta?.connection]?.["term:fontfamily"]; const isFocused = jotai.useAtomValue(model.nodeModel.isFocused); - const isMI = jotai.useAtomValue(atoms.isTermMultiInput); + const isMI = jotai.useAtomValue(tabModel.isTermMultiInput); const isBasicTerm = termMode != "vdom" && blockData?.meta?.controller != "cmd"; // needs to match isBasicTerm // search diff --git a/frontend/app/view/tsunami/tsunami.tsx b/frontend/app/view/tsunami/tsunami.tsx index 84e5706e5b..cca2cb7067 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; +import type { TabModel } from "@/app/store/tab-model"; import { atoms, getApi, globalStore, WOS } from "@/app/store/global"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; @@ -20,8 +21,8 @@ class TsunamiViewModel extends WebViewModel { viewName: jotai.Atom; viewIconColor: jotai.Atom; - constructor(blockId: string, nodeModel: BlockNodeModel) { - super(blockId, nodeModel); + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + super(blockId, nodeModel, tabModel); this.viewType = "tsunami"; this.isRestarting = jotai.atom(false); diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index 745d808ee8..fbe556daba 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -2,6 +2,7 @@ // 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 { makeORef } from "@/app/store/wos"; import { waveEventSubscribe } from "@/app/store/wps"; @@ -105,6 +106,7 @@ class VDomWshClient extends WshClient { export class VDomModel { blockId: string; nodeModel: BlockNodeModel; + tabModel: TabModel; viewType: string; viewIcon: jotai.Atom; viewName: jotai.Atom; @@ -138,10 +140,11 @@ export class VDomModel { hasBackendWork: boolean = false; noPadding: jotai.PrimitiveAtom; - constructor(blockId: string, nodeModel: BlockNodeModel) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.viewType = "vdom"; this.blockId = blockId; this.nodeModel = nodeModel; + this.tabModel = tabModel; this.contextActive = jotai.atom(false); this.reset(); this.viewIcon = jotai.atom("bolt"); diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 048a76b487..7ca1f0626c 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -1,6 +1,8 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { BlockNodeModel } from "@/app/block/blocktypes"; +import type { TabModel } from "@/app/store/tab-model"; import { Button } from "@/app/element/button"; import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; @@ -64,6 +66,8 @@ class AiWshClient extends WshClient { export class WaveAiModel implements ViewModel { viewType: string; blockId: string; + nodeModel: BlockNodeModel; + tabModel: TabModel; blockAtom: Atom; presetKey: Atom; presetMap: Atom<{ [k: string]: MetaType }>; @@ -86,13 +90,15 @@ export class WaveAiModel implements ViewModel { cancel: boolean; aiWshClient: AiWshClient; - constructor(blockId: string) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { + this.blockId = blockId; + this.nodeModel = nodeModel; + this.tabModel = tabModel; this.aiWshClient = new AiWshClient(blockId, this); DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.aiWshClient); this.locked = atom(false); this.cancel = false; this.viewType = "waveai"; - this.blockId = blockId; this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); this.viewIcon = atom("sparkles"); this.viewName = atom("Wave AI"); diff --git a/frontend/app/view/waveconfig/waveconfig-model.ts b/frontend/app/view/waveconfig/waveconfig-model.ts index cd5c58ccbd..7c2af88d6d 100644 --- a/frontend/app/view/waveconfig/waveconfig-model.ts +++ b/frontend/app/view/waveconfig/waveconfig-model.ts @@ -2,6 +2,7 @@ // 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 { RpcApi } from "@/app/store/wshclientapi"; @@ -142,6 +143,7 @@ export class WaveConfigViewModel implements ViewModel { viewComponent = WaveConfigView; noPadding = atom(true); nodeModel: BlockNodeModel; + tabModel: TabModel; selectedFileAtom: PrimitiveAtom; fileContentAtom: PrimitiveAtom; @@ -168,9 +170,10 @@ export class WaveConfigViewModel implements ViewModel { storageBackendErrorAtom: PrimitiveAtom; secretValueRef: HTMLTextAreaElement | null = null; - constructor(blockId: string, nodeModel: BlockNodeModel) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.blockId = blockId; this.nodeModel = nodeModel; + this.tabModel = tabModel; this.configDir = getApi().getConfigDir(); const platform = getApi().getPlatform(); this.saveShortcut = platform === "darwin" ? "Cmd+S" : "Alt+S"; diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 5695f99e63..bfa3476bd3 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -2,6 +2,7 @@ // 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"; @@ -44,6 +45,7 @@ function getWebviewPreloadUrl() { export class WebViewModel implements ViewModel { viewType: string; blockId: string; + tabModel: TabModel; noPadding?: Atom; blockAtom: Atom; viewIcon: Atom; @@ -69,8 +71,9 @@ export class WebViewModel implements ViewModel { partitionOverride: PrimitiveAtom | null; userAgentType: Atom; - constructor(blockId: string, nodeModel: BlockNodeModel) { + constructor(blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) { this.nodeModel = nodeModel; + this.tabModel = tabModel; this.viewType = "web"; this.blockId = blockId; this.noPadding = atom(true); diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index cfd16db97e..23e31f4d1e 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -32,7 +32,6 @@ declare global { notifications: jotai.PrimitiveAtom; notificationPopoverMode: jotai.Atom; reinitVersion: jotai.PrimitiveAtom; - isTermMultiInput: jotai.PrimitiveAtom; waveAIRateLimitInfoAtom: jotai.PrimitiveAtom; }; @@ -286,7 +285,7 @@ declare global { declare type ViewComponent = React.FC; - type ViewModelClass = new (blockId: string, nodeModel: BlockNodeModel) => ViewModel; + type ViewModelClass = new (blockId: string, nodeModel: BlockNodeModel, tabModel: TabModel) => ViewModel; interface ViewModel { // The type of view, used for identifying and rendering the appropriate component. diff --git a/frontend/wave.ts b/frontend/wave.ts index dd41769681..aa2698aae8 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -30,6 +30,7 @@ import { removeNotificationById, subscribeToConnEvents, } from "@/store/global"; +import { activeTabIdAtom } from "@/store/tab-model"; import * as WOS from "@/store/wos"; import { loadFonts } from "@/util/fontutil"; import { setKeyUtilPlatform } from "@/util/keyutil"; @@ -162,6 +163,7 @@ async function initWave(initOpts: WaveInitOpts) { "platform", platform ); + globalStore.set(activeTabIdAtom, initOpts.tabId); initGlobal({ tabId: initOpts.tabId, clientId: initOpts.clientId, From 42d4c8769ac270a5384b8458df2dd8935504250e Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 13:34:34 -0800 Subject: [PATCH 04/11] remove unused typeaheadmodalatom --- frontend/app/store/global.ts | 2 -- frontend/types/custom.d.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 32342776bc..07159b7641 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -172,7 +172,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { }); } - const typeAheadModalAtom = atom({}); const modalOpen = atom(false); const allConnStatusAtom = atom((get) => { const connStatusMap = get(ConnStatusMapAtom); @@ -204,7 +203,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { controlShiftDelayAtom, updaterStatusAtom, prefersReducedMotionAtom, - typeAheadModalAtom, modalOpen, allConnStatus: allConnStatusAtom, flashErrors: flashErrorsAtom, diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 23e31f4d1e..5b645c3cd1 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -25,7 +25,6 @@ declare global { controlShiftDelayAtom: jotai.PrimitiveAtom; prefersReducedMotionAtom: jotai.Atom; updaterStatusAtom: jotai.PrimitiveAtom; - typeAheadModalAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom; allConnStatus: jotai.Atom; flashErrors: jotai.PrimitiveAtom; From 0763aaecb839434177187d0c8e48441104bca4da Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 13:47:04 -0800 Subject: [PATCH 05/11] use tab model here --- frontend/app/aipanel/aipanel.tsx | 4 +++- frontend/app/store/global.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 8d1d8d36f0..3e1aaa3bbf 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -6,6 +6,7 @@ import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils"; import { ErrorBoundary } from "@/app/element/errorboundary"; import { atoms, getSettingsKeyAtom } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; +import { useTabModel } from "@/app/store/tab-model"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isMacOS, isWindows } from "@/util/platformutil"; import { cn } from "@/util/util"; @@ -233,6 +234,7 @@ const AIPanelComponentInner = memo(() => { const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom); const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom()); + const tabModel = useTabModel(); const { messages, sendMessage, status, setMessages, error, stop } = useChat({ transport: new DefaultChatTransport({ @@ -249,7 +251,7 @@ const AIPanelComponentInner = memo(() => { body.builderid = globalStore.get(atoms.builderId); body.builderappid = globalStore.get(atoms.builderAppId); } else { - body.tabid = globalStore.get(atoms.staticTabId); + body.tabid = tabModel.tabId; } return { body }; }, diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 07159b7641..190c436a76 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -210,7 +210,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { notificationPopoverMode: notificationPopoverModeAtom, reinitVersion, waveAIRateLimitInfoAtom: rateLimitInfoAtom, - }; + } as GlobalAtomsType; } function initGlobalWaveEventSubs(initOpts: WaveInitOpts) { From 4980ca0933c9aab4da5f7218e962a63806da77ce Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 13:51:51 -0800 Subject: [PATCH 06/11] use tabmodel tabid --- frontend/app/view/term/term.tsx | 1 + frontend/app/view/term/termwrap.ts | 6 ++++-- frontend/app/view/tsunami/tsunami.tsx | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 9b0b5a7bd7..7e6272003a 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -266,6 +266,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termMacOptionIsMeta = globalStore.get(termMacOptionIsMetaAtom) ?? false; const wasFocused = model.termRef.current != null && globalStore.get(model.nodeModel.isFocused); const termWrap = new TermWrap( + tabModel.tabId, blockId, connectElemRef.current, { diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 8ecaef08cd..f56eed8df7 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -330,6 +330,7 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t } export class TermWrap { + tabId: string; blockId: string; ptyOffset: number; dataBytesProcessed: number; @@ -368,12 +369,14 @@ export class TermWrap { lastPasteTime: number = 0; constructor( + tabId: string, blockId: string, connectElem: HTMLDivElement, options: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions, waveOptions: TermWrapOptions ) { this.loaded = false; + this.tabId = tabId; this.blockId = blockId; this.sendDataHandler = waveOptions.sendDataHandler; this.ptyOffset = 0; @@ -685,11 +688,10 @@ export class TermWrap { async resyncController(reason: string) { dlog("resync controller", this.blockId, reason); - const tabId = globalStore.get(atoms.staticTabId); const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } }; try { await RpcApi.ControllerResyncCommand(TabRpcClient, { - tabid: tabId, + tabid: this.tabId, blockid: this.blockId, rtopts: rtOpts, }); diff --git a/frontend/app/view/tsunami/tsunami.tsx b/frontend/app/view/tsunami/tsunami.tsx index cca2cb7067..dbebb824b3 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -2,8 +2,8 @@ // 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 { atoms, getApi, globalStore, WOS } from "@/app/store/global"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -107,7 +107,7 @@ class TsunamiViewModel extends WebViewModel { this.triggerRestartAtom(); } const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, { - tabid: globalStore.get(atoms.staticTabId), + tabid: this.tabModel.tabId, blockid: this.blockId, forcerestart: forceRestart, }); @@ -135,7 +135,7 @@ class TsunamiViewModel extends WebViewModel { await new Promise((resolve) => setTimeout(resolve, 300)); // Then resync to restart it await RpcApi.ControllerResyncCommand(TabRpcClient, { - tabid: globalStore.get(atoms.staticTabId), + tabid: this.tabModel.tabId, blockid: this.blockId, forcerestart: false, }); From 7903a59d0335ae8284511eb5460d5fda3f1a13f1 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 23:05:23 -0800 Subject: [PATCH 07/11] add global-model --- frontend/app/store/global-model.ts | 60 ++++++++++++++++++++++++++++++ frontend/app/store/global.ts | 10 ----- frontend/app/store/tab-model.ts | 7 ++-- frontend/types/custom.d.ts | 10 +++++ 4 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 frontend/app/store/global-model.ts diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts new file mode 100644 index 0000000000..c255afd47a --- /dev/null +++ b/frontend/app/store/global-model.ts @@ -0,0 +1,60 @@ +// Copyright 2025, Command Line Inc +// SPDX-License-Identifier: Apache-2.0 + +import * as WOS from "@/app/store/wos"; +import { atom, Atom } from "jotai"; + +class GlobalModel { + private static instance: GlobalModel; + + clientId: string; + windowId: string; + builderId: string; + platform: NodeJS.Platform; + + clientAtom!: Atom; + windowDataAtom!: Atom; + workspaceAtom!: Atom; + + private constructor() { + // private constructor for singleton pattern + } + + static getInstance(): GlobalModel { + if (!GlobalModel.instance) { + GlobalModel.instance = new GlobalModel(); + } + return GlobalModel.instance; + } + + async initialize(initOpts: GlobalInitOptions): Promise { + this.clientId = initOpts.clientId; + this.windowId = initOpts.windowId; + this.builderId = initOpts.builderId; + this.platform = initOpts.platform; + + this.clientAtom = atom((get) => { + if (this.clientId == null) { + return null; + } + return WOS.getObjectValue(WOS.makeORef("client", this.clientId), get); + }); + + this.windowDataAtom = atom((get) => { + if (this.windowId == null) { + return null; + } + return WOS.getObjectValue(WOS.makeORef("window", this.windowId), get); + }); + + this.workspaceAtom = atom((get) => { + const windowData = get(this.windowDataAtom); + if (windowData == null) { + return null; + } + return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); + }); + } +} + +export { GlobalModel }; \ No newline at end of file diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 190c436a76..4a43cbd0ff 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -40,16 +40,6 @@ const Counters = new Map(); const ConnStatusMapAtom = atom(new Map>()); const orefAtomCache = new Map>>(); -type GlobalInitOptions = { - tabId?: string; - platform: NodeJS.Platform; - windowId: string; - clientId: string; - environment: "electron" | "renderer"; - primaryTabStartup?: boolean; - builderId?: string; -}; - function initGlobal(initOpts: GlobalInitOptions) { globalEnvironment = initOpts.environment; globalPrimaryTabStartup = initOpts.primaryTabStartup ?? false; diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index 5d46a1fb61..daded66a0d 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -6,6 +6,9 @@ import { createContext, useContext } from "react"; import { globalStore } from "./jotaiStore"; import * as WOS from "./wos"; +const tabModelCache = new Map(); +const activeTabIdAtom = atom(null) as PrimitiveAtom; + class TabModel { tabId: string; tabAtom: Atom; @@ -37,10 +40,6 @@ class TabModel { } } -const tabModelCache = new Map(); - -const activeTabIdAtom = atom(null) as PrimitiveAtom; - function getTabModelByTabId(tabId: string): TabModel { let model = tabModelCache.get(tabId); if (model == null) { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 5b645c3cd1..efafce4a46 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -57,6 +57,16 @@ declare global { blockId: string; }; + type GlobalInitOptions = { + tabId?: string; + platform: NodeJS.Platform; + windowId: string; + clientId: string; + environment: "electron" | "renderer"; + primaryTabStartup?: boolean; + builderId?: string; + }; + type WaveInitOpts = { tabId: string; clientId: string; From 1e099e0ab3c13a6dcb9fce4e558492374fe203dc Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 23:17:33 -0800 Subject: [PATCH 08/11] move clientid to global model --- .../app/onboarding/onboarding-features.tsx | 3 +- .../onboarding/onboarding-upgrade-minor.tsx | 9 +++-- .../onboarding/onboarding-upgrade-patch.tsx | 3 +- frontend/app/onboarding/onboarding.tsx | 5 ++- frontend/app/store/global.ts | 7 +--- frontend/app/view/waveai/waveai.tsx | 3 +- frontend/types/custom.d.ts | 1 - frontend/wave.ts | 40 ++++++------------- 8 files changed, 28 insertions(+), 43 deletions(-) diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx index c81af794d7..5debe6055f 100644 --- a/frontend/app/onboarding/onboarding-features.tsx +++ b/frontend/app/onboarding/onboarding-features.tsx @@ -5,6 +5,7 @@ import Logo from "@/app/asset/logo.svg"; import { Button } from "@/app/element/button"; import { EmojiButton } from "@/app/element/emojibutton"; import { MagnifyIcon } from "@/app/element/magnify"; +import { GlobalModel } from "@/app/store/global-model"; import { atoms, globalStore } from "@/app/store/global"; import * as WOS from "@/app/store/wos"; import { RpcApi } from "@/app/store/wshclientapi"; @@ -314,7 +315,7 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) = const [currentPage, setCurrentPage] = useState("waveai"); useEffect(() => { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding-upgrade-minor.tsx b/frontend/app/onboarding/onboarding-upgrade-minor.tsx index d44ca57713..e19f469530 100644 --- a/frontend/app/onboarding/onboarding-upgrade-minor.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-minor.tsx @@ -6,6 +6,7 @@ import { Button } from "@/app/element/button"; import { FlexiModal } from "@/app/modals/modal"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; import { OnboardingFeatures } from "@/app/onboarding/onboarding-features"; +import { GlobalModel } from "@/app/store/global-model"; import { atoms, globalStore } from "@/app/store/global"; import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; @@ -60,7 +61,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -78,7 +79,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -95,7 +96,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": false }, @@ -104,7 +105,7 @@ const UpgradeOnboardingMinor = () => { }; const handleFeaturesComplete = () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding-upgrade-patch.tsx b/frontend/app/onboarding/onboarding-upgrade-patch.tsx index ee1d7795db..9348c75512 100644 --- a/frontend/app/onboarding/onboarding-upgrade-patch.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-patch.tsx @@ -5,6 +5,7 @@ import Logo from "@/app/asset/logo.svg"; import { Button } from "@/app/element/button"; import { FlexiModal } from "@/app/modals/modal"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; +import { GlobalModel } from "@/app/store/global-model"; import { atoms, globalStore } from "@/app/store/global"; import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; @@ -91,7 +92,7 @@ const UpgradeOnboardingPatch = () => { }, []); const handleClose = () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx index c5c19e602c..2a6d72b4ac 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from "react"; import { debounce } from "throttle-debounce"; import { OnboardingFeatures } from "@/app/onboarding/onboarding-features"; +import { GlobalModel } from "@/app/store/global-model"; import { atoms, globalStore } from "@/app/store/global"; import { modalsModel } from "@/app/store/modalmodel"; import * as WOS from "@/app/store/wos"; @@ -157,7 +158,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { const setPageName = useSetAtom(pageNameAtom); const handleStarClick = async () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -167,7 +168,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { }; const handleMaybeLater = async () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": false }, diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 4a43cbd0ff..5022d83946 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -49,7 +49,6 @@ function initGlobal(initOpts: GlobalInitOptions) { function initGlobalAtoms(initOpts: GlobalInitOptions) { const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom; - const clientIdAtom = atom(initOpts.clientId) as PrimitiveAtom; const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom; const builderAppIdAtom = atom(null) as PrimitiveAtom; const waveWindowTypeAtom = atom((get) => { @@ -92,11 +91,10 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } const clientAtom: Atom = atom((get) => { - const clientId = get(clientIdAtom); - if (clientId == null) { + if (initOpts.clientId == null) { return null; } - return WOS.getObjectValue(WOS.makeORef("client", clientId), get); + return WOS.getObjectValue(WOS.makeORef("client", initOpts.clientId), get); }); const windowDataAtom: Atom = atom((get) => { const windowId = get(windowIdAtom); @@ -175,7 +173,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const rateLimitInfoAtom = atom(null) as PrimitiveAtom; atoms = { // initialized in wave.ts (will not be null inside of application) - clientId: clientIdAtom, builderId: builderIdAtom, builderAppId: builderAppIdAtom, waveWindowType: waveWindowTypeAtom, diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 7ca1f0626c..47889b1bc0 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -10,6 +10,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; +import { GlobalModel } from "@/app/store/global-model"; import { atoms, createBlock, fetchWaveFile, getApi, globalStore, WOS } from "@/store/global"; import { BlockService, ObjectService } from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; @@ -343,7 +344,7 @@ export class WaveAiModel implements ViewModel { } sendMessage(text: string, user: string = "user") { - const clientId = globalStore.get(atoms.clientId); + const clientId = GlobalModel.getInstance().clientId; this.setLocked(true); const newMessage: ChatMessageType = { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index efafce4a46..9ea902de5e 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -7,7 +7,6 @@ import type * as rxjs from "rxjs"; declare global { type GlobalAtomsType = { - clientId: jotai.Atom; // readonly builderId: jotai.PrimitiveAtom; // readonly (for builder mode) builderAppId: jotai.PrimitiveAtom; // app being edited in builder mode waveWindowType: jotai.Atom<"tab" | "builder">; // derived from builderId diff --git a/frontend/wave.ts b/frontend/wave.ts index aa2698aae8..62d1794573 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { App } from "@/app/app"; +import { GlobalModel } from "@/app/store/global-model"; import { globalRefocus, registerBuilderGlobalKeys, @@ -152,26 +153,18 @@ function loadAllWorkspaceTabs(ws: Workspace) { async function initWave(initOpts: WaveInitOpts) { getApi().sendLog("Init Wave " + JSON.stringify(initOpts)); - console.log( - "Wave Init", - "tabid", - initOpts.tabId, - "clientid", - initOpts.clientId, - "windowid", - initOpts.windowId, - "platform", - platform - ); - globalStore.set(activeTabIdAtom, initOpts.tabId); - initGlobal({ + const globalInitOpts: GlobalInitOptions = { tabId: initOpts.tabId, clientId: initOpts.clientId, windowId: initOpts.windowId, platform, environment: "renderer", primaryTabStartup: initOpts.primaryTabStartup, - }); + }; + console.log("Wave Init", globalInitOpts); + globalStore.set(activeTabIdAtom, initOpts.tabId); + await GlobalModel.getInstance().initialize(globalInitOpts); + initGlobal(globalInitOpts); (window as any).globalAtoms = atoms; // Init WPS event handlers @@ -238,25 +231,16 @@ async function initBuilderWrap(initOpts: BuilderInitOpts) { async function initBuilder(initOpts: BuilderInitOpts) { getApi().sendLog("Init Builder " + JSON.stringify(initOpts)); - console.log( - "Tsunami Builder Init", - "builderid", - initOpts.builderId, - "clientid", - initOpts.clientId, - "windowid", - initOpts.windowId, - "platform", - platform - ); - - initGlobal({ + const globalInitOpts: GlobalInitOptions = { clientId: initOpts.clientId, windowId: initOpts.windowId, platform, environment: "renderer", builderId: initOpts.builderId, - }); + }; + console.log("Tsunami Builder Init", globalInitOpts); + await GlobalModel.getInstance().initialize(globalInitOpts); + initGlobal(globalInitOpts); (window as any).globalAtoms = atoms; const globalWS = initWshrpc(makeBuilderRouteId(initOpts.builderId)); From 7a7ed0b74b4645935fa019df75e6bc2d894db282 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 23:25:03 -0800 Subject: [PATCH 09/11] use client model for clientid --- .../app/onboarding/onboarding-features.tsx | 5 ++- .../onboarding/onboarding-upgrade-minor.tsx | 12 +++---- .../onboarding/onboarding-upgrade-patch.tsx | 6 ++-- frontend/app/onboarding/onboarding.tsx | 21 ++++++----- frontend/app/store/client-model.ts | 36 +++++++++++++++++++ frontend/app/store/global-model.ts | 12 ++----- frontend/app/view/waveai/waveai.tsx | 6 ++-- 7 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 frontend/app/store/client-model.ts diff --git a/frontend/app/onboarding/onboarding-features.tsx b/frontend/app/onboarding/onboarding-features.tsx index 5debe6055f..7bd51788b2 100644 --- a/frontend/app/onboarding/onboarding-features.tsx +++ b/frontend/app/onboarding/onboarding-features.tsx @@ -5,8 +5,7 @@ import Logo from "@/app/asset/logo.svg"; import { Button } from "@/app/element/button"; import { EmojiButton } from "@/app/element/emojibutton"; import { MagnifyIcon } from "@/app/element/magnify"; -import { GlobalModel } from "@/app/store/global-model"; -import { atoms, globalStore } from "@/app/store/global"; +import { ClientModel } from "@/app/store/client-model"; import * as WOS from "@/app/store/wos"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -315,7 +314,7 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) = const [currentPage, setCurrentPage] = useState("waveai"); useEffect(() => { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding-upgrade-minor.tsx b/frontend/app/onboarding/onboarding-upgrade-minor.tsx index e19f469530..40693e78d8 100644 --- a/frontend/app/onboarding/onboarding-upgrade-minor.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-minor.tsx @@ -6,8 +6,8 @@ import { Button } from "@/app/element/button"; import { FlexiModal } from "@/app/modals/modal"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; import { OnboardingFeatures } from "@/app/onboarding/onboarding-features"; -import { GlobalModel } from "@/app/store/global-model"; -import { atoms, globalStore } from "@/app/store/global"; +import { ClientModel } from "@/app/store/client-model"; +import { globalStore } from "@/app/store/global"; import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; import * as WOS from "@/app/store/wos"; @@ -61,7 +61,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -79,7 +79,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -96,7 +96,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": false }, @@ -105,7 +105,7 @@ const UpgradeOnboardingMinor = () => { }; const handleFeaturesComplete = () => { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding-upgrade-patch.tsx b/frontend/app/onboarding/onboarding-upgrade-patch.tsx index 9348c75512..665599cc4b 100644 --- a/frontend/app/onboarding/onboarding-upgrade-patch.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-patch.tsx @@ -5,8 +5,8 @@ import Logo from "@/app/asset/logo.svg"; import { Button } from "@/app/element/button"; import { FlexiModal } from "@/app/modals/modal"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; -import { GlobalModel } from "@/app/store/global-model"; -import { atoms, globalStore } from "@/app/store/global"; +import { ClientModel } from "@/app/store/client-model"; +import { globalStore } from "@/app/store/global"; import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; import * as WOS from "@/app/store/wos"; @@ -92,7 +92,7 @@ const UpgradeOnboardingPatch = () => { }, []); const handleClose = () => { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:lastversion": CurrentOnboardingVersion }, diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx index 2a6d72b4ac..4e96057477 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -4,22 +4,21 @@ import Logo from "@/app/asset/logo.svg"; import { Button } from "@/app/element/button"; import { FlexiModal } from "@/app/modals/modal"; -import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; -import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; -import * as services from "@/store/services"; -import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import { useEffect, useRef, useState } from "react"; -import { debounce } from "throttle-debounce"; - import { OnboardingFeatures } from "@/app/onboarding/onboarding-features"; -import { GlobalModel } from "@/app/store/global-model"; -import { atoms, globalStore } from "@/app/store/global"; +import { ClientModel } from "@/app/store/client-model"; +import { atoms } from "@/app/store/global"; +import { disableGlobalKeybindings, enableGlobalKeybindings, globalRefocus } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; import * as WOS from "@/app/store/wos"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; +import * as services from "@/store/services"; import { fireAndForget } from "@/util/util"; import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; +import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; +import { useEffect, useRef, useState } from "react"; +import { debounce } from "throttle-debounce"; // Page flow: // init -> (telemetry enabled) -> features @@ -158,7 +157,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { const setPageName = useSetAtom(pageNameAtom); const handleStarClick = async () => { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -168,7 +167,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { }; const handleMaybeLater = async () => { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": false }, diff --git a/frontend/app/store/client-model.ts b/frontend/app/store/client-model.ts new file mode 100644 index 0000000000..240dc6d03d --- /dev/null +++ b/frontend/app/store/client-model.ts @@ -0,0 +1,36 @@ +// Copyright 2025, Command Line Inc +// SPDX-License-Identifier: Apache-2.0 + +import * as WOS from "@/app/store/wos"; +import { atom, Atom } from "jotai"; + +class ClientModel { + private static instance: ClientModel; + + clientId: string; + clientAtom!: Atom; + + private constructor() { + // private constructor for singleton pattern + } + + static getInstance(): ClientModel { + if (!ClientModel.instance) { + ClientModel.instance = new ClientModel(); + } + return ClientModel.instance; + } + + initialize(clientId: string): void { + this.clientId = clientId; + + this.clientAtom = atom((get) => { + if (this.clientId == null) { + return null; + } + return WOS.getObjectValue(WOS.makeORef("client", this.clientId), get); + }); + } +} + +export { ClientModel }; \ No newline at end of file diff --git a/frontend/app/store/global-model.ts b/frontend/app/store/global-model.ts index c255afd47a..804e3a18f6 100644 --- a/frontend/app/store/global-model.ts +++ b/frontend/app/store/global-model.ts @@ -2,17 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import * as WOS from "@/app/store/wos"; +import { ClientModel } from "@/app/store/client-model"; import { atom, Atom } from "jotai"; class GlobalModel { private static instance: GlobalModel; - clientId: string; windowId: string; builderId: string; platform: NodeJS.Platform; - clientAtom!: Atom; windowDataAtom!: Atom; workspaceAtom!: Atom; @@ -28,18 +27,11 @@ class GlobalModel { } async initialize(initOpts: GlobalInitOptions): Promise { - this.clientId = initOpts.clientId; + ClientModel.getInstance().initialize(initOpts.clientId); this.windowId = initOpts.windowId; this.builderId = initOpts.builderId; this.platform = initOpts.platform; - this.clientAtom = atom((get) => { - if (this.clientId == null) { - return null; - } - return WOS.getObjectValue(WOS.makeORef("client", this.clientId), get); - }); - this.windowDataAtom = atom((get) => { if (this.windowId == null) { return null; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 47889b1bc0..62d140f86d 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -2,15 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import type { TabModel } from "@/app/store/tab-model"; import { Button } from "@/app/element/button"; import { Markdown } from "@/app/element/markdown"; import { TypingIndicator } from "@/app/element/typingindicator"; +import { ClientModel } from "@/app/store/client-model"; +import type { TabModel } from "@/app/store/tab-model"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; -import { GlobalModel } from "@/app/store/global-model"; import { atoms, createBlock, fetchWaveFile, getApi, globalStore, WOS } from "@/store/global"; import { BlockService, ObjectService } from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; @@ -344,7 +344,7 @@ export class WaveAiModel implements ViewModel { } sendMessage(text: string, user: string = "user") { - const clientId = GlobalModel.getInstance().clientId; + const clientId = ClientModel.getInstance().clientId; this.setLocked(true); const newMessage: ChatMessageType = { From 2efe3659a62d633a207443c58c0af4215643b7c8 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 23:29:58 -0800 Subject: [PATCH 10/11] use client atom, move to model --- frontend/app/app.tsx | 3 ++- frontend/app/modals/modalsrenderer.tsx | 3 ++- frontend/app/onboarding/onboarding-upgrade.tsx | 3 ++- frontend/app/onboarding/onboarding.tsx | 4 ++-- frontend/app/store/global.ts | 7 ------- frontend/types/custom.d.ts | 1 - 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index d67dc27f6f..73866c0eb4 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { ClientModel } from "@/app/store/client-model"; import { Workspace } from "@/app/workspace/workspace"; import { ContextMenuModel } from "@/store/contextmenu"; import { atoms, createBlock, getSettingsPrefixAtom, globalStore, isDev, removeFlashError } from "@/store/global"; @@ -273,7 +274,7 @@ const FlashError = () => { const AppInner = () => { const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); - const client = useAtomValue(atoms.client); + const client = useAtomValue(ClientModel.getInstance().clientAtom); const windowData = useAtomValue(atoms.waveWindow); const isFullScreen = useAtomValue(atoms.isFullScreen); diff --git a/frontend/app/modals/modalsrenderer.tsx b/frontend/app/modals/modalsrenderer.tsx index 8576fb3be3..218d777831 100644 --- a/frontend/app/modals/modalsrenderer.tsx +++ b/frontend/app/modals/modalsrenderer.tsx @@ -4,6 +4,7 @@ import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding"; import { CurrentOnboardingVersion } from "@/app/onboarding/onboarding-common"; import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade"; +import { ClientModel } from "@/app/store/client-model"; import { atoms, globalPrimaryTabStartup, globalStore } from "@/store/global"; import { modalsModel } from "@/store/modalmodel"; import * as jotai from "jotai"; @@ -12,7 +13,7 @@ import * as semver from "semver"; import { getModalComponent } from "./modalregistry"; const ModalsRenderer = () => { - const clientData = jotai.useAtomValue(atoms.client); + const clientData = jotai.useAtomValue(ClientModel.getInstance().clientAtom); const [newInstallOnboardingOpen, setNewInstallOnboardingOpen] = jotai.useAtom(modalsModel.newInstallOnboardingOpen); const [upgradeOnboardingOpen, setUpgradeOnboardingOpen] = jotai.useAtom(modalsModel.upgradeOnboardingOpen); const [modals] = jotai.useAtom(modalsModel.modalsAtom); diff --git a/frontend/app/onboarding/onboarding-upgrade.tsx b/frontend/app/onboarding/onboarding-upgrade.tsx index 4305a58be3..11a94ead75 100644 --- a/frontend/app/onboarding/onboarding-upgrade.tsx +++ b/frontend/app/onboarding/onboarding-upgrade.tsx @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { ClientModel } from "@/app/store/client-model"; import { atoms, globalStore } from "@/app/store/global"; import { modalsModel } from "@/app/store/modalmodel"; import { useAtomValue } from "jotai"; @@ -11,7 +12,7 @@ import { UpgradeOnboardingMinor } from "./onboarding-upgrade-minor"; import { UpgradeOnboardingPatch } from "./onboarding-upgrade-patch"; const UpgradeOnboardingModal = () => { - const clientData = useAtomValue(atoms.client); + const clientData = useAtomValue(ClientModel.getInstance().clientAtom); const initialVersionRef = useRef(null); if (initialVersionRef.current == null) { diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx index 4e96057477..b2accee961 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -30,7 +30,7 @@ const pageNameAtom: PrimitiveAtom = atom("init"); const InitPage = ({ isCompact }: { isCompact: boolean }) => { const settings = useAtomValue(atoms.settingsAtom); - const clientData = useAtomValue(atoms.client); + const clientData = useAtomValue(ClientModel.getInstance().clientAtom); const [telemetryEnabled, setTelemetryEnabled] = useState(!!settings["telemetry:enabled"]); const setPageName = useSetAtom(pageNameAtom); @@ -227,7 +227,7 @@ const FeaturesPage = () => { const NewInstallOnboardingModal = () => { const modalRef = useRef(null); const [pageName, setPageName] = useAtom(pageNameAtom); - const clientData = useAtomValue(atoms.client); + const clientData = useAtomValue(ClientModel.getInstance().clientAtom); const [isCompact, setIsCompact] = useState(window.innerHeight < 800); const updateModalHeight = () => { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 5022d83946..8390b270ea 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -90,12 +90,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { console.log("failed to initialize onMenuItemAbout handler", e); } - const clientAtom: Atom = atom((get) => { - if (initOpts.clientId == null) { - return null; - } - return WOS.getObjectValue(WOS.makeORef("client", initOpts.clientId), get); - }); const windowDataAtom: Atom = atom((get) => { const windowId = get(windowIdAtom); if (windowId == null) { @@ -177,7 +171,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { builderAppId: builderAppIdAtom, waveWindowType: waveWindowTypeAtom, uiContext: uiContextAtom, - client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, fullConfigAtom, diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 9ea902de5e..a577f5a09b 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -10,7 +10,6 @@ declare global { builderId: jotai.PrimitiveAtom; // readonly (for builder mode) builderAppId: jotai.PrimitiveAtom; // app being edited in builder mode waveWindowType: jotai.Atom<"tab" | "builder">; // derived from builderId - client: jotai.Atom; // driven from WOS uiContext: jotai.Atom; // driven from windowId, tabId waveWindow: jotai.Atom; // driven from WOS workspace: jotai.Atom; // driven from WOS From e361356c54e2f959b6fd33b78a77040c8472644a Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 14 Dec 2025 23:36:00 -0800 Subject: [PATCH 11/11] use globalmodel for wavewindow data --- frontend/app/app.tsx | 3 ++- frontend/app/store/global.ts | 11 +---------- frontend/types/custom.d.ts | 1 - 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 73866c0eb4..45723fe564 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ClientModel } from "@/app/store/client-model"; +import { GlobalModel } from "@/app/store/global-model"; import { Workspace } from "@/app/workspace/workspace"; import { ContextMenuModel } from "@/store/contextmenu"; import { atoms, createBlock, getSettingsPrefixAtom, globalStore, isDev, removeFlashError } from "@/store/global"; @@ -275,7 +276,7 @@ const FlashError = () => { const AppInner = () => { const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); const client = useAtomValue(ClientModel.getInstance().clientAtom); - const windowData = useAtomValue(atoms.waveWindow); + const windowData = useAtomValue(GlobalModel.getInstance().windowDataAtom); const isFullScreen = useAtomValue(atoms.isFullScreen); if (client == null || windowData == null) { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 8390b270ea..ed8d2ef371 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -90,16 +90,8 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { console.log("failed to initialize onMenuItemAbout handler", e); } - const windowDataAtom: Atom = atom((get) => { - const windowId = get(windowIdAtom); - if (windowId == null) { - return null; - } - const rtn = WOS.getObjectValue(WOS.makeORef("window", windowId), get); - return rtn; - }); const workspaceAtom: Atom = atom((get) => { - const windowData = get(windowDataAtom); + const windowData = WOS.getObjectValue(WOS.makeORef("window", get(windowIdAtom)), get); if (windowData == null) { return null; } @@ -171,7 +163,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { builderAppId: builderAppIdAtom, waveWindowType: waveWindowTypeAtom, uiContext: uiContextAtom, - waveWindow: windowDataAtom, workspace: workspaceAtom, fullConfigAtom, waveaiModeConfigAtom, diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index a577f5a09b..db2999a500 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -11,7 +11,6 @@ declare global { builderAppId: jotai.PrimitiveAtom; // app being edited in builder mode waveWindowType: jotai.Atom<"tab" | "builder">; // derived from builderId uiContext: jotai.Atom; // driven from windowId, tabId - waveWindow: jotai.Atom; // driven from WOS workspace: jotai.Atom; // driven from WOS fullConfigAtom: jotai.PrimitiveAtom; // driven from WOS, settings -- updated via WebSocket waveaiModeConfigAtom: jotai.PrimitiveAtom>; // resolved AI mode configs -- updated via WebSocket