X Tutup
Skip to content

Expanding WaveEnv to cover all of frontend/app/block components#3024

Open
sawka wants to merge 9 commits intomainfrom
sawka/mock-for-block
Open

Expanding WaveEnv to cover all of frontend/app/block components#3024
sawka wants to merge 9 commits intomainfrom
sawka/mock-for-block

Conversation

@sawka
Copy link
Member

@sawka sawka commented Mar 10, 2026

  • lots of updates to WaveEnv to make it cover more functionality
  • Create BlockEnv as a narrowing of WaveEnv that covers all of frontend/app/block functionality
  • Fixed a lot of dependencies in the block components that caused unnecessarily re-renders
  • Added atom caching to WOS

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1ba1765e-1f09-48d3-8fab-261631aba1ba

📥 Commits

Reviewing files that changed from the base of the PR and between 848a378 and 9ba3baa.

📒 Files selected for processing (3)
  • frontend/app/waveenv/waveenv.ts
  • frontend/app/waveenv/waveenvimpl.ts
  • frontend/preview/mock/mockwaveenv.ts

Walkthrough

The PR replaces direct Wave object and global-atom access with a WaveEnv/BlockEnv abstraction across block components. It moves block metadata reads to environment-provided atoms (getBlockMetaKeyAtom), adds BlockEnv type, and updates WaveEnv to expose wos helpers (getWaveObjectAtom, isWaveObjectNullAtom, etc.). Block rendering is restructured: Block/SubBlock now use inner components and derive view models from viewType (makeDefaultViewModel(viewType)). Several utilities and components (header, frame, connection UI, durable session, connection overlay) are adapted to use useWaveEnv. Mock wave env and wos caching were added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main change: expanding WaveEnv to cover block components, which aligns with the core objective of introducing BlockEnv and extending WaveEnv functionality across all frontend/app/block files.
Description check ✅ Passed The description is directly related to the changeset, mentioning WaveEnv updates, BlockEnv creation, dependency fixes for re-renders, and WOS atom caching—all reflected in the code changes across multiple block component files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/mock-for-block

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 10, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (14 files)
  • frontend/app/block/block.tsx
  • frontend/app/block/blockenv.ts
  • frontend/app/block/blockframe-header.tsx
  • frontend/app/block/blockframe.tsx
  • frontend/app/block/blockutil.tsx
  • frontend/app/block/connectionbutton.tsx
  • frontend/app/block/connstatusoverlay.tsx
  • frontend/app/block/durable-session-flyover.tsx
  • frontend/app/store/global.ts
  • frontend/app/store/tab-model.ts
  • frontend/app/store/wos.ts
  • frontend/app/waveenv/waveenv.ts
  • frontend/app/waveenv/waveenvimpl.ts
  • frontend/preview/mock/mockwaveenv.ts

Verification Notes

After thorough analysis of the diff:

  1. Null handling preserved: The Block component properly checks for null using isWaveObjectNullAtom (line 333 in block.tsx), ensuring blocks that dont exist return null before rendering child components.

  2. Connection name access: The connName is correctly obtained from waveEnv.getBlockMetaKeyAtom(blockId, "connection"). When empty/undefined, the fallback behavior matches the old code.

  3. Reactivity preserved: The useEffect dependencies have been updated correctly (e.g., connName instead of blockData) to maintain proper reactivity.

  4. Atom caching: New caching mechanism for getWaveObjectAtom, getWaveObjectLoadingAtom, and isWaveObjectNullAtom improves performance.

The refactoring is well-executed and maintains correct behavior while improving granular reactivity.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 10, 2026

Deploying waveterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9ba3baa
Status: ✅  Deploy successful!
Preview URL: https://6548a50a.waveterm.pages.dev
Branch Preview URL: https://sawka-mock-for-block.waveterm.pages.dev

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/app/block/block.tsx (1)

306-323: ⚠️ Potential issue | 🔴 Critical

Registry lookup must match remount strategy to prevent disposing active viewModels.

When viewType changes, React remounts BlockInner and SubBlockInner by key (blockId:viewType), but the components still read from a registry keyed on blockId alone. The new instance can grab the stale model from the registry before the old instance's cleanup runs, then that cleanup disposes the model while the new tree uses it.

Prevent reuse by gating on viewModel.viewType === props.viewType, and guard cleanup to unregister only if the registry entry still points to the instance being torn down. Update useEffect dependencies to [props.nodeModel.blockId, viewModel] to properly track which instance owns the cleanup.

