Skip to content

Comments

add a preview component for tabs, update contextmenu model w/ getInstance method#2927

Open
sawka wants to merge 14 commits intomainfrom
sawka/tab-preview
Open

add a preview component for tabs, update contextmenu model w/ getInstance method#2927
sawka wants to merge 14 commits intomainfrom
sawka/tab-preview

Conversation

@sawka
Copy link
Member

@sawka sawka commented Feb 24, 2026

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Documentation examples updated to call ContextMenuModel via its singleton accessor (ContextMenuModel.getInstance().showContextMenu(...)). Introduces a new public TabV React component and exports both Tab and TabV; tab context-menu construction was centralized into buildTabContextMenu, and inline rename control is exposed via onRename and renameRef. Adds a TabPreview component that renders TabV instances. Preview UI now loads fonts at startup and renders preview tiles in a wrapped, fixed-width flex layout. Backend workspace tab creation now computes the next tab name by scanning existing names with a regex (getNextTabName). A docs guideline deprecates React.MutableRefObject in favor of React.RefObject.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author; the PR lacks explanatory context about the motivation and scope of changes. Consider adding a description explaining why these changes were made, what problems they solve, and how the new TabV component relates to the ContextMenu updates.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes multiple key changes in the changeset: adding a preview component for tabs and updating the context menu model with a getInstance method.

✏️ 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/tab-preview

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.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 24, 2026

Deploying waveterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: d69e4d4
Status: ✅  Deploy successful!
Preview URL: https://43773fad.waveterm.pages.dev
Branch Preview URL: https://sawka-tab-preview.waveterm.pages.dev

View logs

};

const handleBlur = () => {
let newText = editableRef.current.innerText.trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Potential null reference error

The editableRef.current is accessed without a null check. If the ref is not yet attached, this will throw a runtime error.

Suggested change
let newText = editableRef.current.innerText.trim();
const handleBlur = () => {
if (!editableRef.current) return;
let newText = editableRef.current.innerText.trim();

selectEditableText();
return;
}
const curLen = Array.from(editableRef.current.innerText).length;
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Potential null reference error

The editableRef.current.innerText is accessed without a null check. This could throw a runtime error if the ref is not attached.

Suggested change
const curLen = Array.from(editableRef.current.innerText).length;
if (!editableRef.current) return;
const curLen = Array.from(editableRef.current.innerText).length;

menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
}
menu.push({ label: "Close Tab", click: () => onClose(null) });
const menu = buildTabContextMenu(id, handleRenameTab, onClose);
Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: Missing event.preventDefault()

The context menu handler should call e.preventDefault() to prevent the default browser context menu from appearing. This was present in the original code but removed during refactoring.

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 24, 2026

Code Review Summary

Status: No New Issues Found | Recommendation: Address Existing Comments

Overview

This PR refactors the Tab component to separate presentation (TabV) from container logic (TabInner), adds a preview system for the tab component, and improves tab naming logic in the backend. The refactoring is well-structured and maintains the existing functionality.

Note: There are 12 existing inline comments on this PR that should be addressed. No new issues were identified in this review.

Files Reviewed (6 files)
  • .kilocode/skills/context-menu/SKILL.md - Documentation update for ContextMenuModel API
  • .roo/rules/rules.md - Added rule about React.RefObject deprecation
  • frontend/app/tab/tab.tsx - Major refactoring: split into TabV and TabInner components
  • frontend/preview/preview.tsx - UI improvements for preview index page
  • frontend/preview/previews/tab.preview.tsx - New preview component for Tab
  • pkg/wcore/workspace.go - Improved tab naming logic with regex-based numbering
Key Changes

Frontend Refactoring

  • Tab Component Split: Separated presentation (TabV) from container logic (TabInner)
  • Props Restructuring: TabV now receives explicit props instead of fetching data internally
  • Context Menu Extraction: Moved context menu building to standalone buildTabContextMenu function
  • Preview System: Added interactive preview for Tab component with rename functionality

Backend Improvements

  • Smart Tab Naming: Replaced simple counter with regex-based logic that finds max T[N] and increments
  • Handles Gaps: New logic correctly handles deleted tabs (e.g., T1, T3, T5 → next is T6)

Documentation

  • Updated ContextMenuModel API usage to use getInstance() pattern
  • Added rule about React.RefObject replacing React.MutableRefObject

Fix these issues in Kilo Cloud

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

🧹 Nitpick comments (3)
frontend/app/tab/tab.tsx (2)

369-375: handleRenameTab is not memoized, making the useCallback wrapper ineffective.

handleRenameTab (line 298) is a plain function expression that is recreated every render, causing handleContextMenu's useCallback (which lists it as a dependency) to also recreate every render. This is the pre-existing behavior and not introduced by this PR, but worth noting if you're touching this area.

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

In `@frontend/app/tab/tab.tsx` around lines 369 - 375, The handleContextMenu
useCallback lists handleRenameTab as a dependency but handleRenameTab is
recreated each render; memoize handleRenameTab with React.useCallback (e.g.,
wrap the function originally defined at handleRenameTab) so it remains stable,
ensure you include any state or props it uses in its dependency array, and then
keep handleContextMenu's dependency array as [handleRenameTab, id, onClose];
this will make buildTabContextMenu(id, handleRenameTab, onClose) stable when
ContextMenuModel.getInstance().showContextMenu is invoked.

