Skip to content

TabBar full preview + much more FE mocking via WaveEnv to enable it#3028

Merged
sawka merged 31 commits intomainfrom
copilot/create-waveenv-for-tabbar
Mar 11, 2026
Merged

TabBar full preview + much more FE mocking via WaveEnv to enable it#3028
sawka merged 31 commits intomainfrom
copilot/create-waveenv-for-tabbar

Conversation

Copy link
Contributor

Copilot AI commented Mar 10, 2026

tabbar.tsx did not expose its WaveEnv dependencies explicitly, which made it awkward to preview in isolation. This change introduces a dedicated TabBarEnv contract and adds a full tabbar.preview.tsx that renders the tab bar with preview-safe mocked state for visual inspection.

  • WaveEnv narrowing

    • Added frontend/app/tab/tabbarenv.ts with a focused TabBarEnv subset covering only the tab bar’s actual environment usage:
      • tab/window atoms used by layout and visibility logic
      • electron methods used for tab actions
      • settings keys used by the tab bar UI
    • Updated tabbar.tsx to read those dependencies through useWaveEnv<TabBarEnv>() instead of reaching into broader global helpers for the narrowed surface.
  • Tab bar preview

    • Added frontend/preview/previews/tabbar.preview.tsx with a full visual mock of the tab bar shell.
    • Reuses the real tab rendering pieces (TabV, WaveAIButton, UpdateStatusBanner, ConfigErrorIcon) where that provides useful fidelity.
    • Replaces preview-hostile behavior with lightweight mocks where full functionality is unnecessary:
      • workspace switcher shell instead of live workspace services
      • inert context menu / drag interactions
      • local preview state for adding, selecting, closing, and renaming tabs
  • Preview controls

    • Added knobs to inspect the layout under the main visual variants that affect rendering:
      • frame width
      • platform
      • updater banner state
      • menu bar visibility
      • config error visibility
      • Wave AI button visibility
      • fullscreen / zoom factor
  • Consistency cleanup

    • Switched tab:confirmclose access in tabbar.tsx to the narrowed getSettingsKeyAtom(...) path so settings reads follow the same WaveEnv pattern throughout the component.

Example of the new narrowed env contract:

export type TabBarEnv = WaveEnvSubset<{
    electron: {
        createTab: WaveEnv["electron"]["createTab"];
        closeTab: WaveEnv["electron"]["closeTab"];
        setActiveTab: WaveEnv["electron"]["setActiveTab"];
        showWorkspaceAppMenu: WaveEnv["electron"]["showWorkspaceAppMenu"];
    };
    atoms: {
        fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
        staticTabId: WaveEnv["atoms"]["staticTabId"];
        isFullScreen: WaveEnv["atoms"]["isFullScreen"];
        zoomFactorAtom: WaveEnv["atoms"]["zoomFactorAtom"];
        settingsAtom: WaveEnv["atoms"]["settingsAtom"];
        reinitVersion: WaveEnv["atoms"]["reinitVersion"];
    };
    getSettingsKeyAtom: SettingsKeyAtomFnType<"app:hideaibutton" | "tab:confirmclose">;
    isWindows: WaveEnv["isWindows"];
    isMacOS: WaveEnv["isMacOS"];
}>;

Tab bar preview


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link

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

Deploying waveterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3675696
Status: ✅  Deploy successful!
Preview URL: https://54d06025.waveterm.pages.dev
Branch Preview URL: https://copilot-create-waveenv-for-t.waveterm.pages.dev

View logs

Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
Copilot AI changed the title [WIP] Add WaveEnv for tabbar and create preview file Add a narrowed TabBarEnv and a full tab bar preview Mar 10, 2026
@sawka sawka changed the title Add a narrowed TabBarEnv and a full tab bar preview TabBar full preview + much more FE mocking via WaveEnv to enable it Mar 11, 2026
@sawka sawka marked this pull request as ready for review March 11, 2026 18:09
@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 11, 2026

Code Review Summary

Status: 6 Known Issues | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 6

Known Issues (Previously Identified)

All issues below were previously identified in existing inline comments:

File Line Severity Issue
frontend/app/store/tab-model.ts 67 🟡 Minor Potential null reference if mockModels is undefined
frontend/app/tab/tabbar.tsx 196 🟠 Major Recalculate tab widths when the added chrome changes size
frontend/preview/mock/mockwaveenv.ts 347 🟡 Minor useWaveObjectValue as arrow property pattern (unconventional but functional)
frontend/app/tab/updatebanner.tsx 20 🟡 Minor Return type should be string | null
frontend/preview/mock/mockwaveenv.ts 380 🟡 Minor Preserve async error semantics in callBackendService
frontend/types/custom.d.ts 15 🟠 Major Type declarations don't match nullable runtime behavior

Changes Reviewed

This PR introduces:

  • New RPC commands (UpdateTabNameCommand, UpdateWorkspaceTabIdsCommand) for tab/workspace updates
  • Updated TypeScript service generation with waveEnv injection
  • New preview infrastructure for TabBar component
  • Mock environment improvements for services and wave objects

The core architectural changes appear sound. No new issues were found beyond those already documented above.

Files Reviewed (23 files)
  • .kilocode/skills/add-rpc/SKILL.md
  • .kilocode/skills/waveenv/SKILL.md
  • cmd/generatets/main-generatets.go
  • eslint.config.js
  • frontend/app/element/streamdown.tsx
  • frontend/app/store/badge.ts
  • frontend/app/store/services.ts
  • frontend/app/store/tab-model.ts
  • frontend/app/tab/tabbar.tsx
  • frontend/app/tab/updatebanner.tsx
  • frontend/preview/mock/mockwaveenv.ts
  • frontend/preview/preview.tsx
  • frontend/preview/previews/tabbar.preview.tsx
  • frontend/preview/previews/widgets.preview.tsx
  • frontend/types/custom.d.ts
  • frontend/util/platformutil.ts
  • pkg/service/objectservice/objectservice.go
  • pkg/service/workspaceservice/workspaceservice.go
  • pkg/tsgen/tsgen.go
  • pkg/wshrpc/wshclient/wshclient.go
  • pkg/wshrpc/wshrpctypes.go
  • pkg/wshrpc/wshserver/wshserver.go
  • tsconfig.json

