Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions docs/agent-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ These are two distinct concepts that used to be conflated:
| **Tab** (workspace layout) | Per-client | User opens/closes a view |
| **Archive** (lifecycle) | Global | Explicit lifecycle gesture |

Closing a tab on a **root agent** still archives — the tab is the agent's home, so closing it means "I'm done with this agent." A confirm dialog protects against archiving a running agent by accident.
Agent tab close behavior depends on the client-side workspace organization mode:

Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-only**. The agent stays unarchived and stays in its parent's track. The user can re-open the tab from the track at any time. This is implemented in `handleCloseAgentTab` (`packages/app/src/screens/workspace/workspace-screen.tsx`).
- **Workspace-first** (default): root agent tabs represent workspace threads. Closing a root agent tab archives the agent; closing a subagent tab is layout-only.
- **Thread-first**: sidebar thread rows are the persistent home surface. Closing an agent tab is layout-only for both root agents and subagents; closing a thread from its sidebar row is the explicit lifecycle gesture.

The asymmetry is intentional: a subagent's home is the parent's track, not the tab. Tabs are ephemeral viewing slots; the track is the persistent record of the parent's children.
Archive is always authoritative. Once an agent is archived, pinned or locally-open tab state must not keep it visible.

## Workspace activity

Expand All @@ -72,15 +73,13 @@ Archived subagents disappear from the track, by design. To remove a subagent fro

## Why this shape

The decision was to **decouple "close tab" from "archive" only for subagents**, rather than universally:
The thread-first organization mode decouples "close tab" from "archive":

- **Closing a tab on a root agent still archives** — preserves the existing UX users are trained on
- **Closing a tab on a subagent is layout-only** — fixes the lossy "click to read, close to dismiss view, lose the row" flow
- **Closing any agent tab is layout-only** — tabs are view slots, not lifecycle ownership
- **Close thread on sidebar rows** — gives root agents an explicit lifecycle gesture in their home surface
- **Archive button on track rows** — gives subagents an explicit lifecycle gesture in their home surface
- **Cascade archive on parent** — keeps subagents from leaking when the parent is archived

We considered universal decoupling (no tab close ever archives, archive is always explicit) but rejected it: it changes a behavior root-agent users rely on.

## Limitations

### Subagent accumulation under long-lived parents
Expand All @@ -89,7 +88,7 @@ A parent that spawns many subagents will see the track grow. There's no automati

### Cross-client tab dismissal

Closing a subagent's tab on one client doesn't affect other clients' layouts. This is the expected behavior of decoupled tabs and is consistent with how layouts have always worked. Archive remains the global gesture for cross-client cleanup.
In thread-first mode, closing an agent tab on one client doesn't affect other clients' layouts. This is the expected behavior of decoupled tabs and is consistent with how layouts have always worked. Archive remains the global gesture for cross-client cleanup.

