fix(tabs): allow an agent to have zero AI tabs - #1349
Conversation
Closing the last AI tab always spawned a fresh replacement, so users were stuck with a dead chat tab they could not get rid of even when they had terminal/file/browser tabs open. Keep the replacement only when the agent would otherwise be left with no tabs at all. A brand new agent still always has a chat to type into (the original reason the tab was sticky), but once other tabs exist the last AI tab closes cleanly and the agent sits at zero AI tabs until the user picks something from the "+" menu. Supporting fixes for the newly reachable zero-AI-tab state: - MainPanel gated the whole tab strip on aiTabs.length > 0, which would have hidden the "+" button and stranded the user. Gate on any tab kind. - closeTab left activeTabId pointing at the removed tab; a later switch back to AI mode would render an input bound to nothing. Clear it. - The unread-filter and out-of-sync fallbacks indexed updatedTabs[i] and would throw on an empty array. Guard both. - Session restoration treated empty aiTabs as data corruption and rebuilt a default tab while wiping filePreviewTabs/browserTabs/unifiedTabOrder. Restrict that recovery to sessions with no tabs whatsoever. - Remote broadcast bailed out on empty aiTabs, leaving remote clients showing tabs the user had already closed. - Drop the stale "(creates new tab)" subtext from the Close All command.
📝 WalkthroughWalkthroughThe tab lifecycle now supports sessions with zero AI tabs. Closure, restoration, remote broadcasting, tab-bar rendering, command text, and regression tests were updated for sessions that retain non-AI tabs or contain no tabs. ChangesZero AI tab lifecycle
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR permits agents to retain terminal, file, or browser tabs after their final AI tab is closed, and updates rendering, restoration, remote synchronization, and command text for that state.
Confidence Score: 4/5This needs a restoration fix before merging because restarting an agent whose only remaining tab is a non-persistent terminal can leave the workspace with no usable tabs. The recovery decision is made before terminal restoration removes tabs without startup commands, so the newly reachable zero-AI state can bypass recovery and then lose its sole remaining tab. Files Needing Attention: src/renderer/hooks/session/useSessionRestoration.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Persist as Persisted Session
participant Restore as Session Restoration
participant UI as Main Panel
User->>Persist: Close final AI tab beside ordinary terminal
User->>Restore: Restart Maestro
Restore->>Restore: Count persisted terminal as surviving tab
Restore->>Restore: Skip default AI-tab recovery
Restore->>Restore: Remove terminal without startup command
Restore->>UI: Return session with no tabs
UI-->>User: Blank AI workspace
Reviews (1): Last reviewed commit: "fix(tabs): allow an agent to have zero A..." | Re-trigger Greptile |
| const restoredTabCount = | ||
| (session.aiTabs?.length ?? 0) + | ||
| (session.filePreviewTabs?.length ?? 0) + | ||
| (session.terminalTabs?.length ?? 0) + | ||
| (session.browserTabs?.length ?? 0); |
There was a problem hiding this comment.
Recovery Counts Disposable Terminal Tabs
When the last AI tab is closed beside an ordinary terminal and Maestro restarts, this check counts that terminal and skips recovery before the terminal is later discarded for lacking a startup command, causing the session to open with no usable tabs and a blank AI workspace.
There was a problem hiding this comment.
Pull request overview
This PR updates Maestro tab-management so an agent can legitimately have zero AI tabs, while still ensuring a brand new agent never ends up with zero total tabs and no clear next action.
Changes:
- Adjusts
closeTab()so closing the last AI tab only auto-creates a replacement when no other tab kinds (terminal, file, browser) remain. - Makes the Main Panel tab strip render whenever any tab kind exists, so the
+menu remains accessible when AI tabs reach zero. - Updates session restoration and remote broadcasting logic to treat an empty
aiTabsarray as valid and persistable state, not corruption.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/utils/tabHelpers.ts | Updates close behavior to allow zero AI tabs and adds safety guards for empty-tab edge cases. |
| src/renderer/components/MainPanel/MainPanel.tsx | Shows the tab strip when any tab kind exists, not only when AI tabs exist. |
| src/renderer/hooks/session/useSessionRestoration.ts | Treats zero AI tabs as valid if other tab kinds exist, while still recovering truly tabless sessions. |
| src/renderer/hooks/remote/useRemoteIntegration.ts | Broadcasts tab changes even when aiTabs is empty so remote clients stay in sync. |
| src/renderer/components/QuickActionsModal/commands/tabCommands.ts | Updates Close All Tabs subtext to remove now-inaccurate wording. |
| src/tests/renderer/utils/tabHelpers.test.ts | Adds test coverage for newly reachable zero-AI-tab scenarios and crash regressions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // removed. A dangling id makes a later switch back to AI mode render an input | ||
| // area bound to a tab that no longer exists. Non-AI fallbacks otherwise keep | ||
| // activeTabId so returning to AI mode lands on the same tab as before. | ||
| const survivingActiveTabId = updatedTabs.length === 0 ? '' : session.activeTabId; |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/hooks/session/useSessionRestoration.ts`:
- Around line 206-215: Update the restoredTabCount calculation in the session
restoration flow to count only terminal tabs that will survive restoration,
excluding terminal tabs without a startupCommand. Alternatively, rerun the
empty-session recovery check after transient terminal tabs are discarded so
sessions left with no tabs still receive the default AI tab.
- Around line 259-263: Update the session restoration fallback near the aiTabs
normalization so that when resetAiTabs contains no AI tab,
correctedSession.activeTabId is set to an empty string instead of preserving the
stale ID. Keep the existing selection behavior when a reset AI tab is available.
In `@src/renderer/utils/tabHelpers.ts`:
- Around line 815-819: Update the active-tab cleanup logic around
survivingActiveTabId so it selects an existing surviving AI tab whenever
updatedTabs is non-empty, including when fallbackRef activates a non-AI tab;
clear activeTabId only when no AI tabs remain. Ensure the removed AI tab ID is
never retained in the session.
🪄 Autofix
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 Plus
Run ID: ab4b8fbd-a63b-45e2-848d-965241f8a6a4
📒 Files selected for processing (6)
src/__tests__/renderer/utils/tabHelpers.test.tssrc/renderer/components/MainPanel/MainPanel.tsxsrc/renderer/components/QuickActionsModal/commands/tabCommands.tssrc/renderer/hooks/remote/useRemoteIntegration.tssrc/renderer/hooks/session/useSessionRestoration.tssrc/renderer/utils/tabHelpers.ts
| // An agent may legitimately have zero AI tabs as long as some other tab kind | ||
| // is still open (the user closed the last chat but kept a terminal around). | ||
| // Only a session with no tabs whatsoever is treated as data corruption - | ||
| // recovering the zero-AI-tab case would wipe the tabs the user still has. | ||
| const restoredTabCount = | ||
| (session.aiTabs?.length ?? 0) + | ||
| (session.filePreviewTabs?.length ?? 0) + | ||
| (session.terminalTabs?.length ?? 0) + | ||
| (session.browserTabs?.length ?? 0); | ||
| if (restoredTabCount === 0) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Count only terminal tabs that survive restoration.
A session with zero AI tabs and one terminal tab without startupCommand passes this check. Lines 415-425 then discard that terminal tab. The restored session has no tabs and does not receive the default AI tab.
Count only terminal tabs with a startup command here, or repeat the empty-session recovery check after transient terminal tabs are removed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/hooks/session/useSessionRestoration.ts` around lines 206 - 215,
Update the restoredTabCount calculation in the session restoration flow to count
only terminal tabs that will survive restoration, excluding terminal tabs
without a startupCommand. Alternatively, rerun the empty-session recovery check
after transient terminal tabs are discarded so sessions left with no tabs still
receive the default AI tab.
| // Normalize a missing aiTabs array so the rest of the app can keep calling | ||
| // .find()/.map() on it. Zero AI tabs is a valid state; undefined is not. | ||
| if (!session.aiTabs) { | ||
| session = { ...session, aiTabs: [] }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear stale AI selection during zero-AI restoration.
This normalization accepts an empty aiTabs array, but the later fallback keeps correctedSession.activeTabId when resetAiTabs is empty. A persisted session with non-AI tabs can therefore restore an ID for an AI tab that does not exist.
Use an empty string when no reset AI tab exists.
Proposed fix
const restoredActiveTabId = validAiTabIds.has(correctedSession.activeTabId)
? correctedSession.activeTabId
- : resetAiTabs[0]?.id || correctedSession.activeTabId;
+ : resetAiTabs[0]?.id || '';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/hooks/session/useSessionRestoration.ts` around lines 259 - 263,
Update the session restoration fallback near the aiTabs normalization so that
when resetAiTabs contains no AI tab, correctedSession.activeTabId is set to an
empty string instead of preserving the stale ID. Keep the existing selection
behavior when a reset AI tab is available.
| // With no AI tabs left, activeTabId must stop pointing at the tab we just | ||
| // removed. A dangling id makes a later switch back to AI mode render an input | ||
| // area bound to a tab that no longer exists. Non-AI fallbacks otherwise keep | ||
| // activeTabId so returning to AI mode lands on the same tab as before. | ||
| const survivingActiveTabId = updatedTabs.length === 0 ? '' : session.activeTabId; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep activeTabId valid when a non-AI tab becomes active.
If an active AI tab closes beside a terminal, file, or browser tab while another AI tab remains, fallbackRef selects the non-AI tab but survivingActiveTabId keeps the removed ID. The session then contains an invalid AI selection.
Set activeTabId to a surviving AI tab when updatedTabs is non-empty. Clear it only when no AI tab survives.
Proposed fix
-const survivingActiveTabId = updatedTabs.length === 0 ? '' : session.activeTabId;
+const survivingActiveTabId =
+ updatedTabs.length === 0
+ ? ''
+ : updatedTabs[Math.max(0, tabIndex - 1)]?.id ?? '';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/utils/tabHelpers.ts` around lines 815 - 819, Update the
active-tab cleanup logic around survivingActiveTabId so it selects an existing
surviving AI tab whenever updatedTabs is non-empty, including when fallbackRef
activates a non-AI tab; clear activeTabId only when no AI tabs remain. Ensure
the removed AI tab ID is never retained in the session.
Closes #1062
Problem
Closing the last AI tab always spawned a fresh replacement, so an agent could never sit at zero AI tabs. As @jeffscottward put it in the issue, that leaves you with a dead tab you cannot get rid of:
Approach
This keeps the original intent behind the sticky tab (@pedramamini in the issue: "i left this on purpose so that folks don't have to find how to create a new tab and can easily always at least chat") while unblocking the complaint.
The replacement tab is now created only when the agent would otherwise be left with no tabs at all:
+menu stay on screen, so you explicitly choose what to open next.Changes
Core behavior in
closeTab()(src/renderer/utils/tabHelpers.ts), plus the supporting fixes needed to make the newly reachable zero-AI-tab state actually safe:utils/tabHelpers.tsactiveTabIdwhen no AI tab remains - a dangling id made a later switch back to AI mode render an input area bound to a tab that no longer exists. Guard the unread-filter and out-of-sync fallbacks, which indexedupdatedTabs[i]and would throw on an empty array.components/MainPanel/MainPanel.tsxaiTabs.length > 0. At zero AI tabs that hid the entire strip including the+button, stranding the user in whatever view was last active. Now gated on any tab kind.hooks/session/useSessionRestoration.tsaiTabswas treated as data corruption: on every restart it rebuilt a default tab, flagged the agenterror, and wipedfilePreviewTabs/browserTabs/unifiedTabOrder. Restricted to sessions with no tabs whatsoever; a missing array is still normalized to[].hooks/remote/useRemoteIntegration.tsaiTabs, so remote/mobile clients kept rendering tabs the user had already closed.QuickActionsModal/commands/tabCommands.ts(creates new tab)subtext from Close All Tabs.performCloseAllTabsneeded no change: it snapshots tab ids up front, so it ends at zero AI tabs when other tabs exist and still yields one blank tab otherwise.Testing
tabHelpers.test.tscovering: zero AI tabs beside a terminal tab, beside a browser tab, under the unread filter (the crash path), closing a non-active sole tab, and the preserved fresh-tab behavior when nothing else is open.npm run lint(all three tsconfigs), ESLint, and Prettier clean.Verified on CI matrix rather than local single-OS pass before merge.
Summary by CodeRabbit
Bug Fixes
Improvements