Skip to content

Port browser tabs into the unified rc tab strip#785

Merged
jeffscottward merged 17 commits intoRunMaestro:rcfrom
jeffscottward:browser-tab-rc-sync
Apr 10, 2026
Merged

Port browser tabs into the unified rc tab strip#785
jeffscottward merged 17 commits intoRunMaestro:rcfrom
jeffscottward:browser-tab-rc-sync

Conversation

@jeffscottward
Copy link
Copy Markdown
Contributor

@jeffscottward jeffscottward commented Apr 10, 2026

Summary

This ports the rc browser-tab work into Maestro's unified tab strip and removes the older Globe-button entry path so browser content behaves like any other main-panel tab.

  • adds the new browser-tab interaction path in the unified tab model
  • removes the older Globe-button path in favor of the unified tab-strip flow
  • keeps embedded DevTools omitted from this port to avoid shipping the abandoned experiment and extra lifecycle complexity
  • includes a proof screenshot showing the browser tab active inside the main panel

Screenshot

Browser tab active in unified tab strip

Validation

  • npm run test:e2e -- e2e/browser-tab.spec.ts
  • verified browser tab creation from the new tab interaction
  • verified localhost and external HTTPS navigation
  • verified mixed browser / AI / file / terminal tab behavior in the unified tab strip
  • verified reload, close, reopen, and restart persistence flows

Summary by CodeRabbit

  • New Features

    • First-class browser tabs: create/manage embedded browser tabs in the unified tab strip with address bar, back/forward, reload/stop, favicons, and "New Browser Tab" entry.
    • Browser tabs persist and restore across relaunches, retaining titles and addresses.
  • Security / Behavior

    • Blocked protocols and popup attempts are prevented with inline alerts; external-open and clipboard requests from embedded pages are restricted.
  • Bug Fixes

    • Improved tab-close and restore behaviors for consistent unified tab ordering and keyboard closing.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds first-class "browser tab" support: UI components, persistence utilities, session/tab plumbing, main-process webview hardening, TypeScript types, and extensive unit and E2E tests to validate navigation, security boundaries, persistence, and unified-tab behavior.

Changes