🔁 Minimal guard for the remount path
-    const bcm = getBlockComponentModel(props.nodeModel.blockId);
-    let viewModel = bcm?.viewModel;
-    if (viewModel == null) {
+    const bcm = getBlockComponentModel(props.nodeModel.blockId);
+    let viewModel = bcm?.viewModel;
+    if (viewModel == null || viewModel.viewType !== props.viewType) {
         // viewModel gets the full waveEnv
         viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv);
         registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
     }
     useEffect(() => {
         return () => {
-            unregisterBlockComponentModel(props.nodeModel.blockId);
+            if (getBlockComponentModel(props.nodeModel.blockId)?.viewModel === viewModel) {
+                unregisterBlockComponentModel(props.nodeModel.blockId);
+            }
             viewModel?.dispose?.();
         };
-    }, []);
+    }, [props.nodeModel.blockId, viewModel]);

Apply the same guard to SubBlockInner.

Apply to both BlockInner (lines 306–323) and SubBlockInner (lines 363–370).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/block/block.tsx` around lines 306 - 323, The registry lookup and
cleanup in BlockInner and SubBlockInner can cause the new remounted instance to
reuse a viewModel that the old instance will dispose; fix by ensuring registry
entries are keyed/validated by viewType: when reading viewModel from
getBlockComponentModel(props.nodeModel.blockId) only accept it if
viewModel?.viewType === props.viewType, otherwise create a new viewModel via
makeViewModel(...) and register it with registerBlockComponentModel(...); in the
cleanup useEffect, only call unregisterBlockComponentModel(...) and
viewModel.dispose() if the current registry entry still points to this
instance’s viewModel (i.e., the registered model === viewModel) and change the
effect deps to [props.nodeModel.blockId, viewModel] so the cleanup is associated
with the correct instance; apply identical guards to SubBlockInner.
🧹 Nitpick comments (1)
frontend/app/block/connectionbutton.tsx (1)

24-24: Use useSetAtom instead of subscribing to the modal state.

Line 24 still subscribes this component to changeConnModalAtom even though the value is discarded. That reintroduces a rerender path the PR is trying to remove. const setConnModalOpen = jotai.useSetAtom(changeConnModalAtom) avoids the extra subscription.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/block/connectionbutton.tsx` at line 24, The component is
unnecessarily subscribing to changeConnModalAtom by calling jotai.useAtom and
discarding the value via _connModalOpen; replace that subscription with a
setter-only hook by using jotai.useSetAtom(changeConnModalAtom) so the component
no longer re-renders on atom value changes—update the declaration that currently
reads const [_connModalOpen, setConnModalOpen] =
jotai.useAtom(changeConnModalAtom) to use useSetAtom and keep the existing
setConnModalOpen reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/block/blockenv.ts`:
- Around line 17-19: The BlockEnv type currently defines api.openExternal but
the actual env implementations (makeWaveEnvImpl, makeMockWaveEnv) expose
electron.openExternal, so useWaveEnv<BlockEnv>() will yield undefined for
waveEnv.api; fix by aligning the shape: either rename BlockEnv.api to electron
(update the BlockEnv interface and all consumers to use electron.openExternal)
or add a real api property to makeWaveEnvImpl and makeMockWaveEnv that forwards
to electron.openExternal (e.g., set env.api.openExternal =
env.electron.openExternal) so WaveEnvContext, useWaveEnv, BlockEnv,
makeWaveEnvImpl, and makeMockWaveEnv all share the same property names.

In `@frontend/app/block/durable-session-flyover.tsx`:
- Around line 29-34: The runtime error is caused by calling
waveEnv.api.openExternal where waveEnv.api is undefined; change the call to use
the actual runtime shape—invoke waveEnv.electron.openExternal(...) instead of
waveEnv.api.openExternal (locate the use in durable-session-flyover.tsx where
useWaveEnv<BlockEnv>() returns waveEnv and reference BlockEnv and
makeWaveEnvImpl to confirm the runtime fields), or alternatively add an api
wrapper onto the WaveEnv implementation in makeWaveEnvImpl so waveEnv.api is
defined; prefer the simpler fix of replacing waveEnv.api.openExternal with
waveEnv.electron.openExternal to match the real object shape.

In `@frontend/app/store/tab-model.ts`:
- Around line 49-53: Cache key for TabModel must include the WaveEnv so an
env-less instance doesn't win; update getTabModelByTabId to derive a composite
key from tabId plus a canonical identifier from waveEnv (e.g., env.id or a
stable string) instead of using tabId alone, and use that composite key when
calling tabModelCache.get(...) and tabModelCache.set(...). Ensure TabModel
construction still receives the passed waveEnv and update any helper like
getActiveTabModel to use the same key derivation so env-specific models are
returned consistently.
- Around line 80-87: Rename the hook function maybeUseTabModel to
useMaybeTabModel to follow React hook naming, change its return type from
TabModel to TabModel | undefined, and update its implementation signature
accordingly (it already calls useWaveEnv() and useContext(TabModelContext));
then update all call sites (notably the caller in aipanel.tsx) to handle the
possible undefined return (add a null/undefined check or early return) so
consumers no longer assume a guaranteed TabModel.

In `@frontend/app/store/wos.ts`:
- Around line 221-258: The derived-atom cache entries (created by
getWaveObjectAtom, getWaveObjectLoadingAtom, and isWaveObjectNullAtom) close
over the original wov and must be evicted when the backing WaveObjectValue is
dropped; update the cache-clearing logic
(cleanWaveObjectCache/clearWaveObjectCache) to also remove
waveObjectDerivedAtomCache entries for the same oref by deleting keys with the
suffixes ":value", ":loading", and ":isnull" so that subsequent getWaveObject*
calls recreate atoms bound to the new wov.dataAtom instead of returning stale,
disconnected atoms.

In `@frontend/preview/mock/mockwaveenv.ts`:
- Around line 175-183: getLocalHostDisplayNameAtom currently constructs a new
atom on every call, breaking atom identity; change it to return a cached
singleton like connStatusAtomCache: create a module-/mock-init-scoped variable
(e.g., localHostDisplayNameAtomCache) and initialize it once with the same
atom<string>((get) => { const configValue =
get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; return configValue ??
"user@localhost"; }) then have getLocalHostDisplayNameAtom return that cached
instance instead of creating a new atom on each invocation so consumers (e.g.,
connectionbutton.tsx) subscribe to the same atom.

---

Outside diff comments:
In `@frontend/app/block/block.tsx`:
- Around line 306-323: The registry lookup and cleanup in BlockInner and
SubBlockInner can cause the new remounted instance to reuse a viewModel that the
old instance will dispose; fix by ensuring registry entries are keyed/validated
by viewType: when reading viewModel from
getBlockComponentModel(props.nodeModel.blockId) only accept it if
viewModel?.viewType === props.viewType, otherwise create a new viewModel via
makeViewModel(...) and register it with registerBlockComponentModel(...); in the
cleanup useEffect, only call unregisterBlockComponentModel(...) and
viewModel.dispose() if the current registry entry still points to this
instance’s viewModel (i.e., the registered model === viewModel) and change the
effect deps to [props.nodeModel.blockId, viewModel] so the cleanup is associated
with the correct instance; apply identical guards to SubBlockInner.

---

Nitpick comments:
In `@frontend/app/block/connectionbutton.tsx`:
- Line 24: The component is unnecessarily subscribing to changeConnModalAtom by
calling jotai.useAtom and discarding the value via _connModalOpen; replace that
subscription with a setter-only hook by using
jotai.useSetAtom(changeConnModalAtom) so the component no longer re-renders on
atom value changes—update the declaration that currently reads const
[_connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom) to use
useSetAtom and keep the existing setConnModalOpen reference.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 38ce212c-2005-42cb-b115-6f6083b0fa0d

📥 Commits

Reviewing files that changed from the base of the PR and between 7119970 and 848a378.

📒 Files selected for processing (14)
  • frontend/app/block/block.tsx
  • frontend/app/block/blockenv.ts
  • frontend/app/block/blockframe-header.tsx
  • frontend/app/block/blockframe.tsx
  • frontend/app/block/blockutil.tsx
  • frontend/app/block/connectionbutton.tsx
  • frontend/app/block/connstatusoverlay.tsx
  • frontend/app/block/durable-session-flyover.tsx
  • frontend/app/store/global.ts
  • frontend/app/store/tab-model.ts
  • frontend/app/store/wos.ts
  • frontend/app/waveenv/waveenv.ts
  • frontend/app/waveenv/waveenvimpl.ts
  • frontend/preview/mock/mockwaveenv.ts

Comment on lines +17 to +19
api: {
openExternal: WaveEnv["electron"]["openExternal"];
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

BlockEnv.api doesn't match the provided WaveEnv.

useWaveEnv<BlockEnv>() only narrows/casts the value from WaveEnvContext; it doesn't reshape it. makeWaveEnvImpl() and makeMockWaveEnv() expose electron.openExternal, not api.openExternal, so any block code reading waveEnv.api will get undefined at runtime. Either rename this field to electron, or add a real api property when constructing the env.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/block/blockenv.ts` around lines 17 - 19, The BlockEnv type
currently defines api.openExternal but the actual env implementations
(makeWaveEnvImpl, makeMockWaveEnv) expose electron.openExternal, so
useWaveEnv<BlockEnv>() will yield undefined for waveEnv.api; fix by aligning the
shape: either rename BlockEnv.api to electron (update the BlockEnv interface and
all consumers to use electron.openExternal) or add a real api property to
makeWaveEnvImpl and makeMockWaveEnv that forwards to electron.openExternal
(e.g., set env.api.openExternal = env.electron.openExternal) so WaveEnvContext,
useWaveEnv, BlockEnv, makeWaveEnvImpl, and makeMockWaveEnv all share the same
property names.