@sawka
Copy link
Member

sawka commented Mar 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 19408385-25fc-47c4-b137-7a7579052a1c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR moves many frontend modules to an environment-aware WaveEnv model: service classes are instance-based with a callBackendService wrapper and exported AllServiceTypes/AllServiceImpls; numerous UI components and stores (tab bar, tab model, badges, workspace switcher, previews) now accept and use WaveEnv subsets; mock preview envs and RPC mocks are expanded; two RPC commands (UpdateTabNameCommand, UpdateWorkspaceTabIdsCommand) are added across client/server layers; ObjectService.UpdateTabName and WorkspaceService.UpdateTabIds methods were removed; tsgen now emits service classes wired to WaveEnv; type surfaces (waveenv, atoms, mocks) and previews updated accordingly.

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 11.63% 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 clearly summarizes the main change: adding a full tab bar preview and expanding WaveEnv mocking infrastructure to support it.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the TabBarEnv narrowing, preview implementation, controls, and consistency cleanup changes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/create-waveenv-for-tabbar

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.

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: 3

Caution

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

⚠️ Outside diff range comments (4)
frontend/app/tab/updatebanner.tsx (1)

15-49: ⚠️ Potential issue | 🟡 Minor

dismissBanner is never set to true, making the timeout logic dead code.

The dismissBanner variable is initialized to false (line 17) and never assigned true in any switch case. This means the timeout logic at lines 39-45 will never execute, and lines 33-48 are effectively dead code.

If auto-dismissal is no longer needed, consider removing the unused code:

🧹 Proposed cleanup
     useEffect(() => {
         let message: string;
-        let dismissBanner = false;
         switch (appUpdateStatus) {
             case "ready":
                 message = "Update";
                 break;
             case "downloading":
                 message = "Downloading";
                 break;
             case "installing":
                 message = "Installing";
                 break;
             default:
                 break;
         }
         setUpdateStatusMessage(message);
-
-        // Clear any existing timeout
-        if (dismissBannerTimeout) {
-            clearTimeout(dismissBannerTimeout);
-        }
-
-        // If we want to dismiss the banner, set the new timeout, otherwise clear the state
-        if (dismissBanner) {
-            setDismissBannerTimeout(
-                setTimeout(() => {
-                    setUpdateStatusMessage(null);
-                    setDismissBannerTimeout(null);
-                }, 10000)
-            );
-        } else {
-            setDismissBannerTimeout(null);
-        }
     }, [appUpdateStatus]);

If auto-dismissal is still planned, consider removing the variable and directly checking the condition that should trigger dismissal.

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

In `@frontend/app/tab/updatebanner.tsx` around lines 15 - 49, The local flag
dismissBanner is never set true so the timeout branch is dead; either remove the
auto-dismiss logic (clear references to dismissBanner, dismissBannerTimeout,
setDismissBannerTimeout and the timeout handling inside the useEffect) or set
dismissBanner based on appUpdateStatus (e.g., in the useEffect switch set
dismissBanner = true for the status that should auto-dismiss such as "ready" or
whichever statuses you want) and keep the existing timeout/clear logic using
dismissBannerTimeout, setDismissBannerTimeout and setUpdateStatusMessage.
frontend/app/tab/tabbar.tsx (1)

477-489: ⚠️ Potential issue | 🔴 Critical

Tab reorders can be written to the wrong workspace after a workspace switch.

This debounced callback is memoized once, so it keeps the first workspace.oid. If the same TabBar instance later receives another workspace, dragging tabs will still call UpdateWorkspaceTabIdsCommand for the old workspace and can corrupt its tab order.

