Files
paseo/packages/app/src/panels/agent-tracks.tsx
T
Mohamed BoudraandGitHub a69dca2907
CI / changes (push) Successful in 6m6s
Docker / setup (push) Successful in 14s
Docker / publish (push) Skipped
Docker / build (push) Failing after 10m1s
CI / relay-tests (push) Canceled after 0s
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / server-tests (ubuntu-latest) (push) Canceled after 0s
CI / server-tests (windows-latest) (push) Canceled after 0s
CI / desktop-tests (ubuntu-latest) (push) Canceled after 0s
CI / desktop-tests (windows-latest) (push) Canceled after 0s
CI / app-tests (push) Canceled after 0s
CI / sdk-tests (push) Canceled after 0s
CI / playwright (shard 1/4) (push) Canceled after 0s
CI / playwright (shard 2/4) (push) Canceled after 0s
CI / playwright (shard 3/4) (push) Canceled after 0s
CI / playwright (shard 4/4) (push) Canceled after 0s
CI / format (push) Canceled after 0s
CI / cli-tests (shard 1/3) (push) Canceled after 0s
CI / cli-tests (shard 2/3) (push) Canceled after 0s
CI / cli-tests (shard 3/3) (push) Canceled after 0s
Expand workspace panel actions and keyboard navigation (#3685)
* fix(app): keep file navigation in place

Retargeting a File tab allocated a new tab identity, causing the tab track to unmount and remount during navigation. Preserve the identity for File-to-File changes while retaining new instances for cross-kind replacements.

* feat(app): expand workspace panel actions

Keep the Command Center aligned with workspace tab and pane primitives. Add explicit panel placement, focused open behavior, keyboard navigation for empty panes, and a Changes pill that toggles the visible Side panel without closing its tab.
2026-08-22 14:15:23 +02:00

143 lines
5.0 KiB
TypeScript

import { memo, useCallback, type ReactElement } from "react";
import { WorkspaceDiffStatPill } from "@/composer/diff-stat-pill";
import { useWorkspaceHasDiffStat } from "@/composer/workspace-diff-stat";
import { AgentTaskList } from "@/composer/task-list";
import { ComposerTrackBar } from "@/composer/tracks";
import { supportsDesktopPaneSplits, useIsCompactFormFactor } from "@/constants/layout";
import { usePaneContext } from "@/panels/pane-context";
import { useSettings } from "@/hooks/use-settings";
import { useSessionStore } from "@/stores/session-store";
import {
type ArchiveFinishedStatus,
useArchiveSubagent,
useDetachSubagent,
type SubagentRow,
} from "@/subagents";
import { SubagentsTrack } from "@/subagents/track";
import type { TodoEntry } from "@/types/stream";
import { navigateToAgent } from "@/utils/navigate-to-agent";
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
import { openSupportingTab, toggleSupportingTab } from "@/workspace-tabs/side-panel";
/**
* The pane's ambient context — workspace changes, subagents, and tasks — as a row of pills above
* the composer.
*
* The row shares the composer's keyboard transform and owns the space between itself and the
* transcript. Each pill owns its action while tab placement stays behind the workspace boundary.
*/
export const AgentTracks = memo(function AgentTracks({
serverId,
workspaceId,
subagentRows,
tasks,
archiveFinishedStatus,
onArchiveFinished,
}: {
serverId: string;
workspaceId: string;
subagentRows: SubagentRow[];
tasks: TodoEntry[] | undefined;
archiveFinishedStatus: ArchiveFinishedStatus;
onArchiveFinished: () => void;
}): ReactElement | null {
const { tabId, openTab } = usePaneContext();
const hasWorkspaceDiffStat = useWorkspaceHasDiffStat(serverId, workspaceId);
const isCompact = useIsCompactFormFactor();
const canSplit = supportsDesktopPaneSplits() && !isCompact;
const openInSidePanelByDefault = useSettings(
(settings) => settings.openSupportingTabsInSidePanel,
);
const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
const canDetachSubagents = useSessionStore(
(state) => state.sessions[serverId]?.serverInfo?.features?.agentDetach === true,
);
const archiveSubagent = useArchiveSubagent({ serverId });
const detachSubagent = useDetachSubagent({ serverId });
const handleOpenSubagent = useCallback(
(subagentId: string) => {
const session = useSessionStore.getState().sessions[serverId];
const agent = session?.agents.get(subagentId) ?? session?.agentDetails.get(subagentId);
if (agent?.workspaceId && agent.workspaceId !== workspaceId) {
navigateToAgent({ serverId, agentId: subagentId });
return;
}
if (canSplit && workspaceKey) {
openSupportingTab({
isCompact,
workspaceKey,
target: { kind: "agent", agentId: subagentId },
openInSidePanelByDefault,
parentTabId: tabId,
});
return;
}
navigateToAgent({ serverId, agentId: subagentId });
},
[canSplit, isCompact, openInSidePanelByDefault, serverId, tabId, workspaceId, workspaceKey],
);
const handleOpenProviderSubagent = useCallback(
(parentAgentId: string, subagentId: string) => {
if (canSplit && workspaceKey) {
openSupportingTab({
isCompact,
workspaceKey,
target: { kind: "provider_subagent", parentAgentId, subagentId },
openInSidePanelByDefault,
parentTabId: tabId,
});
return;
}
openTab({ kind: "provider_subagent", parentAgentId, subagentId });
},
[canSplit, isCompact, openInSidePanelByDefault, openTab, tabId, workspaceKey],
);
const handleOpenChanges = useCallback(() => {
if (!workspaceKey) {
return;
}
toggleSupportingTab({
isCompact,
workspaceKey,
target: { kind: "working_diff" },
openInSidePanelByDefault,
});
}, [isCompact, openInSidePanelByDefault, workspaceKey]);
if (!hasWorkspaceDiffStat && !hasAgentTracks({ subagentRows, tasks, archiveFinishedStatus })) {
return null;
}
return (
<ComposerTrackBar>
<AgentTaskList tasks={tasks} />
<SubagentsTrack
rows={subagentRows}
onOpenSubagent={handleOpenSubagent}
onOpenProviderSubagent={handleOpenProviderSubagent}
onArchiveSubagent={archiveSubagent}
onArchiveFinished={onArchiveFinished}
archiveFinishedStatus={archiveFinishedStatus}
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
/>
<WorkspaceDiffStatPill
serverId={serverId}
workspaceId={workspaceId}
onPress={handleOpenChanges}
/>
</ComposerTrackBar>
);
});
export function hasAgentTracks({
subagentRows,
tasks,
archiveFinishedStatus,
}: {
subagentRows: readonly SubagentRow[];
tasks: readonly TodoEntry[] | undefined;
archiveFinishedStatus: ArchiveFinishedStatus;
}): boolean {
return subagentRows.length > 0 || Boolean(tasks?.length) || archiveFinishedStatus.kind !== "idle";
}