feat(app): float composer trackers as pills over the transcript (#3482)
The subagents track and the task list were stacked cards above the composer. Each one pushed the composer further down the pane and painted an opaque band the timeline stopped at, and both spent a full row on a count that fits in a pill. Both are now pills that open a panel — a popover on wide screens, a sheet on compact ones, from the existing menu engine rather than a fourth floating-panel implementation. The row frame is shared, so a subagent row and a task row sit on the same rail whatever their contents. The bar mounts inside the transcript's animated container and pins itself to the bottom edge, so scrolled content passes under the pills instead of stopping above them. It stays a child of that container rather than a sibling floating over the composer: on Android a view outside its parent's bounds paints but takes no touches, and every pill is pressable. The container carries the same keyboard transform the composer does, so the pills track the composer's top edge when the keyboard moves. The tracker state moves out of the composer with it. None of it was composer state; it lived there because that is where it used to render.
This commit is contained in:
@@ -74,7 +74,7 @@ import {
|
||||
deriveRouteBottomAnchorRequest,
|
||||
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
|
||||
import { WorkspaceDraftAgentTab } from "@/composer/draft/workspace-tab";
|
||||
import { AgentTaskList } from "@/composer/task-list";
|
||||
import { AgentTracks } from "@/panels/agent-tracks";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
@@ -87,20 +87,12 @@ import {
|
||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import {
|
||||
useArchiveFinishedSubagents,
|
||||
useArchiveSubagent,
|
||||
useDetachSubagent,
|
||||
useSubagentsForParent,
|
||||
} from "@/subagents";
|
||||
import { SubagentsTrack } from "@/subagents/track";
|
||||
import type { PendingPermission } from "@/types/shared";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
|
||||
import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||
import { applyLegacyDaemonWorkspaceOwnership } from "@/workspace/legacy-daemon-workspaces";
|
||||
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
||||
import { navigateToAgent } from "@/utils/navigate-to-agent";
|
||||
import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { buildDraftAgentSetup, type ClientSlashCommand } from "@/client-slash-commands";
|
||||
|
||||
@@ -1296,8 +1288,15 @@ 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}</ReanimatedAnimated.View>
|
||||
<ReanimatedAnimated.View style={animatedContentStyle}>
|
||||
{streamSection}
|
||||
{hasComposer ? <AgentTracks serverId={serverId} agentId={agentId} /> : null}
|
||||
</ReanimatedAnimated.View>
|
||||
);
|
||||
const contentContainer = <View style={styles.contentContainer}>{streamContent}</View>;
|
||||
|
||||
@@ -1506,37 +1505,11 @@ function ActiveAgentComposer({
|
||||
{ initialIsBelow: isCompactFormFactor },
|
||||
);
|
||||
const paneContext = usePaneContext();
|
||||
const { workspaceId, tabId, retargetCurrentTab, openTab } = paneContext;
|
||||
const { workspaceId, tabId, retargetCurrentTab } = paneContext;
|
||||
const { archiveAgent } = useArchiveAgent();
|
||||
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
||||
const hideWorkspaceAgent = useWorkspaceLayoutStore((state) => state.hideAgent);
|
||||
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
||||
const subagentRows = useSubagentsForParent({
|
||||
serverId,
|
||||
parentAgentId: agentId,
|
||||
});
|
||||
const canDetachSubagents = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.serverInfo?.features?.agentDetach === true,
|
||||
);
|
||||
const handleOpenSubagent = useCallback(
|
||||
(subagentId: string) => {
|
||||
navigateToAgent({ serverId, agentId: subagentId });
|
||||
},
|
||||
[serverId],
|
||||
);
|
||||
const handleOpenProviderSubagent = useCallback(
|
||||
(parentAgentId: string, subagentId: string) => {
|
||||
openTab({ kind: "provider_subagent", parentAgentId, subagentId });
|
||||
},
|
||||
[openTab],
|
||||
);
|
||||
const handleArchiveSubagent = useArchiveSubagent({ serverId });
|
||||
const handleDetachSubagent = useDetachSubagent({ serverId });
|
||||
const archiveFinishedSubagents = useArchiveFinishedSubagents({
|
||||
serverId,
|
||||
parentAgentId: agentId,
|
||||
rows: subagentRows,
|
||||
});
|
||||
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
|
||||
serverId,
|
||||
cwd,
|
||||
@@ -1623,16 +1596,6 @@ function ActiveAgentComposer({
|
||||
|
||||
return (
|
||||
<ReanimatedAnimated.View style={inputAreaStyle} onLayout={onInputAreaLayout}>
|
||||
<AgentTaskList serverId={serverId} agentId={agentId} />
|
||||
<SubagentsTrack
|
||||
rows={subagentRows}
|
||||
onOpenSubagent={handleOpenSubagent}
|
||||
onOpenProviderSubagent={handleOpenProviderSubagent}
|
||||
onArchiveSubagent={handleArchiveSubagent}
|
||||
onArchiveFinished={archiveFinishedSubagents.archiveFinished}
|
||||
archiveFinishedStatus={archiveFinishedSubagents.status}
|
||||
onDetachSubagent={canDetachSubagents ? handleDetachSubagent : undefined}
|
||||
/>
|
||||
<Composer
|
||||
agentId={agentId}
|
||||
serverId={serverId}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { memo, useCallback, type ReactElement } from "react";
|
||||
import { AgentTaskList } from "@/composer/task-list";
|
||||
import { ComposerTrackBar } from "@/composer/tracks";
|
||||
import { usePaneContext } from "@/panels/pane-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
useArchiveFinishedSubagents,
|
||||
useArchiveSubagent,
|
||||
useDetachSubagent,
|
||||
useSubagentsForParent,
|
||||
} from "@/subagents";
|
||||
import { SubagentsTrack } from "@/subagents/track";
|
||||
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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export const AgentTracks = memo(function AgentTracks({
|
||||
serverId,
|
||||
agentId,
|
||||
}: {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
}): ReactElement {
|
||||
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 });
|
||||
},
|
||||
[serverId],
|
||||
);
|
||||
const handleOpenProviderSubagent = useCallback(
|
||||
(parentAgentId: string, subagentId: string) => {
|
||||
openTab({ kind: "provider_subagent", parentAgentId, subagentId });
|
||||
},
|
||||
[openTab],
|
||||
);
|
||||
|
||||
return (
|
||||
<ComposerTrackBar>
|
||||
<SubagentsTrack
|
||||
rows={subagentRows}
|
||||
onOpenSubagent={handleOpenSubagent}
|
||||
onOpenProviderSubagent={handleOpenProviderSubagent}
|
||||
onArchiveSubagent={archiveSubagent}
|
||||
onArchiveFinished={archiveFinishedSubagents.archiveFinished}
|
||||
archiveFinishedStatus={archiveFinishedSubagents.status}
|
||||
onDetachSubagent={canDetachSubagents ? detachSubagent : undefined}
|
||||
/>
|
||||
<AgentTaskList serverId={serverId} agentId={agentId} />
|
||||
</ComposerTrackBar>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user