🐛 Proposed fix
-import { createRef, memo, useCallback, useEffect, useRef, useState } from "react";
+import { createRef, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@
-    const setUpdatedTabsDebounced = useCallback(
-        debounce(300, (tabIds: string[]) => {
+    const setUpdatedTabsDebounced = useMemo(
+        () =>
+            debounce(300, (nextTabIds: string[]) => {
             // Reset styles
             tabRefs.current.forEach((ref) => {
                 ref.current.style.zIndex = "0";
                 ref.current.classList.remove("animate");
             });
             // Reset dragging state
             setDraggingTab(null);
             // Update workspace tab ids
-            fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, tabIds));
-        }),
-        []
+                fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, nextTabIds));
+            }),
+        [env.rpc, workspace.oid]
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tabbar.tsx` around lines 477 - 489, The debounced callback
setUpdatedTabsDebounced captures the initial workspace.oid and never updates, so
calls to env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid,
tabIds) may target the wrong workspace; fix by recreating the debounced callback
when workspace changes (or by reading the latest workspace.oid at call time).
Concretely, move the debounce(...) creation into a useEffect that lists
workspace.oid (and any other changing refs like TabRpcClient) in its dependency
array and rebind setUpdatedTabsDebounced there, and ensure you clean up/cancel
the previous debounced function on effect cleanup; keep the existing reset logic
that touches tabRefs.current and setDraggingTab(null).
pkg/tsgen/tsgen.go (1)

415-449: ⚠️ Potential issue | 🟠 Major

Services are created as singletons with undefined waveEnv, making env-scoped routing ineffective.

Generated service methods depend on this.waveEnv to dispatch through the environment's backend, but AllServiceImpls exports zero-argument singletons where waveEnv is never initialized. All service calls fall through to WOS.callBackendService, bypassing environment-specific dispatch needed for mocks and previews. Either create service instances per WaveEnv (passing env to constructor), or initialize waveEnv on singletons when environments are created.

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

In `@pkg/tsgen/tsgen.go` around lines 415 - 449, The generated services are
exported as zero-arg singletons with waveEnv unset (see GenerateServiceClass,
the waveEnv field and constructor, and the final "export const %s = new
%sType()" line), which breaks env-scoped routing; change the generation to
either export a factory that accepts a WaveEnv and returns a new
tsServiceNameType with waveEnv initialized (e.g. export function
New<tsServiceName>(waveEnv: WaveEnv) { return new <tsServiceName>Type(waveEnv);
}) or set waveEnv on the singleton when an environment is created; update the
constructor signature in GenerateServiceClass to accept waveEnv (and assign it)
and replace the "export const" singleton emission with a factory function (or an
exported function to setWaveEnv) so each service instance is created/initialized
with the correct WaveEnv.
frontend/app/store/badge.ts (1)

116-146: ⚠️ Potential issue | 🟡 Minor

Potential cache inconsistency when mixing env/non-env calls.

getTabBadgeAtom caches the result by tabId (line 120-123, 144), but the internal tabAtom is created based on whether env is provided (line 126-127). If called first without env (production path), it caches an atom using WOS.getWaveObjectAtom. Subsequent calls with env (mock path) will return the cached production atom instead of creating a mock-aware one.

This may not be an issue if production and mock environments are never mixed in the same runtime, but it could cause subtle bugs during development or testing if both paths are exercised.

💡 Possible fix: include env identity in cache key
 function getTabBadgeAtom(tabId: string, env?: TabBadgesEnv): Atom<Badge[]> {
     if (tabId == null) {
         return NullAtom as Atom<Badge[]>;
     }
-    let rtn = TabBadgeAtomCache.get(tabId);
+    const cacheKey = env?.isMock ? `mock:${tabId}` : tabId;
+    let rtn = TabBadgeAtomCache.get(cacheKey);
     if (rtn != null) {
         return rtn;
     }
     // ... rest of function
-    TabBadgeAtomCache.set(tabId, rtn);
+    TabBadgeAtomCache.set(cacheKey, rtn);
     return rtn;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/store/badge.ts` around lines 116 - 146, The cached atom key must
include the env identity so we don't mix production and mock atoms: change
getTabBadgeAtom to derive a cache key from tabId plus an env discriminator
(e.g., env?.id or a boolean like envProvided) before looking up
TabBadgeAtomCache, and use that composite key when setting/getting the cache;
ensure the branch that creates tabAtom still uses env?.wos.getWaveObjectAtom vs
WOS.getWaveObjectAtom (referencing getTabBadgeAtom, TabBadgeAtomCache, tabAtom,
WOS.getWaveObjectAtom, env.wos.getWaveObjectAtom) so mock and non-mock atoms are
cached separately.
🧹 Nitpick comments (1)
eslint.config.js (1)

89-95: Consider documenting why no-unused-vars is disabled for this file.

Completely disabling @typescript-eslint/no-unused-vars for services.ts is a broad suppression. If this is due to generated code or intentional service mappings, a brief comment would help future maintainers understand the rationale.

📝 Proposed documentation
     {
+        // services.ts contains generated service types and mappings where
+        // unused vars are intentional for type completeness
         files: ["frontend/app/store/services.ts"],
         rules: {
             "@typescript-eslint/no-unused-vars": "off",
             "prefer-rest-params": "off",
         },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.js` around lines 89 - 95, Add a short explanatory comment next
to the override that disables "@typescript-eslint/no-unused-vars" for
"frontend/app/store/services.ts" in eslint.config.js explaining why the rule is
suppressed (e.g., generated file, intentional exported-but-unused service
mappings), and if possible prefer a narrower approach (use file-level
eslint-disable in the generated file or configure ignorePatterns for generated
artifacts) or justify why a global override for that single file is required;
reference the rule name "@typescript-eslint/no-unused-vars" and the affected
path "frontend/app/store/services.ts" so maintainers can locate and understand
the rationale.
🤖 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/store/tab-model.ts`:
- Around line 62-67: The code assumes waveEnv.mockModels exists when creating a
TabModel, which can be undefined per waveenv.ts; update the TabModel creation
path to defensively ensure mockModels is present (e.g., if waveEnv.mockModels is
null/undefined, assign a new Map and store it on waveEnv) before calling
get/set, and/or use a safe-get pattern; reference the TabModel constructor usage
and waveEnv.mockModels access in the block shown and ensure makeMockWaveEnv (or
its callers) initializes mockModels or the TabModel factory initializes it when
missing.

In `@frontend/app/tab/tabbar.tsx`:
- Around line 181-193: The tab bar doesn't recalc when chrome widths change
because changes to zoomFactor, app-menu/Wave visibility, or the new right-side
container don't call setSizeAndPosition; add a useEffect in tabbar.tsx that
installs a ResizeObserver on rightContainerRef, workspaceSwitcherRef,
waveAIButtonRef, and appMenuButtonRef (and/or subscribes to the atoms that
control their visibility like zoomFactor and the relevant env.atoms keys) to
call setSizeAndPosition whenever any observed element resizes or those atoms
change, and ensure you disconnect the observer on cleanup; reference
setSizeAndPosition, rightContainerRef, workspaceSwitcherRef, waveAIButtonRef,
appMenuButtonRef, and zoomFactor to locate the code to update.

In `@frontend/preview/mock/mockwaveenv.ts`:
- Around line 340-343: The mock exposes useWaveObjectValue as an arrow property
that calls the React hook useAtomValue, which is unconventional for custom
hooks; refactor it into a proper hook function named useWaveObjectValue<T
extends WaveObj>(oref: string): [T, boolean] { ... } that internally calls
env.wos.getWaveObjectAtom<T>(oref) and useAtomValue(objAtom) and returns the
value/flag; replace the property assignment with this function export so the
mock signature and usage match the real implementation in
frontend/app/store/wos.ts while keeping the same return behavior.

---

Outside diff comments:
In `@frontend/app/store/badge.ts`:
- Around line 116-146: The cached atom key must include the env identity so we
don't mix production and mock atoms: change getTabBadgeAtom to derive a cache
key from tabId plus an env discriminator (e.g., env?.id or a boolean like
envProvided) before looking up TabBadgeAtomCache, and use that composite key
when setting/getting the cache; ensure the branch that creates tabAtom still
uses env?.wos.getWaveObjectAtom vs WOS.getWaveObjectAtom (referencing
getTabBadgeAtom, TabBadgeAtomCache, tabAtom, WOS.getWaveObjectAtom,
env.wos.getWaveObjectAtom) so mock and non-mock atoms are cached separately.

In `@frontend/app/tab/tabbar.tsx`:
- Around line 477-489: The debounced callback setUpdatedTabsDebounced captures
the initial workspace.oid and never updates, so calls to
env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, tabIds) may
target the wrong workspace; fix by recreating the debounced callback when
workspace changes (or by reading the latest workspace.oid at call time).
Concretely, move the debounce(...) creation into a useEffect that lists
workspace.oid (and any other changing refs like TabRpcClient) in its dependency
array and rebind setUpdatedTabsDebounced there, and ensure you clean up/cancel
the previous debounced function on effect cleanup; keep the existing reset logic
that touches tabRefs.current and setDraggingTab(null).