42-176: Significant duplication between TabV and TabInner.

TabV replicates most of the editing logic, key handling, blur handling, text selection, animation setup, and render structure from TabInner (lines 258–423). The only meaningful differences are the data source (prop vs. useWaveObjectValue) and the rename/blur side effects.

Consider extracting the shared editing behavior (editable state, key handler, blur handler, text selection) into a custom hook (e.g., useEditableTab) or a shared inner component that both TabV and TabInner compose. This would reduce the maintenance surface and prevent the two implementations from drifting apart.

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

In `@frontend/app/tab/tab.tsx` around lines 42 - 176, TabV duplicates the
editable-tab logic already implemented in TabInner (editable state, editableRef,
selectEditableText, handleRenameTab, handleBlur, handleKeyDown,
editableTimeoutRef, animation setup). Extract the shared behavior into a
reusable piece (either a custom hook named useEditableTab or a small shared
component used by both TabV and TabInner) so both use the same implementation:
move state (isEditable, originalName), refs (editableRef, editableTimeoutRef),
handlers (selectEditableText, handleRenameTab, handleBlur, handleKeyDown), and
the new-tab animation setup into the shared hook/component, update TabV to
call/use useEditableTab (or render the shared inner component) and only keep
TabV-specific bits (prop-based tabName source, onRename/onClose/onClick wiring).
frontend/preview/previews/tab.preview.tsx (1)

44-75: Nit: activeIndex recomputed on every map iteration.

tabDefs.findIndex(...) on line 47 is called once per tab inside the .map() callback. Since activeTabId doesn't change within a single render pass, compute it once before the map.