Cohort / File(s) Summary
E2E Tests
e2e/browser-tab.spec.ts
Two Playwright suites exercising browser-tab creation, navigation, blocked-protocol/clipboard behavior, external-open interception, persistence across relaunch, and tab-closing keyboard flow.
Main-process webview security
src/main/app-lifecycle/window-manager.ts, src/__tests__/main/.../window-manager.test.ts
Enable webviewTag, validate/harden webPreferences on will-attach-webview, block unsafe src/partition, deny guest popups/navigation, and restrict clipboard permission to window webContents; added tests and mocks for guest handlers.
Types & session shape
src/renderer/types/index.ts, src/renderer/global.d.ts
Added BrowserTab interface, extended UnifiedTab/UnifiedTabRef/ClosedTabEntry, added browserTabs & activeBrowserTabId to Session, and declared JSX webview typing and HTMLWebViewElement.
Persistence & helpers
src/renderer/utils/browserTabPersistence.ts, src/renderer/utils/tabHelpers.ts, src/renderer/utils/tabHelpers.ts tests
New browser-tab persistence utilities (URL/partition normalization, sanitization, rehydration) and extended tabHelpers to build/close/reopen browser unified entries and maintain closed-tab history.
Renderer UI components
src/renderer/components/MainPanel/BrowserTabView.tsx, src/renderer/components/TabBar/BrowserTabItem.tsx, src/renderer/components/TabBar/NewTabPopover.tsx, src/renderer/components/TabBar/TabBar.tsx, src/renderer/components/TabBar/*
Added BrowserTabView (webview + address UI) and BrowserTabItem (tab strip item, favicon/overlay), wired New Browser Tab action and browser branch in TabBar/NewTabPopover/tabBar utils.
MainPanel plumbing & props
src/renderer/App.tsx, src/renderer/components/MainPanel/*, src/renderer/components/MainPanel/types.ts, src/renderer/hooks/props/useMainPanelProps.ts
Threaded activeBrowserTabId/activeBrowserTab and browser handlers (create/select/close/update) through App, useMainPanelProps, MainPanel, MainPanelContent, and types.
Hooks, handlers & persistence integration
src/renderer/hooks/tabs/useTabHandlers.ts, src/renderer/hooks/utils/useDebouncedPersistence.ts, src/renderer/hooks/session/*, src/renderer/hooks/* tests
Added browser-tab handlers (create/select/close/update), exposed activeBrowserTab in hook return, persisted sanitized browserTabs and activeBrowserTabId, and updated session restoration/creation hooks with rehydration and migrations; tests added/extended accordingly.
Tests & test fixtures
multiple under src/__tests__/renderer/* and src/__tests__/renderer/utils/*
New unit tests for BrowserTabView, TabBar, tabHandlers, persistence, tabHelpers, session restoration, and test helper updates (browser tab fixtures).
Misc imports & small updates
src/renderer/components/CuePipelineEditor/*, src/renderer/hooks/cue/usePipelineState.ts, package.json, scripts/*, vite.config.mts, src/main/deep-links.ts
Centralized Cue type imports to shared/cue; replaced dev script orchestration with scripts/dev.mjs/port discovery; added strict Vite port; minor deep-link single-instance behavior gating and small session-init additions across various hooks.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Renderer as Renderer Process
    participant BrowserTab as BrowserTabView
    participant Webview as Electron Webview
    participant Main as Main Process

    User->>Renderer: Click "New Browser Tab"
    Renderer->>BrowserTab: Render BrowserTabView (about:blank)
    BrowserTab->>Webview: attach <webview> (partition/src)
    Webview->>Main: will-attach-webview
    Main->>Main: validate src & partition, harden webPreferences
    Main-->>Webview: allow attachment (or preventDefault if invalid)
    Webview->>Main: did-attach-webview
    BrowserTab->>Webview: set src on navigation submit
    Webview->>Main: will-navigate / will-redirect
    Main->>Main: enforce allowed protocols (http/https/about:blank)
    Main-->>Webview: allow/deny navigation
    Webview->>BrowserTab: dom-ready / did-finish-load events
    BrowserTab->>Renderer: onUpdateTab(url,title,isLoading,webContentsId)
    User->>BrowserTab: in-page window.open(...) or clipboard access
    Webview->>Main: setWindowOpenHandler / permission-request
    Main->>Main: deny popup and deny clipboard for webview guests
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested labels

approved

Poem

🐰 A little tab hopped into place,

webviews snug in a sandboxed space,
partitions tidy, URLs made neat,
unified tabs all in a neat little fleet,
persistence hums: "Come back, take a seat!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Port browser tabs into the unified rc tab strip' directly and clearly summarizes the main change: integrating browser tabs into the unified tab system.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 10, 2026

Greptile Summary

This PR ports browser tabs into Maestro's unified tab strip (rc branch), adding a new browser type to UnifiedTab, a BrowserTabView webview component with address bar controls, BrowserTabItem for the tab strip, browser tab persistence/rehydration, and related CRUD/navigation wiring across useTabHandlers, tabHelpers, and session restoration.

  • The unifiedTabs useMemo in useTabHandlers.ts is missing activeSession?.browserTabs in its dependency array, so browser tab metadata (title, URL, favicon) shown in the unified tab strip will not update while the user browses — the tab bar shows stale data until an unrelated state change triggers a recompute.
  • Browser tabs are silently skipped during showUnreadOnly keyboard navigation in both navigateToNextUnifiedTab and navigateToPrevUnifiedTab, while the tab bar correctly keeps them visible in that filter mode.

Confidence Score: 4/5

Safe to merge after fixing the missing browserTabs dependency in the unifiedTabs useMemo — it causes the tab strip to show stale titles/favicons during active browsing.

One P1 defect (stale browser tab metadata in the tab strip due to missing useMemo dependency) requires a one-line fix before merge. The rest of the integration — persistence, session restoration, close/reopen, drag-and-drop, keyboard navigation in non-filter mode — is well-structured and follows existing patterns. The unread-filter keyboard nav gap for browser tabs is a P2 inconvenience, not a data loss or crash risk.

src/renderer/hooks/tabs/useTabHandlers.ts (missing dep), src/renderer/utils/tabHelpers.ts (browser tab nav in showUnreadOnly)

Important Files Changed

Filename Overview
src/renderer/hooks/tabs/useTabHandlers.ts Adds browser tab CRUD handlers (new, select, close, update) and wires them into the unified tab model; missing browserTabs dep in unifiedTabs useMemo causes stale tab-strip labels during navigation.
src/renderer/utils/tabHelpers.ts Extends all unified-tab helpers (build, repair, navigate, close, reopen) with browser-tab support; browser tabs are silently skipped in showUnreadOnly keyboard navigation in both next/prev functions.
src/renderer/components/MainPanel/BrowserTabView.tsx New webview-based browser tab component with address bar, back/forward/reload, favicon display, and navigation event wiring; hardcoded bg-white may flash in dark themes.
src/renderer/components/TabBar/BrowserTabItem.tsx New browser tab strip item with drag-and-drop, hover overlay menu, favicon fallback, and middle-click-to-close; implementation mirrors existing tab item patterns cleanly.
src/renderer/utils/browserTabPersistence.ts New utility for URL resolution (search fallback, scheme inference, local-address detection), partition key sanitization, and browser tab sanitize/rehydrate for persistence; logic is thorough and well-tested.
src/renderer/hooks/session/useSessionRestoration.ts Adds browser tab rehydration on session restore, validation of activeBrowserTabId against persisted tabs, and migration of activeBrowserTabId to null for non-AI inputMode; handles all edge cases correctly.
src/renderer/hooks/utils/useDebouncedPersistence.ts Adds sanitizeBrowserTabForPersistence and activeBrowserTabId handling to session persistence; correctly strips runtime webview state before disk writes.
src/renderer/stores/tabStore.ts No browser-tab specific actions added to the store; existing selectFileTab correctly clears activeBrowserTabId when switching to file tabs. Store is a pass-through to sessionStore mutations.
src/renderer/components/TabBar/TabBar.tsx Renders BrowserTabItem in the unified tab strip with correct drag-and-drop, active detection, and context menu wiring; browser tabs always shown in unread filter mode (consistent with TabBar behavior, but inconsistent with keyboard nav).

Sequence Diagram

sequenceDiagram
    participant User
    participant TabBar
    participant useTabHandlers
    participant sessionStore
    participant BrowserTabView
    participant Webview

    User->>TabBar: Click "New Tab" → Browser Tab
    TabBar->>useTabHandlers: handleNewBrowserTab()
    useTabHandlers->>sessionStore: setSessions (add BrowserTab, set activeBrowserTabId)
    sessionStore-->>TabBar: unifiedTabs recomputes → BrowserTabItem renders

    User->>TabBar: Click BrowserTabItem
    TabBar->>useTabHandlers: handleSelectBrowserTab(tabId)
    useTabHandlers->>sessionStore: setSessions (activeBrowserTabId = tabId)
    sessionStore-->>BrowserTabView: activeBrowserTab prop updates → webview shown

    User->>BrowserTabView: Enter URL / navigate
    BrowserTabView->>Webview: webview.src = nextUrl
    Webview-->>BrowserTabView: did-navigate / page-title-updated / page-favicon-updated
    BrowserTabView->>useTabHandlers: handleUpdateBrowserTab(sessionId, tabId, updates)
    useTabHandlers->>sessionStore: setSessions (browserTabs updated)
    Note over sessionStore,TabBar: ⚠️ unifiedTabs useMemo missing browserTabs dep → BrowserTabItem shows stale title/favicon

    User->>TabBar: Click X on BrowserTabItem
    TabBar->>useTabHandlers: handleCloseBrowserTab(tabId)
    useTabHandlers->>sessionStore: closeBrowserTab() → fallback tab activated
    sessionStore-->>TabBar: unifiedTabs recomputes → BrowserTabItem removed
Loading

Comments Outside Diff (2)

  1. src/renderer/hooks/tabs/useTabHandlers.ts, line 199-204 (link)

    P1 Missing browserTabs in unifiedTabs dependency array

    buildUnifiedTabs pulls data from all four tab arrays, but activeSession?.browserTabs is absent from the deps list. When handleUpdateBrowserTab fires (title, URL, favicon update), only browserTabs mutates on the session object — aiTabs, filePreviewTabs, terminalTabs, and unifiedTabOrder all retain the same references, so the memo never recomputes. The browser tab item in the tab strip will show stale title/favicon until something else causes a re-render.

  2. src/renderer/utils/tabHelpers.ts, line 1912-1946 (link)

    P2 Browser tabs silently skipped in showUnreadOnly keyboard navigation

    When showUnreadOnly is true, the loop explicitly handles file (conditional) and terminal (always navigable) tab types with early continues, then falls through to an AI-tab lookup that returns undefined for any browser ref — so browser tabs are quietly skipped. The TabBar already renders browser tabs while the unread filter is active (they fall through to return true in displayedUnifiedTabs), making keyboard navigation inconsistent with the visual state.

    The same issue exists in the equivalent block in navigateToPrevUnifiedTab (around line 2002–2034). Adding an explicit case for browser refs — identical to the terminal case — fixes both directions:

    if (tabRef.type === 'browser') {
        const result = navigateToUnifiedTabByIndex(session, nextIndex);
        if (result) return result;
        continue;
    }

Reviews (1): Last reviewed commit: "MAESTRO: add browser tab verification sc..." | Re-trigger Greptile

Comment on lines +387 to +394
<webview
ref={(element) => {
webviewRef.current = element as unknown as ElectronWebviewElement | null;
}}
className="w-full h-full border-0 bg-white"
partition={tab.partition}
src={tab.url || DEFAULT_BROWSER_TAB_URL}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Hardcoded bg-white flashes in dark themes

The webview element has a hardcoded bg-white class. While the webview itself renders the page content, the white background is visible while pages are loading and in dark-mode themes it creates a brief white flash on navigation. Consider using a neutral background derived from the theme or bg-transparent once the webview is ready.

Suggested change
<webview
ref={(element) => {
webviewRef.current = element as unknown as ElectronWebviewElement | null;
}}
className="w-full h-full border-0 bg-white"
partition={tab.partition}
src={tab.url || DEFAULT_BROWSER_TAB_URL}
/>
className="w-full h-full border-0"
style={{ backgroundColor: theme.colors.bgMain }}

Copy link
Copy Markdown

@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: 12

Caution

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

⚠️ Outside diff range comments (4)
src/__tests__/renderer/stores/tabStore.test.ts (1)

983-1018: ⚠️ Potential issue | 🟡 Minor

Missing browserTabs and activeBrowserTabId in terminal tab test helper.

The setupSessionWithTerminalTabs helper creates a session object inline but omits the new required browser tab fields that were added to createMockSession. This inconsistency could cause type errors or unexpected behavior.

🔧 Proposed fix
 function setupSessionWithTerminalTabs(terminalTabs: TerminalTab[]): string {
 	const sessionId = 'test-terminal-session';
 	const session = {
 		id: sessionId,
 		name: 'Test Session',
 		toolType: 'claude-code',
 		state: 'idle',
 		cwd: '/test',
 		fullPath: '/test',
 		projectRoot: '/test',
 		aiLogs: [],
 		shellLogs: [],
 		workLog: [],
 		contextUsage: 0,
 		inputMode: 'terminal' as const,
 		aiPid: 0,
 		terminalPid: 0,
 		port: 0,
 		isLive: false,
 		changedFiles: [],
 		isGitRepo: false,
 		fileTree: [],
 		fileExplorerExpanded: [],
 		fileExplorerScrollPos: 0,
 		executionQueue: [],
 		activeTimeMs: 0,
 		aiTabs: [],
 		activeTabId: '',
 		closedTabHistory: [],
 		filePreviewTabs: [],
 		activeFileTabId: null,
+		browserTabs: [],
+		activeBrowserTabId: null,
 		unifiedTabOrder: terminalTabs.map((t) => ({ type: 'terminal' as const, id: t.id })),
 		unifiedClosedTabHistory: [],
 		terminalTabs,
 		activeTerminalTabId: terminalTabs[0]?.id ?? null,
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/stores/tabStore.test.ts` around lines 983 - 1018, The
test helper setupSessionWithTerminalTabs constructs a session object but omits
the newly required browserTabs and activeBrowserTabId fields (added in
createMockSession), causing type mismatches; update setupSessionWithTerminalTabs
to include browserTabs: [] and activeBrowserTabId: null (or an appropriate
default) in the returned session object so it matches createMockSession's shape
and test expectations, referencing the setupSessionWithTerminalTabs function and
createMockSession for consistency.
src/renderer/utils/tabHelpers.ts (2)

443-456: ⚠️ Potential issue | 🟠 Major

New AI tabs don't deselect the active browser tab.

createTab() clears file/terminal selection but leaves activeBrowserTabId intact. If the user creates a chat tab while a browser tab is visible, the browser ref still wins and the new AI tab is not actually activated.

Suggested fix
 	const updatedSession: Session = {
 		...session,
 		aiTabs: [...(session.aiTabs || []), newTab],
 		activeTabId: newTab.id,
 		activeFileTabId: null,
+		activeBrowserTabId: null,
 		activeTerminalTabId: null,
 		inputMode: 'ai' as const,
 		unifiedTabOrder: [...(session.unifiedTabOrder || []), newTabRef],
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/tabHelpers.ts` around lines 443 - 456, The new AI tab
creation updates session state but forgets to clear the browser selection, so
add activeBrowserTabId: null to the updated session object (the same place where
activeFileTabId and activeTerminalTabId are cleared) — update the updatedSession
construction (referencing newTabRef and updatedSession in tabHelpers.ts /
createTab flow) to include activeBrowserTabId: null so the AI tab becomes the
effective activeTabId.

1699-1714: ⚠️ Potential issue | 🟠 Major

The AI early-return still treats a browser-selected session as already active.

activeTabId is intentionally preserved when a browser tab is selected. Without checking activeBrowserTabId === null here, unified keyboard navigation back to that AI tab can no-op and leave the browser view visible.

Suggested fix
 		if (
 			session.activeTabId === targetTabRef.id &&
 			session.activeFileTabId === null &&
+			session.activeBrowserTabId === null &&
 			session.activeTerminalTabId === null &&
 			session.inputMode === 'ai'
 		) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/tabHelpers.ts` around lines 1699 - 1714, The early-return
in the AI-mode branch incorrectly treats a browser-selected session as already
active; update the conditional that checks session.activeTabId ===
targetTabRef.id && session.activeFileTabId === null &&
session.activeTerminalTabId === null && session.inputMode === 'ai' to also
require session.activeBrowserTabId === null so the return (which yields { type:
'ai', id: targetTabRef.id, session: repairedSession }) only happens when no
browser tab is selected, preventing unified keyboard navigation from no-oping
and leaving the browser view visible.
src/renderer/hooks/tabs/useTabHandlers.ts (1)

196-204: ⚠️ Potential issue | 🟠 Major

Include activeSession.browserTabs in the unifiedTabs memo deps.

buildUnifiedTabs(activeSession) now reads browserTabs, but this memo never invalidates when only browser-tab metadata changes. That leaves the unified strip stuck with stale browser titles/favicons/loading state after navigation.

Suggested fix
 	}, [
 		activeSession?.aiTabs,
+		activeSession?.browserTabs,
 		activeSession?.filePreviewTabs,
 		activeSession?.terminalTabs,
 		activeSession?.unifiedTabOrder,
 	]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/tabs/useTabHandlers.ts` around lines 196 - 204, The memo
for unifiedTabs (the useMemo that returns buildUnifiedTabs(activeSession)) is
missing activeSession.browserTabs in its dependency array, so updates to browser
tab metadata don't invalidate the memo; add activeSession.browserTabs to the
dependency list of the unifiedTabs useMemo so buildUnifiedTabs(activeSession)
reruns when browser tab state (titles/favicons/loading) changes.
🧹 Nitpick comments (3)
src/renderer/components/MainPanel/types.ts (1)

163-166: Narrow the browser-tab update payload.

Line 166 exposes Partial<BrowserTab>, which includes fields like id, createdAt, and partition. src/renderer/hooks/tabs/useTabHandlers.ts currently spreads updates straight into the stored tab, so any future caller can accidentally rewrite identity or partition state. A dedicated update type for the mutable chrome fields would make this boundary safer.

♻️ Suggested shape
+export type BrowserTabUpdate = Pick<
+	BrowserTab,
+	'url' | 'title' | 'canGoBack' | 'canGoForward' | 'isLoading' | 'favicon' | 'webContentsId'
+>;
+
 	onNewBrowserTab?: () => void;
 	onBrowserTabSelect?: (tabId: string) => void;
 	onBrowserTabClose?: (tabId: string) => void;
-	onBrowserTabUpdate?: (sessionId: string, tabId: string, updates: Partial<BrowserTab>) => void;
+	onBrowserTabUpdate?: (sessionId: string, tabId: string, updates: BrowserTabUpdate) => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/MainPanel/types.ts` around lines 163 - 166, The
onBrowserTabUpdate signature currently accepts Partial<BrowserTab>, which allows
callers to overwrite immutable identity fields; change this to a dedicated
update type (e.g., BrowserTabUpdatePayload or MutableBrowserTabFields)
containing only the mutable fields you want to allow (title, url, favIcon,
active, loading, lastAccessed, etc.), update the type alias in types.ts to use
that new payload instead of Partial<BrowserTab>, and then update useTabHandlers
(and any callers) to accept and spread only these allowed fields when merging
into the stored tab so id/createdAt/partition/identity fields cannot be
overwritten.
src/renderer/components/TabBar/TabBar.tsx (1)

68-77: Warn on missing browser-tab handlers too.

Lines 546-547 fall back to no-op onSelect / onClose, but the dev-only guard at Lines 68-77 only checks file and terminal handlers. If a future wiring regression drops the browser callbacks, the tabs become inert with no signal. Matching the existing warning pattern here would make that failure obvious during integration.

Also applies to: 546-547

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

In `@src/renderer/components/TabBar/TabBar.tsx` around lines 68 - 77, In TabBar,
when unifiedTabs is true the dev-only checks currently warn if
onFileTabSelect/onFileTabClose or onTerminalTabSelect/onTerminalTabClose are
missing; add the same console.warn checks for the browser tab handlers (e.g.,
onBrowserTabSelect and onBrowserTabClose) inside the process.env.NODE_ENV !==
'production' && unifiedTabs guard so missing browser callbacks are logged during
development—mirror the existing warning messages and placement used for
file/terminal handlers to catch the fallback no-op behavior seen later where
missing handlers default to inert no-ops.
src/__tests__/renderer/utils/tabHelpers.test.ts (1)

150-160: Type the browser-tab fixture instead of casting it back from any.

Lines 150-160 build an untyped object, and Lines 3489, 3508, and 3528 cast it into browserTabs. That drops most of the type-safety from the new coverage, so a BrowserTab shape change can leave these tests green while the fixture drifts. Returning BrowserTab with Partial<BrowserTab> overrides would keep the test contract aligned with production code.

Also applies to: 3489-3489, 3508-3508, 3528-3528

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

In `@src/__tests__/renderer/utils/tabHelpers.test.ts` around lines 150 - 160, The
fixture createMockBrowserTab should return a typed BrowserTab rather than an
untyped object and callers should use Partial<BrowserTab> overrides: import the
BrowserTab type and change createMockBrowserTab signature to accept overrides:
Partial<BrowserTab> and return a BrowserTab, ensure the default properties match
BrowserTab fields, and remove any downstream casts from any to BrowserTab in
tests (the places that currently cast the fixture to browserTabs should just
call createMockBrowserTab and rely on its typed return).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/browser-tab.spec.ts`:
- Around line 195-200: Teardown currently calls server.close() without awaiting
its completion, which can cause EADDRINUSE when the next suite binds to the same
fixed port (7101); change the teardown to await server.close() (handle/ignore
errors with try/catch if necessary) and ensure it completes before closing
firstApp and secondApp (i.e., await server.close() before reusing the port), and
make the same change at the other teardown location that mirrors lines 285-287;
reference the server.close(), firstApp.close(), and secondApp.close() calls when
updating the teardown sequence.
- Around line 150-153: The test currently uses a machine-specific absolute path
passed to openFileTab (the hardcoded
'/Users/jeffscottward/.../ARCHITECTURE.md'); replace that with a portable
fixture by resolving ARCHITECTURE.md from the repository checkout or by
copying/staging the file into the test harness testDataDir and passing that path
to openFileTab. Update the spec to compute the path at runtime (e.g., using repo
root or testDataDir) and call openFileTab(window, resolvedPath) so the test no
longer depends on a local developer's filesystem.

In `@src/main/app-lifecycle/window-manager.ts`:
- Around line 64-66: The current isAllowedBrowserTabPartition uses startsWith
which is too permissive; replace that check with a strict, anchored format
validation (e.g. use a RegExp test) that verifies the partition string exactly
matches the expected pattern prefixed by BROWSER_TAB_PARTITION_PREFIX and
followed only by the allowed identifier characters/delimiter (no extra
characters or partial matches). Update isAllowedBrowserTabPartition to use that
anchored regex test (referencing BROWSER_TAB_PARTITION_PREFIX and
isAllowedBrowserTabPartition) so the main-process gate enforces the same full
partition format as the renderer sanitizer.
- Around line 268-271: The partition variable is being read from webPreferences
but the webview tag's partition attribute comes via params; update the
will-attach-webview handler (mainWindow.webContents.on('will-attach-webview',
...)) to read partition from params.partition (falling back to
webPreferences.partition if needed) so valid HTML-specified partitions are
validated correctly; keep the existing src handling and security checks but use
params.partition as the primary source.

In `@src/renderer/components/MainPanel/BrowserTabView.tsx`:
- Around line 156-178: The did-fail-load handler (handleDidFailLoad) should
ignore aborted navigation failures so temporary/redirect loads don't clear
isLoading or overwrite URL/title: add an early return for aborted errors by
checking the event's errorCode/errorDescription (Electron abort is errorCode ===
-3 / errorDescription 'ERR_ABORTED') before using
validatedURL/getURL/latestTabRef and calling
setAddressValue/setAddressError/updateTabState; keep the existing main-frame
check and only proceed for real failures.
- Around line 231-246: The code sets isLoading true unconditionally before
verifying whether navigation will occur, causing the tab to appear stuck when
the address equals the current webview.src; change the logic in the submit
handler so you first compute whether a navigation will happen (compare nextUrl
to webviewRef.current?.src) and only call onUpdateTab with isLoading: true when
they differ (otherwise preserve or set isLoading:false), and only assign
webview.src when webviewRef.current && webviewRef.current.src !== nextUrl;
adjust the calls to setAddressValue, setAddressError, and onUpdateTab (the
update for tab.id, url, title, isLoading) accordingly so the spinner only
appears when an actual navigation is triggered.

In `@src/renderer/components/TabBar/BrowserTabItem.tsx`:
- Around line 253-260: The BrowserTabItem currently renders tab.favicon directly
into an <img> (see tab.favicon usage and setFaviconFailed in BrowserTabItem),
which allows untrusted schemes to trigger renderer fetches; sanitize or
normalize the favicon before use by either whitelisting allowed URI schemes
(e.g., only http, https, data) and rejecting/ignoring anything else (file:,
about:, chrome:, custom schemes), or convert/validate favicons upstream to
trusted data URLs and only pass safe data URLs into this component; ensure
BrowserTabItem checks the sanitized value and falls back to a default icon
and/or calls setFaviconFailed when the scheme is disallowed or parsing fails.
- Around line 244-251: Browser tabs are missing shortcut badges and bulk-close
actions because BrowserTabItem's props (shortcutHint, onCloseOtherTabs,
onCloseTabsLeft, onCloseTabsRight, totalTabs, tabIndex) are not being forwarded
from the TabBar wiring; update the TabBar rendering logic that instantiates
BrowserTabItem (where TabBar maps/creates tab items) to pass these props through
using the existing tab data and handlers (ensure shortcutHint is derived from
the tab metadata and the three close handlers and totalTabs/tabIndex are
provided from TabBar's state/props), so BrowserTabItem can render the shortcut
badge and close-left/close-right/close-others actions.
- Line 221: In BrowserTabItem remove the native tooltip by deleting the
title={tab.url || label} prop in the JSX so browser tabs no longer get the
default hover tooltip; keep any existing custom overlay/tooltip logic (and if
accessibility needs remain, use aria-label or the custom overlay instead of
title) and ensure no other place in BrowserTabItem reintroduces title for
browser tabs.

In `@src/renderer/components/TabBar/NewTabPopover.tsx`:
- Around line 62-63: The NewTabPopover always renders a "New Terminal" option
even when onNewTerminalTab is undefined; update the component (NewTabPopover) so
the "New Terminal" menu item/option is only rendered when onNewTerminalTab is
truthy, and wire its click handler to call onNewTerminalTab (fall back to
onNewTab only if that is intended); apply the same conditional rendering change
for the other occurrences referenced (the other menu blocks where "New Terminal"
is shown, e.g., the places around the checks for
onNewBrowserTab/onNewTerminalTab) so the terminal entry is hidden when terminal
creation is unavailable.

In `@src/renderer/hooks/session/useSessionRestoration.ts`:
- Around line 386-407: restoredActiveFileTabId is taken directly from
correctedSession.activeFileTabId and can be stale, which then wrongly suppresses
a valid restoredActiveBrowserTabId in the else if (restoredActiveFileTabId)
branch; validate the file tab ID against the same set used for other types
(e.g., validFileTabIds) when assigning restoredActiveFileTabId (similar to how
restoredActiveBrowserTabId and restoredActiveTerminalTabId are computed) and if
the ID is not in validFileTabIds set, set restoredActiveFileTabId = null before
the inputMode/branch logic so a valid browser selection is not accidentally
cleared.

In `@src/renderer/utils/tabHelpers.ts`:
- Around line 1197-1201: The reopen-file-tab branches set activeFileTabId but
forget to clear activeTerminalTabId and reset inputMode, so reopening a file
while a terminal is active leaves the terminal mounted; update both branches
that set activeFileTabId (the one using existingTab.id and the other around
lines 1237-1243) to also set activeTerminalTabId: null and inputMode: 'ai' after
ensuring unifiedTabOrder/unifiedClosedTabHistory (referencing session,
existingTab.id, remainingHistory, ensureInUnifiedTabOrder) so the UI actually
switches from terminal mode back to the file tab.

---

Outside diff comments:
In `@src/__tests__/renderer/stores/tabStore.test.ts`:
- Around line 983-1018: The test helper setupSessionWithTerminalTabs constructs
a session object but omits the newly required browserTabs and activeBrowserTabId
fields (added in createMockSession), causing type mismatches; update
setupSessionWithTerminalTabs to include browserTabs: [] and activeBrowserTabId:
null (or an appropriate default) in the returned session object so it matches
createMockSession's shape and test expectations, referencing the
setupSessionWithTerminalTabs function and createMockSession for consistency.

In `@src/renderer/hooks/tabs/useTabHandlers.ts`:
- Around line 196-204: The memo for unifiedTabs (the useMemo that returns
buildUnifiedTabs(activeSession)) is missing activeSession.browserTabs in its
dependency array, so updates to browser tab metadata don't invalidate the memo;
add activeSession.browserTabs to the dependency list of the unifiedTabs useMemo
so buildUnifiedTabs(activeSession) reruns when browser tab state
(titles/favicons/loading) changes.

In `@src/renderer/utils/tabHelpers.ts`:
- Around line 443-456: The new AI tab creation updates session state but forgets
to clear the browser selection, so add activeBrowserTabId: null to the updated
session object (the same place where activeFileTabId and activeTerminalTabId are
cleared) — update the updatedSession construction (referencing newTabRef and
updatedSession in tabHelpers.ts / createTab flow) to include activeBrowserTabId:
null so the AI tab becomes the effective activeTabId.
- Around line 1699-1714: The early-return in the AI-mode branch incorrectly
treats a browser-selected session as already active; update the conditional that
checks session.activeTabId === targetTabRef.id && session.activeFileTabId ===
null && session.activeTerminalTabId === null && session.inputMode === 'ai' to
also require session.activeBrowserTabId === null so the return (which yields {
type: 'ai', id: targetTabRef.id, session: repairedSession }) only happens when
no browser tab is selected, preventing unified keyboard navigation from no-oping
and leaving the browser view visible.

---

Nitpick comments:
In `@src/__tests__/renderer/utils/tabHelpers.test.ts`:
- Around line 150-160: The fixture createMockBrowserTab should return a typed
BrowserTab rather than an untyped object and callers should use
Partial<BrowserTab> overrides: import the BrowserTab type and change
createMockBrowserTab signature to accept overrides: Partial<BrowserTab> and
return a BrowserTab, ensure the default properties match BrowserTab fields, and
remove any downstream casts from any to BrowserTab in tests (the places that
currently cast the fixture to browserTabs should just call createMockBrowserTab
and rely on its typed return).

In `@src/renderer/components/MainPanel/types.ts`:
- Around line 163-166: The onBrowserTabUpdate signature currently accepts
Partial<BrowserTab>, which allows callers to overwrite immutable identity
fields; change this to a dedicated update type (e.g., BrowserTabUpdatePayload or
MutableBrowserTabFields) containing only the mutable fields you want to allow
(title, url, favIcon, active, loading, lastAccessed, etc.), update the type
alias in types.ts to use that new payload instead of Partial<BrowserTab>, and
then update useTabHandlers (and any callers) to accept and spread only these
allowed fields when merging into the stored tab so
id/createdAt/partition/identity fields cannot be overwritten.

In `@src/renderer/components/TabBar/TabBar.tsx`:
- Around line 68-77: In TabBar, when unifiedTabs is true the dev-only checks
currently warn if onFileTabSelect/onFileTabClose or
onTerminalTabSelect/onTerminalTabClose are missing; add the same console.warn
checks for the browser tab handlers (e.g., onBrowserTabSelect and
onBrowserTabClose) inside the process.env.NODE_ENV !== 'production' &&
unifiedTabs guard so missing browser callbacks are logged during
development—mirror the existing warning messages and placement used for
file/terminal handlers to catch the fallback no-op behavior seen later where
missing handlers default to inert no-ops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0904df7d-e63c-4297-8dce-889d43dfdb82

📥 Commits

Reviewing files that changed from the base of the PR and between 578e3df and ffa6b4c.

⛔ Files ignored due to path filters (1)
  • docs/screenshots/browser-tab-unified-tab-strip.png is excluded by !**/*.png