In `@frontend/app/tab/updatebanner.tsx`:
- Around line 15-49: The local flag dismissBanner is never set true so the
timeout branch is dead; either remove the auto-dismiss logic (clear references
to dismissBanner, dismissBannerTimeout, setDismissBannerTimeout and the timeout
handling inside the useEffect) or set dismissBanner based on appUpdateStatus
(e.g., in the useEffect switch set dismissBanner = true for the status that
should auto-dismiss such as "ready" or whichever statuses you want) and keep the
existing timeout/clear logic using dismissBannerTimeout, setDismissBannerTimeout
and setUpdateStatusMessage.

In `@pkg/tsgen/tsgen.go`:
- Around line 415-449: The generated services are exported as zero-arg
singletons with waveEnv unset (see GenerateServiceClass, the waveEnv field and
constructor, and the final "export const %s = new %sType()" line), which breaks
env-scoped routing; change the generation to either export a factory that
accepts a WaveEnv and returns a new tsServiceNameType with waveEnv initialized
(e.g. export function New<tsServiceName>(waveEnv: WaveEnv) { return new
<tsServiceName>Type(waveEnv); }) or set waveEnv on the singleton when an
environment is created; update the constructor signature in GenerateServiceClass
to accept waveEnv (and assign it) and replace the "export const" singleton
emission with a factory function (or an exported function to setWaveEnv) so each
service instance is created/initialized with the correct WaveEnv.

---

Nitpick comments:
In `@eslint.config.js`:
- Around line 89-95: Add a short explanatory comment next to the override that
disables "@typescript-eslint/no-unused-vars" for
"frontend/app/store/services.ts" in eslint.config.js explaining why the rule is
suppressed (e.g., generated file, intentional exported-but-unused service
mappings), and if possible prefer a narrower approach (use file-level
eslint-disable in the generated file or configure ignorePatterns for generated
artifacts) or justify why a global override for that single file is required;
reference the rule name "@typescript-eslint/no-unused-vars" and the affected
path "frontend/app/store/services.ts" so maintainers can locate and understand
the rationale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1dca2d06-04da-4de7-9c9c-0c1499945c4e

📥 Commits

Reviewing files that changed from the base of the PR and between 568027d and d006371.

📒 Files selected for processing (36)
  • .kilocode/skills/add-rpc/SKILL.md
  • .kilocode/skills/waveenv/SKILL.md
  • cmd/generatets/main-generatets.go
  • eslint.config.js
  • frontend/app/store/badge.ts
  • frontend/app/store/contextmenu.ts
  • frontend/app/store/global-atoms.ts
  • frontend/app/store/global.ts
  • frontend/app/store/keymodel.ts
  • frontend/app/store/services.ts
  • frontend/app/store/tab-model.ts
  • frontend/app/store/wps.ts
  • frontend/app/store/wshclientapi.ts
  • frontend/app/tab/tab.tsx
  • frontend/app/tab/tabbar.scss
  • frontend/app/tab/tabbar.tsx
  • frontend/app/tab/tabbarenv.ts
  • frontend/app/tab/updatebanner.tsx
  • frontend/app/tab/workspaceswitcher.tsx
  • frontend/app/view/term/osc-handlers.ts
  • frontend/app/waveenv/waveenv.ts
  • frontend/app/waveenv/waveenvimpl.ts
  • frontend/app/workspace/widgets.tsx
  • frontend/preview/mock/mockwaveenv.ts
  • frontend/preview/preview.tsx
  • frontend/preview/previews/tabbar.preview.tsx
  • frontend/preview/previews/widgets.preview.tsx
  • frontend/types/custom.d.ts
  • frontend/util/platformutil.ts
  • pkg/service/objectservice/objectservice.go
  • pkg/service/workspaceservice/workspaceservice.go
  • pkg/tsgen/tsgen.go
  • pkg/wshrpc/wshclient/wshclient.go
  • pkg/wshrpc/wshrpctypes.go
  • pkg/wshrpc/wshserver/wshserver.go
  • tsconfig.json