Proposed fix
+    const activeIndex = tabDefs.findIndex((t) => t.tabId === activeTabId);
+
     return (
         <div style={{ position: "relative", width: TAB_WIDTH * tabDefs.length, height: TAB_HEIGHT }}>
             {tabDefs.map((tab, index) => {
-                const activeIndex = tabDefs.findIndex((t) => t.tabId === activeTabId);
                 const isActive = tab.tabId === activeTabId;
                 const isBeforeActive = index === activeIndex - 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/previews/tab.preview.tsx` around lines 44 - 75, Compute
activeIndex once before mapping instead of inside the tabDefs.map callback: move
the call to tabDefs.findIndex((t) => t.tabId === activeTabId) out of the map
into a local const (e.g., const activeIndex = ...), then use that constant
inside the map to determine isBeforeActive/isActive; update references in the
TabV render (props like active, isBeforeActive, tabRefs.current assignment,
onClick handler using setActiveTabId) to use the precomputed activeIndex.
🤖 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/tab.tsx`:
- Around line 65-75: In selectEditableText (and the similar logic in TabInner)
guard against window.getSelection() returning null before calling methods on it:
after creating the range, call const selection = window.getSelection() and if
(!selection) return; (or bail) so you don't call selection.removeAllRanges() /
selection.addRange(range) when selection is null; update both selectEditableText
and the corresponding code in TabInner to perform this null-check using the
existing editableRef and range logic.

---

Nitpick comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 369-375: The handleContextMenu useCallback lists handleRenameTab
as a dependency but handleRenameTab is recreated each render; memoize
handleRenameTab with React.useCallback (e.g., wrap the function originally
defined at handleRenameTab) so it remains stable, ensure you include any state
or props it uses in its dependency array, and then keep handleContextMenu's
dependency array as [handleRenameTab, id, onClose]; this will make
buildTabContextMenu(id, handleRenameTab, onClose) stable when
ContextMenuModel.getInstance().showContextMenu is invoked.
- Around line 42-176: TabV duplicates the editable-tab logic already implemented
in TabInner (editable state, editableRef, selectEditableText, handleRenameTab,
handleBlur, handleKeyDown, editableTimeoutRef, animation setup). Extract the
shared behavior into a reusable piece (either a custom hook named useEditableTab
or a small shared component used by both TabV and TabInner) so both use the same
implementation: move state (isEditable, originalName), refs (editableRef,
editableTimeoutRef), handlers (selectEditableText, handleRenameTab, handleBlur,
handleKeyDown), and the new-tab animation setup into the shared hook/component,
update TabV to call/use useEditableTab (or render the shared inner component)
and only keep TabV-specific bits (prop-based tabName source,
onRename/onClose/onClick wiring).

In `@frontend/preview/previews/tab.preview.tsx`:
- Around line 44-75: Compute activeIndex once before mapping instead of inside
the tabDefs.map callback: move the call to tabDefs.findIndex((t) => t.tabId ===
activeTabId) out of the map into a local const (e.g., const activeIndex = ...),
then use that constant inside the map to determine isBeforeActive/isActive;
update references in the TabV render (props like active, isBeforeActive,
tabRefs.current assignment, onClick handler using setActiveTabId) to use the
precomputed activeIndex.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4dd83c3 and 6585600.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • .kilocode/skills/context-menu/SKILL.md
  • frontend/app/tab/tab.tsx
  • frontend/preview/preview.tsx
  • frontend/preview/previews/tab.preview.tsx

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

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

312-314: Null guards added for window.getSelection() and editableRef.current.

These address the previously reported potential null-reference issues.

Also applies to: 329-329, 344-344

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

In `@frontend/app/tab/tab.tsx` around lines 312 - 314, The code must guard against
null selections and missing editable DOM before using them: update the handlers
that call window.getSelection() and use editableRef.current (the checks around
the current early return at the selection check) to explicitly null-check
window.getSelection() and editableRef.current, and return early if either is
null; apply the same null-guard pattern to the other places mentioned (the logic
around lines referencing window.getSelection() at the spots corresponding to the
other checks) so functions that manipulate selection or rely on
editableRef.current safely exit when selection or editableRef.current is absent.
🧹 Nitpick comments (1)
frontend/app/tab/tab.tsx (1)

42-195: Substantial duplication between TabV and TabInner.

TabV and TabInner share nearly identical logic for editable text selection, rename handling, keyboard handling, blur handling, close button behavior, width animation, and JSX structure. The key difference is that TabInner fetches/persists tab data internally while TabV is a controlled/presentational component.

Consider having TabInner compose TabV internally — passing tabData.name as tabName, wiring onRename to ObjectService.UpdateTabName, and layering on the indicator-clear-on-click and onLoaded behaviors. This would eliminate ~100 lines of duplicated logic and ensure bug fixes (e.g., the null guards just added) don't need to be applied in two places.

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

In `@frontend/app/tab/tab.tsx` around lines 42 - 195, Refactor to remove
duplication by having TabInner compose TabV: keep TabV as a pure/presentational
component accepting props (tabName, onRename, onClick, onClose, onDragStart,
onContextMenu, isNew, tabWidth, indicator, onLoaded optional) and move all
data-fetching/persistence and side-effects into TabInner — pass tabData.name as
tabName and implement onRename in TabInner to call ObjectService.UpdateTabName;
also implement indicator-clear-on-click and the onLoaded behavior inside
TabInner (wiring TabV's onClick to a handler that clears indicator then calls
the outer click), and delete duplicate
editable/select/blur/keydown/initial-width logic from TabV so TabInner contains
the stateful wiring while TabV stays controlled/presentational.
🤖 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/tab.tsx`:
- Around line 199-220: The context menu calls handleRenameTab(null) but
handleRenameTab is currently typed as React.MouseEventHandler<HTMLDivElement>;
change its type to accept null by updating the signature to (event:
React.MouseEvent<HTMLDivElement> | null) => void (or add a separate no-arg
rename callback) and then update all related declarations and call sites (e.g.
buildTabContextMenu, TabV, TabInner) so they use the widened type (or the new
no-arg callback) to preserve type-safety and avoid fabricating a MouseEvent.

---

Duplicate comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 312-314: The code must guard against null selections and missing
editable DOM before using them: update the handlers that call
window.getSelection() and use editableRef.current (the checks around the current
early return at the selection check) to explicitly null-check
window.getSelection() and editableRef.current, and return early if either is
null; apply the same null-guard pattern to the other places mentioned (the logic
around lines referencing window.getSelection() at the spots corresponding to the
other checks) so functions that manipulate selection or rely on
editableRef.current safely exit when selection or editableRef.current is absent.

---

Nitpick comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 42-195: Refactor to remove duplication by having TabInner compose
TabV: keep TabV as a pure/presentational component accepting props (tabName,
onRename, onClick, onClose, onDragStart, onContextMenu, isNew, tabWidth,
indicator, onLoaded optional) and move all data-fetching/persistence and
side-effects into TabInner — pass tabData.name as tabName and implement onRename
in TabInner to call ObjectService.UpdateTabName; also implement
indicator-clear-on-click and the onLoaded behavior inside TabInner (wiring
TabV's onClick to a handler that clears indicator then calls the outer click),
and delete duplicate editable/select/blur/keydown/initial-width logic from TabV
so TabInner contains the stateful wiring while TabV stays
controlled/presentational.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6585600 and e44362f.

📒 Files selected for processing (1)
  • frontend/app/tab/tab.tsx


// Expose startRename to external callers (e.g. context menu in TabInner)
if (renameRef != null) {
(renameRef as React.MutableRefObject<(() => void) | null>).current = startRename;
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Type assertion violates new RefObject rules

This code casts renameRef to React.MutableRefObject to mutate its .current property, but:

  1. The .roo/rules/rules.md update in this PR states: "React.MutableRefObject is deprecated, just use React.RefObject now (current is now always mutable)"
  2. The prop is typed as React.RefObject<(() => void) | null> (line 41), which should be read-only
  3. This type assertion is hiding a type error

The comment on line 112 says "Expose startRename to external callers" but mutating a RefObject violates React's ref contract. Consider using a callback prop or useImperativeHandle instead of mutating the ref directly.

// Expose startRename to external callers (e.g. context menu in TabInner)
if (renameRef != null) {
renameRef.current = startRename;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: Side effect during render

Assigning to renameRef.current directly in the component body is a side effect that should be wrapped in useEffect or useImperativeHandle. This violates React's rules about side effects during render and can cause issues with concurrent rendering.

Consider using useImperativeHandle instead:

useImperativeHandle(renameRef, () => startRename, [startRename]);

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

🧹 Nitpick comments (4)
pkg/wcore/workspace.go (2)

234-234: tab == nil guard is dead code.

Per DBMustGet's implementation, when rtn == nil it always returns (zeroVal, ErrNotFound) — a non-nil error — so tab can only be nil when err != nil is already true. The extra check is harmless but misleading.

♻️ Proposed cleanup
-        if err != nil || tab == nil {
+        if err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wcore/workspace.go` at line 234, The guard checking "tab == nil" in the
error branch is dead code because DBMustGet returns a non-nil error when it
would otherwise return a nil result; update the conditional (in
pkg/wcore/workspace.go where DBMustGet is called and the variable tab is
assigned) to only check for err != nil (remove the "|| tab == nil" clause) and
adjust any related error handling or comments to avoid implying tab can be nil
when err is nil.

231-239: N+1 DB round-trips on every anonymous tab creation.

Each call to CreateTab with an empty name now issues one DBMustGet per existing tab. For workspaces with dozens of tabs this is a tight loop of synchronous DB reads. Since this runs in a user-facing path (opening a new tab), consider a bulk fetch if the store supports it, or batch-read tab names in a single query.

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

In `@pkg/wcore/workspace.go` around lines 231 - 239, The loop in CreateTab does an
N+1 read by calling wstore.DBMustGet for every tabId; replace it with a single
batch fetch of tabs by IDs (e.g., a wstore method that returns []*waveobj.Tab or
a map[id]*waveobj.Tab for ws.TabIds) and then collect tab.Name values and pass
them to getNextTabName. Use a bulk API such as wstore.DBGetMany or add one if
missing, skip nil/missing entries, and preserve the current nil/error handling
behavior while avoiding per-ID synchronous DB round-trips.
frontend/app/tab/tab.tsx (2)

117-124: onRename is called even when the tab name is unchanged.

When the user focuses and blurs without editing, handleBlur still calls onRename(newText), triggering an unnecessary ObjectService.UpdateTabName RPC call in TabInner.

♻️ Proposed fix
     const handleBlur = () => {
         if (!editableRef.current) return;
         let newText = editableRef.current.innerText.trim();
         newText = newText || originalName;
         editableRef.current.innerText = newText;
         setIsEditable(false);
-        onRename(newText);
+        if (newText !== originalName) {
+            onRename(newText);
+        }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 117 - 124, handleBlur currently always
calls onRename even when the text is unchanged; change it so after computing
newText (using editableRef.current.innerText.trim() and falling back to
originalName) you update editableRef.current.innerText and setIsEditable(false)
as before but only call onRename(newText) if newText !== originalName (strict
inequality); reference handleBlur, editableRef, originalName, setIsEditable, and
onRename when making the change.

117-124: onRename fires unnecessarily when the name is unchanged.

When the user focuses then blurs without editing, handleBlur still invokes onRename, which triggers ObjectService.UpdateTabName in TabInner. Add an equality guard:

♻️ Proposed fix
     const handleBlur = () => {
         if (!editableRef.current) return;
         let newText = editableRef.current.innerText.trim();
         newText = newText || originalName;
         editableRef.current.innerText = newText;
         setIsEditable(false);
-        onRename(newText);
+        if (newText !== originalName) {
+            onRename(newText);
+        }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 117 - 124, handleBlur currently always
calls onRename even when the name hasn't changed; update handleBlur (the
function using editableRef, originalName, setIsEditable and onRename) to compute
newText as you already do, then only call onRename(newText) when newText !==
originalName (skip the call if unchanged), and still reset
editableRef.current.innerText and setIsEditable(false) as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.roo/rules/rules.md:
- Line 96: Update the wording that currently says "deprecated" for
React.MutableRefObject to state it was "removed" in React 19 and
`@types/react`@^19, and clarify that developers should use React.RefObject
(current refs are always mutable) — change the sentence referencing
React.MutableRefObject to mention removal in React 19/@types/react@^19 and
recommend React.RefObject as the replacement.

In `@frontend/app/tab/tab.tsx`:
- Around line 112-115: The assignment to renameRef.current = startRename is a
render-side effect; move that population into a React effect so it only runs
after commit: create a useEffect that sets (renameRef as
React.MutableRefObject<(() => void) | null>).current = startRename and cleans up
on unmount (set to null), or, if you need a forwarded imperative API, replace
this pattern with useImperativeHandle on a forwarded ref; reference the existing
renameRef and startRename identifiers and update the Tab component to use
useEffect/useImperativeHandle accordingly.
- Around line 113-115: The render assigns to renameRef.current directly, causing
a render-side effect; move that assignment into a React useEffect so it runs
after render and is stable under Strict Mode. Wrap the assignment of (renameRef
as React.MutableRefObject<(() => void) | null>).current = startRename inside
useEffect, with a dependency array containing renameRef and startRename, and
optionally return a cleanup that sets renameRef.current = null to avoid leaking
stale references; reference the renameRef and startRename symbols and ensure the
effect only runs when either changes.

In `@pkg/wcore/workspace.go`:
- Around line 231-239: When iterating ws.TabIds in workspace.go and calling
wstore.DBMustGet[*waveobj.Tab](ctx, tabId), do not silently ignore failures:
when err != nil (or tab == nil) emit a diagnostic log including the tabId and
the error (or a nil-tab message) before continuing, so the resulting tabNames
passed to getNextTabName reflects that an entry failed and leaves a trace for
debugging; update the loop around DBMustGet and use the established logger in
this package to log at warn/error level.

---

Nitpick comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 117-124: handleBlur currently always calls onRename even when the
text is unchanged; change it so after computing newText (using
editableRef.current.innerText.trim() and falling back to originalName) you
update editableRef.current.innerText and setIsEditable(false) as before but only
call onRename(newText) if newText !== originalName (strict inequality);
reference handleBlur, editableRef, originalName, setIsEditable, and onRename
when making the change.
- Around line 117-124: handleBlur currently always calls onRename even when the
name hasn't changed; update handleBlur (the function using editableRef,
originalName, setIsEditable and onRename) to compute newText as you already do,
then only call onRename(newText) when newText !== originalName (skip the call if
unchanged), and still reset editableRef.current.innerText and
setIsEditable(false) as before.

In `@pkg/wcore/workspace.go`:
- Line 234: The guard checking "tab == nil" in the error branch is dead code
because DBMustGet returns a non-nil error when it would otherwise return a nil
result; update the conditional (in pkg/wcore/workspace.go where DBMustGet is
called and the variable tab is assigned) to only check for err != nil (remove
the "|| tab == nil" clause) and adjust any related error handling or comments to
avoid implying tab can be nil when err is nil.
- Around line 231-239: The loop in CreateTab does an N+1 read by calling
wstore.DBMustGet for every tabId; replace it with a single batch fetch of tabs
by IDs (e.g., a wstore method that returns []*waveobj.Tab or a
map[id]*waveobj.Tab for ws.TabIds) and then collect tab.Name values and pass
them to getNextTabName. Use a bulk API such as wstore.DBGetMany or add one if
missing, skip nil/missing entries, and preserve the current nil/error handling
behavior while avoiding per-ID synchronous DB round-trips.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e44362f and 51e7327.

📒 Files selected for processing (3)
  • .roo/rules/rules.md
  • frontend/app/tab/tab.tsx
  • pkg/wcore/workspace.go

- **CRITICAL** - useAtomValue and useAtom are React HOOKS. They cannot be used inline in JSX code, they must appear at the top of a component in the hooks area of the react code.
- for simple functions, we prefer `if (!cond) { return }; functionality;` pattern overn `if (cond) { functionality }` because it produces less indentation and is easier to follow.
- It is now 2026, so if you write new files, or update files use 2026 for the copyright year
- React.MutableRefObject is deprecated, just use React.RefObject now (current is now always mutable)
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

"Deprecated" is imprecise — MutableRefObject was removed, not deprecated, in React 19.

In @types/react@^19, React.MutableRefObject was outright removed (not just deprecated). Saying "deprecated" implies it still exists but is discouraged; developers may go looking for it and be surprised when the type is simply absent.

✏️ Suggested wording
-  React.MutableRefObject is deprecated, just use React.RefObject now (current is now always mutable)
+  React.MutableRefObject has been removed in React 19. Use React.RefObject instead (current is now always mutable).
📝 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
- React.MutableRefObject is deprecated, just use React.RefObject now (current is now always mutable)
- React.MutableRefObject has been removed in React 19. Use React.RefObject instead (current is now always mutable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.roo/rules/rules.md at line 96, Update the wording that currently says
"deprecated" for React.MutableRefObject to state it was "removed" in React 19
and `@types/react`@^19, and clarify that developers should use React.RefObject
(current refs are always mutable) — change the sentence referencing
React.MutableRefObject to mention removal in React 19/@types/react@^19 and
recommend React.RefObject as the replacement.

Comment on lines 112 to 115
// Expose startRename to external callers (e.g. context menu in TabInner)
if (renameRef != null) {
(renameRef as React.MutableRefObject<(() => void) | null>).current = startRename;
}
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 and read the actual file at the specified location
cat -n frontend/app/tab/tab.tsx | sed -n '100,130p'

Repository: wavetermdev/waveterm

Length of output: 1257


🏁 Script executed:

# Check `@types/react` version in the project
fd package.json -type f | head -5 | xargs grep -l '@types/react' | head -1 | xargs cat | grep -A2 -B2 '@types/react'

Repository: wavetermdev/waveterm

Length of output: 234


🏁 Script executed:

# Also check if there's a tsconfig or package-lock.json for more detail
fd package-lock.json -o package.json | head -5 | xargs grep '@types/react' -A1 | head -20

Repository: wavetermdev/waveterm

Length of output: 207


🏁 Script executed:

# Find and read package.json files to locate `@types/react` version
find . -name "package.json" -type f | head -3 | xargs grep '"@types/react"'

Repository: wavetermdev/waveterm

Length of output: 226


🏁 Script executed:

# Check the component file for the renameRef prop definition
head -50 frontend/app/tab/tab.tsx

Repository: wavetermdev/waveterm

Length of output: 1752


🏁 Script executed:

# Search for interface or type definitions of this component
grep -n "interface\|type.*Props\|export.*function Tab\|export.*const Tab" frontend/app/tab/tab.tsx | head -10

Repository: wavetermdev/waveterm

Length of output: 113


🏁 Script executed:

# Check the RefObject definition in `@types/react` 19
find node_modules/@types/react -name "*.d.ts" | head -5 | xargs grep -A5 "interface RefObject"

Repository: wavetermdev/waveterm

Length of output: 126


🏁 Script executed:

# Find and check tsconfig.json for strict settings
find . -name "tsconfig.json" -type f | head -3 | xargs cat

Repository: wavetermdev/waveterm

Length of output: 2298


🏁 Script executed:

# Check if there are any TS errors or type issues in this file
grep -n "MutableRefObject\|as React\|@ts-" frontend/app/tab/tab.tsx

Repository: wavetermdev/waveterm

Length of output: 46


Side effect during render — move renameRef population to a useEffect.

Lines 112–115 assign renameRef.current = startRename directly in the component body during render, which is a side effect that runs on every render. This violates React's render-purity contract and can cause unpredictable behavior in Strict Mode or if React replays renders.

Move this assignment into a useEffect:

♻️ Proposed fix
-    // Expose startRename to external callers (e.g. context menu in TabInner)
-    if (renameRef != null) {
-        renameRef.current = startRename;
-    }
+    // Expose startRename to external callers (e.g. context menu in TabInner)
+    useEffect(() => {
+        if (renameRef != null) {
+            renameRef.current = startRename;
+        }
+    }, [renameRef, startRename]);

Alternatively, refactor to use useImperativeHandle on a forwarded ref if imperative control is essential.

Note: The code currently assigns to a RefObject property, which is typed as readonly in @types/react 19. The assignment succeeds due to strict: false in tsconfig, but would fail under strict type checking. The effect-based approach above handles this idiomatically.

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

In `@frontend/app/tab/tab.tsx` around lines 112 - 115, The assignment to
renameRef.current = startRename is a render-side effect; move that population
into a React effect so it only runs after commit: create a useEffect that sets
(renameRef as React.MutableRefObject<(() => void) | null>).current = startRename
and cleans up on unmount (set to null), or, if you need a forwarded imperative
API, replace this pattern with useImperativeHandle on a forwarded ref; reference
the existing renameRef and startRename identifiers and update the Tab component
to use useEffect/useImperativeHandle accordingly.

Comment on lines 113 to 115
if (renameRef != null) {
(renameRef as React.MutableRefObject<(() => void) | null>).current = startRename;
}
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

🌐 Web query:

React 19 MutableRefObject removed RefObject.current writable

💡 Result:

In React 19’s TypeScript types (@types/react@19), the ref typings were simplified so that refs are mutable by default:

  • RefObject.current is no longer readonly (so ref.current = … is allowed), fixing the old “Cannot assign to 'current' because it is a read-only property” issue that happened when useRef<T>(null) inferred a read-only RefObject. [1]
  • The old split between RefObject (readonly) vs MutableRefObject (writable) is effectively removed from normal usage: useRef now always returns a single mutable RefObject<T> type. React’s upgrade guide notes this as “MutableRef is now deprecated in favor of a single RefObject type”. [1]
  • MutableRefObject still exists in the typings but is marked deprecated (“Use RefObject instead.”). [2]

Migration implications:

  • Replace React.MutableRefObject<T> annotations with React.RefObject<T>. [1][2]
  • useRef now requires an argument; use useRef<T>(undefined) if you previously wrote useRef<T>(), and keep useRef<T>(null) for DOM refs that start as null. [1]
  • Background/rationale for these React-19 type changes was tracked in DefinitelyTyped’s “Types for 19” discussion (including “refs are mutable by default” and “require initial value for useRef”). [3]

Sources: React 19 Upgrade Guide (TypeScript changes) [1]; @types/react docs showing MutableRefObject deprecated [2]; DefinitelyTyped discussion on React 19 type changes [3].


🏁 Script executed:

# Inspect the code context at lines 113-115 and surrounding area
cat -n frontend/app/tab/tab.tsx | sed -n '100,130p'

Repository: wavetermdev/waveterm

Length of output: 1257


🏁 Script executed:

# Check the type definition of renameRef in the component
cat -n frontend/app/tab/tab.tsx | sed -n '26,50p'

Repository: wavetermdev/waveterm

Length of output: 1077


Move side effect out of render body using useEffect.

Writing to renameRef.current directly in the render body violates React's render-purity guarantee — this code runs on every render, including Strict Mode double-invocations, causing unintended side effects.

Wrap the assignment in a useEffect:

♻️ Proposed fix
-    // Expose startRename to external callers (e.g. context menu in TabInner)
-    if (renameRef != null) {
-        renameRef.current = startRename;
-    }
+    // Expose startRename to external callers (e.g. context menu in TabInner)
+    useEffect(() => {
+        if (renameRef != null) {
+            renameRef.current = startRename;
+        }
+    }, [renameRef, startRename]);
📝 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
if (renameRef != null) {
(renameRef as React.MutableRefObject<(() => void) | null>).current = startRename;
}
const handleBlur = () => {
if (!editableRef.current) return;
let newText = editableRef.current.innerText.trim();
if (newText && newText !== originalName) {
onRename(newText);
}
setIsEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleBlur();
} else if (e.key === 'Escape') {
setIsEditing(false);
}
};
// Expose startRename to external callers (e.g. context menu in TabInner)
useEffect(() => {
if (renameRef != null) {
renameRef.current = startRename;
}
}, [renameRef, startRename]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 113 - 115, The render assigns to
renameRef.current directly, causing a render-side effect; move that assignment
into a React useEffect so it runs after render and is stable under Strict Mode.
Wrap the assignment of (renameRef as React.MutableRefObject<(() => void) |
null>).current = startRename inside useEffect, with a dependency array
containing renameRef and startRename, and optionally return a cleanup that sets
renameRef.current = null to avoid leaking stale references; reference the
renameRef and startRename symbols and ensure the effect only runs when either
changes.

Comment on lines +231 to +239
tabNames := make([]string, 0, len(ws.TabIds))
for _, tabId := range ws.TabIds {
tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
if err != nil || tab == nil {
continue
}
tabNames = append(tabNames, tab.Name)
}
tabName = getNextTabName(tabNames)
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

Silent tab-fetch failures leave no diagnostic trace.

When DBMustGet returns an error, the tab's name is silently skipped. If a tab in the workspace is legitimately unreachable (e.g., DB corruption), getNextTabName sees an incomplete name list and can assign a name already "owned" by the skipped tab. Since tab names are display-only this is non-fatal, but a log line would aid debugging:

🔧 Proposed fix
 for _, tabId := range ws.TabIds {
     tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
     if err != nil || tab == nil {
+        log.Printf("warning: could not fetch tab %s for name scan: %v\n", tabId, err)
         continue
     }
     tabNames = append(tabNames, tab.Name)
 }
📝 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
tabNames := make([]string, 0, len(ws.TabIds))
for _, tabId := range ws.TabIds {
tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
if err != nil || tab == nil {
continue
}
tabNames = append(tabNames, tab.Name)
}
tabName = getNextTabName(tabNames)
for _, tabId := range ws.TabIds {
tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId)
if err != nil || tab == nil {
log.Printf("warning: could not fetch tab %s for name scan: %v\n", tabId, err)
continue
}
tabNames = append(tabNames, tab.Name)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wcore/workspace.go` around lines 231 - 239, When iterating ws.TabIds in
workspace.go and calling wstore.DBMustGet[*waveobj.Tab](ctx, tabId), do not
silently ignore failures: when err != nil (or tab == nil) emit a diagnostic log
including the tabId and the error (or a nil-tab message) before continuing, so
the resulting tabNames passed to getNextTabName reflects that an entry failed
and leaves a trace for debugging; update the loop around DBMustGet and use the
established logger in this package to log at warn/error level.

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

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

112-115: ⚠️ Potential issue | 🟠 Major

Side effect during render — renameRef.current assignment still not moved to useEffect.

This direct assignment runs on every render, violating React's render-purity contract (Strict Mode will double-invoke it).

♻️ Proposed fix
-    // Expose startRename to external callers (e.g. context menu in TabInner)
-    if (renameRef != null) {
-        renameRef.current = startRename;
-    }
+    // Expose startRename to external callers (e.g. context menu in TabInner)
+    useEffect(() => {
+        if (renameRef != null) {
+            renameRef.current = startRename;
+        }
+        return () => {
+            if (renameRef != null) {
+                renameRef.current = null;
+            }
+        };
+    }, [renameRef, startRename]);

Alternatively, use useImperativeHandle with a forwarded ref if a formal imperative API is preferred.

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

In `@frontend/app/tab/tab.tsx` around lines 112 - 115, The direct assignment to
renameRef.current inside the render (the renameRef/current = startRename block)
causes a side effect; move that assignment into a useEffect that depends on
renameRef and startRename (or convert the component to forwardRef and expose
startRename via useImperativeHandle) so the imperative ref is set only after
render and not during render; update the code that currently assigns
renameRef.current to instead set it inside useEffect (or implement
useImperativeHandle) to preserve render purity.
🤖 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/tab.tsx`:
- Around line 249-253: The comparator passed to bgPresets.sort can throw when a
preset value is null; update the sort to defensively handle missing/null presets
by using optional chaining and a default (e.g. treat
missing/fullConfig.presets[a] or fullConfig.presets[b] as having display:order =
0) or pre-filter bgPresets to remove null/undefined keys before sorting;
reference the bgPresets.sort comparator and fullConfig.presets["display:order"]
access and change it to use fullConfig.presets[a]?.["display:order"] ?? 0 (and
same for b) or filter out null entries from bgPresets first.

---

Duplicate comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 112-115: The direct assignment to renameRef.current inside the
render (the renameRef/current = startRename block) causes a side effect; move
that assignment into a useEffect that depends on renameRef and startRename (or
convert the component to forwardRef and expose startRename via
useImperativeHandle) so the imperative ref is set only after render and not
during render; update the code that currently assigns renameRef.current to
instead set it inside useEffect (or implement useImperativeHandle) to preserve
render purity.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51e7327 and a69e72d.

📒 Files selected for processing (1)
  • frontend/app/tab/tab.tsx

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

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

112-115: Move renameRef assignment out of render.

Line 113–Line 115 mutates renameRef.current during render. This is a render side effect and can behave poorly under Strict Mode replays.

Suggested refactor
-    // Expose startRename to external callers (e.g. context menu in TabInner)
-    if (renameRef != null) {
-        renameRef.current = startRename;
-    }
+    // Expose startRename to external callers (e.g. context menu in TabInner)
+    useEffect(() => {
+        if (!renameRef) return;
+        renameRef.current = startRename;
+        return () => {
+            if (renameRef.current === startRename) {
+                renameRef.current = null;
+            }
+        };
+    }, [renameRef, startRename]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 112 - 115, The code mutates
renameRef.current during render (renameRef.current = startRename), which causes
render-side effects; move this assignment into a post-render effect: in the
component that defines startRename and receives renameRef, set renameRef.current
= startRename inside a useEffect (or useLayoutEffect if necessary) that depends
on startRename and renameRef to avoid mutating refs during render and ensure
stable behavior under Strict Mode.
🤖 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/tab.tsx`:
- Around line 61-72: Introduce a new state displayName
(useState<string>(tabName)), keep it synced with tabName in the existing
useEffect, update displayName on each edit input change and in handleBlur set
displayName to the edited value before calling setIsEditable(false) and invoking
props.onRename; update the render spot that currently prints {tabName} (line
~188) to render displayName so the UI shows the optimistic name until the
parent-provided tabName prop updates; keep originalName, isEditable,
editableRef, editableTimeoutRef, tabRef and useImperativeHandle as-is.

---

Duplicate comments:
In `@frontend/app/tab/tab.tsx`:
- Around line 112-115: The code mutates renameRef.current during render
(renameRef.current = startRename), which causes render-side effects; move this
assignment into a post-render effect: in the component that defines startRename
and receives renameRef, set renameRef.current = startRename inside a useEffect
(or useLayoutEffect if necessary) that depends on startRename and renameRef to
avoid mutating refs during render and ensure stable behavior under Strict Mode.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a69e72d and d69e4d4.

📒 Files selected for processing (1)
  • frontend/app/tab/tab.tsx

Comment on lines +61 to +72
const [originalName, setOriginalName] = useState(tabName);
const [isEditable, setIsEditable] = useState(false);
const indicator = useAtomValue(getTabIndicatorAtom(id));

const editableRef = useRef<HTMLDivElement>(null);
const editableTimeoutRef = useRef<NodeJS.Timeout>(null);
const loadedRef = useRef(false);
const tabRef = useRef<HTMLDivElement>(null);

useImperativeHandle(ref, () => tabRef.current as HTMLDivElement);

useEffect(() => {
if (tabData?.name) {
setOriginalName(tabData.name);
}
}, [tabData]);
setOriginalName(tabName);
}, [tabName]);
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:

find . -type f -name "tab.tsx" | head -5

Repository: wavetermdev/waveterm

Length of output: 90


🏁 Script executed:

cat -n frontend/app/tab/tab.tsx | head -100

Repository: wavetermdev/waveterm

Length of output: 3862


🏁 Script executed:

cat -n frontend/app/tab/tab.tsx | sed -n '50,130p'

Repository: wavetermdev/waveterm

Length of output: 3003


🏁 Script executed:

cat -n frontend/app/tab/tab.tsx | sed -n '180,200p'

Repository: wavetermdev/waveterm

Length of output: 1131


🏁 Script executed:

cat -n frontend/app/tab/tab.tsx | wc -l

Repository: wavetermdev/waveterm

Length of output: 67


Add optimistic display state to prevent text snap-back on blur.

When handleBlur calls setIsEditable(false), React re-renders before the parent's onRename async handler updates the tabName prop. The component renders line 188's {tabName} which still contains the old value, causing a brief visual snap-back to the previous name.

The suggested fix adds a displayName state that tracks the edited value immediately on blur, preventing the reversion until the parent state propagates upstream.

Suggested fix (optimistic display state)
-    const [originalName, setOriginalName] = useState(tabName);
+    const [originalName, setOriginalName] = useState(tabName);
+    const [displayName, setDisplayName] = useState(tabName);

     useEffect(() => {
         setOriginalName(tabName);
-    }, [tabName]);
+        if (!isEditable) {
+            setDisplayName(tabName);
+        }
+    }, [tabName, isEditable]);

     const handleBlur = () => {
         if (!editableRef.current) return;
         let newText = editableRef.current.innerText.trim();
         newText = newText || originalName;
         editableRef.current.innerText = newText;
         setIsEditable(false);
+        setDisplayName(newText);
         onRename(newText);
     };
...
-                    {tabName}
+                    {displayName}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/tab/tab.tsx` around lines 61 - 72, Introduce a new state
displayName (useState<string>(tabName)), keep it synced with tabName in the
existing useEffect, update displayName on each edit input change and in
handleBlur set displayName to the edited value before calling
setIsEditable(false) and invoking props.onRename; update the render spot that
currently prints {tabName} (line ~188) to render displayName so the UI shows the
optimistic name until the parent-provided tabName prop updates; keep
originalName, isEditable, editableRef, editableTimeoutRef, tabRef and
useImperativeHandle as-is.

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