Skip to content
4 changes: 3 additions & 1 deletion frontend/app/aipanel/aipanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -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 };
},
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/app.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 8 additions & 4 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<Block>(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(() => {
Expand All @@ -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<Block>(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(() => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -492,28 +493,27 @@ 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);
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true;
const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId));
const [blockData] = WOS.useWaveObjectValue<Block>(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;
}
if (blockData?.meta?.["frame:activebordercolor"]) {
style.borderColor = blockData.meta["frame:activebordercolor"];
}
} else {
const tabData = jotai.useAtomValue(atoms.tabAtom);
const tabBorderColor = tabData?.meta?.["bg:bordercolor"];
if (tabBorderColor) {
style.borderColor = tabBorderColor;
}
Expand Down Expand Up @@ -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<Block>(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 <BlockFrame_Default {...props} numBlocksInTab={numBlocks} />;
});

Expand Down
3 changes: 2 additions & 1 deletion frontend/app/modals/modalsrenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/onboarding/onboarding-features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -314,7 +314,7 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
const [currentPage, setCurrentPage] = useState<FeaturePageName>("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 },
Expand Down
11 changes: 6 additions & 5 deletions frontend/app/onboarding/onboarding-upgrade-minor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand Down
5 changes: 3 additions & 2 deletions frontend/app/onboarding/onboarding-upgrade-patch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 },
Expand Down
3 changes: 2 additions & 1 deletion frontend/app/onboarding/onboarding-upgrade.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string | null>(null);

if (initialVersionRef.current == null) {
Expand Down
24 changes: 12 additions & 12 deletions frontend/app/onboarding/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +30,7 @@ const pageNameAtom: PrimitiveAtom<PageName> = atom<PageName>("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<boolean>(!!settings["telemetry:enabled"]);
const setPageName = useSetAtom(pageNameAtom);

Expand Down Expand Up @@ -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 },
Expand All @@ -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 },
Expand Down Expand Up @@ -227,7 +227,7 @@ const FeaturesPage = () => {
const NewInstallOnboardingModal = () => {
const modalRef = useRef<HTMLDivElement | null>(null);
const [pageName, setPageName] = useAtom(pageNameAtom);
const clientData = useAtomValue(atoms.client);
const clientData = useAtomValue(ClientModel.getInstance().clientAtom);
const [isCompact, setIsCompact] = useState<boolean>(window.innerHeight < 800);

const updateModalHeight = () => {
Expand Down
36 changes: 36 additions & 0 deletions frontend/app/store/client-model.ts
Original file line number Diff line number Diff line change
@@ -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<Client>;

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 };
Loading
Loading