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:
@@ -25,6 +25,7 @@ import { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { SidebarCallout } from "@/components/sidebar-callout";
|
||||
import { Composer } from "@/composer";
|
||||
import { COMPOSER_PILL_CLEARANCE, COMPOSER_PILL_MIN_HEIGHT } from "@/composer/pill-styles";
|
||||
import { getActiveMessageSubmissions } from "@/composer/submission/model";
|
||||
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
@@ -74,7 +75,7 @@ import {
|
||||
deriveRouteBottomAnchorRequest,
|
||||
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
|
||||
import { WorkspaceDraftAgentTab } from "@/composer/draft/workspace-tab";
|
||||
import { AgentTracks } from "@/panels/agent-tracks";
|
||||
import { AgentTracks, hasAgentTracks } from "@/panels/agent-tracks";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
@@ -88,7 +89,8 @@ import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { PendingPermission } from "@/types/shared";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import type { StreamItem, TodoEntry } from "@/types/stream";
|
||||
import { useArchiveFinishedSubagents, useSubagentsForParent } from "@/subagents";
|
||||
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
|
||||
import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||
import { applyLegacyDaemonWorkspaceOwnership } from "@/workspace/legacy-daemon-workspaces";
|
||||
@@ -1210,6 +1212,23 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId });
|
||||
const tasks = useSessionStore((state): TodoEntry[] | undefined =>
|
||||
state.sessions[serverId]?.agentTasks.get(agentId),
|
||||
);
|
||||
const archiveFinishedSubagents = useArchiveFinishedSubagents({
|
||||
serverId,
|
||||
parentAgentId: agentId,
|
||||
rows: subagentRows,
|
||||
});
|
||||
const showAgentTracks = hasAgentTracks({
|
||||
subagentRows,
|
||||
tasks,
|
||||
archiveFinishedStatus: archiveFinishedSubagents.status,
|
||||
});
|
||||
const [agentTracksHeight, setAgentTracksHeight] = useState(
|
||||
COMPOSER_PILL_MIN_HEIGHT + COMPOSER_PILL_CLEARANCE,
|
||||
);
|
||||
const rawAgentInputDraft = useAgentInputDraft({
|
||||
draftKey: buildDraftStoreKey({
|
||||
serverId,
|
||||
@@ -1256,20 +1275,6 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
composerState,
|
||||
],
|
||||
);
|
||||
const streamSection = (
|
||||
<RenderProfile id={`AgentStreamSection:${agentId}`}>
|
||||
<AgentStreamSection
|
||||
streamViewRef={streamViewRef}
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
agent={effectiveAgent}
|
||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
||||
toast={toastApi}
|
||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||
/>
|
||||
</RenderProfile>
|
||||
);
|
||||
const composerSection = (
|
||||
<RenderProfile id={`AgentComposerSection:${agentId}`}>
|
||||
<AgentComposerSection
|
||||
@@ -1288,14 +1293,32 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
/>
|
||||
</RenderProfile>
|
||||
);
|
||||
// The tracks float at the foot of the transcript, inside the same transform the composer uses,
|
||||
// so the pills sit on its top edge with the timeline running under them. They follow the
|
||||
// composer: an archived or archiving agent has none, and its children can no longer be acted on.
|
||||
const hasComposer = !agentState.archivedAt && !isArchivingCurrentAgent;
|
||||
const streamContent = (
|
||||
<ReanimatedAnimated.View style={animatedContentStyle}>
|
||||
{streamSection}
|
||||
{hasComposer ? <AgentTracks serverId={serverId} agentId={agentId} /> : null}
|
||||
<RenderProfile id={`AgentStreamSection:${agentId}`}>
|
||||
<AgentStreamSection
|
||||
streamViewRef={streamViewRef}
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
agent={effectiveAgent}
|
||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
|
||||
bottomOverlayHeight={showAgentTracks ? agentTracksHeight : 0}
|
||||
bottomOverlayClearance={showAgentTracks ? COMPOSER_PILL_CLEARANCE : 0}
|
||||
toast={toastApi}
|
||||
onOpenWorkspaceFile={onOpenWorkspaceFile}
|
||||
/>
|
||||
</RenderProfile>
|
||||
{showAgentTracks ? (
|
||||
<AgentTracks
|
||||
serverId={serverId}
|
||||
subagentRows={subagentRows}
|
||||
tasks={tasks}
|
||||
archiveFinishedStatus={archiveFinishedSubagents.status}
|
||||
onArchiveFinished={archiveFinishedSubagents.archiveFinished}
|
||||
onHeightChange={setAgentTracksHeight}
|
||||
/>
|
||||
) : null}
|
||||
</ReanimatedAnimated.View>
|
||||
);
|
||||
const contentContainer = <View style={styles.contentContainer}>{streamContent}</View>;
|
||||
@@ -1347,6 +1370,8 @@ const AgentStreamSection = memo(function AgentStreamSection({
|
||||
agent,
|
||||
routeBottomAnchorRequest,
|
||||
hasAppliedAuthoritativeHistory,
|
||||
bottomOverlayHeight,
|
||||
bottomOverlayClearance,
|
||||
toast,
|
||||
onOpenWorkspaceFile,
|
||||
}: {
|
||||
@@ -1356,6 +1381,8 @@ const AgentStreamSection = memo(function AgentStreamSection({
|
||||
agent: AgentScreenAgent;
|
||||
routeBottomAnchorRequest: RouteBottomAnchorRequest;
|
||||
hasAppliedAuthoritativeHistory: boolean;
|
||||
bottomOverlayHeight: number;
|
||||
bottomOverlayClearance: number;
|
||||
toast: ReturnType<typeof useToastHost>["api"];
|
||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||
}) {
|
||||
@@ -1414,6 +1441,8 @@ const AgentStreamSection = memo(function AgentStreamSection({
|
||||
pendingPermissions={pendingPermissions}
|
||||
routeBottomAnchorRequest={routeBottomAnchorRequest}
|
||||
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
|
||||
bottomOverlayHeight={bottomOverlayHeight}
|
||||
bottomOverlayClearance={bottomOverlayClearance}
|
||||
toast={toast}
|
||||
pendingMessageSubmissions={pendingMessageSubmissions}
|
||||
turnPresentation={turnPresentation}
|
||||
@@ -1744,13 +1773,13 @@ const styles = StyleSheet.create((theme) => ({
|
||||
zIndex: 50,
|
||||
},
|
||||
archivingTitle: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
textAlign: "center",
|
||||
},
|
||||
archivingSubtitle: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textAlign: "center",
|
||||
},
|
||||
@@ -1777,14 +1806,14 @@ const styles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "center",
|
||||
},
|
||||
errorText: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textAlign: "center",
|
||||
},
|
||||
statusText: {
|
||||
marginTop: theme.spacing[2],
|
||||
textAlign: "center",
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
offlineTitle: {
|
||||
@@ -1794,12 +1823,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
textAlign: "center",
|
||||
},
|
||||
offlineDescription: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textAlign: "center",
|
||||
},
|
||||
offlineDetails: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textAlign: "center",
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user