📒 Files selected for processing (42)
  • e2e/browser-tab.spec.ts
  • src/__tests__/main/app-lifecycle/window-manager.test.ts
  • src/__tests__/renderer/components/MainPanel/BrowserTabView.test.tsx
  • src/__tests__/renderer/components/MainPanel/MainPanelContent.test.tsx
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/__tests__/renderer/hooks/useSessionRestoration.test.ts
  • src/__tests__/renderer/hooks/useTabHandlers.test.ts
  • src/__tests__/renderer/hooks/utils/useDebouncedPersistence.test.ts
  • src/__tests__/renderer/stores/tabStore.test.ts
  • src/__tests__/renderer/utils/browserTabPersistence.test.ts
  • src/__tests__/renderer/utils/tabHelpers.test.ts
  • src/main/app-lifecycle/window-manager.ts
  • src/renderer/App.tsx
  • src/renderer/components/CuePipelineEditor/PipelineCanvas.tsx
  • src/renderer/components/CuePipelineEditor/panels/CueSettingsPanel.tsx
  • src/renderer/components/CuePipelineEditor/utils/pipelineToYaml.ts
  • src/renderer/components/CuePipelineEditor/utils/yamlToPipeline.ts
  • src/renderer/components/MainPanel/BrowserTabView.tsx
  • src/renderer/components/MainPanel/MainPanel.tsx
  • src/renderer/components/MainPanel/MainPanelContent.tsx
  • src/renderer/components/MainPanel/types.ts
  • src/renderer/components/TabBar/BrowserTabItem.tsx
  • src/renderer/components/TabBar/NewTabPopover.tsx
  • src/renderer/components/TabBar/TabBar.tsx
  • src/renderer/components/TabBar/tabBarUtils.ts
  • src/renderer/components/TabBar/types.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/cue/usePipelineState.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/remote/useAppRemoteEventListeners.ts
  • src/renderer/hooks/session/useSessionCrud.ts
  • src/renderer/hooks/session/useSessionRestoration.ts
  • src/renderer/hooks/symphony/useSymphonyContribution.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/utils/useDebouncedPersistence.ts
  • src/renderer/hooks/wizard/useWizardHandlers.ts
  • src/renderer/stores/tabStore.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/browserTabPersistence.ts
  • src/renderer/utils/tabHelpers.ts
  • src/renderer/utils/terminalTabHelpers.ts
  • src/renderer/utils/worktreeSession.ts

