Skip to content

fix(tabs): allow an agent to have zero AI tabs - #1349

Open
pedramamini wants to merge 1 commit into
mainfrom
fix/1062-allow-zero-ai-tabs
Open

fix(tabs): allow an agent to have zero AI tabs#1349
pedramamini wants to merge 1 commit into
mainfrom
fix/1062-allow-zero-ai-tabs

Conversation

@pedramamini

@pedramamini pedramamini commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

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:

I hate when I have 3 or 4 open and one of them is essentially a dead tab. Bugs the hell out of me. Forced to give up screen real estate. Also I can't clear out my tabs for the day when I'm done.

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:

  • A brand new agent still always opens with a chat tab, and closing its only tab still hands you a fresh one. Nobody can end up staring at a workspace with no obvious way forward.
  • Once you have other tabs open (terminal / file / browser), the last AI tab closes cleanly and the agent sits at zero AI tabs. The tab strip and its + 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:

File Why
utils/tabHelpers.ts Only auto-create the replacement when no other tab kind survives. Clear activeTabId when 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 indexed updatedTabs[i] and would throw on an empty array.
components/MainPanel/MainPanel.tsx The tab strip was gated on aiTabs.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.ts Empty aiTabs was treated as data corruption: on every restart it rebuilt a default tab, flagged the agent error, and wiped filePreviewTabs/browserTabs/unifiedTabOrder. Restricted to sessions with no tabs whatsoever; a missing array is still normalized to [].
hooks/remote/useRemoteIntegration.ts Bailed out on empty aiTabs, so remote/mobile clients kept rendering tabs the user had already closed.
QuickActionsModal/commands/tabCommands.ts Dropped the now-inaccurate (creates new tab) subtext from Close All Tabs.

performCloseAllTabs needed 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

  • 5 new cases in tabHelpers.test.ts covering: 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.
  • Full suite: 32,546 passed, 0 failures.
  • 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

    • Closing the last AI tab no longer creates an unnecessary replacement when file, terminal, or browser tabs remain.
    • Tab selection and active-state handling now remain consistent when AI tabs are closed.
    • Sessions with no AI tabs are restored correctly without being treated as corrupted.
    • Remote views now stay synchronized after all AI tabs are closed.
    • Closing the final tab still creates a fresh AI tab when no other tabs remain.
  • Improvements

    • The tab bar remains available when non-AI tabs are open.
    • Close-tab actions now display clearer wording for one versus multiple tabs.

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.
Copilot AI lite review requested due to automatic review settings August 4, 2026 14:14
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Zero AI tab lifecycle

Layer / File(s) Summary
Close-tab behavior and validation
src/renderer/utils/tabHelpers.ts, src/__tests__/renderer/utils/tabHelpers.test.ts
closeTab creates a replacement AI tab only when no tabs remain. It clears stale active AI references and covers sole-AI-tab scenarios.
Restoration and remote state propagation
src/renderer/hooks/session/useSessionRestoration.ts, src/renderer/hooks/remote/useRemoteIntegration.ts
Session restoration checks all tab types and normalizes missing aiTabs to an empty array. Remote broadcasting accepts empty AI-tab arrays.
Tab-bar availability and command text
src/renderer/components/MainPanel/MainPanel.tsx, src/renderer/components/QuickActionsModal/commands/tabCommands.ts
The tab bar renders when any tab type exists. The close-all command uses singular or plural wording without stating that a new tab is created.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: copilot, reachrazamair

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The changes allow zero AI tabs but still create a replacement when no other tabs remain, so they do not meet issue #1062's zero-session-tabs requirement. Allow closing the final tab without creating a replacement, and keep the tab bar usable so users can create a tab later via the plus button.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: allowing agents to have zero AI tabs.
Out of Scope Changes check ✅ Passed The supporting changes address tab closure, restoration, remote updates, UI visibility, and command text for the zero-AI-tab behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1062-allow-zero-ai-tabs

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.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown

Greptile Summary

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

  • Keeps the unified tab strip visible when only non-AI tabs remain.
  • Clears obsolete AI selection state and broadcasts an empty AI-tab list to remote clients.
  • Changes restoration recovery to consider all tab kinds, but currently counts terminal tabs that will be discarded later in the same restoration pass.
  • Adds focused close-tab coverage for zero-AI-tab transitions.

Confidence Score: 4/5

This 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

Filename Overview
src/renderer/utils/tabHelpers.ts Allows the final AI tab to close when another tab kind survives, clears the AI selection, and safely chooses unified fallbacks.
src/renderer/components/MainPanel/MainPanel.tsx Keeps TabBar mounted whenever any supported tab kind exists.
src/renderer/hooks/session/useSessionRestoration.ts Accepts zero-AI sessions but performs its all-tabs-empty check before removing non-persistent terminal tabs, allowing a tabless restored session.
src/renderer/hooks/remote/useRemoteIntegration.ts Broadcasts the valid transition from one AI tab to an empty AI-tab list.
src/renderer/components/QuickActionsModal/commands/tabCommands.ts Updates Close All Tabs copy so it no longer promises that a replacement tab will always be created.
src/tests/renderer/utils/tabHelpers.test.ts Covers close-time zero-AI behavior but not restart restoration when the only remaining terminal is non-persistent.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "fix(tabs): allow an agent to have zero A..." | Re-trigger Greptile

Comment on lines +210 to +214
const restoredTabCount =
(session.aiTabs?.length ?? 0) +
(session.filePreviewTabs?.length ?? 0) +
(session.terminalTabs?.length ?? 0) +
(session.browserTabs?.length ?? 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 aiTabs array 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;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between e3479fe and 6c1606f.

📒 Files selected for processing (6)
  • src/__tests__/renderer/utils/tabHelpers.test.ts
  • src/renderer/components/MainPanel/MainPanel.tsx
  • src/renderer/components/QuickActionsModal/commands/tabCommands.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/renderer/hooks/session/useSessionRestoration.ts
  • src/renderer/utils/tabHelpers.ts

Comment on lines +206 to +215
// 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment on lines +259 to +263
// 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: [] };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment on lines +815 to +819
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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.

Allow agents to have no open session tabs

2 participants