diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 36b0f7d061..440ffd4a41 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"; @@ -254,6 +255,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 defaultMode = jotai.useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs); @@ -277,7 +279,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/app.tsx b/frontend/app/app.tsx index d67dc27f6f..45723fe564 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -1,6 +1,8 @@ // Copyright 2025, Command Line Inc. // 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"; @@ -273,8 +275,8 @@ const FlashError = () => { const AppInner = () => { const prefersReducedMotion = useAtomValue(atoms.prefersReducedMotionAtom); - const client = useAtomValue(atoms.client); - const windowData = useAtomValue(atoms.waveWindow); + const client = useAtomValue(ClientModel.getInstance().clientAtom); + const windowData = useAtomValue(GlobalModel.getInstance().windowDataAtom); const isFullScreen = useAtomValue(atoms.isFullScreen); if (client == null || windowData == null) { 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/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/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-features.tsx b/frontend/app/onboarding/onboarding-features.tsx index c81af794d7..7bd51788b2 100644 --- a/frontend/app/onboarding/onboarding-features.tsx +++ b/frontend/app/onboarding/onboarding-features.tsx @@ -5,7 +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 { 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"; @@ -314,7 +314,7 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) = const [currentPage, setCurrentPage] = useState("waveai"); useEffect(() => { - const clientId = globalStore.get(atoms.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 d44ca57713..40693e78d8 100644 --- a/frontend/app/onboarding/onboarding-upgrade-minor.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-minor.tsx @@ -6,7 +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 { 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"; @@ -60,7 +61,7 @@ const UpgradeOnboardingMinor = () => { }, { noresponse: true } ); - const clientId = globalStore.get(atoms.clientId); + const clientId = ClientModel.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 = ClientModel.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 = ClientModel.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 = 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 cd8ad94709..282b8ea87a 100644 --- a/frontend/app/onboarding/onboarding-upgrade-patch.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-patch.tsx @@ -5,7 +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 { 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"; @@ -98,7 +99,7 @@ const UpgradeOnboardingPatch = () => { }, []); const handleClose = () => { - const clientId = globalStore.get(atoms.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.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 c5c19e602c..b2accee961 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -4,21 +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 { 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 @@ -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); @@ -157,7 +157,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { const setPageName = useSetAtom(pageNameAtom); const handleStarClick = async () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": true }, @@ -167,7 +167,7 @@ const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => { }; const handleMaybeLater = async () => { - const clientId = globalStore.get(atoms.clientId); + const clientId = ClientModel.getInstance().clientId; await RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("client", clientId), meta: { "onboarding:githubstar": false }, @@ -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/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 new file mode 100644 index 0000000000..804e3a18f6 --- /dev/null +++ b/frontend/app/store/global-model.ts @@ -0,0 +1,52 @@ +// Copyright 2025, Command Line Inc +// 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; + + windowId: string; + builderId: string; + platform: NodeJS.Platform; + + 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 { + ClientModel.getInstance().initialize(initOpts.clientId); + this.windowId = initOpts.windowId; + this.builderId = initOpts.builderId; + this.platform = initOpts.platform; + + 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 35eb9f1585..ed8d2ef371 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; @@ -59,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) => { @@ -101,23 +90,8 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { console.log("failed to initialize onMenuItemAbout handler", e); } - const clientAtom: Atom = atom((get) => { - const clientId = get(clientIdAtom); - if (clientId == null) { - return null; - } - return WOS.getObjectValue(WOS.makeORef("client", clientId), get); - }); - 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; } @@ -140,9 +114,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); @@ -175,7 +146,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { }); } - const typeAheadModalAtom = atom({}); const modalOpen = atom(false); const allConnStatusAtom = atom((get) => { const connStatusMap = get(ConnStatusMapAtom); @@ -189,35 +159,29 @@ 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, uiContext: uiContextAtom, - client: clientAtom, - waveWindow: windowDataAtom, workspace: workspaceAtom, fullConfigAtom, waveaiModeConfigAtom, settingsAtom, hasCustomAIPresetsAtom, - tabAtom, staticTabId: staticTabIdAtom, isFullScreen: isFullScreenAtom, zoomFactorAtom, controlShiftDelayAtom, updaterStatusAtom, prefersReducedMotionAtom, - typeAheadModalAtom, modalOpen, allConnStatus: allConnStatusAtom, flashErrors: flashErrorsAtom, notifications: notificationsAtom, notificationPopoverMode: notificationPopoverModeAtom, reinitVersion, - isTermMultiInput: atom(false), waveAIRateLimitInfoAtom: rateLimitInfoAtom, - }; + } as GlobalAtomsType; } function initGlobalWaveEventSubs(initOpts: WaveInitOpts) { diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 3ade22c748..78ade22bab 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -20,6 +20,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"; @@ -587,12 +588,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 new file mode 100644 index 0000000000..daded66a0d --- /dev/null +++ b/frontend/app/store/tab-model.ts @@ -0,0 +1,70 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { atom, Atom, PrimitiveAtom } from "jotai"; +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; + tabNumBlocksAtom: Atom; + isTermMultiInput = atom(false) as PrimitiveAtom; + 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; + } +} + +function getTabModelByTabId(tabId: string): TabModel { + let model = tabModelCache.get(tabId); + if (model == null) { + model = new TabModel(tabId); + tabModelCache.set(tabId, model); + } + 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 { + const model = useContext(TabModelContext); + if (model == null) { + throw new Error("useTabModel must be used within a TabModelProvider"); + } + return model; +} + +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 27482550cd..d1eed4cfbc 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); }, }); } @@ -917,7 +920,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..7e6272003a 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 @@ -264,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 0ad7fa7495..a06a28767e 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -342,6 +342,7 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t } export class TermWrap { + tabId: string; blockId: string; ptyOffset: number; dataBytesProcessed: number; @@ -380,12 +381,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; @@ -697,11 +700,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 84e5706e5b..dbebb824b3 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import { atoms, getApi, globalStore, WOS } from "@/app/store/global"; +import { getApi, globalStore, WOS } from "@/app/store/global"; +import type { TabModel } from "@/app/store/tab-model"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -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); @@ -106,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, }); @@ -134,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, }); 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..62d140f86d 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -1,9 +1,12 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { BlockNodeModel } from "@/app/block/blocktypes"; 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"; @@ -64,6 +67,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 +91,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"); @@ -337,7 +344,7 @@ export class WaveAiModel implements ViewModel { } sendMessage(text: string, user: string = "user") { - const clientId = globalStore.get(atoms.clientId); + const clientId = ClientModel.getInstance().clientId; this.setLocked(true); const newMessage: ChatMessageType = { 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/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 ) : (
- + + +
)} diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe5392..db2999a500 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -7,33 +7,27 @@ 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 - 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 fullConfigAtom: jotai.PrimitiveAtom; // driven from WOS, settings -- updated via WebSocket 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; controlShiftDelayAtom: jotai.PrimitiveAtom; prefersReducedMotionAtom: jotai.Atom; updaterStatusAtom: jotai.PrimitiveAtom; - typeAheadModalAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom; allConnStatus: jotai.Atom; flashErrors: jotai.PrimitiveAtom; notifications: jotai.PrimitiveAtom; notificationPopoverMode: jotai.Atom; reinitVersion: jotai.PrimitiveAtom; - isTermMultiInput: jotai.PrimitiveAtom; waveAIRateLimitInfoAtom: jotai.PrimitiveAtom; }; @@ -60,6 +54,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; @@ -287,7 +291,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..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, @@ -30,6 +31,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"; @@ -151,25 +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 - ); - 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 @@ -236,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));