fix(app): float composer track pills consistently

Keep agent and task pills as a transparent transcript overlay while measuring their rendered height for scroll-tail clearance. Share geometry and spacing with the import-session pill so borders, type, corners, and composer separation match across platforms.
This commit is contained in:
Mohamed Boudra
2026-08-17 22:55:45 +02:00
parent d7f892251f
commit df3e646fbe
13 changed files with 350 additions and 134 deletions
+38 -25
View File
@@ -4,46 +4,43 @@ import { ComposerTrackBar } from "@/composer/tracks";
import { usePaneContext } from "@/panels/pane-context";
import { useSessionStore } from "@/stores/session-store";
import {
useArchiveFinishedSubagents,
type ArchiveFinishedStatus,
useArchiveSubagent,
useDetachSubagent,
useSubagentsForParent,
type SubagentRow,
} from "@/subagents";
import { SubagentsTrack } from "@/subagents/track";
import type { TodoEntry } from "@/types/stream";
import { navigateToAgent } from "@/utils/navigate-to-agent";
/**
* The pane's trackers — its subagents and its task list — as a row of pills over the foot of
* the transcript.
* The pane's trackers — its subagents and its task list — as a row of pills above the composer.
*
* It is mounted inside the transcript's animated container rather than above the composer, and
* that placement is the whole design: the pills paint over the timeline, so scrolled content
* passes under them instead of stopping at a band, and the container carries the same keyboard
* transform the composer does, so the pills stay glued to its top edge while the keyboard moves.
*
* Its state was living in the composer only because that is where it used to render. None of it
* is composer state — a subagent row opens a tab, the task list reads the agent's stream.
* The row shares the composer's keyboard transform and owns the space between itself and the
* transcript. Its data remains agent state: a subagent row opens a tab and the task list reads
* the agent's stream.
*/
export const AgentTracks = memo(function AgentTracks({
serverId,
agentId,
subagentRows,
tasks,
archiveFinishedStatus,
onArchiveFinished,
onHeightChange,
}: {
serverId: string;
agentId: string;
}): ReactElement {
subagentRows: SubagentRow[];
tasks: TodoEntry[] | undefined;
archiveFinishedStatus: ArchiveFinishedStatus;
onArchiveFinished: () => void;
onHeightChange?: (height: number) => void;
}): ReactElement | null {
const { openTab } = usePaneContext();
const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId });
const canDetachSubagents = useSessionStore(
(state) => state.sessions[serverId]?.serverInfo?.features?.agentDetach === true,
);
const archiveSubagent = useArchiveSubagent({ serverId });
const detachSubagent = useDetachSubagent({ serverId });
const archiveFinishedSubagents = useArchiveFinishedSubagents({
serverId,
parentAgentId: agentId,
rows: subagentRows,
});
const handleOpenSubagent = useCallback(
(subagentId: string) => {
navigateToAgent({ serverId, agentId: subagentId });
@@ -57,18 +54,34 @@ export const AgentTracks = memo(function AgentTracks({
[openTab],
);
if (!hasAgentTracks({ subagentRows, tasks, archiveFinishedStatus })) {
return null;
}
return (
<ComposerTrackBar>
<ComposerTrackBar onHeightChange={onHeightChange}>
<SubagentsTrack
rows={subagentRows}
onOpenSubagent={handleOpenSubagent}
onOpenProviderSubagent={handleOpenProviderSubagent}
onArchiveSubagent={archiveSubagent}
onArchiveFinished={archiveFinishedSubagents.archiveFinished}
archiveFinishedStatus={archiveFinishedSubagents.status}
onArchiveFinished={onArchiveFinished}
archiveFinishedStatus={archiveFinishedStatus}
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
/>
<AgentTaskList serverId={serverId} agentId={agentId} />
<AgentTaskList tasks={tasks} />
</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";
}