💤 Files with no reviewable changes (4)
  • pkg/service/workspaceservice/workspaceservice.go
  • pkg/service/objectservice/objectservice.go
  • frontend/app/tab/tabbar.scss
  • frontend/preview/previews/widgets.preview.tsx

Comment on lines +62 to 67
const key = `TabModel:${tabId}`;
let model = waveEnv.mockModels.get(key);
if (model == null) {
model = new TabModel(tabId, waveEnv);
tabModelCache.set(tabId, model);
waveEnv.mockModels.set(key, 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 | 🟡 Minor

Potential null reference if mockModels is undefined.

When waveEnv.isMock is true, the code accesses waveEnv.mockModels.get/set without a null check. While mockModels is always provided in makeMockWaveEnv, the type definition in waveenv.ts marks it as optional (mockModels?: Map<any, any>). This could cause a runtime error if a mock environment is created without mockModels.

🛡️ Proposed defensive fix
     const key = `TabModel:${tabId}`;
-    let model = waveEnv.mockModels.get(key);
+    let model = waveEnv.mockModels?.get(key);
     if (model == null) {
         model = new TabModel(tabId, waveEnv);
-        waveEnv.mockModels.set(key, model);
+        waveEnv.mockModels?.set(key, model);
     }
     return model;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const key = `TabModel:${tabId}`;
let model = waveEnv.mockModels.get(key);
if (model == null) {
model = new TabModel(tabId, waveEnv);
tabModelCache.set(tabId, model);
waveEnv.mockModels.set(key, model);
}
const key = `TabModel:${tabId}`;
let model = waveEnv.mockModels?.get(key);
if (model == null) {
model = new TabModel(tabId, waveEnv);
waveEnv.mockModels?.set(key, model);
}
🤖 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 62 - 67, The code assumes
waveEnv.mockModels exists when creating a TabModel, which can be undefined per
waveenv.ts; update the TabModel creation path to defensively ensure mockModels
is present (e.g., if waveEnv.mockModels is null/undefined, assign a new Map and
store it on waveEnv) before calling get/set, and/or use a safe-get pattern;
reference the TabModel constructor usage and waveEnv.mockModels access in the
block shown and ensure makeMockWaveEnv (or its callers) initializes mockModels
or the TabModel factory initializes it when missing.

Comment on lines +181 to 193
const rightContainerRef = useRef<HTMLDivElement>(null);
const workspaceSwitcherRef = useRef<HTMLDivElement>(null);
const waveAIButtonRef = useRef<HTMLDivElement>(null);
const appMenuButtonRef = useRef<HTMLDivElement>(null);
const tabWidthRef = useRef<number>(TabDefaultWidth);
const scrollableRef = useRef<boolean>(false);
const updateStatusBannerRef = useRef<HTMLButtonElement>(null);
const configErrorButtonRef = useRef<HTMLElement>(null);
const prevAllLoadedRef = useRef<boolean>(false);
const activeTabId = useAtomValue(atoms.staticTabId);
const isFullScreen = useAtomValue(atoms.isFullScreen);
const zoomFactor = useAtomValue(atoms.zoomFactorAtom);
const settings = useAtomValue(atoms.settingsAtom);
const activeTabId = useAtomValue(env.atoms.staticTabId);
const isFullScreen = useAtomValue(env.atoms.isFullScreen);
const zoomFactor = useAtomValue(env.atoms.zoomFactorAtom);
const settings = useAtomValue(env.atoms.settingsAtom);
const confirmClose = useAtomValue(env.getSettingsKeyAtom("tab:confirmclose")) ?? false;

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

Recalculate tab widths when the added chrome changes size.

spaceForTabs now depends on zoomFactor, app-menu visibility, Wave AI visibility, and the new right-side banner/error container, but those state changes do not trigger setSizeAndPosition(). The bar stays sized against stale widths until the window is manually resized.

A ResizeObserver on the chrome refs, or an effect keyed off the new atoms that affect those widths, would keep the layout in sync.

Also applies to: 238-252, 613-633

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

In `@frontend/app/tab/tabbar.tsx` around lines 181 - 193, The tab bar doesn't
recalc when chrome widths change because changes to zoomFactor, app-menu/Wave
visibility, or the new right-side container don't call setSizeAndPosition; add a
useEffect in tabbar.tsx that installs a ResizeObserver on rightContainerRef,
workspaceSwitcherRef, waveAIButtonRef, and appMenuButtonRef (and/or subscribes
to the atoms that control their visibility like zoomFactor and the relevant
env.atoms keys) to call setSizeAndPosition whenever any observed element resizes
or those atoms change, and ensure you disconnect the observer on cleanup;
reference setSizeAndPosition, rightContainerRef, workspaceSwitcherRef,
waveAIButtonRef, appMenuButtonRef, and zoomFactor to locate the code to update.

Comment on lines 340 to 343
useWaveObjectValue: <T extends WaveObj>(oref: string): [T, boolean] => {
const obj = (overrides.mockWaveObjs?.[oref] ?? null) as T;
return [obj, false];
const objAtom = env.wos.getWaveObjectAtom<T>(oref);
return [useAtomValue(objAtom), false];
},
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that useWaveObjectValue is only called from React components/hooks
# and follows the hooks naming convention in its call sites.

# Search for usages of useWaveObjectValue to ensure it's called correctly
rg -n "useWaveObjectValue" --type=ts -A2 -B2

Repository: wavetermdev/waveterm

Length of output: 4321


🏁 Script executed:

# Get the full context of the actual useWaveObjectValue implementation
sed -n '251,270p' frontend/app/store/wos.ts

# Check if mockwaveenv.ts is used only in preview/test scenarios
fd "mockwaveenv" --type f
rg -l "mockwaveenv" --type=ts

Repository: wavetermdev/waveterm

Length of output: 940


🏁 Script executed:

# Check how mockwaveenv is used in preview.tsx
sed -n '1,50p' frontend/preview/preview.tsx | head -30

# Check the structure - is this a storybook or similar preview system?
cat frontend/preview/preview.tsx | head -50

# Also check if the mock is meant to be a hook or just a utility object
sed -n '300,360p' frontend/preview/mock/mockwaveenv.ts

Repository: wavetermdev/waveterm

Length of output: 6915


🏁 Script executed:

# Check if there's a way the mock could be restructured to follow hooks rules more strictly
# Look at how the mock wos object is used in preview components

sed -n '1,100p' frontend/preview/previews/widgets.preview.tsx

# Also check if there are any TypeScript or ESLint warnings about this pattern
rg "rules of hooks|useWaveObjectValue" --type=ts frontend/preview/

Repository: wavetermdev/waveterm

Length of output: 3309


The mock implementation works correctly but uses an unconventional pattern.

While useWaveObjectValue calls useAtomValue (a React hook), the pattern is functionally sound because it's invoked from React components during render. However, for consistency with the actual implementation in frontend/app/store/wos.ts, consider refactoring this arrow function property into a proper custom hook function. The current pattern, while working, doesn't follow standard custom hook conventions where the function itself would be declared as function useWaveObjectValue(...) rather than assigned as a property.

🤖 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 340 - 343, The mock
exposes useWaveObjectValue as an arrow property that calls the React hook
useAtomValue, which is unconventional for custom hooks; refactor it into a
proper hook function named useWaveObjectValue<T extends WaveObj>(oref: string):
[T, boolean] { ... } that internally calls env.wos.getWaveObjectAtom<T>(oref)
and useAtomValue(objAtom) and returns the value/flag; replace the property
assignment with this function export so the mock signature and usage match the
real implementation in frontend/app/store/wos.ts while keeping the same return
behavior.

@sawka
Copy link
Member

sawka commented Mar 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 3

♻️ Duplicate comments (1)
frontend/app/tab/tabbar.tsx (1)

245-250: ⚠️ Potential issue | 🟠 Major

Relayout still misses app-menu visibility changes.

spaceForTabs subtracts appMenuButtonWidth, and Line 623 can flip showAppMenuButton when window:showmenubar changes, but the relayout effect never keys off that state. In the new preview, turning "Show menu bar" on/off leaves tab widths calculated against the previous chrome until the window is resized.

🩹 Minimal fix
-    }, [tabIds, tabsLoaded, newTabId, saveTabsPosition, hideAiButton, appUpdateStatus, hasConfigErrors, zoomFactor]);
+    }, [
+        tabIds,
+        tabsLoaded,
+        newTabId,
+        saveTabsPosition,
+        hideAiButton,
+        showAppMenuButton,
+        appUpdateStatus,
+        hasConfigErrors,
+        zoomFactor,
+    ]);

