From c9b15895b033ff21ac2064c6cf8ba1996c7abc19 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 18:09:23 -0700 Subject: [PATCH 01/14] add useWaveObjectValue --- frontend/app/waveenv/waveenv.ts | 1 + frontend/app/waveenv/waveenvimpl.ts | 1 + frontend/preview/mock/mockwaveenv.ts | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index 365bf74be9..5dbfeb3e2a 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -23,6 +23,7 @@ export type WaveEnv = { showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => void; getConnStatusAtom: (conn: string) => PrimitiveAtom; getWaveObjectAtom: (oref: string) => Atom; + useWaveObjectValue: (oref: string) => [T, boolean]; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; }; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 50aa4ef7ea..4fb71f0d35 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -33,6 +33,7 @@ export function makeWaveEnvImpl(): WaveEnv { }, getConnStatusAtom, getWaveObjectAtom: WOS.getWaveObjectAtom, + useWaveObjectValue: WOS.useWaveObjectValue, getBlockMetaKeyAtom, }; } diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 913cbc1501..4f4661467d 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -181,6 +181,10 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { } return waveObjectAtomCache.get(oref) as PrimitiveAtom; }, + useWaveObjectValue: (oref: string): [T, boolean] => { + const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; + return [obj, false]; + }, getBlockMetaKeyAtom: (blockId: string, key: T) => { const cacheKey = blockId + "#meta-" + key; if (!blockMetaKeyAtomCache.has(cacheKey)) { From 1b40a8a77e305143bb3a5adf10dd8ad078de8a65 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 19:05:17 -0700 Subject: [PATCH 02/14] working on integrating waveenv through block.tsx... --- frontend/app/block/block.tsx | 27 ++++++++-------- frontend/app/block/blockframe-header.tsx | 7 ++-- frontend/app/block/blockframe.tsx | 21 +++++++----- frontend/app/store/tab-model.ts | 35 ++++++++++++++------ frontend/app/waveenv/waveenv.ts | 6 ++-- frontend/app/waveenv/waveenvimpl.ts | 6 ++-- frontend/preview/mock/mockwaveenv.ts | 41 +++++++++++++++++------- 7 files changed, 94 insertions(+), 49 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 37453473c9..d3c234b33e 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -23,13 +23,11 @@ import { CenteredDiv } from "@/element/quickelems"; import { useDebouncedNodeInnerRect } from "@/layout/index"; import { counterInc } from "@/store/counters"; import { - atoms, getBlockComponentModel, - getSettingsKeyAtom, registerBlockComponentModel, unregisterBlockComponentModel, } from "@/store/global"; -import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; +import { makeORef } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { isBlank, useAtomValueSafe } from "@/util/util"; import { HelpViewModel } from "@/view/helpview/helpview"; @@ -71,7 +69,7 @@ function makeViewModel( if (ctor != null) { return new ctor({ blockId, nodeModel, tabModel, waveEnv }); } - return makeDefaultViewModel(blockId, blockView); + return makeDefaultViewModel(blockId, blockView, waveEnv); } function getViewElem( @@ -91,8 +89,8 @@ function getViewElem( return ; } -function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { - const blockDataAtom = getWaveObjectAtom(makeORef("block", blockId)); +function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEnv): ViewModel { + const blockDataAtom = waveEnv.getWaveObjectAtom(makeORef("block", blockId)); const viewModel: ViewModel = { viewType: viewType, viewIcon: atom((get) => { @@ -111,7 +109,8 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { } const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { - const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const waveEnv = useWaveEnv(); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); if (!blockData) { return null; } @@ -127,7 +126,8 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { }); const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { - const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const waveEnv = useWaveEnv(); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const blockRef = useRef(null); const contentRef = useRef(null); const viewElem = useMemo( @@ -149,18 +149,19 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { counterInc("render-BlockFull"); + const waveEnv = useWaveEnv(); const focusElemRef = useRef(null); const blockRef = useRef(null); const contentRef = useRef(null); const [blockClicked, setBlockClicked] = useState(false); - const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const isFocused = useAtomValue(nodeModel.isFocused); const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents); const isResizing = useAtomValue(nodeModel.isResizing); const isMagnified = useAtomValue(nodeModel.isMagnified); const anyMagnified = useAtomValue(nodeModel.anyMagnified); - const modalOpen = useAtomValue(atoms.modalOpen); - const focusFollowsCursorMode = useAtomValue(getSettingsKeyAtom("app:focusfollowscursor")) ?? "off"; + const modalOpen = useAtomValue(waveEnv.atoms.modalOpen); + const focusFollowsCursorMode = useAtomValue(waveEnv.settingsAtoms["app:focusfollowscursor"]) ?? "off"; const innerRect = useDebouncedNodeInnerRect(nodeModel); const noPadding = useAtomValueSafe(viewModel.noPadding); @@ -316,7 +317,7 @@ const Block = memo((props: BlockProps) => { counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const tabModel = useTabModel(); const waveEnv = useWaveEnv(); - const [blockData, loading] = useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); + const [blockData, loading] = waveEnv.useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { @@ -343,7 +344,7 @@ const SubBlock = memo((props: SubBlockProps) => { counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const tabModel = useTabModel(); const waveEnv = useWaveEnv(); - const [blockData, loading] = useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); + const [blockData, loading] = waveEnv.useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index 420a6889c8..2ac93f32dd 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -12,7 +12,7 @@ import { ConnectionButton } from "@/app/block/connectionbutton"; import { DurableSessionFlyover } from "@/app/block/durable-session-flyover"; import { getBlockBadgeAtom } from "@/app/store/badge"; import { ContextMenuModel } from "@/app/store/contextmenu"; -import { recordTEvent, refocusNode, WOS } from "@/app/store/global"; +import { recordTEvent, refocusNode } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; import { uxCloseBlock } from "@/app/store/keymodel"; import { RpcApi } from "@/app/store/wshclientapi"; @@ -21,6 +21,8 @@ import { IconButton } from "@/element/iconbutton"; import { NodeModel } from "@/layout/index"; import * as util from "@/util/util"; import { cn, makeIconClass } from "@/util/util"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { makeORef } from "@/store/wos"; import * as jotai from "jotai"; import * as React from "react"; import { BlockFrameProps } from "./blocktypes"; @@ -171,7 +173,8 @@ const BlockFrame_Header = ({ changeConnModalAtom, error, }: BlockFrameProps & { changeConnModalAtom: jotai.PrimitiveAtom; error?: Error }) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); + const waveEnv = useWaveEnv(); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(blockData?.meta?.view); let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton); diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 1ed88fb574..327486c1a0 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -6,7 +6,7 @@ import { BlockFrame_Header } from "@/app/block/blockframe-header"; import { blockViewToIcon, getViewIconElem } from "@/app/block/blockutil"; import { ConnStatusOverlay } from "@/app/block/connstatusoverlay"; import { ChangeConnectionBlockModal } from "@/app/modals/conntypeahead"; -import { atoms, getBlockComponentModel, getSettingsKeyAtom, globalStore, useBlockAtom, WOS } from "@/app/store/global"; +import { getBlockComponentModel, globalStore, useBlockAtom } from "@/app/store/global"; import { useTabModel } from "@/app/store/tab-model"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -16,20 +16,23 @@ import { NodeModel } from "@/layout/index"; import * as util from "@/util/util"; import { makeIconClass } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { makeORef } from "@/store/wos"; import clsx from "clsx"; import * as jotai from "jotai"; import * as React from "react"; import { BlockFrameProps } from "./blocktypes"; const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { + const waveEnv = useWaveEnv(); 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 isLayoutMode = jotai.useAtomValue(waveEnv.atoms.controlShiftDelayAtom); + const showOverlayBlockNums = jotai.useAtomValue(waveEnv.settingsAtoms["app:showoverlayblocknums"]) ?? true; const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const tabActiveBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:activebordercolor")); const tabBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:bordercolor")); const style: React.CSSProperties = {}; @@ -87,8 +90,9 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { }); const BlockFrame_Default_Component = (props: BlockFrameProps) => { + const waveEnv = useWaveEnv(); const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props; - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const isFocused = jotai.useAtomValue(nodeModel.isFocused); const aiPanelVisible = jotai.useAtomValue(WorkspaceLayoutModel.getInstance().panelVisibleAtom); const viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); @@ -100,9 +104,9 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const connModalOpen = jotai.useAtomValue(changeConnModalAtom); const isMagnified = jotai.useAtomValue(nodeModel.isMagnified); const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral); - const [magnifiedBlockBlurAtom] = React.useState(() => getSettingsKeyAtom("window:magnifiedblockblurprimarypx")); + const [magnifiedBlockBlurAtom] = React.useState(() => waveEnv.settingsAtoms["window:magnifiedblockblurprimarypx"]); const magnifiedBlockBlur = jotai.useAtomValue(magnifiedBlockBlurAtom); - const [magnifiedBlockOpacityAtom] = React.useState(() => getSettingsKeyAtom("window:magnifiedblockopacity")); + const [magnifiedBlockOpacityAtom] = React.useState(() => waveEnv.settingsAtoms["window:magnifiedblockopacity"]); const magnifiedBlockOpacity = jotai.useAtomValue(magnifiedBlockOpacityAtom); const connBtnRef = React.useRef(null); const noHeader = util.useAtomValueSafe(viewModel?.noHeader); @@ -203,9 +207,10 @@ 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 waveEnv = useWaveEnv(); const tabModel = useTabModel(); const blockId = props.nodeModel.blockId; - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", blockId)); const numBlocks = jotai.useAtomValue(tabModel.tabNumBlocksAtom); if (!blockId || !blockData) { return null; diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index ec5ab94c16..b97aa77aa2 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -1,8 +1,9 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { atom, Atom, PrimitiveAtom } from "jotai"; import { createContext, useContext } from "react"; +import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv"; import { globalStore } from "./jotaiStore"; import * as WOS from "./wos"; @@ -11,14 +12,19 @@ export const activeTabIdAtom = atom(null) as PrimitiveAtom; export class TabModel { tabId: string; + waveEnv: WaveEnv; tabAtom: Atom; tabNumBlocksAtom: Atom; isTermMultiInput = atom(false) as PrimitiveAtom; metaCache: Map> = new Map(); - constructor(tabId: string) { + constructor(tabId: string, waveEnv?: WaveEnv) { this.tabId = tabId; + this.waveEnv = waveEnv; this.tabAtom = atom((get) => { + if (this.waveEnv != null) { + return get(this.waveEnv.getWaveObjectAtom(WOS.makeORef("tab", this.tabId))); + } return WOS.getObjectValue(WOS.makeORef("tab", this.tabId), get); }); this.tabNumBlocksAtom = atom((get) => { @@ -40,33 +46,42 @@ export class TabModel { } } -export function getTabModelByTabId(tabId: string): TabModel { +export function getTabModelByTabId(tabId: string, waveEnv?: WaveEnv): TabModel { let model = tabModelCache.get(tabId); if (model == null) { - model = new TabModel(tabId); + model = new TabModel(tabId, waveEnv); tabModelCache.set(tabId, model); } return model; } -export function getActiveTabModel(): TabModel | null { +export function getActiveTabModel(waveEnv?: WaveEnv): TabModel | null { const activeTabId = globalStore.get(activeTabIdAtom); if (activeTabId == null) { return null; } - return getTabModelByTabId(activeTabId); + return getTabModelByTabId(activeTabId, waveEnv); } export const TabModelContext = createContext(undefined); export function useTabModel(): TabModel { - const model = useContext(TabModelContext); - if (model == null) { + const waveEnv = useWaveEnv(); + const ctxModel = useContext(TabModelContext); + if (waveEnv?.mockTabModel != null) { + return waveEnv.mockTabModel; + } + if (ctxModel == null) { throw new Error("useTabModel must be used within a TabModelProvider"); } - return model; + return ctxModel; } export function maybeUseTabModel(): TabModel { - return useContext(TabModelContext); + const waveEnv = useWaveEnv(); + const ctxModel = useContext(TabModelContext); + if (waveEnv?.mockTabModel != null) { + return waveEnv.mockTabModel; + } + return ctxModel; } diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index 5dbfeb3e2a..d7302b5b77 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -1,11 +1,12 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import type { TabModel } from "@/app/store/tab-model"; import { RpcApiType } from "@/app/store/wshclientapi"; import { Atom, PrimitiveAtom } from "jotai"; import React from "react"; -type ConfigAtoms = { [K in keyof SettingsType]: Atom }; +type SettingsAtoms = { [K in keyof SettingsType]: Atom }; export type BlockMetaKeyAtomFnType = ( blockId: string, @@ -16,7 +17,7 @@ export type BlockMetaKeyAtomFnType export type WaveEnv = { electron: ElectronApi; rpc: RpcApiType; - configAtoms: ConfigAtoms; + settingsAtoms: SettingsAtoms; isDev: () => boolean; atoms: GlobalAtomsType; createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => Promise; @@ -25,6 +26,7 @@ export type WaveEnv = { getWaveObjectAtom: (oref: string) => Atom; useWaveObjectValue: (oref: string) => [T, boolean]; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; + mockTabModel?: TabModel; }; export const WaveEnvContext = React.createContext(null); diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 4fb71f0d35..0396a05419 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -14,8 +14,8 @@ import { import { RpcApi } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; -const configAtoms = new Proxy({} as WaveEnv["configAtoms"], { - get(_target: WaveEnv["configAtoms"], key: K) { +const settingsAtoms = new Proxy({} as WaveEnv["settingsAtoms"], { + get(_target: WaveEnv["settingsAtoms"], key: K) { return getSettingsKeyAtom(key); }, }); @@ -24,7 +24,7 @@ export function makeWaveEnvImpl(): WaveEnv { return { electron: (window as any).api, rpc: RpcApi, - configAtoms, + settingsAtoms, isDev, atoms, createBlock, diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 4f4661467d..ae7c4c92cb 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -1,7 +1,8 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getSettingsKeyAtom, makeDefaultConnStatus } from "@/app/store/global"; +import { makeDefaultConnStatus } from "@/app/store/global"; +import { TabModel } from "@/app/store/tab-model"; import { RpcApiType } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; import { Atom, atom, PrimitiveAtom } from "jotai"; @@ -14,6 +15,7 @@ type RpcOverrides = { export type MockEnv = { isDev?: boolean; + tabId?: string; settings?: Partial; rpc?: RpcOverrides; atoms?: Partial; @@ -36,6 +38,7 @@ function mergeRecords(base: Record, overrides: Record): export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv { return { isDev: overrides.isDev ?? base.isDev, + tabId: overrides.tabId ?? base.tabId, settings: mergeRecords(base.settings, overrides.settings), rpc: mergeRecords(base.rpc as any, overrides.rpc as any) as RpcOverrides, atoms: overrides.atoms != null || base.atoms != null ? { ...base.atoms, ...overrides.atoms } : undefined, @@ -50,26 +53,37 @@ export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv { }; } -function makeMockConfigAtoms(overrides?: Partial): WaveEnv["configAtoms"] { +function makeMockSettingsAtoms( + settingsAtom: Atom, + overrides?: Partial +): WaveEnv["settingsAtoms"] { const overrideAtoms = new Map>(); if (overrides) { for (const key of Object.keys(overrides) as (keyof SettingsType)[]) { overrideAtoms.set(key, atom(overrides[key])); } } - return new Proxy({} as WaveEnv["configAtoms"], { - get(_target: WaveEnv["configAtoms"], key: K) { + const keyAtomCache = new Map>(); + return new Proxy({} as WaveEnv["settingsAtoms"], { + get(_target: WaveEnv["settingsAtoms"], key: K) { if (overrideAtoms.has(key)) { return overrideAtoms.get(key); } - return getSettingsKeyAtom(key); + if (!keyAtomCache.has(key)) { + keyAtomCache.set( + key, + atom((get) => get(settingsAtom)?.[key]) + ); + } + return keyAtomCache.get(key); }, }); } function makeMockGlobalAtoms( settingsOverrides?: Partial, - atomOverrides?: Partial + atomOverrides?: Partial, + tabId?: string ): GlobalAtomsType { let fullConfig = DefaultFullConfig; if (settingsOverrides) { @@ -83,13 +97,13 @@ function makeMockGlobalAtoms( const defaults: GlobalAtomsType = { builderId: atom(""), builderAppId: atom("") as any, - uiContext: atom({} as UIContext), + uiContext: atom({ windowid: "", activetabid: tabId ?? "" } as UIContext), workspace: atom(null as Workspace), fullConfigAtom, waveaiModeConfigAtom: atom({}) as any, settingsAtom, hasCustomAIPresetsAtom: atom(false), - staticTabId: atom(""), + staticTabId: atom(tabId ?? ""), isFullScreen: atom(false) as any, zoomFactorAtom: atom(1.0) as any, controlShiftDelayAtom: atom(false) as any, @@ -149,12 +163,13 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const connStatusAtomCache = new Map>(); const waveObjectAtomCache = new Map>(); const blockMetaKeyAtomCache = new Map>(); + const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId); const env = { mockEnv: overrides, electron: overrides.electron ? { ...previewElectronApi, ...overrides.electron } : previewElectronApi, rpc: makeMockRpc(overrides.rpc), - configAtoms: makeMockConfigAtoms(overrides.settings), - atoms: makeMockGlobalAtoms(overrides.settings, overrides.atoms), + atoms, + settingsAtoms: makeMockSettingsAtoms(atoms.settingsAtom, overrides.settings), isDev: () => overrides.isDev ?? true, createBlock: overrides.createBlock ?? @@ -198,6 +213,10 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { } return blockMetaKeyAtomCache.get(cacheKey) as Atom; }, - }; + mockTabModel: null as TabModel, + } as MockWaveEnv; + if (overrides.tabId != null) { + env.mockTabModel = new TabModel(overrides.tabId, env); + } return env; } From 527c7fa083b18ebddc451dfd13841f1c5854df32 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 20:56:25 -0700 Subject: [PATCH 03/14] fix loading bug and also remove unused func --- frontend/app/store/wos.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index f2395e12d0..7fa0142799 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -227,9 +227,6 @@ function getWaveObjectLoadingAtom(oref: string): Atom { const wov = getWaveObjectValue(oref); return atom((get) => { const dataValue = get(wov.dataAtom); - if (dataValue.loading) { - return null; - } return dataValue.loading; }); } From 923b242df85e660e2a7952b39c1405dcd347ab1e Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 21:45:47 -0700 Subject: [PATCH 04/14] more methods to waveenv, fix block caching in wos, reduce blockdata deps in block.tsx --- frontend/app/block/block.tsx | 57 +++++++++++++++--------- frontend/app/block/blockenv.ts | 24 ++++++++++ frontend/app/block/blockframe-header.tsx | 42 +++++++++-------- frontend/app/block/blockframe.tsx | 50 +++++++++++---------- frontend/app/block/blockutil.tsx | 20 +++------ frontend/app/store/global.ts | 8 ---- frontend/app/store/wos.ts | 32 ++++++++++++- frontend/app/waveenv/waveenv.ts | 2 + frontend/app/waveenv/waveenvimpl.ts | 2 + frontend/preview/mock/mockwaveenv.ts | 23 ++++++++-- 10 files changed, 169 insertions(+), 91 deletions(-) create mode 100644 frontend/app/block/blockenv.ts diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index d3c234b33e..464224f8d7 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -22,11 +22,7 @@ import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; import { useDebouncedNodeInnerRect } from "@/layout/index"; import { counterInc } from "@/store/counters"; -import { - getBlockComponentModel, - registerBlockComponentModel, - unregisterBlockComponentModel, -} from "@/store/global"; +import { getBlockComponentModel, registerBlockComponentModel, unregisterBlockComponentModel } from "@/store/global"; import { makeORef } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { isBlank, useAtomValueSafe } from "@/util/util"; @@ -40,6 +36,7 @@ import { memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRe import { QuickTipsViewModel } from "../view/quicktipsview/quicktipsview"; import { WaveConfigViewModel } from "../view/waveconfig/waveconfig-model"; import "./block.scss"; +import { BlockEnv } from "./blockenv"; import { BlockFrame } from "./blockframe"; import { blockViewToIcon, blockViewToName } from "./blockutil"; @@ -109,7 +106,7 @@ function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEn } const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { - const waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); if (!blockData) { return null; @@ -126,7 +123,7 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { }); const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { - const waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const blockRef = useRef(null); const contentRef = useRef(null); @@ -149,7 +146,7 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { counterInc("render-BlockFull"); - const waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const focusElemRef = useRef(null); const blockRef = useRef(null); const contentRef = useRef(null); @@ -312,16 +309,16 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { ); }); -const Block = memo((props: BlockProps) => { +const BlockInner = memo((props: BlockProps & { viewType: string }) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const tabModel = useTabModel(); const waveEnv = useWaveEnv(); - const [blockData, loading] = waveEnv.useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; - if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { - viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv); + if (viewModel == null) { + // viewModel gets the full waveEnv + viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { @@ -330,25 +327,33 @@ const Block = memo((props: BlockProps) => { viewModel?.dispose?.(); }; }, []); - if (loading || isBlank(props.nodeModel.blockId) || blockData == null) { - return null; - } if (props.preview) { return ; } return ; }); +BlockInner.displayName = "BlockInner"; -const SubBlock = memo((props: SubBlockProps) => { +const Block = memo((props: BlockProps) => { + const waveEnv = useWaveEnv(); + const isNull = useAtomValue(waveEnv.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); + const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? ""; + if (isNull || isBlank(props.nodeModel.blockId)) { + return null; + } + return ; +}); + +const SubBlockInner = memo((props: SubBlockProps & { viewType: string }) => { counterInc("render-Block"); - counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); + counterInc("render-Block-" + props.nodeModel.blockId?.substring(0, 8)); const tabModel = useTabModel(); const waveEnv = useWaveEnv(); - const [blockData, loading] = waveEnv.useWaveObjectValue(makeORef("block", props.nodeModel.blockId)); const bcm = getBlockComponentModel(props.nodeModel.blockId); let viewModel = bcm?.viewModel; - if (viewModel == null || viewModel.viewType != blockData?.meta?.view) { - viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel, tabModel, waveEnv); + if (viewModel == null) { + // viewModel gets the full waveEnv + viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv); registerBlockComponentModel(props.nodeModel.blockId, { viewModel }); } useEffect(() => { @@ -357,10 +362,18 @@ const SubBlock = memo((props: SubBlockProps) => { viewModel?.dispose?.(); }; }, []); - if (loading || isBlank(props.nodeModel.blockId) || blockData == null) { + return ; +}); +SubBlockInner.displayName = "SubBlockInner"; + +const SubBlock = memo((props: SubBlockProps) => { + const waveEnv = useWaveEnv(); + const isNull = useAtomValue(waveEnv.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); + const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? ""; + if (isNull || isBlank(props.nodeModel.blockId)) { return null; } - return ; + return ; }); export { Block, SubBlock }; diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts new file mode 100644 index 0000000000..e9a8a953e1 --- /dev/null +++ b/frontend/app/block/blockenv.ts @@ -0,0 +1,24 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { BlockMetaKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv"; + +export type BlockEnv = { + settingsAtoms: { + "app:focusfollowscursor": WaveEnv["settingsAtoms"]["app:focusfollowscursor"]; + "app:showoverlayblocknums": WaveEnv["settingsAtoms"]["app:showoverlayblocknums"]; + "window:magnifiedblockblurprimarypx": WaveEnv["settingsAtoms"]["window:magnifiedblockblurprimarypx"]; + "window:magnifiedblockopacity": WaveEnv["settingsAtoms"]["window:magnifiedblockopacity"]; + }; + atoms: { + modalOpen: WaveEnv["atoms"]["modalOpen"]; + controlShiftDelayAtom: WaveEnv["atoms"]["controlShiftDelayAtom"]; + }; + rpc: { + ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; + ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; + }; + useWaveObjectValue: WaveEnv["useWaveObjectValue"]; + isWaveObjectNullAtom: WaveEnv["isWaveObjectNullAtom"]; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"frame:text" | "frame:activebordercolor" | "frame:bordercolor" | "view" | "connection" | "icon:color" | "frame:title" | "frame:icon">; +}; diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index 2ac93f32dd..252f1f8845 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -15,14 +15,13 @@ import { ContextMenuModel } from "@/app/store/contextmenu"; import { recordTEvent, refocusNode } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; import { uxCloseBlock } from "@/app/store/keymodel"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { BlockEnv } from "./blockenv"; import { IconButton } from "@/element/iconbutton"; import { NodeModel } from "@/layout/index"; import * as util from "@/util/util"; import { cn, makeIconClass } from "@/util/util"; -import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { makeORef } from "@/store/wos"; import * as jotai from "jotai"; import * as React from "react"; import { BlockFrameProps } from "./blocktypes"; @@ -36,7 +35,7 @@ function handleHeaderContextMenu( e.preventDefault(); e.stopPropagation(); const magnified = globalStore.get(nodeModel.isMagnified); - let menu: ContextMenuItem[] = [ + const menu: ContextMenuItem[] = [ { label: magnified ? "Un-Magnify Block" : "Magnify Block", click: () => { @@ -65,14 +64,17 @@ function handleHeaderContextMenu( type HeaderTextElemsProps = { viewModel: ViewModel; - blockData: Block; + blockId: string; preview: boolean; error?: Error; }; -const HeaderTextElems = React.memo(({ viewModel, blockData, preview, error }: HeaderTextElemsProps) => { +const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: HeaderTextElemsProps) => { + const waveEnv = useWaveEnv(); + const frameTextAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:text"); + const frameText = jotai.useAtomValue(frameTextAtom); let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText); - headerTextUnion = blockData?.meta?.["frame:text"] ?? headerTextUnion; + headerTextUnion = frameText ?? headerTextUnion; const headerTextElems: React.ReactElement[] = []; if (typeof headerTextUnion === "string") { @@ -173,10 +175,13 @@ const BlockFrame_Header = ({ changeConnModalAtom, error, }: BlockFrameProps & { changeConnModalAtom: jotai.PrimitiveAtom; error?: Error }) => { - const waveEnv = useWaveEnv(); - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); - let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(blockData?.meta?.view); - let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); + const waveEnv = useWaveEnv(); + const metaView = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")); + const metaFrameTitle = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:title")); + const metaFrameIcon = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:icon")); + const metaConnection = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection")); + let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(metaView); + let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(metaView); const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton); const useTermHeader = util.useAtomValueSafe(viewModel?.useTermHeader); const termConfigedDurable = util.useAtomValueSafe(viewModel?.termConfigedDurable); @@ -185,20 +190,21 @@ const BlockFrame_Header = ({ const magnified = jotai.useAtomValue(nodeModel.isMagnified); const prevMagifiedState = React.useRef(magnified); const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection); + const iconColor = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "icon:color")); const dragHandleRef = preview ? null : nodeModel.dragHandleRef; - const isTerminalBlock = blockData?.meta?.view === "term"; - viewName = blockData?.meta?.["frame:title"] ?? viewName; - viewIconUnion = blockData?.meta?.["frame:icon"] ?? viewIconUnion; + const isTerminalBlock = metaView === "term"; + viewName = metaFrameTitle ?? viewName; + viewIconUnion = metaFrameIcon ?? viewIconUnion; React.useEffect(() => { if (magnified && !preview && !prevMagifiedState.current) { - RpcApi.ActivityCommand(TabRpcClient, { nummagnify: 1 }); + waveEnv.rpc.ActivityCommand(TabRpcClient, { nummagnify: 1 }); recordTEvent("action:magnify", { "block:view": viewName }); } prevMagifiedState.current = magnified; }, [magnified]); - const viewIconElem = getViewIconElem(viewIconUnion, blockData); + const viewIconElem = getViewIconElem(viewIconUnion, iconColor); return (
@@ -239,7 +245,7 @@ const BlockFrame_Header = ({
)} - + ); diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 327486c1a0..a1122e2fd5 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -8,23 +8,23 @@ import { ConnStatusOverlay } from "@/app/block/connstatusoverlay"; import { ChangeConnectionBlockModal } from "@/app/modals/conntypeahead"; import { getBlockComponentModel, globalStore, useBlockAtom } from "@/app/store/global"; import { useTabModel } from "@/app/store/tab-model"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { ErrorBoundary } from "@/element/errorboundary"; import { NodeModel } from "@/layout/index"; +import { makeORef } from "@/store/wos"; import * as util from "@/util/util"; import { makeIconClass } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; -import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { makeORef } from "@/store/wos"; import clsx from "clsx"; import * as jotai from "jotai"; import * as React from "react"; +import { BlockEnv } from "./blockenv"; import { BlockFrameProps } from "./blocktypes"; const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { - const waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const tabModel = useTabModel(); const isFocused = jotai.useAtomValue(nodeModel.isFocused); const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral); @@ -32,7 +32,10 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const isLayoutMode = jotai.useAtomValue(waveEnv.atoms.controlShiftDelayAtom); const showOverlayBlockNums = jotai.useAtomValue(waveEnv.settingsAtoms["app:showoverlayblocknums"]) ?? true; const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const frameActiveBorderColor = jotai.useAtomValue( + waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:activebordercolor") + ); + const frameBorderColor = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:bordercolor")); const tabActiveBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:activebordercolor")); const tabBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:bordercolor")); const style: React.CSSProperties = {}; @@ -42,15 +45,15 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { if (tabActiveBorderColor) { style.borderColor = tabActiveBorderColor; } - if (blockData?.meta?.["frame:activebordercolor"]) { - style.borderColor = blockData.meta["frame:activebordercolor"]; + if (frameActiveBorderColor) { + style.borderColor = frameActiveBorderColor; } } else { if (tabBorderColor) { style.borderColor = tabBorderColor; } - if (blockData?.meta?.["frame:bordercolor"]) { - style.borderColor = blockData.meta["frame:bordercolor"]; + if (frameBorderColor) { + style.borderColor = frameBorderColor; } if (isEphemeral && !style.borderColor) { style.borderColor = "rgba(255, 255, 255, 0.7)"; @@ -90,12 +93,12 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { }); const BlockFrame_Default_Component = (props: BlockFrameProps) => { - const waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props; - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const isFocused = jotai.useAtomValue(nodeModel.isFocused); const aiPanelVisible = jotai.useAtomValue(WorkspaceLayoutModel.getInstance().panelVisibleAtom); - const viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view); + const metaView = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")); + const viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(metaView); const customBg = util.useAtomValueSafe(viewModel?.blockBg); const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection); const changeConnModalAtom = useBlockAtom(nodeModel.blockId, "changeConn", () => { @@ -109,6 +112,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const [magnifiedBlockOpacityAtom] = React.useState(() => waveEnv.settingsAtoms["window:magnifiedblockopacity"]); const magnifiedBlockOpacity = jotai.useAtomValue(magnifiedBlockOpacityAtom); const connBtnRef = React.useRef(null); + const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection")); + const iconColor = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "icon:color")); const noHeader = util.useAtomValueSafe(viewModel?.noHeader); React.useEffect(() => { @@ -130,23 +135,20 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { }, [manageConnection]); React.useEffect(() => { // on mount, if manageConnection, call ConnEnsure - if (!manageConnection || blockData == null || preview) { + if (!manageConnection || preview) { return; } - const connName = blockData?.meta?.connection; if (!util.isLocalConnName(connName)) { console.log("ensure conn", nodeModel.blockId, connName); - RpcApi.ConnEnsureCommand( - TabRpcClient, - { connname: connName, logblockid: nodeModel.blockId }, - { timeout: 60000 } - ).catch((e) => { - console.log("error ensuring connection", nodeModel.blockId, connName, e); - }); + waveEnv.rpc + .ConnEnsureCommand(TabRpcClient, { connname: connName, logblockid: nodeModel.blockId }, { timeout: 60000 }) + .catch((e) => { + console.log("error ensuring connection", nodeModel.blockId, connName, e); + }); } - }, [manageConnection, blockData]); + }, [manageConnection, connName]); - const viewIconElem = getViewIconElem(viewIconUnion, blockData); + const viewIconElem = getViewIconElem(viewIconUnion, iconColor); let innerStyle: React.CSSProperties = {}; if (!preview) { innerStyle = computeBgStyleFromMeta(customBg); @@ -207,7 +209,7 @@ 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 waveEnv = useWaveEnv(); + const waveEnv = useWaveEnv(); const tabModel = useTabModel(); const blockId = props.nodeModel.blockId; const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", blockId)); diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index 542f9f352a..01346183a0 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -66,7 +66,7 @@ export function processTitleString(titleString: string): React.ReactNode[] { const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g; let lastIdx = 0; let match; - let partsStack = [[]]; + const partsStack = [[]]; while ((match = tagRegex.exec(titleString)) != null) { const lastPart = partsStack[partsStack.length - 1]; const before = titleString.substring(lastIdx, match.index); @@ -98,7 +98,7 @@ export function processTitleString(titleString: string): React.ReactNode[] { if (!tagParam.match(colorRegex)) { continue; } - let children = []; + const children = []; const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children); lastPart.push(rtag); partsStack.push(children); @@ -112,7 +112,7 @@ export function processTitleString(titleString: string): React.ReactNode[] { partsStack.pop(); continue; } - let children = []; + const children = []; const rtag = React.createElement(tagName, { key: match.index }, children); lastPart.push(rtag); partsStack.push(children); @@ -123,12 +123,12 @@ export function processTitleString(titleString: string): React.ReactNode[] { return partsStack[0]; } -export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode { +export function getBlockHeaderIcon(blockIcon: string, overrideIconColor?: string): React.ReactNode { let blockIconElem: React.ReactNode = null; if (util.isBlank(blockIcon)) { blockIcon = "square"; } - let iconColor = blockData?.meta?.["icon:color"]; + let iconColor = overrideIconColor; if (iconColor && !iconColor.match(colorRegex)) { iconColor = null; } @@ -145,17 +145,11 @@ export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.R export function getViewIconElem( viewIconUnion: string | IconButtonDecl, - blockData: Block, - iconColor?: string + overrideIconColor?: string ): React.ReactElement { if (viewIconUnion == null || typeof viewIconUnion === "string") { const viewIcon = viewIconUnion as string; - const style: React.CSSProperties = iconColor ? { color: iconColor, opacity: 1.0 } : {}; - return ( -
- {getBlockHeaderIcon(viewIcon, blockData)} -
- ); + return
{getBlockHeaderIcon(viewIcon, overrideIconColor)}
; } else { return ; } diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 2ae7cb47c6..e64e8b124c 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -324,13 +324,6 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => Atom): return atom as Atom; } -function useBlockDataLoaded(blockId: string): boolean { - const loadedAtom = useBlockAtom(blockId, "block-loaded", () => { - return WOS.getWaveObjectLoadingAtom(WOS.makeORef("block", blockId)); - }); - return useAtomValue(loadedAtom); -} - /** * Safely read an atom value, returning null if the atom is null. */ @@ -703,7 +696,6 @@ export { unregisterBlockComponentModel, useBlockAtom, useBlockCache, - useBlockDataLoaded, useOrefMetaKeyAtom, useOverrideConfigAtom, useSettingsKeyAtom, diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 7fa0142799..645949ced4 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -218,17 +218,44 @@ function loadAndPinWaveObject(oref: string): Promise { return wov.pendingPromise; } +const waveObjectDerivedAtomCache = new Map>(); + function getWaveObjectAtom(oref: string): Atom { + const cacheKey = oref + ":value"; + let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom; + if (cachedAtom != null) { + return cachedAtom; + } const wov = getWaveObjectValue(oref); - return atom((get) => get(wov.dataAtom).value); + cachedAtom = atom((get) => get(wov.dataAtom).value); + waveObjectDerivedAtomCache.set(cacheKey, cachedAtom); + return cachedAtom; } function getWaveObjectLoadingAtom(oref: string): Atom { + const cacheKey = oref + ":loading"; + let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom; + if (cachedAtom != null) { + return cachedAtom; + } const wov = getWaveObjectValue(oref); - return atom((get) => { + cachedAtom = atom((get) => { const dataValue = get(wov.dataAtom); return dataValue.loading; }); + waveObjectDerivedAtomCache.set(cacheKey, cachedAtom); + return cachedAtom; +} + +function isWaveObjectNullAtom(oref: string): Atom { + const cacheKey = oref + ":isnull"; + let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom; + if (cachedAtom != null) { + return cachedAtom; + } + cachedAtom = atom((get) => get(getWaveObjectAtom(oref)) == null); + waveObjectDerivedAtomCache.set(cacheKey, cachedAtom); + return cachedAtom; } function useWaveObjectValue(oref: string): [T, boolean] { @@ -320,6 +347,7 @@ export { getObjectValue, getWaveObjectAtom, getWaveObjectLoadingAtom, + isWaveObjectNullAtom, loadAndPinWaveObject, makeORef, mockObjectForPreview, diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index d7302b5b77..740ace6050 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -24,6 +24,8 @@ export type WaveEnv = { showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => void; getConnStatusAtom: (conn: string) => PrimitiveAtom; getWaveObjectAtom: (oref: string) => Atom; + getWaveObjectLoadingAtom: (oref: string) => Atom; + isWaveObjectNullAtom: (oref: string) => Atom; useWaveObjectValue: (oref: string) => [T, boolean]; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; mockTabModel?: TabModel; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 0396a05419..d30256f7b7 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -33,6 +33,8 @@ export function makeWaveEnvImpl(): WaveEnv { }, getConnStatusAtom, getWaveObjectAtom: WOS.getWaveObjectAtom, + getWaveObjectLoadingAtom: WOS.getWaveObjectLoadingAtom, + isWaveObjectNullAtom: WOS.isWaveObjectNullAtom, useWaveObjectValue: WOS.useWaveObjectValue, getBlockMetaKeyAtom, }; diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index ae7c4c92cb..136148b2fb 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -161,7 +161,7 @@ export function applyMockEnvOverrides(env: WaveEnv, newOverrides: MockEnv): Mock export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const overrides: MockEnv = mockEnv ?? {}; const connStatusAtomCache = new Map>(); - const waveObjectAtomCache = new Map>(); + const waveObjectAtomCache = new Map>(); const blockMetaKeyAtomCache = new Map>(); const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId); const env = { @@ -190,11 +190,26 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { return connStatusAtomCache.get(conn); }, getWaveObjectAtom: (oref: string) => { - if (!waveObjectAtomCache.has(oref)) { + const cacheKey = oref + ":value"; + if (!waveObjectAtomCache.has(cacheKey)) { const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; - waveObjectAtomCache.set(oref, atom(obj)); + waveObjectAtomCache.set(cacheKey, atom(obj)); } - return waveObjectAtomCache.get(oref) as PrimitiveAtom; + return waveObjectAtomCache.get(cacheKey) as PrimitiveAtom; + }, + getWaveObjectLoadingAtom: (oref: string) => { + const cacheKey = oref + ":loading"; + if (!waveObjectAtomCache.has(cacheKey)) { + waveObjectAtomCache.set(cacheKey, atom(false)); + } + return waveObjectAtomCache.get(cacheKey) as Atom; + }, + isWaveObjectNullAtom: (oref: string) => { + const cacheKey = oref + ":isnull"; + if (!waveObjectAtomCache.has(cacheKey)) { + waveObjectAtomCache.set(cacheKey, atom((get) => get(env.getWaveObjectAtom(oref)) == null)); + } + return waveObjectAtomCache.get(cacheKey) as Atom; }, useWaveObjectValue: (oref: string): [T, boolean] => { const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; From ffd3e32f10982828b99f86cd3ecc20cee7f8b3e0 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 21:53:04 -0700 Subject: [PATCH 05/14] refactor wos methods --- frontend/app/block/block.tsx | 12 +++--- frontend/app/block/blockenv.ts | 3 +- frontend/app/block/blockframe.tsx | 2 +- frontend/app/store/tab-model.ts | 2 +- frontend/app/waveenv/waveenv.ts | 10 +++-- frontend/app/waveenv/waveenvimpl.ts | 10 +++-- frontend/preview/mock/mockwaveenv.ts | 55 +++++++++++++++------------- 7 files changed, 51 insertions(+), 43 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 464224f8d7..ef7fb26f41 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -87,7 +87,7 @@ function getViewElem( } function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEnv): ViewModel { - const blockDataAtom = waveEnv.getWaveObjectAtom(makeORef("block", blockId)); + const blockDataAtom = waveEnv.wos.getWaveObjectAtom(makeORef("block", blockId)); const viewModel: ViewModel = { viewType: viewType, viewIcon: atom((get) => { @@ -107,7 +107,7 @@ function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEn const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { const waveEnv = useWaveEnv(); - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); if (!blockData) { return null; } @@ -124,7 +124,7 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { const waveEnv = useWaveEnv(); - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const blockRef = useRef(null); const contentRef = useRef(null); const viewElem = useMemo( @@ -151,7 +151,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { const blockRef = useRef(null); const contentRef = useRef(null); const [blockClicked, setBlockClicked] = useState(false); - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); const isFocused = useAtomValue(nodeModel.isFocused); const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents); const isResizing = useAtomValue(nodeModel.isResizing); @@ -336,7 +336,7 @@ BlockInner.displayName = "BlockInner"; const Block = memo((props: BlockProps) => { const waveEnv = useWaveEnv(); - const isNull = useAtomValue(waveEnv.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); + const isNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? ""; if (isNull || isBlank(props.nodeModel.blockId)) { return null; @@ -368,7 +368,7 @@ SubBlockInner.displayName = "SubBlockInner"; const SubBlock = memo((props: SubBlockProps) => { const waveEnv = useWaveEnv(); - const isNull = useAtomValue(waveEnv.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); + const isNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", props.nodeModel.blockId))); const viewType = useAtomValue(waveEnv.getBlockMetaKeyAtom(props.nodeModel.blockId, "view")) ?? ""; if (isNull || isBlank(props.nodeModel.blockId)) { return null; diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index e9a8a953e1..e59ee10cb4 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -18,7 +18,6 @@ export type BlockEnv = { ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; }; - useWaveObjectValue: WaveEnv["useWaveObjectValue"]; - isWaveObjectNullAtom: WaveEnv["isWaveObjectNullAtom"]; + wos: WaveEnv["wos"]; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"frame:text" | "frame:activebordercolor" | "frame:bordercolor" | "view" | "connection" | "icon:color" | "frame:title" | "frame:icon">; }; diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index a1122e2fd5..50a419c104 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -212,7 +212,7 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { const waveEnv = useWaveEnv(); const tabModel = useTabModel(); const blockId = props.nodeModel.blockId; - const [blockData] = waveEnv.useWaveObjectValue(makeORef("block", blockId)); + const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", blockId)); const numBlocks = jotai.useAtomValue(tabModel.tabNumBlocksAtom); if (!blockId || !blockData) { return null; diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index b97aa77aa2..316209f69b 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -23,7 +23,7 @@ export class TabModel { this.waveEnv = waveEnv; this.tabAtom = atom((get) => { if (this.waveEnv != null) { - return get(this.waveEnv.getWaveObjectAtom(WOS.makeORef("tab", this.tabId))); + return get(this.waveEnv.wos.getWaveObjectAtom(WOS.makeORef("tab", this.tabId))); } return WOS.getObjectValue(WOS.makeORef("tab", this.tabId), get); }); diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index 740ace6050..8b37840015 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -23,10 +23,12 @@ export type WaveEnv = { createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => Promise; showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => void; getConnStatusAtom: (conn: string) => PrimitiveAtom; - getWaveObjectAtom: (oref: string) => Atom; - getWaveObjectLoadingAtom: (oref: string) => Atom; - isWaveObjectNullAtom: (oref: string) => Atom; - useWaveObjectValue: (oref: string) => [T, boolean]; + wos: { + getWaveObjectAtom: (oref: string) => Atom; + getWaveObjectLoadingAtom: (oref: string) => Atom; + isWaveObjectNullAtom: (oref: string) => Atom; + useWaveObjectValue: (oref: string) => [T, boolean]; + }; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; mockTabModel?: TabModel; }; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index d30256f7b7..4f0e421a2c 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -32,10 +32,12 @@ export function makeWaveEnvImpl(): WaveEnv { ContextMenuModel.getInstance().showContextMenu(menu, e); }, getConnStatusAtom, - getWaveObjectAtom: WOS.getWaveObjectAtom, - getWaveObjectLoadingAtom: WOS.getWaveObjectLoadingAtom, - isWaveObjectNullAtom: WOS.isWaveObjectNullAtom, - useWaveObjectValue: WOS.useWaveObjectValue, + wos: { + getWaveObjectAtom: WOS.getWaveObjectAtom, + getWaveObjectLoadingAtom: WOS.getWaveObjectLoadingAtom, + isWaveObjectNullAtom: WOS.isWaveObjectNullAtom, + useWaveObjectValue: WOS.useWaveObjectValue, + }, getBlockMetaKeyAtom, }; } diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 136148b2fb..9aeef93849 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -189,38 +189,43 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { } return connStatusAtomCache.get(conn); }, - getWaveObjectAtom: (oref: string) => { - const cacheKey = oref + ":value"; - if (!waveObjectAtomCache.has(cacheKey)) { + wos: { + getWaveObjectAtom: (oref: string) => { + const cacheKey = oref + ":value"; + if (!waveObjectAtomCache.has(cacheKey)) { + const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; + waveObjectAtomCache.set(cacheKey, atom(obj)); + } + return waveObjectAtomCache.get(cacheKey) as PrimitiveAtom; + }, + getWaveObjectLoadingAtom: (oref: string) => { + const cacheKey = oref + ":loading"; + if (!waveObjectAtomCache.has(cacheKey)) { + waveObjectAtomCache.set(cacheKey, atom(false)); + } + return waveObjectAtomCache.get(cacheKey) as Atom; + }, + isWaveObjectNullAtom: (oref: string) => { + const cacheKey = oref + ":isnull"; + if (!waveObjectAtomCache.has(cacheKey)) { + waveObjectAtomCache.set( + cacheKey, + atom((get) => get(env.wos.getWaveObjectAtom(oref)) == null) + ); + } + return waveObjectAtomCache.get(cacheKey) as Atom; + }, + useWaveObjectValue: (oref: string): [T, boolean] => { const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; - waveObjectAtomCache.set(cacheKey, atom(obj)); - } - return waveObjectAtomCache.get(cacheKey) as PrimitiveAtom; - }, - getWaveObjectLoadingAtom: (oref: string) => { - const cacheKey = oref + ":loading"; - if (!waveObjectAtomCache.has(cacheKey)) { - waveObjectAtomCache.set(cacheKey, atom(false)); - } - return waveObjectAtomCache.get(cacheKey) as Atom; - }, - isWaveObjectNullAtom: (oref: string) => { - const cacheKey = oref + ":isnull"; - if (!waveObjectAtomCache.has(cacheKey)) { - waveObjectAtomCache.set(cacheKey, atom((get) => get(env.getWaveObjectAtom(oref)) == null)); - } - return waveObjectAtomCache.get(cacheKey) as Atom; - }, - useWaveObjectValue: (oref: string): [T, boolean] => { - const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T; - return [obj, false]; + return [obj, false]; + }, }, getBlockMetaKeyAtom: (blockId: string, key: T) => { const cacheKey = blockId + "#meta-" + key; if (!blockMetaKeyAtomCache.has(cacheKey)) { const metaAtom = atom((get) => { const blockORef = "block:" + blockId; - const blockAtom = env.getWaveObjectAtom(blockORef); + const blockAtom = env.wos.getWaveObjectAtom(blockORef); const blockData = get(blockAtom); return blockData?.meta?.[key] as MetaType[T]; }); From 63247a50eda764c788999a8d41326abbfdca0d27 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 22:11:10 -0700 Subject: [PATCH 06/14] more block simplification of dependencies --- frontend/app/block/block.tsx | 38 ++++++++----------- frontend/app/block/blockframe.tsx | 4 +- frontend/app/block/connstatusoverlay.tsx | 10 +++-- .../app/block/durable-session-flyover.tsx | 10 +++-- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index ef7fb26f41..1132bfa115 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -66,7 +66,7 @@ function makeViewModel( if (ctor != null) { return new ctor({ blockId, nodeModel, tabModel, waveEnv }); } - return makeDefaultViewModel(blockId, blockView, waveEnv); + return makeDefaultViewModel(blockView); } function getViewElem( @@ -86,18 +86,11 @@ function getViewElem( return ; } -function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEnv): ViewModel { - const blockDataAtom = waveEnv.wos.getWaveObjectAtom(makeORef("block", blockId)); +function makeDefaultViewModel(viewType: string): ViewModel { const viewModel: ViewModel = { viewType: viewType, - viewIcon: atom((get) => { - const blockData = get(blockDataAtom); - return blockViewToIcon(blockData?.meta?.view); - }), - viewName: atom((get) => { - const blockData = get(blockDataAtom); - return blockViewToName(blockData?.meta?.view); - }), + viewIcon: atom(blockViewToIcon(viewType)), + viewName: atom(blockViewToName(viewType)), preIconButton: atom(null), endIconButtons: atom(null), viewComponent: null, @@ -107,8 +100,8 @@ function makeDefaultViewModel(blockId: string, viewType: string, waveEnv: WaveEn const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { const waveEnv = useWaveEnv(); - const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); - if (!blockData) { + const blockIsNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", nodeModel.blockId))); + if (blockIsNull) { return null; } return ( @@ -124,15 +117,16 @@ const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { const waveEnv = useWaveEnv(); - const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const blockIsNull = useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", nodeModel.blockId))); + const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? ""; const blockRef = useRef(null); const contentRef = useRef(null); const viewElem = useMemo( - () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel), - [nodeModel.blockId, blockData?.meta?.view, viewModel] + () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockView, viewModel), + [nodeModel.blockId, blockView, viewModel] ); const noPadding = useAtomValueSafe(viewModel.noPadding); - if (!blockData) { + if (blockIsNull) { return null; } return ( @@ -151,7 +145,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { const blockRef = useRef(null); const contentRef = useRef(null); const [blockClicked, setBlockClicked] = useState(false); - const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", nodeModel.blockId)); + const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? ""; const isFocused = useAtomValue(nodeModel.isFocused); const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents); const isResizing = useAtomValue(nodeModel.isResizing); @@ -211,8 +205,8 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { }, [innerRect, disablePointerEvents, blockContentOffset]); const viewElem = useMemo( - () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel), - [nodeModel.blockId, blockData?.meta?.view, viewModel] + () => getViewElem(nodeModel.blockId, blockRef, contentRef, blockView, viewModel), + [nodeModel.blockId, blockView, viewModel] ); const handleChildFocus = useCallback( @@ -238,7 +232,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { (event: React.PointerEvent) => { const focusFollowsCursorEnabled = focusFollowsCursorMode === "on" || - (focusFollowsCursorMode === "term" && blockData?.meta?.view === "term"); + (focusFollowsCursorMode === "term" && blockView === "term"); if (!focusFollowsCursorEnabled || event.pointerType === "touch" || event.buttons > 0) { return; } @@ -255,7 +249,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { }, [ focusFollowsCursorMode, - blockData?.meta?.view, + blockView, modalOpen, disablePointerEvents, isResizing, diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 50a419c104..6112aea830 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -212,9 +212,9 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { const waveEnv = useWaveEnv(); const tabModel = useTabModel(); const blockId = props.nodeModel.blockId; - const [blockData] = waveEnv.wos.useWaveObjectValue(makeORef("block", blockId)); + const blockIsNull = jotai.useAtomValue(waveEnv.wos.isWaveObjectNullAtom(makeORef("block", blockId))); const numBlocks = jotai.useAtomValue(tabModel.tabNumBlocksAtom); - if (!blockId || !blockData) { + if (!blockId || blockIsNull) { return null; } return ; diff --git a/frontend/app/block/connstatusoverlay.tsx b/frontend/app/block/connstatusoverlay.tsx index 526dbbae32..887729cf15 100644 --- a/frontend/app/block/connstatusoverlay.tsx +++ b/frontend/app/block/connstatusoverlay.tsx @@ -4,15 +4,17 @@ import { Button } from "@/app/element/button"; import { CopyButton } from "@/app/element/copybutton"; import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions"; -import { atoms, getConnStatusAtom, WOS } from "@/app/store/global"; +import { atoms, getConnStatusAtom } from "@/app/store/global"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import { NodeModel } from "@/layout/index"; import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; import * as React from "react"; +import { BlockEnv } from "./blockenv"; function formatElapsedTime(elapsedMs: number): string { if (elapsedMs <= 0) { @@ -118,9 +120,9 @@ export const ConnStatusOverlay = React.memo( viewModel: ViewModel; changeConnModalAtom: jotai.PrimitiveAtom; }) => { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); + const waveEnv = useWaveEnv(); + const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection")); const [connModalOpen] = jotai.useAtom(changeConnModalAtom); - const connName = blockData?.meta?.connection; const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); @@ -215,7 +217,7 @@ export const ConnStatusOverlay = React.memo( [showError, showWshError, connStatus.error, connStatus.wsherror] ); - let showStalled = connStatus.status == "connected" && connStatus.connhealthstatus == "stalled"; + const showStalled = connStatus.status == "connected" && connStatus.connhealthstatus == "stalled"; if (!showWshError && !showStalled && (isLayoutMode || connStatus.status == "connected" || connModalOpen)) { return null; } diff --git a/frontend/app/block/durable-session-flyover.tsx b/frontend/app/block/durable-session-flyover.tsx index 620c57731f..29f72e4b75 100644 --- a/frontend/app/block/durable-session-flyover.tsx +++ b/frontend/app/block/durable-session-flyover.tsx @@ -1,8 +1,9 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getApi, getConnStatusAtom, recordTEvent, WOS } from "@/app/store/global"; +import { getApi, getConnStatusAtom, recordTEvent } from "@/app/store/global"; import { TermViewModel } from "@/app/view/term/term-model"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import * as util from "@/util/util"; import { cn } from "@/util/util"; import { @@ -18,6 +19,7 @@ import { } from "@floating-ui/react"; import * as jotai from "jotai"; import { useEffect, useRef, useState } from "react"; +import { BlockEnv } from "./blockenv"; function isTermViewModel(viewModel: ViewModel): viewModel is TermViewModel { return viewModel?.viewType === "term"; @@ -194,7 +196,7 @@ function DurableEndedContent({ doneReason, startupError, viewModel, onClose }: D let titleText = "Durable Session (Ended)"; let descriptionText = "The durable session has ended. This block is still configured for durable sessions."; - let showRestartButton = true; + const showRestartButton = true; if (doneReason === "terminated") { titleText = "Durable Session (Ended, Exited)"; @@ -333,10 +335,10 @@ export function DurableSessionFlyover({ placement = "bottom", divClassName, }: DurableSessionFlyoverProps) { - const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const waveEnv = useWaveEnv(); + const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(blockId, "connection")); const termDurableStatus = util.useAtomValueSafe(viewModel?.termDurableStatus); const termConfigedDurable = util.useAtomValueSafe(viewModel?.termConfigedDurable); - const connName = blockData?.meta?.connection; const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); const { color: durableIconColor, iconType: durableIconType } = getIconProps( From 5df50577ee6fb61c3bc35445ca0a89013bf7604c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 22:37:46 -0700 Subject: [PATCH 07/14] simplify, get connstatusoverlay and durable flyover to work with waveenv... --- frontend/app/block/block.tsx | 2 +- frontend/app/block/blockenv.ts | 34 ++++++++++---- frontend/app/block/blockframe.tsx | 6 +-- frontend/app/block/connstatusoverlay.tsx | 27 +++++------ .../app/block/durable-session-flyover.tsx | 14 +++--- frontend/app/store/global.ts | 1 + frontend/app/waveenv/waveenv.ts | 14 ++++-- frontend/app/waveenv/waveenvimpl.ts | 10 ++-- frontend/preview/mock/mockwaveenv.ts | 47 ++++++++++--------- 9 files changed, 89 insertions(+), 66 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 1132bfa115..126f208813 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -152,7 +152,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { const isMagnified = useAtomValue(nodeModel.isMagnified); const anyMagnified = useAtomValue(nodeModel.anyMagnified); const modalOpen = useAtomValue(waveEnv.atoms.modalOpen); - const focusFollowsCursorMode = useAtomValue(waveEnv.settingsAtoms["app:focusfollowscursor"]) ?? "off"; + const focusFollowsCursorMode = useAtomValue(waveEnv.getSettingsKeyAtom("app:focusfollowscursor")) ?? "off"; const innerRect = useDebouncedNodeInnerRect(nodeModel); const noPadding = useAtomValueSafe(viewModel.noPadding); diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index e59ee10cb4..9a769bb7be 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -1,23 +1,41 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockMetaKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv"; +import { BlockMetaKeyAtomFnType, ConnConfigKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv"; export type BlockEnv = { - settingsAtoms: { - "app:focusfollowscursor": WaveEnv["settingsAtoms"]["app:focusfollowscursor"]; - "app:showoverlayblocknums": WaveEnv["settingsAtoms"]["app:showoverlayblocknums"]; - "window:magnifiedblockblurprimarypx": WaveEnv["settingsAtoms"]["window:magnifiedblockblurprimarypx"]; - "window:magnifiedblockopacity": WaveEnv["settingsAtoms"]["window:magnifiedblockopacity"]; - }; + getSettingsKeyAtom: SettingsKeyAtomFnType< + | "app:focusfollowscursor" + | "app:showoverlayblocknums" + | "window:magnifiedblockblurprimarypx" + | "window:magnifiedblockopacity" + >; atoms: { modalOpen: WaveEnv["atoms"]["modalOpen"]; controlShiftDelayAtom: WaveEnv["atoms"]["controlShiftDelayAtom"]; }; + api: { + openExternal: WaveEnv["electron"]["openExternal"]; + }; rpc: { ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; + ConnDisconnectCommand: WaveEnv["rpc"]["ConnDisconnectCommand"]; + ConnConnectCommand: WaveEnv["rpc"]["ConnConnectCommand"]; + SetConnectionsConfigCommand: WaveEnv["rpc"]["SetConnectionsConfigCommand"]; + DismissWshFailCommand: WaveEnv["rpc"]["DismissWshFailCommand"]; }; wos: WaveEnv["wos"]; - getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"frame:text" | "frame:activebordercolor" | "frame:bordercolor" | "view" | "connection" | "icon:color" | "frame:title" | "frame:icon">; + getConnStatusAtom: WaveEnv["getConnStatusAtom"]; + getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType< + | "frame:text" + | "frame:activebordercolor" + | "frame:bordercolor" + | "view" + | "connection" + | "icon:color" + | "frame:title" + | "frame:icon" + >; }; diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 6112aea830..0b4abb755b 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -30,7 +30,7 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral); const blockNum = jotai.useAtomValue(nodeModel.blockNum); const isLayoutMode = jotai.useAtomValue(waveEnv.atoms.controlShiftDelayAtom); - const showOverlayBlockNums = jotai.useAtomValue(waveEnv.settingsAtoms["app:showoverlayblocknums"]) ?? true; + const showOverlayBlockNums = jotai.useAtomValue(waveEnv.getSettingsKeyAtom("app:showoverlayblocknums")) ?? true; const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); const frameActiveBorderColor = jotai.useAtomValue( waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:activebordercolor") @@ -107,9 +107,9 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const connModalOpen = jotai.useAtomValue(changeConnModalAtom); const isMagnified = jotai.useAtomValue(nodeModel.isMagnified); const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral); - const [magnifiedBlockBlurAtom] = React.useState(() => waveEnv.settingsAtoms["window:magnifiedblockblurprimarypx"]); + const [magnifiedBlockBlurAtom] = React.useState(() => waveEnv.getSettingsKeyAtom("window:magnifiedblockblurprimarypx")); const magnifiedBlockBlur = jotai.useAtomValue(magnifiedBlockBlurAtom); - const [magnifiedBlockOpacityAtom] = React.useState(() => waveEnv.settingsAtoms["window:magnifiedblockopacity"]); + const [magnifiedBlockOpacityAtom] = React.useState(() => waveEnv.getSettingsKeyAtom("window:magnifiedblockopacity")); const magnifiedBlockOpacity = jotai.useAtomValue(magnifiedBlockOpacityAtom); const connBtnRef = React.useRef(null); const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection")); diff --git a/frontend/app/block/connstatusoverlay.tsx b/frontend/app/block/connstatusoverlay.tsx index 887729cf15..d4d6ad14b8 100644 --- a/frontend/app/block/connstatusoverlay.tsx +++ b/frontend/app/block/connstatusoverlay.tsx @@ -4,8 +4,6 @@ import { Button } from "@/app/element/button"; import { CopyButton } from "@/app/element/copybutton"; import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions"; -import { atoms, getConnStatusAtom } from "@/app/store/global"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { NodeModel } from "@/layout/index"; @@ -57,10 +55,11 @@ const StalledOverlay = React.memo( }) => { const [elapsedTime, setElapsedTime] = React.useState(""); + const waveEnv = useWaveEnv(); const handleDisconnect = React.useCallback(() => { - const prtn = RpcApi.ConnDisconnectCommand(TabRpcClient, connName, { timeout: 5000 }); + const prtn = waveEnv.rpc.ConnDisconnectCommand(TabRpcClient, connName, { timeout: 5000 }); prtn.catch((e) => console.log("error disconnecting", connName, e)); - }, [connName]); + }, [connName, waveEnv]); React.useEffect(() => { if (!connStatus.lastactivitybeforestalledtime) { @@ -123,12 +122,13 @@ export const ConnStatusOverlay = React.memo( const waveEnv = useWaveEnv(); const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection")); const [connModalOpen] = jotai.useAtom(changeConnModalAtom); - const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); - const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); + const connStatus = jotai.useAtomValue(waveEnv.getConnStatusAtom(connName)); + const isLayoutMode = jotai.useAtomValue(waveEnv.atoms.controlShiftDelayAtom); const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); const width = domRect?.width; const [showError, setShowError] = React.useState(false); - const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); + const wshConfigEnabled = + jotai.useAtomValue(waveEnv.getConnConfigKeyAtom(connName, "conn:wshenabled")) ?? true; const [showWshError, setShowWshError] = React.useState(false); React.useEffect(() => { @@ -140,13 +140,13 @@ export const ConnStatusOverlay = React.memo( }, [width, connStatus, setShowError]); const handleTryReconnect = React.useCallback(() => { - const prtn = RpcApi.ConnConnectCommand( + const prtn = waveEnv.rpc.ConnConnectCommand( TabRpcClient, { host: connName, logblockid: nodeModel.blockId }, { timeout: 60000 } ); prtn.catch((e) => console.log("error reconnecting", connName, e)); - }, [connName, nodeModel.blockId]); + }, [connName, nodeModel.blockId, waveEnv]); const handleDisableWsh = React.useCallback(async () => { const metamaptype: unknown = { @@ -157,19 +157,19 @@ export const ConnStatusOverlay = React.memo( metamaptype: metamaptype, }; try { - await RpcApi.SetConnectionsConfigCommand(TabRpcClient, data); + await waveEnv.rpc.SetConnectionsConfigCommand(TabRpcClient, data); } catch (e) { console.log("problem setting connection config: ", e); } - }, [connName]); + }, [connName, waveEnv]); const handleRemoveWshError = React.useCallback(async () => { try { - await RpcApi.DismissWshFailCommand(TabRpcClient, connName); + await waveEnv.rpc.DismissWshFailCommand(TabRpcClient, connName); } catch (e) { console.log("unable to dismiss wsh error: ", e); } - }, [connName]); + }, [connName, waveEnv]); let statusText = `Disconnected from "${connName}"`; let showReconnect = true; @@ -191,7 +191,6 @@ export const ConnStatusOverlay = React.memo( } const showIcon = connStatus.status != "connecting"; - const wshConfigEnabled = fullConfig?.connections?.[connName]?.["conn:wshenabled"] ?? true; React.useEffect(() => { const showWshErrorTemp = connStatus.status == "connected" && diff --git a/frontend/app/block/durable-session-flyover.tsx b/frontend/app/block/durable-session-flyover.tsx index 29f72e4b75..bce82bcd52 100644 --- a/frontend/app/block/durable-session-flyover.tsx +++ b/frontend/app/block/durable-session-flyover.tsx @@ -1,7 +1,7 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getApi, getConnStatusAtom, recordTEvent } from "@/app/store/global"; +import { recordTEvent } from "@/app/store/global"; import { TermViewModel } from "@/app/view/term/term-model"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import * as util from "@/util/util"; @@ -25,13 +25,13 @@ function isTermViewModel(viewModel: ViewModel): viewModel is TermViewModel { return viewModel?.viewType === "term"; } -function handleLearnMore() { - getApi().openExternal("https://docs.waveterm.dev/durable-sessions"); -} - function LearnMoreButton() { + const waveEnv = useWaveEnv(); return ( - ); @@ -339,7 +339,7 @@ export function DurableSessionFlyover({ const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(blockId, "connection")); const termDurableStatus = util.useAtomValueSafe(viewModel?.termDurableStatus); const termConfigedDurable = util.useAtomValueSafe(viewModel?.termConfigedDurable); - const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); + const connStatus = jotai.useAtomValue(waveEnv.getConnStatusAtom(connName)); const { color: durableIconColor, iconType: durableIconType } = getIconProps( termDurableStatus, diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index e64e8b124c..eea579b8ce 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -665,6 +665,7 @@ export { getApi, getBlockComponentModel, getBlockMetaKeyAtom, + getConnConfigKeyAtom, getBlockTermDurableAtom, getConnStatusAtom, getFocusedBlockId, diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index 8b37840015..b8be22bd1c 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -6,18 +6,24 @@ import { RpcApiType } from "@/app/store/wshclientapi"; import { Atom, PrimitiveAtom } from "jotai"; import React from "react"; -type SettingsAtoms = { [K in keyof SettingsType]: Atom }; - export type BlockMetaKeyAtomFnType = ( blockId: string, key: T ) => Atom; +export type ConnConfigKeyAtomFnType = ( + connName: string, + key: T +) => Atom; + +export type SettingsKeyAtomFnType = ( + key: T +) => Atom; + // default implementation for production is in ./waveenvimpl.ts export type WaveEnv = { electron: ElectronApi; rpc: RpcApiType; - settingsAtoms: SettingsAtoms; isDev: () => boolean; atoms: GlobalAtomsType; createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => Promise; @@ -29,7 +35,9 @@ export type WaveEnv = { isWaveObjectNullAtom: (oref: string) => Atom; useWaveObjectValue: (oref: string) => [T, boolean]; }; + getSettingsKeyAtom: SettingsKeyAtomFnType; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; + getConnConfigKeyAtom: ConnConfigKeyAtomFnType; mockTabModel?: TabModel; }; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 4f0e421a2c..f2e06e8847 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -6,6 +6,7 @@ import { atoms, createBlock, getBlockMetaKeyAtom, + getConnConfigKeyAtom, getConnStatusAtom, getSettingsKeyAtom, isDev, @@ -14,17 +15,11 @@ import { import { RpcApi } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; -const settingsAtoms = new Proxy({} as WaveEnv["settingsAtoms"], { - get(_target: WaveEnv["settingsAtoms"], key: K) { - return getSettingsKeyAtom(key); - }, -}); - export function makeWaveEnvImpl(): WaveEnv { return { electron: (window as any).api, rpc: RpcApi, - settingsAtoms, + getSettingsKeyAtom, isDev, atoms, createBlock, @@ -39,5 +34,6 @@ export function makeWaveEnvImpl(): WaveEnv { useWaveObjectValue: WOS.useWaveObjectValue, }, getBlockMetaKeyAtom, + getConnConfigKeyAtom, }; } diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 9aeef93849..c07b1d22f5 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -53,31 +53,20 @@ export function mergeMockEnv(base: MockEnv, overrides: MockEnv): MockEnv { }; } -function makeMockSettingsAtoms( +function makeMockSettingsKeyAtom( settingsAtom: Atom, overrides?: Partial -): WaveEnv["settingsAtoms"] { - const overrideAtoms = new Map>(); - if (overrides) { - for (const key of Object.keys(overrides) as (keyof SettingsType)[]) { - overrideAtoms.set(key, atom(overrides[key])); - } - } +): WaveEnv["getSettingsKeyAtom"] { const keyAtomCache = new Map>(); - return new Proxy({} as WaveEnv["settingsAtoms"], { - get(_target: WaveEnv["settingsAtoms"], key: K) { - if (overrideAtoms.has(key)) { - return overrideAtoms.get(key); - } - if (!keyAtomCache.has(key)) { - keyAtomCache.set( - key, - atom((get) => get(settingsAtom)?.[key]) - ); - } - return keyAtomCache.get(key); - }, - }); + return (key: T) => { + if (!keyAtomCache.has(key)) { + keyAtomCache.set( + key, + atom((get) => (overrides?.[key] !== undefined ? overrides[key] : get(settingsAtom)?.[key])) + ); + } + return keyAtomCache.get(key) as Atom; + }; } function makeMockGlobalAtoms( @@ -163,13 +152,14 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const connStatusAtomCache = new Map>(); const waveObjectAtomCache = new Map>(); const blockMetaKeyAtomCache = new Map>(); + const connConfigKeyAtomCache = new Map>(); const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId); const env = { mockEnv: overrides, electron: overrides.electron ? { ...previewElectronApi, ...overrides.electron } : previewElectronApi, rpc: makeMockRpc(overrides.rpc), atoms, - settingsAtoms: makeMockSettingsAtoms(atoms.settingsAtom, overrides.settings), + getSettingsKeyAtom: makeMockSettingsKeyAtom(atoms.settingsAtom, overrides.settings), isDev: () => overrides.isDev ?? true, createBlock: overrides.createBlock ?? @@ -233,6 +223,17 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { } return blockMetaKeyAtomCache.get(cacheKey) as Atom; }, + getConnConfigKeyAtom: (connName: string, key: T) => { + const cacheKey = connName + "#conn-" + key; + if (!connConfigKeyAtomCache.has(cacheKey)) { + const keyAtom = atom((get) => { + const fullConfig = get(atoms.fullConfigAtom); + return fullConfig.connections?.[connName]?.[key]; + }); + connConfigKeyAtomCache.set(cacheKey, keyAtom); + } + return connConfigKeyAtomCache.get(cacheKey) as Atom; + }, mockTabModel: null as TabModel, } as MockWaveEnv; if (overrides.tabId != null) { From 848a3784eb0061e0eb7a066aaa3e027695264b55 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 9 Mar 2026 22:45:16 -0700 Subject: [PATCH 08/14] handle connection button --- frontend/app/block/blockenv.ts | 1 + frontend/app/block/connectionbutton.tsx | 12 +++++++----- frontend/app/waveenv/waveenv.ts | 1 + frontend/app/waveenv/waveenvimpl.ts | 2 ++ frontend/preview/mock/mockwaveenv.ts | 9 +++++++++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index 9a769bb7be..03e13ee618 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -27,6 +27,7 @@ export type BlockEnv = { }; wos: WaveEnv["wos"]; getConnStatusAtom: WaveEnv["getConnStatusAtom"]; + getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"]; getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">; getBlockMetaKeyAtom: BlockMetaKeyAtomFnType< | "frame:text" diff --git a/frontend/app/block/connectionbutton.tsx b/frontend/app/block/connectionbutton.tsx index c0a37659cb..c5a9b635c3 100644 --- a/frontend/app/block/connectionbutton.tsx +++ b/frontend/app/block/connectionbutton.tsx @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { computeConnColorNum } from "@/app/block/blockutil"; -import { getConnStatusAtom, getLocalHostDisplayNameAtom, recordTEvent } from "@/app/store/global"; +import { recordTEvent } from "@/app/store/global"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import { IconButton } from "@/element/iconbutton"; import * as util from "@/util/util"; import * as jotai from "jotai"; import * as React from "react"; import DotsSvg from "../asset/dots-anim-4.svg"; +import { BlockEnv } from "./blockenv"; interface ConnectionButtonProps { connection: string; @@ -18,11 +20,11 @@ interface ConnectionButtonProps { export const ConnectionButton = React.memo( React.forwardRef( ({ connection, changeConnModalAtom, isTerminalBlock }: ConnectionButtonProps, ref) => { - const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom); + const waveEnv = useWaveEnv(); + const [_connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom); const isLocal = util.isLocalConnName(connection); - const connStatusAtom = getConnStatusAtom(connection); - const connStatus = jotai.useAtomValue(connStatusAtom); - const localName = jotai.useAtomValue(getLocalHostDisplayNameAtom()); + const connStatus = jotai.useAtomValue(waveEnv.getConnStatusAtom(connection)); + const localName = jotai.useAtomValue(waveEnv.getLocalHostDisplayNameAtom()); let showDisconnectedSlash = false; let connIconElem: React.ReactNode = null; const connColorNum = computeConnColorNum(connStatus); diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index b8be22bd1c..bba793de98 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -29,6 +29,7 @@ export type WaveEnv = { createBlock: (blockDef: BlockDef, magnified?: boolean, ephemeral?: boolean) => Promise; showContextMenu: (menu: ContextMenuItem[], e: React.MouseEvent) => void; getConnStatusAtom: (conn: string) => PrimitiveAtom; + getLocalHostDisplayNameAtom: () => Atom; wos: { getWaveObjectAtom: (oref: string) => Atom; getWaveObjectLoadingAtom: (oref: string) => Atom; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index f2e06e8847..8a255c5f2a 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -8,6 +8,7 @@ import { getBlockMetaKeyAtom, getConnConfigKeyAtom, getConnStatusAtom, + getLocalHostDisplayNameAtom, getSettingsKeyAtom, isDev, WOS, @@ -27,6 +28,7 @@ export function makeWaveEnvImpl(): WaveEnv { ContextMenuModel.getInstance().showContextMenu(menu, e); }, getConnStatusAtom, + getLocalHostDisplayNameAtom, wos: { getWaveObjectAtom: WOS.getWaveObjectAtom, getWaveObjectLoadingAtom: WOS.getWaveObjectLoadingAtom, diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index c07b1d22f5..81178b2721 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -172,6 +172,15 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { ((menu, e) => { console.log("[mock showContextMenu]", menu, e); }), + getLocalHostDisplayNameAtom: () => { + return atom((get) => { + const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; + if (configValue != null) { + return configValue; + } + return "user@localhost"; + }); + }, getConnStatusAtom: (conn: string) => { if (!connStatusAtomCache.has(conn)) { const connStatus = overrides.connStatus?.[conn] ?? makeDefaultConnStatus(conn); From 4ad7badd756c459f6ecb5244fb929e85e0cda193 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 09:37:57 -0700 Subject: [PATCH 09/14] fix mistake (should use electron not api) --- frontend/app/block/blockenv.ts | 2 +- frontend/app/block/durable-session-flyover.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index 03e13ee618..e96a3e5b54 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -14,7 +14,7 @@ export type BlockEnv = { modalOpen: WaveEnv["atoms"]["modalOpen"]; controlShiftDelayAtom: WaveEnv["atoms"]["controlShiftDelayAtom"]; }; - api: { + electron: { openExternal: WaveEnv["electron"]["openExternal"]; }; rpc: { diff --git a/frontend/app/block/durable-session-flyover.tsx b/frontend/app/block/durable-session-flyover.tsx index bce82bcd52..7ab7fa0b10 100644 --- a/frontend/app/block/durable-session-flyover.tsx +++ b/frontend/app/block/durable-session-flyover.tsx @@ -30,7 +30,7 @@ function LearnMoreButton() { return ( From 92d7bdeefd1118c2eae21d388566239226e75a11 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 11:22:44 -0700 Subject: [PATCH 10/14] better typescript machinery to check for extra keys, add a new skill for creating waveenv narrowing --- .kilocode/skills/waveenv/SKILL.md | 122 +++++++++++++++++++++++++++++ frontend/app/block/blockenv.ts | 12 ++- frontend/app/waveenv/waveenv.ts | 23 ++++++ frontend/app/workspace/widgets.tsx | 6 +- 4 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 .kilocode/skills/waveenv/SKILL.md diff --git a/.kilocode/skills/waveenv/SKILL.md b/.kilocode/skills/waveenv/SKILL.md new file mode 100644 index 0000000000..28f66c27e8 --- /dev/null +++ b/.kilocode/skills/waveenv/SKILL.md @@ -0,0 +1,122 @@ +--- +name: waveenv +description: Guide for creating WaveEnv narrowings in Wave Terminal. Use when writing a named subset type of WaveEnv for a component tree, documenting environmental dependencies, or enabling mock environments for preview/test server usage. +--- + +# WaveEnv Narrowing Skill + +## Purpose + +A WaveEnv narrowing creates a _named subset type_ of `WaveEnv` that: + +1. Documents exactly which parts of the environment a component tree actually uses. +2. Forms a type contract so callers and tests know what to provide. +3. Enables mocking in the preview/test server — you only need to implement what's listed. + +## When To Create One + +Create a narrowing whenever you are writing a component (or group of components) that you want to test in the preview server, or when you want to make the environmental dependencies of a component tree explicit. + +## Core Principle: Only Include What You Use + +**Only list the fields, methods, atoms, and keys that the component tree actually accesses.** If you don't call `wos`, don't include `wos`. If you only call one RPC command, only list that one command. The narrowing is a precise dependency declaration — not a copy of `WaveEnv`. + +## File Location + +- **Separate file** (preferred for shared/complex envs): name it `env.ts` next to the component, e.g. [`frontend/app/block/blockenv.ts`](frontend/app/block/blockenv.ts). +- **Inline** (acceptable for small, single-file components): export the type directly from the component file, e.g. `WidgetsEnv` in [`frontend/app/workspace/widgets.tsx`](frontend/app/workspace/widgets.tsx:23). + +## Imports Required + +```ts +import { + BlockMetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom + ConnConfigKeyAtomFnType, // only if you use getConnConfigKeyAtom + SettingsKeyAtomFnType, // only if you use getSettingsKeyAtom + WaveEnv, + WaveEnvSubset, +} from "@/app/waveenv/waveenv"; +``` + +## The Shape + +```ts +export type MyEnv = WaveEnvSubset<{ + // --- Simple WaveEnv properties --- + // Copy the type verbatim from WaveEnv with WaveEnv["key"] syntax. + isDev: WaveEnv["isDev"]; + createBlock: WaveEnv["createBlock"]; + showContextMenu: WaveEnv["showContextMenu"]; + platform: WaveEnv["platform"]; + + // --- electron: list only the methods you call --- + electron: { + openExternal: WaveEnv["electron"]["openExternal"]; + }; + + // --- rpc: list only the commands you call --- + rpc: { + ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; + ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; + }; + + // --- atoms: list only the atoms you read --- + atoms: { + modalOpen: WaveEnv["atoms"]["modalOpen"]; + fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + }; + + // --- wos: always take the whole thing, no sub-typing needed --- + wos: WaveEnv["wos"]; + + // --- key-parameterized atom factories: enumerate the keys you use --- + getSettingsKeyAtom: SettingsKeyAtomFnType< + | "app:focusfollowscursor" + | "window:magnifiedblockopacity" + >; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType< + | "view" + | "frame:title" + | "connection" + >; + getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">; + + // --- other atom helpers: copy verbatim --- + getConnStatusAtom: WaveEnv["getConnStatusAtom"]; + getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"]; +}>; +``` + +### Rules for Each Section + +| Section | Pattern | Notes | +|---|---|---| +| `electron` | `electron: { method: WaveEnv["electron"]["method"]; }` | List every method called; omit the rest. | +| `rpc` | `rpc: { Cmd: WaveEnv["rpc"]["Cmd"]; }` | List every RPC command called; omit the rest. | +| `atoms` | `atoms: { atom: WaveEnv["atoms"]["atom"]; }` | List every atom read; omit the rest. | +| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. | +| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. | +| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. | +| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. | +| All other `WaveEnv` fields | `WaveEnv["fieldName"]` | Copy type verbatim. | + +## Using the Narrowed Type in Components + +```ts +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { MyEnv } from "./myenv"; + +const MyComponent = memo(() => { + const env = useWaveEnv(); + // TypeScript now enforces you only access what's in MyEnv. + const val = useAtomValue(env.getSettingsKeyAtom("app:focusfollowscursor")); + ... +}); +``` + +The generic parameter on `useWaveEnv()` casts the context to your narrowed type. The real production `WaveEnv` satisfies every narrowing; mock envs only need to implement the listed subset. + +## Real Examples + +- [`BlockEnv`](frontend/app/block/blockenv.ts:12) — complex narrowing with all section types, in a separate file. +- [`WidgetsEnv`](frontend/app/workspace/widgets.tsx:23) — smaller narrowing defined inline in the component file. diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index e96a3e5b54..b2df51192d 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -1,9 +1,15 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockMetaKeyAtomFnType, ConnConfigKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv } from "@/app/waveenv/waveenv"; +import { + BlockMetaKeyAtomFnType, + ConnConfigKeyAtomFnType, + SettingsKeyAtomFnType, + WaveEnv, + WaveEnvSubset, +} from "@/app/waveenv/waveenv"; -export type BlockEnv = { +export type BlockEnv = WaveEnvSubset<{ getSettingsKeyAtom: SettingsKeyAtomFnType< | "app:focusfollowscursor" | "app:showoverlayblocknums" @@ -39,4 +45,4 @@ export type BlockEnv = { | "frame:title" | "frame:icon" >; -}; +}>; diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index 260badcdab..8a75072d79 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -20,6 +20,29 @@ export type SettingsKeyAtomFnType Atom; +type OmitNever = { + [K in keyof T as [T[K]] extends [never] ? never : K]: T[K]; +}; + +type Subset = OmitNever<{ + [K in keyof T]: K extends keyof U ? T[K] : never; +}>; + +type ComplexWaveEnvKeys = { + rpc: WaveEnv["rpc"]; + electron: WaveEnv["electron"]; + atoms: WaveEnv["atoms"]; + wos: WaveEnv["wos"]; +}; + +export type WaveEnvSubset = OmitNever<{ + [K in keyof T]: K extends keyof ComplexWaveEnvKeys + ? Subset + : K extends keyof WaveEnv + ? T[K] + : never; +}>; + // default implementation for production is in ./waveenvimpl.ts export type WaveEnv = { electron: ElectronApi; diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index bfdc8dc119..2d73119154 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -3,7 +3,7 @@ import { Tooltip } from "@/app/element/tooltip"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv"; +import { useWaveEnv, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; import { shouldIncludeWidgetForWorkspace } from "@/app/workspace/widgetfilter"; import { modalsModel } from "@/store/modalmodel"; import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; @@ -20,7 +20,7 @@ import clsx from "clsx"; import { useAtomValue } from "jotai"; import { memo, useCallback, useEffect, useRef, useState } from "react"; -export type WidgetsEnv = { +export type WidgetsEnv = WaveEnvSubset<{ isDev: WaveEnv["isDev"]; electron: { openBuilder: WaveEnv["electron"]["openBuilder"]; @@ -35,7 +35,7 @@ export type WidgetsEnv = { }; createBlock: WaveEnv["createBlock"]; showContextMenu: WaveEnv["showContextMenu"]; -}; +}>; function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetConfigType[] { if (wmap == null) { From 3c8668c0a886527e6fd521c26a30a0ac9843a51c Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 11:27:00 -0700 Subject: [PATCH 11/14] remove "links", just put filenames directly --- .kilocode/skills/waveenv/SKILL.md | 115 ++++++++++++++---------------- 1 file changed, 54 insertions(+), 61 deletions(-) diff --git a/.kilocode/skills/waveenv/SKILL.md b/.kilocode/skills/waveenv/SKILL.md index 28f66c27e8..a78490f449 100644 --- a/.kilocode/skills/waveenv/SKILL.md +++ b/.kilocode/skills/waveenv/SKILL.md @@ -23,18 +23,18 @@ Create a narrowing whenever you are writing a component (or group of components) ## File Location -- **Separate file** (preferred for shared/complex envs): name it `env.ts` next to the component, e.g. [`frontend/app/block/blockenv.ts`](frontend/app/block/blockenv.ts). -- **Inline** (acceptable for small, single-file components): export the type directly from the component file, e.g. `WidgetsEnv` in [`frontend/app/workspace/widgets.tsx`](frontend/app/workspace/widgets.tsx:23). +- **Separate file** (preferred for shared/complex envs): name it `env.ts` next to the component, e.g. `frontend/app/block/blockenv.ts`. +- **Inline** (acceptable for small, single-file components): export the type directly from the component file, e.g. `WidgetsEnv` in `frontend/app/workspace/widgets.tsx`. ## Imports Required ```ts import { - BlockMetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom - ConnConfigKeyAtomFnType, // only if you use getConnConfigKeyAtom - SettingsKeyAtomFnType, // only if you use getSettingsKeyAtom - WaveEnv, - WaveEnvSubset, + BlockMetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom + ConnConfigKeyAtomFnType, // only if you use getConnConfigKeyAtom + SettingsKeyAtomFnType, // only if you use getSettingsKeyAtom + WaveEnv, + WaveEnvSubset, } from "@/app/waveenv/waveenv"; ``` @@ -42,63 +42,56 @@ import { ```ts export type MyEnv = WaveEnvSubset<{ - // --- Simple WaveEnv properties --- - // Copy the type verbatim from WaveEnv with WaveEnv["key"] syntax. - isDev: WaveEnv["isDev"]; - createBlock: WaveEnv["createBlock"]; - showContextMenu: WaveEnv["showContextMenu"]; - platform: WaveEnv["platform"]; - - // --- electron: list only the methods you call --- - electron: { - openExternal: WaveEnv["electron"]["openExternal"]; - }; - - // --- rpc: list only the commands you call --- - rpc: { - ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; - ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; - }; - - // --- atoms: list only the atoms you read --- - atoms: { - modalOpen: WaveEnv["atoms"]["modalOpen"]; - fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; - }; - - // --- wos: always take the whole thing, no sub-typing needed --- - wos: WaveEnv["wos"]; - - // --- key-parameterized atom factories: enumerate the keys you use --- - getSettingsKeyAtom: SettingsKeyAtomFnType< - | "app:focusfollowscursor" - | "window:magnifiedblockopacity" - >; - getBlockMetaKeyAtom: BlockMetaKeyAtomFnType< - | "view" - | "frame:title" - | "connection" - >; - getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">; - - // --- other atom helpers: copy verbatim --- - getConnStatusAtom: WaveEnv["getConnStatusAtom"]; - getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"]; + // --- Simple WaveEnv properties --- + // Copy the type verbatim from WaveEnv with WaveEnv["key"] syntax. + isDev: WaveEnv["isDev"]; + createBlock: WaveEnv["createBlock"]; + showContextMenu: WaveEnv["showContextMenu"]; + platform: WaveEnv["platform"]; + + // --- electron: list only the methods you call --- + electron: { + openExternal: WaveEnv["electron"]["openExternal"]; + }; + + // --- rpc: list only the commands you call --- + rpc: { + ActivityCommand: WaveEnv["rpc"]["ActivityCommand"]; + ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; + }; + + // --- atoms: list only the atoms you read --- + atoms: { + modalOpen: WaveEnv["atoms"]["modalOpen"]; + fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + }; + + // --- wos: always take the whole thing, no sub-typing needed --- + wos: WaveEnv["wos"]; + + // --- key-parameterized atom factories: enumerate the keys you use --- + getSettingsKeyAtom: SettingsKeyAtomFnType<"app:focusfollowscursor" | "window:magnifiedblockopacity">; + getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"view" | "frame:title" | "connection">; + getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">; + + // --- other atom helpers: copy verbatim --- + getConnStatusAtom: WaveEnv["getConnStatusAtom"]; + getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"]; }>; ``` ### Rules for Each Section -| Section | Pattern | Notes | -|---|---|---| -| `electron` | `electron: { method: WaveEnv["electron"]["method"]; }` | List every method called; omit the rest. | -| `rpc` | `rpc: { Cmd: WaveEnv["rpc"]["Cmd"]; }` | List every RPC command called; omit the rest. | -| `atoms` | `atoms: { atom: WaveEnv["atoms"]["atom"]; }` | List every atom read; omit the rest. | -| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. | -| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. | -| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. | -| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. | -| All other `WaveEnv` fields | `WaveEnv["fieldName"]` | Copy type verbatim. | +| Section | Pattern | Notes | +| -------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | +| `electron` | `electron: { method: WaveEnv["electron"]["method"]; }` | List every method called; omit the rest. | +| `rpc` | `rpc: { Cmd: WaveEnv["rpc"]["Cmd"]; }` | List every RPC command called; omit the rest. | +| `atoms` | `atoms: { atom: WaveEnv["atoms"]["atom"]; }` | List every atom read; omit the rest. | +| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. | +| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. | +| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. | +| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. | +| All other `WaveEnv` fields | `WaveEnv["fieldName"]` | Copy type verbatim. | ## Using the Narrowed Type in Components @@ -118,5 +111,5 @@ The generic parameter on `useWaveEnv()` casts the context to your narrowe ## Real Examples -- [`BlockEnv`](frontend/app/block/blockenv.ts:12) — complex narrowing with all section types, in a separate file. -- [`WidgetsEnv`](frontend/app/workspace/widgets.tsx:23) — smaller narrowing defined inline in the component file. +- `BlockEnv` in `frontend/app/block/blockenv.ts` — complex narrowing with all section types, in a separate file. +- `WidgetsEnv` in `frontend/app/workspace/widgets.tsx` — smaller narrowing defined inline in the component file. From 81e9fff8dccc672744ad74ce6bf86c214c10b234 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 15:14:48 -0700 Subject: [PATCH 12/14] fix nit --- frontend/app/aipanel/aipanel.tsx | 10 +++++----- frontend/app/store/tab-model.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index dded015f85..112a4cc79e 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu"; @@ -6,8 +6,8 @@ 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 { useTabModelMaybe } from "@/app/store/tab-model"; import { isBuilderWindow } from "@/app/store/windowtype"; -import { maybeUseTabModel } from "@/app/store/tab-model"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isMacOS, isWindows } from "@/util/platformutil"; import { cn } from "@/util/util"; @@ -257,7 +257,7 @@ const AIPanelComponentInner = memo(() => { const focusFollowsCursorMode = jotai.useAtomValue(getSettingsKeyAtom("app:focusfollowscursor")) ?? "off"; const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom()); - const tabModel = maybeUseTabModel(); + const tabModel = useTabModelMaybe(); const defaultMode = jotai.useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs); @@ -268,7 +268,7 @@ const AIPanelComponentInner = memo(() => { const { messages, sendMessage, status, setMessages, error, stop } = useChat({ transport: new DefaultChatTransport({ api: model.getUseChatEndpointUrl(), - prepareSendMessagesRequest: (opts) => { + prepareSendMessagesRequest: (_opts) => { const msg = model.getAndClearMessage(); const body: any = { msg, @@ -503,7 +503,7 @@ const AIPanelComponentInner = memo(() => { }, [drop]); const handleFocusCapture = useCallback( - (event: React.FocusEvent) => { + (_event: React.FocusEvent) => { // console.log("Wave AI focus capture", getElemAsStr(event.target)); model.requestWaveAIFocus(); }, diff --git a/frontend/app/store/tab-model.ts b/frontend/app/store/tab-model.ts index 316209f69b..6c41e2fd84 100644 --- a/frontend/app/store/tab-model.ts +++ b/frontend/app/store/tab-model.ts @@ -1,9 +1,9 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv"; import { atom, Atom, PrimitiveAtom } from "jotai"; import { createContext, useContext } from "react"; -import { useWaveEnv, WaveEnv } from "@/app/waveenv/waveenv"; import { globalStore } from "./jotaiStore"; import * as WOS from "./wos"; @@ -77,7 +77,7 @@ export function useTabModel(): TabModel { return ctxModel; } -export function maybeUseTabModel(): TabModel { +export function useTabModelMaybe(): TabModel { const waveEnv = useWaveEnv(); const ctxModel = useContext(TabModelContext); if (waveEnv?.mockTabModel != null) { From 0bebc203c0cacb9192455b59405e8149ea4c1991 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 15:28:54 -0700 Subject: [PATCH 13/14] fix localhostdisplaynameatom --- frontend/preview/mock/mockwaveenv.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index ebc430966f..fdcfb02ba3 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -158,6 +158,13 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const blockMetaKeyAtomCache = new Map>(); const connConfigKeyAtomCache = new Map>(); const atoms = makeMockGlobalAtoms(overrides.settings, overrides.atoms, overrides.tabId); + const localHostDisplayNameAtom = atom((get) => { + const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; + if (configValue != null) { + return configValue; + } + return "user@localhost"; + }); const env = { mockEnv: overrides, electron: { @@ -184,13 +191,7 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { console.log("[mock showContextMenu]", menu, e); }), getLocalHostDisplayNameAtom: () => { - return atom((get) => { - const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; - if (configValue != null) { - return configValue; - } - return "user@localhost"; - }); + return localHostDisplayNameAtom; }, getConnStatusAtom: (conn: string) => { if (!connStatusAtomCache.has(conn)) { From fc0c580545caef3ba140e4577d6b86611b5beb0f Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 10 Mar 2026 15:38:42 -0700 Subject: [PATCH 14/14] remove old reference counting and clearing functions from wos. totally unnecessary and adds overhead/complexity --- frontend/app/store/wos.ts | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 645949ced4..72ca022750 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -9,7 +9,6 @@ import { getWebServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; import { fireAndForget } from "@/util/util"; import { atom, Atom, Getter, PrimitiveAtom, Setter, useAtomValue } from "jotai"; -import { useEffect } from "react"; import { globalStore } from "./jotaiStore"; import { ObjectService } from "./services"; @@ -21,8 +20,6 @@ type WaveObjectDataItemType = { type WaveObjectValue = { pendingPromise: Promise; dataAtom: PrimitiveAtom>; - refCount: number; - holdTime: number; }; function splitORef(oref: string): [string, string] { @@ -151,12 +148,6 @@ function callBackendService(service: string, method: string, args: any[], noUICo const waveObjectValueCache = new Map>(); -function clearWaveObjectCache() { - waveObjectValueCache.clear(); -} - -const defaultHoldTime = 5000; // 5-seconds - function reloadWaveObject(oref: string): Promise { let wov = waveObjectValueCache.get(oref); if (wov === undefined) { @@ -171,7 +162,7 @@ function reloadWaveObject(oref: string): Promise { } function createWaveValueObject(oref: string, shouldFetch: boolean): WaveObjectValue { - const wov = { pendingPromise: null, dataAtom: null, refCount: 0, holdTime: Date.now() + 5000 }; + const wov = { pendingPromise: null, dataAtom: null }; wov.dataAtom = atom({ value: null, loading: true }); if (!shouldFetch) { return wov; @@ -210,7 +201,6 @@ function getWaveObjectValue(oref: string, createIfMissing = t function loadAndPinWaveObject(oref: string): Promise { const wov = getWaveObjectValue(oref); - wov.refCount++; if (wov.pendingPromise == null) { const dataValue = globalStore.get(wov.dataAtom); return Promise.resolve(dataValue.value); @@ -260,12 +250,6 @@ function isWaveObjectNullAtom(oref: string): Atom { function useWaveObjectValue(oref: string): [T, boolean] { const wov = getWaveObjectValue(oref); - useEffect(() => { - wov.refCount++; - return () => { - wov.refCount--; - }; - }, [oref]); const atomVal = useAtomValue(wov.dataAtom); return [atomVal.value, atomVal.loading]; } @@ -291,7 +275,6 @@ function updateWaveObject(update: WaveObjUpdate) { console.log("WaveObj updated", oref); globalStore.set(wov.dataAtom, { value: update.obj, loading: false }); } - wov.holdTime = Date.now() + defaultHoldTime; return; } @@ -301,15 +284,6 @@ function updateWaveObjects(vals: WaveObjUpdate[]) { } } -function cleanWaveObjectCache() { - const now = Date.now(); - for (const [oref, wov] of waveObjectValueCache) { - if (wov.refCount == 0 && wov.holdTime < now) { - waveObjectValueCache.delete(oref); - } - } -} - // gets the value of a WaveObject from the cache. // should provide getFn if it is available (e.g. inside of a jotai atom) // otherwise it will use the globalStore.get function @@ -342,8 +316,6 @@ function setObjectValue(value: T, setFn?: Setter, pushToServe export { callBackendService, - cleanWaveObjectCache, - clearWaveObjectCache, getObjectValue, getWaveObjectAtom, getWaveObjectLoadingAtom,