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
* 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.
143 lines
5.0 KiB
TypeScript
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";
|
|
}
|