Also applies to: 333-345, 623-623

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

In `@frontend/app/tab/tabbar.tsx` around lines 245 - 250, The relayout effect that
computes spaceForTabs uses appMenuButtonWidth but does not re-run when the
computed visibility state showAppMenuButton changes, so toggling "Show menu bar"
doesn't update tab widths until a resize; update the relevant useEffect(s) that
compute tab widths (the effect reading tabbarWrapperRef, draggerLeftRef,
rightContainerRef, addBtnRef, appMenuButtonRef, workspaceSwitcherRef and
computing spaceForTabs) to include showAppMenuButton (or the derived
appMenuButtonWidth state) in its dependency array so the effect reruns when
showAppMenuButton flips; ensure any other width-calculation effects (the ones
around the block that reads those refs and the handler that flips
showAppMenuButton) also observe that state to keep layouts in sync.
🧹 Nitpick comments (1)
frontend/app/waveenv/waveenv.ts (1)

39-52: Keep WaveEnvSubset narrow by making mock fields opt-in.

WaveEnvSubset<T> now injects isMock / mockSetWaveObj / mockModels into every named env, so contracts like frontend/app/view/sysinfo/sysinfo.tsx:20-30, frontend/app/store/tab-model.ts:6-8, frontend/app/store/badge.ts:9-22, and frontend/app/workspace/widgets.tsx:11-18 silently pick up preview-only APIs they never asked for. That defeats the narrowing this PR is trying to introduce.