## Storage

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/agent-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export function AgentList({
serverId,
agentId,
pin: Boolean(agent.archivedAt),
allowArchived: Boolean(agent.archivedAt),
});
},
[isActionSheetVisible, onAgentSelect],
Expand Down
77 changes: 57 additions & 20 deletions packages/app/src/components/left-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ import {
selectIsAgentListOpen,
usePanelStore,
} from "@/stores/panel-store";
import { useSessionStore } from "@/stores/session-store";
import {
useWorkspaceOrganizationStore,
type WorkspaceOrganizationMode,
} from "@/stores/workspace-organization-store";
import { resolveActiveHost } from "@/utils/active-host";
import { formatConnectionStatus } from "@/utils/daemons";
import { useWindowControlsPadding } from "@/utils/desktop-window";
Expand Down Expand Up @@ -97,6 +102,8 @@ interface SidebarSharedProps {
isInitialLoad: boolean;
isRevalidating: boolean;
isManualRefresh: boolean;
selectedAgentId?: string;
organizationMode: WorkspaceOrganizationMode;
groupMode: SidebarGroupMode;
collapsedProjectKeys: SidebarShortcutModel["collapsedProjectKeys"];
shortcutIndexByWorkspaceKey: SidebarShortcutModel["shortcutIndexByWorkspaceKey"];
Expand Down Expand Up @@ -128,11 +135,7 @@ interface DesktopSidebarProps extends SidebarSharedProps {
handleViewMore: () => void;
}

export const LeftSidebar = memo(function LeftSidebar({
selectedAgentId: _selectedAgentId,
}: LeftSidebarProps) {
void _selectedAgentId;

export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const isCompactLayout = useIsCompactFormFactor();
Expand All @@ -147,6 +150,13 @@ export const LeftSidebar = memo(function LeftSidebar({
[daemons, pathname],
);
const activeServerId = activeDaemon?.serverId ?? null;
const focusedAgentId = useSessionStore((state) =>
activeServerId ? (state.sessions[activeServerId]?.focusedAgentId ?? null) : null,
);
const organizationMode = useWorkspaceOrganizationStore((state) => state.mode);
const effectiveSelectedAgentId =
selectedAgentId ??
(activeServerId && focusedAgentId ? `${activeServerId}:${focusedAgentId}` : undefined);
const activeHostLabel = useMemo(() => {
if (!activeDaemon) return "No host";
const trimmed = activeDaemon.label?.trim();
Expand Down Expand Up @@ -199,7 +209,11 @@ export const LeftSidebar = memo(function LeftSidebar({
enabled: isCompactLayout || isOpen,
});
const { collapsedProjectKeys, shortcutIndexByWorkspaceKey, toggleProjectCollapsed } =
useSidebarShortcutModel({ projects, isInitialLoad });
useSidebarShortcutModel({
projects,
isInitialLoad,
organizationMode,
});

const groupMode = useSidebarViewStore((state) =>
activeServerId ? state.getGroupMode(activeServerId) : "project",
Expand Down Expand Up @@ -281,6 +295,8 @@ export const LeftSidebar = memo(function LeftSidebar({
isInitialLoad,
isRevalidating,
isManualRefresh,
selectedAgentId: effectiveSelectedAgentId,
organizationMode,
groupMode,
collapsedProjectKeys,
shortcutIndexByWorkspaceKey,
Expand Down Expand Up @@ -557,6 +573,8 @@ function MobileSidebar({
handleOpenProject,
handleHome,
handleSettings,
selectedAgentId,
organizationMode,
insetsTop,
insetsBottom,
isOpen,
Expand Down Expand Up @@ -745,7 +763,10 @@ function MobileSidebar({
testID="sidebar-sessions"
/>
</View>
<WorkspacesSectionHeader serverId={activeServerId} />
<WorkspacesSectionHeader
serverId={activeServerId}
organizationMode={organizationMode}
/>
<Pressable
style={styles.mobileCloseButton}
onPress={closeToAgent}
Expand Down Expand Up @@ -776,6 +797,8 @@ function MobileSidebar({
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
groupMode={groupMode}
projects={projects}
organizationMode={organizationMode}
selectedAgentId={selectedAgentId}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onWorkspacePress={handleWorkspacePress}
Expand Down Expand Up @@ -829,6 +852,8 @@ function DesktopSidebar({
handleOpenProject,
handleHome,
handleSettings,
selectedAgentId,
organizationMode,
insetsTop,
isOpen,
handleViewMore,
Expand Down Expand Up @@ -913,7 +938,7 @@ function DesktopSidebar({
/>
</View>
</View>
<WorkspacesSectionHeader serverId={activeServerId} />
<WorkspacesSectionHeader serverId={activeServerId} organizationMode={organizationMode} />

{isInitialLoad ? (
<SidebarAgentListSkeleton />
Expand All @@ -925,6 +950,8 @@ function DesktopSidebar({
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
groupMode={groupMode}
projects={projects}
organizationMode={organizationMode}
selectedAgentId={selectedAgentId}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onAddProject={handleOpenProject}
Expand Down Expand Up @@ -958,11 +985,19 @@ function DesktopSidebar({
);
}

function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
function WorkspacesSectionHeader({
serverId,
organizationMode,
}: {
serverId: string | null;
organizationMode: WorkspaceOrganizationMode;
}) {
const { theme } = useUnistyles();
const setCommandCenterOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen);
const commandCenterKeys = useShortcutKeys("toggle-command-center");
const handleSearchPress = useCallback(() => setCommandCenterOpen(true), [setCommandCenterOpen]);
const sectionTitle = organizationMode === "thread-first" ? "Threads" : "Workspaces";
const showGroupingSelector = organizationMode === "workspace-first";
const handleNewWorkspacePress = useCallback(() => {
if (!serverId) {
return;
Expand All @@ -979,7 +1014,7 @@ function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {

return (
<View style={styles.workspacesSectionHeader}>
<Text style={styles.workspacesSectionTitle}>Workspaces</Text>
<Text style={styles.workspacesSectionTitle}>{sectionTitle}</Text>
<View style={styles.workspacesSectionActions}>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -1027,16 +1062,18 @@ function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
<HeaderIconTooltipContent label="Search" shortcutKeys={commandCenterKeys} />
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<View>
<SidebarGroupingSelector serverId={serverId} />
</View>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<HeaderIconTooltipContent label="Display preferences" />
</TooltipContent>
</Tooltip>
{showGroupingSelector ? (
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<View>
<SidebarGroupingSelector serverId={serverId} />
</View>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<HeaderIconTooltipContent label="Display preferences" />
</TooltipContent>
</Tooltip>
) : null}
</View>
</View>
);
Expand Down
Loading
Loading