Comment thread e2e/browser-tab.spec.ts
Comment on lines +150 to +153
await openFileTab(
window,
'/Users/jeffscottward/Github/tools/Maestro-worktrees/browser-tab/ARCHITECTURE.md'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace the machine-specific file fixture path.

Line 152 hardcodes Jeff's local worktree path, so this spec will fail on CI and on any other developer machine. Resolve ARCHITECTURE.md from the checked-out repo root or stage a fixture into testDataDir instead.

Suggested fix
+import path from 'path';
 import http from 'http';
 ...
 			await openFileTab(
 				window,
-				'/Users/jeffscottward/Github/tools/Maestro-worktrees/browser-tab/ARCHITECTURE.md'
+				path.join(process.cwd(), 'ARCHITECTURE.md')
 			);
📝 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
await openFileTab(
window,
'/Users/jeffscottward/Github/tools/Maestro-worktrees/browser-tab/ARCHITECTURE.md'
);
await openFileTab(
window,
path.join(process.cwd(), 'ARCHITECTURE.md')
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/browser-tab.spec.ts` around lines 150 - 153, The test currently uses a
machine-specific absolute path passed to openFileTab (the hardcoded
'/Users/jeffscottward/.../ARCHITECTURE.md'); replace that with a portable
fixture by resolving ARCHITECTURE.md from the repository checkout or by
copying/staging the file into the test harness testDataDir and passing that path
to openFileTab. Update the spec to compute the path at runtime (e.g., using repo
root or testDataDir) and call openFileTab(window, resolvedPath) so the test no
longer depends on a local developer's filesystem.

Comment thread e2e/browser-tab.spec.ts
Comment on lines +195 to +200
server.close();
if (firstApp) {
await firstApp.close().catch(() => {});
}
if (secondApp) {
await secondApp.close().catch(() => {});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await server shutdown before reusing port 7101.

server.close() is async. Because both suites bind the same fixed port, the second test can race the first teardown and intermittently hit EADDRINUSE.

Suggested fix
+async function closeLocalTestServer(server: http.Server): Promise<void> {
+	return new Promise((resolve, reject) => {
+		server.close((error) => {
+			if (error) {
+				reject(error);
+				return;
+			}
+			resolve();
+		});
+	});
+}
...
 		} finally {
-			server.close();
+			await closeLocalTestServer(server);
 			if (firstApp) {
 				await firstApp.close().catch(() => {});
 			}
 ...
 		} finally {
-			server.close();
+			await closeLocalTestServer(server);
 			if (app) {
 				await app.close().catch(() => {});
 			}

Also applies to: 285-287

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

In `@e2e/browser-tab.spec.ts` around lines 195 - 200, Teardown currently calls
server.close() without awaiting its completion, which can cause EADDRINUSE when
the next suite binds to the same fixed port (7101); change the teardown to await
server.close() (handle/ignore errors with try/catch if necessary) and ensure it
completes before closing firstApp and secondApp (i.e., await server.close()
before reusing the port), and make the same change at the other teardown
location that mirrors lines 285-287; reference the server.close(),
firstApp.close(), and secondApp.close() calls when updating the teardown
sequence.

Comment on lines +64 to +66
function isAllowedBrowserTabPartition(partition: string): boolean {
return partition.startsWith(BROWSER_TAB_PARTITION_PREFIX);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten the partition allowlist.

startsWith() accepts values like a bare prefix or other unexpected names under the same namespace, so the main-process gate is weaker than the renderer-side partition sanitizer. This hardening layer should enforce the full expected partition format, not just the prefix.

Suggested fix
 const BROWSER_TAB_PARTITION_PREFIX = 'persist:maestro-browser-session-';
+const BROWSER_TAB_PARTITION_PATTERN = /^persist:maestro-browser-session-[a-zA-Z0-9_-]+$/;
 ...
 function isAllowedBrowserTabPartition(partition: string): boolean {
-	return partition.startsWith(BROWSER_TAB_PARTITION_PREFIX);
+	return BROWSER_TAB_PARTITION_PATTERN.test(partition);
 }
📝 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 isAllowedBrowserTabPartition(partition: string): boolean {
return partition.startsWith(BROWSER_TAB_PARTITION_PREFIX);
}
const BROWSER_TAB_PARTITION_PREFIX = 'persist:maestro-browser-session-';
const BROWSER_TAB_PARTITION_PATTERN = /^persist:maestro-browser-session-[a-zA-Z0-9_-]+$/;
function isAllowedBrowserTabPartition(partition: string): boolean {
return BROWSER_TAB_PARTITION_PATTERN.test(partition);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/app-lifecycle/window-manager.ts` around lines 64 - 66, The current
isAllowedBrowserTabPartition uses startsWith which is too permissive; replace
that check with a strict, anchored format validation (e.g. use a RegExp test)
that verifies the partition string exactly matches the expected pattern prefixed
by BROWSER_TAB_PARTITION_PREFIX and followed only by the allowed identifier
characters/delimiter (no extra characters or partial matches). Update
isAllowedBrowserTabPartition to use that anchored regex test (referencing
BROWSER_TAB_PARTITION_PREFIX and isAllowedBrowserTabPartition) so the
main-process gate enforces the same full partition format as the renderer
sanitizer.

Comment on lines +268 to +271
mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
const src = typeof params.src === 'string' ? params.src : '';
const partition =
typeof webPreferences.partition === 'string' ? webPreferences.partition : '';
Copy link
Copy Markdown

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:

Electron will-attach-webview event params partition attribute

💡 Result:

The 'will-attach-webview' event is emitted on a WebContents (e.g., win.webContents) when a tag's contents are being attached. It allows configuring the guest WebContents before loading and preventing attachment via event.preventDefault. Event parameters: - event Event - webPreferences WebPreferences - The web preferences for the guest page (modifiable). - params Record<string, any> - Other parameters, including 'partition', 'src', etc. (modifiable). The 'partition' attribute from the tag is included in the 'params' object, which can be modified in the event handler (e.g., event.params.partition = 'new-value';). This provides finer control beyond HTML attributes, such as setting session partitions dynamically. Note: Requires webviewTag: true in BrowserWindow webPreferences. WebContents is created only after this event if not prevented (since recent versions). Example usage: win.webContents.on('will-attach-webview', (event, webPreferences, params) => { // Modify partition params.partition = 'persist:my-session'; // Or enforce/modify other settings webPreferences.nodeIntegration = false; // Prevent if invalid if (!params.src.startsWith('https://allowed.com')) { event.preventDefault; } });

Citations:


🏁 Script executed:

# Get the full context of the code at lines 268-271
sed -n '260,290p' src/main/app-lifecycle/window-manager.ts

Repository: RunMaestro/Maestro

Length of output: 1310


🏁 Script executed:

# Search for how partition is used after being read
sed -n '268,310p' src/main/app-lifecycle/window-manager.ts

Repository: RunMaestro/Maestro

Length of output: 1762


Read partition from params, not just webPreferences.

Electron's will-attach-webview event passes the <webview> tag's partition attribute via the params object, not webPreferences. The current code reads from webPreferences.partition, which will always be empty when a partition is specified in HTML, causing the security validation to fail for valid webviews.

Suggested fix
 			mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
 				const src = typeof params.src === 'string' ? params.src : '';
-				const partition =
-					typeof webPreferences.partition === 'string' ? webPreferences.partition : '';
+				const partition =
+					typeof params.partition === 'string'
+						? params.partition
+						: typeof webPreferences.partition === 'string'
+							? webPreferences.partition
+							: '';
📝 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
mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
const src = typeof params.src === 'string' ? params.src : '';
const partition =
typeof webPreferences.partition === 'string' ? webPreferences.partition : '';
mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
const src = typeof params.src === 'string' ? params.src : '';
const partition =
typeof params.partition === 'string'
? params.partition
: typeof webPreferences.partition === 'string'
? webPreferences.partition
: '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/app-lifecycle/window-manager.ts` around lines 268 - 271, The
partition variable is being read from webPreferences but the webview tag's
partition attribute comes via params; update the will-attach-webview handler
(mainWindow.webContents.on('will-attach-webview', ...)) to read partition from
params.partition (falling back to webPreferences.partition if needed) so valid
HTML-specified partitions are validated correctly; keep the existing src
handling and security checks but use params.partition as the primary source.

Comment on lines +156 to +178
const handleDidFailLoad = (event: Event) => {
if ((event as Event & { isMainFrame?: boolean }).isMainFrame === false) return;
const nextUrl =
(event as Event & { validatedURL?: string; url?: string }).validatedURL ||
(event as Event & { validatedURL?: string; url?: string }).url ||
webview.getURL?.() ||
latestTabRef.current.url ||
DEFAULT_BROWSER_TAB_URL;
if (!isAddressFocusedRef.current) {
setAddressValue(nextUrl);
}
setAddressError(null);
updateTabState({
url: nextUrl,
title: getBrowserTabTitle(nextUrl),
canGoBack: isDomReadyRef.current ? webview.canGoBack() : latestTabRef.current.canGoBack,
canGoForward: isDomReadyRef.current
? webview.canGoForward()
: latestTabRef.current.canGoForward,
isLoading: false,
webContentsId: webview.getWebContentsId?.(),
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ignore aborted did-fail-load events.

This handler treats every main-frame load failure as terminal. On redirects/reloads Electron also emits aborted failures, so Line 156 can clear isLoading and overwrite the tab URL/title with an intermediate navigation that never actually committed.

Suggested fix
 		const handleDidFailLoad = (event: Event) => {
 			if ((event as Event & { isMainFrame?: boolean }).isMainFrame === false) return;
+			if ((event as Event & { errorCode?: number }).errorCode === -3) return;
 			const nextUrl =
 				(event as Event & { validatedURL?: string; url?: string }).validatedURL ||
 				(event as Event & { validatedURL?: string; url?: string }).url ||
📝 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 handleDidFailLoad = (event: Event) => {
if ((event as Event & { isMainFrame?: boolean }).isMainFrame === false) return;
const nextUrl =
(event as Event & { validatedURL?: string; url?: string }).validatedURL ||
(event as Event & { validatedURL?: string; url?: string }).url ||
webview.getURL?.() ||
latestTabRef.current.url ||
DEFAULT_BROWSER_TAB_URL;
if (!isAddressFocusedRef.current) {
setAddressValue(nextUrl);
}
setAddressError(null);
updateTabState({
url: nextUrl,
title: getBrowserTabTitle(nextUrl),
canGoBack: isDomReadyRef.current ? webview.canGoBack() : latestTabRef.current.canGoBack,
canGoForward: isDomReadyRef.current
? webview.canGoForward()
: latestTabRef.current.canGoForward,
isLoading: false,
webContentsId: webview.getWebContentsId?.(),
});
};
const handleDidFailLoad = (event: Event) => {
if ((event as Event & { isMainFrame?: boolean }).isMainFrame === false) return;
if ((event as Event & { errorCode?: number }).errorCode === -3) return;
const nextUrl =
(event as Event & { validatedURL?: string; url?: string }).validatedURL ||
(event as Event & { validatedURL?: string; url?: string }).url ||
webview.getURL?.() ||
latestTabRef.current.url ||
DEFAULT_BROWSER_TAB_URL;
if (!isAddressFocusedRef.current) {
setAddressValue(nextUrl);
}
setAddressError(null);
updateTabState({
url: nextUrl,
title: getBrowserTabTitle(nextUrl),
canGoBack: isDomReadyRef.current ? webview.canGoBack() : latestTabRef.current.canGoBack,
canGoForward: isDomReadyRef.current
? webview.canGoForward()
: latestTabRef.current.canGoForward,
isLoading: false,
webContentsId: webview.getWebContentsId?.(),
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/MainPanel/BrowserTabView.tsx` around lines 156 - 178,
The did-fail-load handler (handleDidFailLoad) should ignore aborted navigation
failures so temporary/redirect loads don't clear isLoading or overwrite
URL/title: add an early return for aborted errors by checking the event's
errorCode/errorDescription (Electron abort is errorCode === -3 /
errorDescription 'ERR_ABORTED') before using validatedURL/getURL/latestTabRef
and calling setAddressValue/setAddressError/updateTabState; keep the existing
main-frame check and only proceed for real failures.

Comment on lines +244 to +251
{shortcutHint !== null && shortcutHint !== undefined && (
<span
className="w-4 h-4 flex items-center justify-center rounded text-[10px] font-medium shrink-0 opacity-50"
style={{ backgroundColor: theme.colors.border, color: theme.colors.textMain }}
>
{shortcutHint}
</span>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Browser tabs still miss some unified-tab affordances.

These shortcut and bulk-close paths only render when shortcutHint, onCloseOtherTabs, onCloseTabsLeft, onCloseTabsRight, totalTabs, and tabIndex are populated, but the current src/renderer/components/TabBar/TabBar.tsx:535-560 wiring shown in context does not pass them through. In practice that leaves browser tabs without the same close-other/left/right actions and shortcut badges the other unified tab types get.

Also applies to: 380-425

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

In `@src/renderer/components/TabBar/BrowserTabItem.tsx` around lines 244 - 251,
Browser tabs are missing shortcut badges and bulk-close actions because
BrowserTabItem's props (shortcutHint, onCloseOtherTabs, onCloseTabsLeft,
onCloseTabsRight, totalTabs, tabIndex) are not being forwarded from the TabBar
wiring; update the TabBar rendering logic that instantiates BrowserTabItem
(where TabBar maps/creates tab items) to pass these props through using the
existing tab data and handlers (ensure shortcutHint is derived from the tab
metadata and the three close handlers and totalTabs/tabIndex are provided from
TabBar's state/props), so BrowserTabItem can render the shortcut badge and
close-left/close-right/close-others actions.

Comment on lines +253 to +260
{tab.favicon && !faviconFailed ? (
<img
src={tab.favicon}
alt=""
aria-hidden="true"
className="w-3.5 h-3.5 shrink-0 rounded-sm object-contain"
onError={() => setFaviconFailed(true)}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat tab.favicon as untrusted input before rendering it.

This value comes from arbitrary page metadata, and feeding it straight into an app-level <img src> lets sites trigger renderer-side fetches for file:, custom schemes, or internal URLs whenever the tab strip renders. Please whitelist safe schemes here at minimum, or better, normalize favicons to trusted data URLs before they reach this component.

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

In `@src/renderer/components/TabBar/BrowserTabItem.tsx` around lines 253 - 260,
The BrowserTabItem currently renders tab.favicon directly into an <img> (see
tab.favicon usage and setFaviconFailed in BrowserTabItem), which allows
untrusted schemes to trigger renderer fetches; sanitize or normalize the favicon
before use by either whitelisting allowed URI schemes (e.g., only http, https,
data) and rejecting/ignoring anything else (file:, about:, chrome:, custom
schemes), or convert/validate favicons upstream to trusted data URLs and only
pass safe data URLs into this component; ensure BrowserTabItem checks the
sanitized value and falls back to a default icon and/or calls setFaviconFailed
when the scheme is disallowed or parsing fails.

Comment on lines +62 to 63
if (!onNewTerminalTab && !onNewBrowserTab) {
onNewTab();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hide terminal option when terminal creation is unavailable.

Line 127–147 always renders New Terminal, but with browser-only mode (onNewBrowserTab present, onNewTerminalTab absent) that action does nothing.

💡 Suggested fix
-					title={onNewTerminalTab ? 'New tab…' : `New tab (${formatShortcutKeys(newTabKeys)})`}
+					title={
+						onNewTerminalTab || onNewBrowserTab
+							? 'New tab…'
+							: `New tab (${formatShortcutKeys(newTabKeys)})`
+					}
@@
-						<button
-							className="flex items-center gap-2 w-full px-3 py-2 text-sm text-left hover:bg-white/10 transition-colors"
-							style={{ color: theme.colors.textMain }}
-							onClick={() => closeAndDo(() => onNewTerminalTab?.())}
-						>
-							<Terminal className="w-3.5 h-3.5" style={{ color: theme.colors.textDim }} />
-							New Terminal
-							<span className="ml-auto text-xs" style={{ color: theme.colors.textDim }}>
-								{formatShortcutKeys(terminalKeys)}
-							</span>
-						</button>
+						{onNewTerminalTab && (
+							<button
+								className="flex items-center gap-2 w-full px-3 py-2 text-sm text-left hover:bg-white/10 transition-colors"
+								style={{ color: theme.colors.textMain }}
+								onClick={() => closeAndDo(onNewTerminalTab)}
+							>
+								<Terminal className="w-3.5 h-3.5" style={{ color: theme.colors.textDim }} />
+								New Terminal
+								<span className="ml-auto text-xs" style={{ color: theme.colors.textDim }}>
+									{formatShortcutKeys(terminalKeys)}
+								</span>
+							</button>
+						)}

Also applies to: 89-90, 127-147

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

In `@src/renderer/components/TabBar/NewTabPopover.tsx` around lines 62 - 63, The
NewTabPopover always renders a "New Terminal" option even when onNewTerminalTab
is undefined; update the component (NewTabPopover) so the "New Terminal" menu
item/option is only rendered when onNewTerminalTab is truthy, and wire its click
handler to call onNewTerminalTab (fall back to onNewTab only if that is
intended); apply the same conditional rendering change for the other occurrences
referenced (the other menu blocks where "New Terminal" is shown, e.g., the
places around the checks for onNewBrowserTab/onNewTerminalTab) so the terminal
entry is hidden when terminal creation is unavailable.

Comment on lines +386 to +407
let restoredActiveFileTabId = correctedSession.activeFileTabId ?? null;
let restoredActiveBrowserTabId =
correctedSession.activeBrowserTabId &&
validBrowserTabIds.has(correctedSession.activeBrowserTabId)
? correctedSession.activeBrowserTabId
: null;
const restoredActiveTerminalTabId =
correctedSession.activeTerminalTabId &&
validTerminalTabIds.has(correctedSession.activeTerminalTabId)
? correctedSession.activeTerminalTabId
: null;
let restoredInputMode = correctedSession.inputMode;

if (restoredInputMode === 'terminal') {
restoredActiveFileTabId = null;
restoredActiveBrowserTabId = null;
if (!restoredActiveTerminalTabId) {
restoredInputMode = 'ai';
}
} else if (restoredActiveFileTabId) {
restoredActiveBrowserTabId = null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate activeFileTabId before it suppresses browser focus.

restoredActiveFileTabId is copied straight from persisted state, unlike the AI/browser/terminal refs. If that file tab was deleted or the ID is stale, restore keeps an orphaned activeFileTabId and clears a valid activeBrowserTabId in the else if (restoredActiveFileTabId) branch, leaving the session with no valid non-AI selection.

Suggested fix
 			const resetBrowserTabs = (correctedSession.browserTabs || []).map((tab) =>
 				rehydrateBrowserTab(tab, correctedSession.id)
 			);
 			const validAiTabIds = new Set(resetAiTabs.map((tab) => tab.id));
+			const validFileTabIds = new Set(
+				(correctedSession.filePreviewTabs || []).map((tab) => tab.id)
+			);
 			const validBrowserTabIds = new Set(resetBrowserTabs.map((tab) => tab.id));
 			const validTerminalTabIds = new Set(resetTerminalTabs.map((tab) => tab.id));
 
 			const restoredActiveTabId = validAiTabIds.has(correctedSession.activeTabId)
 				? correctedSession.activeTabId
 				: resetAiTabs[0]?.id || correctedSession.activeTabId;
-			let restoredActiveFileTabId = correctedSession.activeFileTabId ?? null;
+			let restoredActiveFileTabId =
+				correctedSession.activeFileTabId &&
+				validFileTabIds.has(correctedSession.activeFileTabId)
+					? correctedSession.activeFileTabId
+					: null;
 			let restoredActiveBrowserTabId =
 				correctedSession.activeBrowserTabId &&
 				validBrowserTabIds.has(correctedSession.activeBrowserTabId)
 					? correctedSession.activeBrowserTabId
 					: 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
let restoredActiveFileTabId = correctedSession.activeFileTabId ?? null;
let restoredActiveBrowserTabId =
correctedSession.activeBrowserTabId &&
validBrowserTabIds.has(correctedSession.activeBrowserTabId)
? correctedSession.activeBrowserTabId
: null;
const restoredActiveTerminalTabId =
correctedSession.activeTerminalTabId &&
validTerminalTabIds.has(correctedSession.activeTerminalTabId)
? correctedSession.activeTerminalTabId
: null;
let restoredInputMode = correctedSession.inputMode;
if (restoredInputMode === 'terminal') {
restoredActiveFileTabId = null;
restoredActiveBrowserTabId = null;
if (!restoredActiveTerminalTabId) {
restoredInputMode = 'ai';
}
} else if (restoredActiveFileTabId) {
restoredActiveBrowserTabId = null;
}
const resetBrowserTabs = (correctedSession.browserTabs || []).map((tab) =>
rehydrateBrowserTab(tab, correctedSession.id)
);
const validAiTabIds = new Set(resetAiTabs.map((tab) => tab.id));
const validFileTabIds = new Set(
(correctedSession.filePreviewTabs || []).map((tab) => tab.id)
);
const validBrowserTabIds = new Set(resetBrowserTabs.map((tab) => tab.id));
const validTerminalTabIds = new Set(resetTerminalTabs.map((tab) => tab.id));
const restoredActiveTabId = validAiTabIds.has(correctedSession.activeTabId)
? correctedSession.activeTabId
: resetAiTabs[0]?.id || correctedSession.activeTabId;
let restoredActiveFileTabId =
correctedSession.activeFileTabId &&
validFileTabIds.has(correctedSession.activeFileTabId)
? correctedSession.activeFileTabId
: null;
let restoredActiveBrowserTabId =
correctedSession.activeBrowserTabId &&
validBrowserTabIds.has(correctedSession.activeBrowserTabId)
? correctedSession.activeBrowserTabId
: null;
const restoredActiveTerminalTabId =
correctedSession.activeTerminalTabId &&
validTerminalTabIds.has(correctedSession.activeTerminalTabId)
? correctedSession.activeTerminalTabId
: null;
let restoredInputMode = correctedSession.inputMode;
if (restoredInputMode === 'terminal') {
restoredActiveFileTabId = null;
restoredActiveBrowserTabId = null;
if (!restoredActiveTerminalTabId) {
restoredInputMode = 'ai';
}
} else if (restoredActiveFileTabId) {
restoredActiveBrowserTabId = null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/session/useSessionRestoration.ts` around lines 386 - 407,
restoredActiveFileTabId is taken directly from correctedSession.activeFileTabId
and can be stale, which then wrongly suppresses a valid
restoredActiveBrowserTabId in the else if (restoredActiveFileTabId) branch;
validate the file tab ID against the same set used for other types (e.g.,
validFileTabIds) when assigning restoredActiveFileTabId (similar to how
restoredActiveBrowserTabId and restoredActiveTerminalTabId are computed) and if
the ID is not in validFileTabIds set, set restoredActiveFileTabId = null before
the inputMode/branch logic so a valid browser selection is not accidentally
cleared.

Comment on lines 1197 to 1201
...session,
activeFileTabId: existingTab.id,
activeBrowserTabId: null,
unifiedTabOrder: ensureInUnifiedTabOrder(session.unifiedTabOrder, 'file', existingTab.id),
unifiedClosedTabHistory: remainingHistory,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reopened file tabs won't surface from terminal mode.

Both file-tab reopen branches set activeFileTabId, but neither clears activeTerminalTabId nor forces inputMode back to 'ai'. Reopening a file tab while the terminal is active will leave the terminal mounted instead of the restored file.

Suggested fix
 				session: {
 					...session,
 					activeFileTabId: existingTab.id,
 					activeBrowserTabId: null,
+					activeTerminalTabId: null,
+					inputMode: 'ai',
 					unifiedTabOrder: ensureInUnifiedTabOrder(session.unifiedTabOrder, 'file', existingTab.id),
 					unifiedClosedTabHistory: remainingHistory,
 				},
...
 			session: {
 				...session,
 				filePreviewTabs: updatedFilePreviewTabs,
 				activeFileTabId: restoredTab.id,
 				activeBrowserTabId: null,
+				activeTerminalTabId: null,
+				inputMode: 'ai',
 				unifiedTabOrder: updatedUnifiedTabOrder,
 				unifiedClosedTabHistory: remainingHistory,
 			},

Also applies to: 1237-1243

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

In `@src/renderer/utils/tabHelpers.ts` around lines 1197 - 1201, The
reopen-file-tab branches set activeFileTabId but forget to clear
activeTerminalTabId and reset inputMode, so reopening a file while a terminal is
active leaves the terminal mounted; update both branches that set
activeFileTabId (the one using existingTab.id and the other around lines
1237-1243) to also set activeTerminalTabId: null and inputMode: 'ai' after
ensuring unifiedTabOrder/unifiedClosedTabHistory (referencing session,
existingTab.id, remainingHistory, ensureInUnifiedTabOrder) so the UI actually
switches from terminal mode back to the file tab.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
scripts/dev-port.mjs (1)

16-40: Consider adding a connection timeout to prevent hangs.

The isPortInUse function relies on implicit OS-level TCP timeouts. On some systems or network configurations, a connection attempt to a port that isn't explicitly refused could hang for an extended period before the OS times out.

💡 Proposed fix to add explicit timeout
 function isPortInUse(port) {
 	return new Promise((resolve) => {
 		let resolved = false;
 		let pending = LOCALHOST_HOSTS.length;
+		const SOCKET_TIMEOUT_MS = 1000;

 		for (const host of LOCALHOST_HOSTS) {
 			const socket = net.createConnection({ host, port });
+			socket.setTimeout(SOCKET_TIMEOUT_MS);

 			socket.once('connect', () => {
 				if (resolved) return;
 				resolved = true;
 				socket.end();
 				resolve(true);
 			});

+			socket.once('timeout', () => {
+				socket.destroy();
+				pending -= 1;
+				if (!resolved && pending === 0) {
+					resolve(false);
+				}
+			});
+
 			socket.once('error', () => {
 				socket.destroy();
 				pending -= 1;
 				if (!resolved && pending === 0) {
 					resolve(false);
 				}
 			});
 		}
 	});
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-port.mjs` around lines 16 - 40, The isPortInUse function can hang
waiting on OS TCP timeouts; modify it to set an explicit per-socket connection
timeout by using socket.setTimeout(...) after creating the socket (and handle
the 'timeout' event similar to 'error' by destroying the socket, decrementing
pending, and resolving false when all hosts done). Ensure you clear the
timeout/handlers when 'connect' succeeds (socket.end()) and avoid double-resolve
by keeping the resolved flag; reference LOCALHOST_HOSTS, net.createConnection,
socket, and the pending/resolved logic when implementing the timeout (choose a
sensible timeout like a few hundred milliseconds).
scripts/start-dev.ps1 (1)

17-19: Hardcoded 5-second sleep is less robust than dev.mjs's port polling.

Unlike dev.mjs which actively polls the port for up to 20 seconds before launching Electron, this script uses a fixed 5-second sleep. On slower machines or under heavy load, Vite might not be ready in time, causing Electron to fail to connect.

Consider adding a PowerShell equivalent of port polling for parity with the Unix flow. If this is acceptable as-is for Windows development, the current approach works for typical scenarios.

💡 Optional: Add port polling to match dev.mjs behavior
 Write-Host "Waiting for renderer dev server on port $vitePort..." -ForegroundColor Yellow
-Start-Sleep -Seconds 5
+$timeout = 20
+$elapsed = 0
+while ($elapsed -lt $timeout) {
+    try {
+        $tcpClient = New-Object System.Net.Sockets.TcpClient
+        $tcpClient.Connect('127.0.0.1', $vitePort)
+        $tcpClient.Close()
+        break
+    } catch {
+        Start-Sleep -Seconds 1
+        $elapsed++
+    }
+}
+if ($elapsed -ge $timeout) {
+    Write-Host "Timed out waiting for Vite dev server on port $vitePort" -ForegroundColor Red
+    exit 1
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/start-dev.ps1` around lines 17 - 19, Replace the fixed Start-Sleep 5
with a PowerShell port-polling loop that checks $vitePort readiness (e.g., using
Test-NetConnection or System.Net.Sockets.TcpClient) and retries at short
intervals up to ~20 seconds before continuing; keep the existing Write-Host
message, poll until the port is reachable or timeout, and only proceed to launch
Electron when the check for $vitePort succeeds to mirror the dev.mjs behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/dev-port.mjs`:
- Around line 16-40: The isPortInUse function can hang waiting on OS TCP
timeouts; modify it to set an explicit per-socket connection timeout by using
socket.setTimeout(...) after creating the socket (and handle the 'timeout' event
similar to 'error' by destroying the socket, decrementing pending, and resolving
false when all hosts done). Ensure you clear the timeout/handlers when 'connect'
succeeds (socket.end()) and avoid double-resolve by keeping the resolved flag;
reference LOCALHOST_HOSTS, net.createConnection, socket, and the
pending/resolved logic when implementing the timeout (choose a sensible timeout
like a few hundred milliseconds).

In `@scripts/start-dev.ps1`:
- Around line 17-19: Replace the fixed Start-Sleep 5 with a PowerShell
port-polling loop that checks $vitePort readiness (e.g., using
Test-NetConnection or System.Net.Sockets.TcpClient) and retries at short
intervals up to ~20 seconds before continuing; keep the existing Write-Host
message, poll until the port is reachable or timeout, and only proceed to launch
Electron when the check for $vitePort succeeds to mirror the dev.mjs behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e15725a7-f2f7-4d89-a249-b88f3a47926d

📥 Commits

Reviewing files that changed from the base of the PR and between ffa6b4c and e206398.

📒 Files selected for processing (6)
  • package.json
  • scripts/dev-port.mjs
  • scripts/dev.mjs
  • scripts/start-dev.ps1
  • src/main/deep-links.ts
  • vite.config.mts

@jeffscottward
Copy link
Copy Markdown
Contributor Author

Screenshot 2026-04-10 at 6 40 27 PM

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