Comment on lines +29 to +34
const waveEnv = useWaveEnv<BlockEnv>();
return (
<button className="text-muted text-xs hover:underline cursor-pointer text-left" onClick={handleLearnMore}>
<button
className="text-muted text-xs hover:underline cursor-pointer text-left"
onClick={() => waveEnv.api.openExternal("https://docs.waveterm.dev/durable-sessions")}
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find WaveEnv-related files
find . -type f -name "*waveenv*" | head -20

Repository: wavetermdev/waveterm

Length of output: 174


🏁 Script executed:

# Check the BlockEnv type definition
fd "blockenv" --type f

Repository: wavetermdev/waveterm

Length of output: 94


🏁 Script executed:

# Look for the main WaveEnv definition
fd "waveenv\.ts" --type f

Repository: wavetermdev/waveterm

Length of output: 132


🏁 Script executed:

# Search for uses of api.openExternal in the codebase
rg "api\.openExternal" --type ts --type tsx

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

# Check the structure of WaveEnvImpl and what it exposes
rg "class WaveEnvImpl|const waveEnvImpl|export.*api" --type ts --type tsx -A 5

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

# Read waveenv.ts to understand the interface
cat frontend/app/waveenv/waveenv.ts

Repository: wavetermdev/waveterm

Length of output: 2196


🏁 Script executed:

# Read waveenvimpl.ts to understand the implementation
cat frontend/app/waveenv/waveenvimpl.ts

Repository: wavetermdev/waveterm

Length of output: 1400


🏁 Script executed:

# Read blockenv.ts to understand the narrowed type
cat frontend/app/block/blockenv.ts

Repository: wavetermdev/waveterm

Length of output: 1637


🏁 Script executed:

# Look for useWaveEnv hook
rg "useWaveEnv" --max-count 5 -B 2 -A 10

Repository: wavetermdev/waveterm

Length of output: 32509


🏁 Script executed:

# Search for ElectronApi definition and openExternal method
rg "ElectronApi|openExternal" --max-count 20 -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 11772


🏁 Script executed:

# Check the mock waveenv implementation
cat frontend/preview/mock/mockwaveenv.ts

Repository: wavetermdev/waveterm

Length of output: 11086


Fix runtime error: waveEnv.api does not exist.

The BlockEnv type defines api as a type-level alias to WaveEnv["electron"]["openExternal"], but this is a compile-time narrowing only. The actual WaveEnv object created by makeWaveEnvImpl() has only the electron field, not api. The useWaveEnv<BlockEnv>() call performs a type cast with no runtime transformation.

Calling waveEnv.api.openExternal(...) will fail at runtime with "Cannot read property 'openExternal' of undefined". Either use waveEnv.electron.openExternal(...) here or add an api property to the WaveEnv implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/block/durable-session-flyover.tsx` around lines 29 - 34, The
runtime error is caused by calling waveEnv.api.openExternal where waveEnv.api is
undefined; change the call to use the actual runtime shape—invoke
waveEnv.electron.openExternal(...) instead of waveEnv.api.openExternal (locate
the use in durable-session-flyover.tsx where useWaveEnv<BlockEnv>() returns
waveEnv and reference BlockEnv and makeWaveEnvImpl to confirm the runtime
fields), or alternatively add an api wrapper onto the WaveEnv implementation in
makeWaveEnvImpl so waveEnv.api is defined; prefer the simpler fix of replacing
waveEnv.api.openExternal with waveEnv.electron.openExternal to match the real
object shape.

Comment on lines +49 to 53
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include waveEnv in the TabModel cache key.

tabModelCache is still keyed only by tabId, so the first model created for a tab wins. frontend/app/app.tsx:42-57 currently seeds the provider with getTabModelByTabId(tabId) without an env, which means later getTabModelByTabId(tabId, waveEnv) / getActiveTabModel(waveEnv) calls can still get the env-less instance and bypass the new mock/WaveEnv-backed path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/store/tab-model.ts` around lines 49 - 53, Cache key for TabModel
must include the WaveEnv so an env-less instance doesn't win; update
getTabModelByTabId to derive a composite key from tabId plus a canonical
identifier from waveEnv (e.g., env.id or a stable string) instead of using tabId
alone, and use that composite key when calling tabModelCache.get(...) and
tabModelCache.set(...). Ensure TabModel construction still receives the passed
waveEnv and update any helper like getActiveTabModel to use the same key
derivation so env-specific models are returned consistently.

Comment on lines 80 to 87
export function maybeUseTabModel(): TabModel {
return useContext(TabModelContext);
const waveEnv = useWaveEnv();
const ctxModel = useContext(TabModelContext);
if (waveEnv?.mockTabModel != null) {
return waveEnv.mockTabModel;
}
return ctxModel;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/app/store/tab-model.ts | head -100

Repository: wavetermdev/waveterm

Length of output: 3498


🏁 Script executed:

rg "maybeUseTabModel" --type ts --type tsx -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

fd tsconfig.json -o exec cat {} \;

Repository: wavetermdev/waveterm

Length of output: 191


🏁 Script executed:

rg "maybeUseTabModel" -A 2 -B 2

Repository: wavetermdev/waveterm

Length of output: 1377


🏁 Script executed:

cat tsconfig.json

Repository: wavetermdev/waveterm

Length of output: 1138


🏁 Script executed:

cat -n frontend/app/aipanel/aipanel.tsx | grep -A 5 "maybeUseTabModel()"

Repository: wavetermdev/waveterm

Length of output: 484


Rename maybeUseTabModel to useMaybeTabModel and update return type to TabModel | undefined.

The function calls hooks (useWaveEnv() and useContext()) on lines 81–82 but doesn't follow the React hooks naming convention. Line 86 returns ctxModel, which has type TabModel | undefined per the TabModelContext definition (line 66), creating a type mismatch with the declared TabModel return type. The caller in frontend/app/aipanel/aipanel.tsx (line 260) doesn't check for undefined.

🧰 Tools
🪛 Biome (2.4.6)

[error] 81-81: This hook is being called from within a function or method that is not a hook or component.

(lint/correctness/useHookAtTopLevel)


[error] 82-82: This hook is being called from within a function or method that is not a hook or component.

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/store/tab-model.ts` around lines 80 - 87, Rename the hook
function maybeUseTabModel to useMaybeTabModel to follow React hook naming,
change its return type from TabModel to TabModel | undefined, and update its
implementation signature accordingly (it already calls useWaveEnv() and
useContext(TabModelContext)); then update all call sites (notably the caller in
aipanel.tsx) to handle the possible undefined return (add a null/undefined check
or early return) so consumers no longer assume a guaranteed TabModel.

Comment on lines +221 to +258
const waveObjectDerivedAtomCache = new Map<string, Atom<any>>();

function getWaveObjectAtom<T extends WaveObj>(oref: string): Atom<T> {
const cacheKey = oref + ":value";
let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom<T>;
if (cachedAtom != null) {
return cachedAtom;
}
const wov = getWaveObjectValue<T>(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<boolean> {
const cacheKey = oref + ":loading";
let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom<boolean>;
if (cachedAtom != null) {
return cachedAtom;
}
const wov = getWaveObjectValue(oref);
return atom((get) => {
cachedAtom = atom((get) => {
const dataValue = get(wov.dataAtom);
if (dataValue.loading) {
return null;
}
return dataValue.loading;
});
waveObjectDerivedAtomCache.set(cacheKey, cachedAtom);
return cachedAtom;
}

function isWaveObjectNullAtom(oref: string): Atom<boolean> {
const cacheKey = oref + ":isnull";
let cachedAtom = waveObjectDerivedAtomCache.get(cacheKey) as Atom<boolean>;
if (cachedAtom != null) {
return cachedAtom;
}
cachedAtom = atom((get) => get(getWaveObjectAtom(oref)) == null);
waveObjectDerivedAtomCache.set(cacheKey, cachedAtom);
return cachedAtom;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Evict derived atoms when the backing wov is dropped.

These cached atoms close over the wov.dataAtom captured at creation time. cleanWaveObjectCache() / clearWaveObjectCache() only remove waveObjectValueCache entries, so a later read can return a stale derived atom that's disconnected from the newly created wov and never sees fresh loads or updates. Clear the :value / :loading / :isnull entries alongside the corresponding oref.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/store/wos.ts` around lines 221 - 258, The derived-atom cache
entries (created by getWaveObjectAtom, getWaveObjectLoadingAtom, and
isWaveObjectNullAtom) close over the original wov and must be evicted when the
backing WaveObjectValue is dropped; update the cache-clearing logic
(cleanWaveObjectCache/clearWaveObjectCache) to also remove
waveObjectDerivedAtomCache entries for the same oref by deleting keys with the
suffixes ":value", ":loading", and ":isnull" so that subsequent getWaveObject*
calls recreate atoms bound to the new wov.dataAtom instead of returning stale,
disconnected atoms.

Comment on lines +175 to +183
getLocalHostDisplayNameAtom: () => {
return atom<string>((get) => {
const configValue = get(atoms.settingsAtom)?.["conn:localhostdisplayname"];
if (configValue != null) {
return configValue;
}
return "user@localhost";
});
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the mock wave env file
fd mockwaveenv.ts

Repository: wavetermdev/waveterm

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Check the structure and find line numbers in mockwaveenv.ts
cat -n frontend/preview/mock/mockwaveenv.ts | head -200

Repository: wavetermdev/waveterm

Length of output: 9517


🏁 Script executed:

#!/bin/bash
# Examine connectionbutton.tsx to see how getLocalHostDisplayNameAtom is called
fd connectionbutton.tsx

Repository: wavetermdev/waveterm

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Once we find connectionbutton.tsx, check line 27
cat -n frontend/app/block/connectionbutton.tsx | sed -n '20,35p'

Repository: wavetermdev/waveterm

Length of output: 1205


🏁 Script executed:

#!/bin/bash
# Check the production implementation in global.ts
fd global.ts | grep -E "app/store"

Repository: wavetermdev/waveterm

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Examine global.ts to see the production pattern around lines 509-511
wc -l frontend/app/store/global.ts

Repository: wavetermdev/waveterm

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Check production implementation in global.ts around lines 509-511
cat -n frontend/app/store/global.ts | sed -n '505,520p'

Repository: wavetermdev/waveterm

Length of output: 803


🏁 Script executed:

#!/bin/bash
# Also check the full getLocalHostDisplayNameAtom implementation in global.ts
rg "getLocalHostDisplayNameAtom" -A 10 -B 2 frontend/app/store/global.ts

Repository: wavetermdev/waveterm

Length of output: 903


🏁 Script executed:

#!/bin/bash
# Find where LocalHostDisplayNameAtom is defined
rg "LocalHostDisplayNameAtom\s*=" -B 5 -A 2 frontend/app/store/global.ts

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Broader search for LocalHostDisplayNameAtom
rg "LocalHostDisplayNameAtom" frontend/app/store/global.ts

Repository: wavetermdev/waveterm

Length of output: 251


🏁 Script executed:

#!/bin/bash
# Check for const declarations around the getLocalHostDisplayNameAtom function
cat -n frontend/app/store/global.ts | sed -n '490,530p'

Repository: wavetermdev/waveterm

Length of output: 1564


Cache the LocalHostDisplayNameAtom singleton in the mock environment.

This function creates a fresh atom on every call, breaking atom identity. Components like connectionbutton.tsx:27 subscribe to it during render via useAtomValue(), causing Jotai to treat each call as a new subscription rather than reusing the same atom. The production implementation (global.ts:509-511) returns a cached singleton. Apply the same caching pattern already used for connStatusAtomCache in this function—create the atom once at mock initialization and return the cached instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/mock/mockwaveenv.ts` around lines 175 - 183,
getLocalHostDisplayNameAtom currently constructs a new atom on every call,
breaking atom identity; change it to return a cached singleton like
connStatusAtomCache: create a module-/mock-init-scoped variable (e.g.,
localHostDisplayNameAtomCache) and initialize it once with the same
atom<string>((get) => { const configValue =
get(atoms.settingsAtom)?.["conn:localhostdisplayname"]; return configValue ??
"user@localhost"; }) then have getLocalHostDisplayNameAtom return that cached
instance instead of creating a new atom on each invocation so consumers (e.g.,
connectionbutton.tsx) subscribe to the same atom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

X Tutup