♻️ Suggested shape
-type WaveEnvMockFields = {
+export type WaveEnvMockFields = {
     isMock: WaveEnv["isMock"];
     mockSetWaveObj: WaveEnv["mockSetWaveObj"];
     mockModels: WaveEnv["mockModels"];
 };
 
-export type WaveEnvSubset<T> = WaveEnvMockFields &
-    OmitNever<{
+export type WaveEnvSubset<T> = OmitNever<{
         [K in keyof T]: K extends keyof ComplexWaveEnvKeys
             ? Subset<T[K], ComplexWaveEnvKeys[K]>
             : K extends keyof WaveEnv
               ? T[K]
               : never;
     }>;

Then only the few preview-facing env types that really need these helpers can intersect WaveEnvMockFields explicitly.

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

In `@frontend/app/waveenv/waveenv.ts` around lines 39 - 52, WaveEnvSubset
currently always injects WaveEnvMockFields (isMock, mockSetWaveObj, mockModels)
into every resulting type; remove that unconditional inclusion so mock helpers
are opt-in. Update WaveEnvSubset<T> to only construct the mapped subset (the
OmitNever<{ ... }>) and export WaveEnvMockFields separately, or alternatively
add a second generic flag (e.g., WaveEnvSubset<T, WithMocks extends boolean =
false>) that only intersects WaveEnvMockFields when WithMocks is true; adjust
call sites that need preview helpers to explicitly intersect with
WaveEnvMockFields (or pass WithMocks=true) rather than relying on automatic
injection from WaveEnvSubset.
🤖 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/tab/updatebanner.tsx`:
- Around line 10-20: The function getUpdateStatusMessage currently declares a
return type of string but returns null in the default branch; change its
signature to return string | null to match the implementation (update the
function declaration getUpdateStatusMessage(status: string): string | null) and
verify any callers handle a possible null result (or narrow the return to always
string if you prefer, but the immediate fix is to change the annotated return
type to string | null).

In `@frontend/preview/mock/mockwaveenv.ts`:
- Around line 374-380: callBackendService currently calls the override
synchronously via Promise.resolve(fn(...args)), which means exceptions thrown by
fn propagate synchronously; change the invocation to create the promise first
and then call the override so thrown errors become rejected promises (e.g.
return Promise.resolve().then(() =>
overrides.services?.[service]?.[method](...args))). Update the call in
callBackendService and reference overrides.services and the local variable fn to
locate the change.

In `@frontend/types/custom.d.ts`:
- Around line 14-15: The type declarations in custom.d.ts are incorrect for
nullable runtime atoms; change workspaceId: jotai.Atom<string> to
jotai.Atom<string | null> and workspace: jotai.Atom<Workspace | null> so the
types reflect that global-atoms.ts can return null, then update any callers that
assume non-null (e.g., WorkspaceElem which accesses ws.oid) to handle the null
case or use the existing useAtomValueSafe helper; ensure workspaceIdAtom and
workspaceAtom are the referenced symbols that get the nullable types so
TypeScript prevents unsafe accesses.

---

Duplicate comments:
In `@frontend/app/tab/tabbar.tsx`:
- Around line 245-250: The relayout effect that computes spaceForTabs uses
appMenuButtonWidth but does not re-run when the computed visibility state
showAppMenuButton changes, so toggling "Show menu bar" doesn't update tab widths
until a resize; update the relevant useEffect(s) that compute tab widths (the
effect reading tabbarWrapperRef, draggerLeftRef, rightContainerRef, addBtnRef,
appMenuButtonRef, workspaceSwitcherRef and computing spaceForTabs) to include
showAppMenuButton (or the derived appMenuButtonWidth state) in its dependency
array so the effect reruns when showAppMenuButton flips; ensure any other
width-calculation effects (the ones around the block that reads those refs and
the handler that flips showAppMenuButton) also observe that state to keep
layouts in sync.

---

Nitpick comments:
In `@frontend/app/waveenv/waveenv.ts`:
- Around line 39-52: WaveEnvSubset currently always injects WaveEnvMockFields
(isMock, mockSetWaveObj, mockModels) into every resulting type; remove that
unconditional inclusion so mock helpers are opt-in. Update WaveEnvSubset<T> to
only construct the mapped subset (the OmitNever<{ ... }>) and export
WaveEnvMockFields separately, or alternatively add a second generic flag (e.g.,
WaveEnvSubset<T, WithMocks extends boolean = false>) that only intersects
WaveEnvMockFields when WithMocks is true; adjust call sites that need preview
helpers to explicitly intersect with WaveEnvMockFields (or pass WithMocks=true)
rather than relying on automatic injection from WaveEnvSubset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e2ce79d5-ff32-4e2b-aa6e-9542cbd00129

📥 Commits

Reviewing files that changed from the base of the PR and between d006371 and 19b7a42.

📒 Files selected for processing (9)
  • frontend/app/store/global-atoms.ts
  • frontend/app/tab/tabbar.tsx
  • frontend/app/tab/tabbarenv.ts
  • frontend/app/tab/updatebanner.tsx
  • frontend/app/view/sysinfo/sysinfo.tsx
  • frontend/app/waveenv/waveenv.ts
  • frontend/preview/mock/mockwaveenv.ts
  • frontend/preview/previews/tabbar.preview.tsx
  • frontend/types/custom.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/app/store/global-atoms.ts
  • frontend/app/tab/tabbarenv.ts

Comment on lines +10 to +20
function getUpdateStatusMessage(status: string): string {
switch (status) {
case "ready":
return "Update";
case "downloading":
return "Downloading";
case "installing":
return "Installing";
default:
return null;
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the frontend TypeScript config enables strict null checking,
# which would make the current signature invalid.
fd 'tsconfig*.json' -t f | while read -r file; do
  echo "== $file =="
  rg -n '"strict"\s*:|"strictNullChecks"\s*:' "$file"
done

echo
echo "== updatebanner helper =="
sed -n '10,20p' frontend/app/tab/updatebanner.tsx

Repository: wavetermdev/waveterm

Length of output: 567


Change getUpdateStatusMessage return type to string | null.

The function is declared as returning string, but the default case at line 19 returns null. Although strictNullChecks is disabled in the frontend tsconfig, the annotation is still incorrect and misleading—the actual return type is string | null. Update the signature to match the implementation:

-function getUpdateStatusMessage(status: string): string {
+function getUpdateStatusMessage(status: string): string | null {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getUpdateStatusMessage(status: string): string {
switch (status) {
case "ready":
return "Update";
case "downloading":
return "Downloading";
case "installing":
return "Installing";
default:
return null;
}
function getUpdateStatusMessage(status: string): string | null {
switch (status) {
case "ready":
return "Update";
case "downloading":
return "Downloading";
case "installing":
return "Installing";
default:
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/updatebanner.tsx` around lines 10 - 20, The function
getUpdateStatusMessage currently declares a return type of string but returns
null in the default branch; change its signature to return string | null to
match the implementation (update the function declaration
getUpdateStatusMessage(status: string): string | null) and verify any callers
handle a possible null result (or narrow the return to always string if you
prefer, but the immediate fix is to change the annotated return type to string |
null).

Comment on lines +374 to +380
callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => {
const fn = overrides.services?.[service]?.[method];
if (fn) {
return Promise.resolve(fn(...args));
}
console.log("[mock callBackendService]", service, method, args, noUIContext);
return Promise.resolve(null);
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 | 🟡 Minor

Preserve async error semantics in callBackendService.

Promise.resolve(fn(...args)) still invokes fn before a promise exists. If an override throws, callBackendService will throw synchronously instead of returning a rejected promise, so previews won't exercise the same failure path as production.

🩹 Minimal fix
         callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => {
             const fn = overrides.services?.[service]?.[method];
             if (fn) {
-                return Promise.resolve(fn(...args));
+                return Promise.resolve().then(() => fn(...args));
             }
             console.log("[mock callBackendService]", service, method, args, noUIContext);
             return Promise.resolve(null);
         },
🤖 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 374 - 380,
callBackendService currently calls the override synchronously via
Promise.resolve(fn(...args)), which means exceptions thrown by fn propagate
synchronously; change the invocation to create the promise first and then call
the override so thrown errors become rejected promises (e.g. return
Promise.resolve().then(() => overrides.services?.[service]?.[method](...args))).
Update the call in callBackendService and reference overrides.services and the
local variable fn to locate the change.

Comment on lines +14 to +15
workspaceId: jotai.Atom<string>; // derived from window WOS object
workspace: jotai.Atom<Workspace>; // driven from workspaceId via WOS
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
# Search for direct property accesses on workspace atom value without null checks
rg -n -C3 'useAtomValue\(.*workspace\)' --type=ts --type=tsx -g '!*.d.ts' | head -50

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

# Search for workspace atom usages without null checks
rg -n -C3 'useAtomValue\(.*workspace' --type=ts -g '!*.d.ts' | head -100

Repository: wavetermdev/waveterm

Length of output: 2867


🏁 Script executed:

# Locate global-atoms.ts and inspect the implementations
fd 'global-atoms' --type f -x cat {} \; -print

Repository: wavetermdev/waveterm

Length of output: 294


🏁 Script executed:

# Verify the actual type declarations in custom.d.ts
rg -n 'workspaceId|workspace.*Atom' frontend/types/custom.d.ts

Repository: wavetermdev/waveterm

Length of output: 699


🏁 Script executed:

# Find WorkspaceElem and check for the unsafe ws.oid access
rg -n 'WorkspaceElem' --type=ts -A 20 -B 2 | head -80

Repository: wavetermdev/waveterm

Length of output: 2243


🏁 Script executed:

# Find and inspect global-atoms.ts
fd 'global-atoms' -t f

Repository: wavetermdev/waveterm

Length of output: 138


🏁 Script executed:

# Check what happens with ws in WorkspaceElem after line 26
sed -n '26,80p' frontend/app/workspace/workspace.tsx

Repository: wavetermdev/waveterm

Length of output: 2646


🏁 Script executed:

# Search for useAtomValueSafe to understand the null-safe pattern
rg -n 'useAtomValueSafe' --type=ts | head -20

Repository: wavetermdev/waveterm

Length of output: 2225


🏁 Script executed:

# Check if ws is accessed directly or with null checks in WorkspaceElem
rg -n 'ws\.' frontend/app/workspace/workspace.tsx

Repository: wavetermdev/waveterm

Length of output: 117


🏁 Script executed:

cat frontend/app/store/global-atoms.ts

Repository: wavetermdev/waveterm

Length of output: 6795


Type declarations don't match nullable runtime behavior.

The implementation in global-atoms.ts returns null for both atoms:

  • workspaceIdAtom returns windowData?.workspaceid ?? null but is typed as Atom<string>
  • workspaceAtom returns null when workspaceId == null but is typed as Atom<Workspace>

This causes unsafe accesses like ws.oid in WorkspaceElem (line 56 of workspace.tsx) to pass type-checking while being vulnerable to runtime crashes. Other components use useAtomValueSafe to handle nulls, confirming the runtime possibility.

Proposed fix
-        workspaceId: jotai.Atom<string>; // derived from window WOS object
-        workspace: jotai.Atom<Workspace>; // driven from workspaceId via WOS
+        workspaceId: jotai.Atom<string | null>; // derived from window WOS object
+        workspace: jotai.Atom<Workspace | null>; // driven from workspaceId via WOS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
workspaceId: jotai.Atom<string>; // derived from window WOS object
workspace: jotai.Atom<Workspace>; // driven from workspaceId via WOS
workspaceId: jotai.Atom<string | null>; // derived from window WOS object
workspace: jotai.Atom<Workspace | null>; // driven from workspaceId via WOS
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/types/custom.d.ts` around lines 14 - 15, The type declarations in
custom.d.ts are incorrect for nullable runtime atoms; change workspaceId:
jotai.Atom<string> to jotai.Atom<string | null> and workspace:
jotai.Atom<Workspace | null> so the types reflect that global-atoms.ts can
return null, then update any callers that assume non-null (e.g., WorkspaceElem
which accesses ws.oid) to handle the null case or use the existing
useAtomValueSafe helper; ensure workspaceIdAtom and workspaceAtom are the
referenced symbols that get the nullable types so TypeScript prevents unsafe
accesses.

@sawka sawka merged commit ecccad6 into main Mar 11, 2026
7 checks passed
@sawka sawka deleted the copilot/create-waveenv-for-tabbar branch March 11, 2026 20:54
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.

2 participants