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:
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { PaseoSubagentRow, ProviderSubagentRow, SubagentRow } from "./select";
|
||||
import {
|
||||
aggregateSubagentStatusBucket,
|
||||
buildSubagentRowPresentationData,
|
||||
countFinishedSubagents,
|
||||
formatHeaderLabel,
|
||||
resolveRowLabel,
|
||||
} from "./track-presentation";
|
||||
|
||||
@@ -23,55 +23,28 @@ function row(
|
||||
};
|
||||
}
|
||||
|
||||
describe("formatHeaderLabel", () => {
|
||||
it("uses singular 'subagent' for a single row", () => {
|
||||
expect(formatHeaderLabel([row({ id: "a" })])).toBe("1 subagent");
|
||||
describe("aggregateSubagentStatusBucket", () => {
|
||||
it("has no bucket without rows", () => {
|
||||
expect(aggregateSubagentStatusBucket([])).toBeNull();
|
||||
});
|
||||
|
||||
it("uses plural 'subagents' for two rows with no running rows", () => {
|
||||
expect(formatHeaderLabel([row({ id: "a" }), row({ id: "b" })])).toBe("2 subagents");
|
||||
it("has no bucket when every child is done", () => {
|
||||
expect(aggregateSubagentStatusBucket([row({ id: "a" }), row({ id: "b" })])).toBeNull();
|
||||
});
|
||||
|
||||
it("appends the running count when at least one row is running", () => {
|
||||
it("reports running when any child is running", () => {
|
||||
expect(
|
||||
formatHeaderLabel([row({ id: "a", status: "running" }), row({ id: "b" }), row({ id: "c" })]),
|
||||
).toBe("3 subagents · 1 running");
|
||||
aggregateSubagentStatusBucket([row({ id: "a" }), row({ id: "b", status: "running" })]),
|
||||
).toBe("running");
|
||||
});
|
||||
|
||||
it("counts every running row in the suffix", () => {
|
||||
it("ranks a failed child above a running one", () => {
|
||||
expect(
|
||||
formatHeaderLabel([
|
||||
aggregateSubagentStatusBucket([
|
||||
row({ id: "a", status: "running" }),
|
||||
row({ id: "b", status: "running" }),
|
||||
row({ id: "c", requiresAttention: true }),
|
||||
row({ id: "d" }),
|
||||
row({ id: "e" }),
|
||||
row({ id: "b", status: "error", requiresAttention: true }),
|
||||
]),
|
||||
).toBe("5 subagents · 2 running");
|
||||
});
|
||||
|
||||
it("ignores requiresAttention on non-running rows in the header copy", () => {
|
||||
expect(
|
||||
formatHeaderLabel([
|
||||
row({ id: "a", status: "error", requiresAttention: false }),
|
||||
row({ id: "b", status: "idle", requiresAttention: false }),
|
||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||
]),
|
||||
).toBe("3 subagents");
|
||||
});
|
||||
|
||||
it("still counts running rows even when they require attention", () => {
|
||||
expect(
|
||||
formatHeaderLabel([
|
||||
row({ id: "a", status: "error", requiresAttention: true }),
|
||||
row({ id: "b", status: "running", requiresAttention: true }),
|
||||
row({ id: "c", status: "idle", requiresAttention: true }),
|
||||
]),
|
||||
).toBe("3 subagents · 1 running");
|
||||
});
|
||||
|
||||
it("uses singular 'subagent' for a single row that requires attention upstream", () => {
|
||||
expect(formatHeaderLabel([row({ id: "a", requiresAttention: true })])).toBe("1 subagent");
|
||||
).toBe("failed");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import {
|
||||
aggregateSidebarStateBuckets,
|
||||
deriveSidebarStateBucket,
|
||||
} from "@/utils/sidebar-agent-state";
|
||||
import type { SubagentRow } from "./select";
|
||||
import { isFinishedSubagent } from "./archive-finished";
|
||||
import { providerSubagentLifecycleStatus } from "./provider-store";
|
||||
@@ -40,19 +43,25 @@ export function buildSubagentRowPresentationData(row: SubagentRow): SubagentRowP
|
||||
};
|
||||
}
|
||||
|
||||
export function formatHeaderLabel(rows: readonly SubagentRow[]): string {
|
||||
let runningCount = 0;
|
||||
for (const row of rows) {
|
||||
if (row.status === "running") {
|
||||
runningCount += 1;
|
||||
}
|
||||
/**
|
||||
* The one state the collapsed pill can show. The pill has room for a dot and a count, so the
|
||||
* children collapse into the most urgent bucket among them — the same rule a collapsed project
|
||||
* row in the sidebar uses, and for the same reason.
|
||||
*
|
||||
* `null` when every child is done: a finished fan-out is not worth a colour above the composer.
|
||||
*/
|
||||
export function aggregateSubagentStatusBucket(
|
||||
rows: readonly SubagentRow[],
|
||||
): SidebarStateBucket | null {
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = [`${rows.length} ${rows.length === 1 ? "subagent" : "subagents"}`];
|
||||
if (runningCount > 0) {
|
||||
parts.push(`${runningCount} running`);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
const buckets = rows.flatMap((row) => {
|
||||
const bucket = buildSubagentRowPresentationData(row).statusBucket;
|
||||
return bucket ? [bucket] : [];
|
||||
});
|
||||
const aggregate = aggregateSidebarStateBuckets(buckets);
|
||||
return aggregate === "done" ? null : aggregate;
|
||||
}
|
||||
|
||||
export function countFinishedSubagents(rows: readonly SubagentRow[]): number {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { Pressable, ScrollView, Text, View } from "react-native";
|
||||
import { useCallback, useMemo, type ReactElement } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Archive, ChevronDown, ChevronRight, Unlink } from "lucide-react-native";
|
||||
import { Archive, Unlink } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ComposerTrackPill, ComposerTrackRow } from "@/composer/tracks";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useIsCompactFormFactor, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import {
|
||||
WorkspaceTabIcon,
|
||||
@@ -16,9 +16,9 @@ import type { Theme } from "@/styles/theme";
|
||||
import type { SubagentRow } from "./select";
|
||||
import type { ArchiveFinishedStatus } from "./use-archive-finished";
|
||||
import {
|
||||
aggregateSubagentStatusBucket,
|
||||
buildSubagentRowPresentationData,
|
||||
countFinishedSubagents,
|
||||
formatHeaderLabel,
|
||||
} from "./track-presentation";
|
||||
|
||||
const ThemedArchive = withUnistyles(Archive);
|
||||
@@ -39,7 +39,10 @@ export interface SubagentsTrackProps {
|
||||
onDetachSubagent?: (id: string) => void;
|
||||
}
|
||||
|
||||
const SUBAGENTS_LIST_MAX_HEIGHT = 200;
|
||||
const IDLE_ARCHIVE_FINISHED_STATUS: ArchiveFinishedStatus = { kind: "idle" };
|
||||
|
||||
/** Leading and action glyphs share one size so rows keep a single icon column. */
|
||||
const ROW_ICON_SIZE = 14;
|
||||
|
||||
function buildRowPresentation(row: SubagentRow): WorkspaceTabPresentation {
|
||||
const data = buildSubagentRowPresentationData(row);
|
||||
@@ -57,26 +60,10 @@ export function SubagentsTrack({
|
||||
onOpenProviderSubagent,
|
||||
onArchiveSubagent,
|
||||
onArchiveFinished,
|
||||
archiveFinishedStatus = { kind: "idle" },
|
||||
archiveFinishedStatus = IDLE_ARCHIVE_FINISHED_STATUS,
|
||||
onDetachSubagent,
|
||||
}: SubagentsTrackProps): ReactElement | null {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setExpanded((current) => !current);
|
||||
}, []);
|
||||
|
||||
const surfaceStyle = useMemo(
|
||||
() => [styles.surface, expanded && styles.surfaceExpanded],
|
||||
[expanded],
|
||||
);
|
||||
|
||||
const headerContainerStyle = useMemo(
|
||||
() => [styles.header, expanded ? styles.headerDivider : styles.headerCollapsed],
|
||||
[expanded],
|
||||
);
|
||||
const headerAccessibilityState = useMemo(() => ({ expanded }), [expanded]);
|
||||
|
||||
const isArchivingFinished = archiveFinishedStatus.kind === "archiving";
|
||||
const isArchiveFinishedFailed = archiveFinishedStatus.kind === "failed";
|
||||
@@ -84,83 +71,94 @@ export function SubagentsTrack({
|
||||
return null;
|
||||
}
|
||||
|
||||
const headerLabel = formatHeaderLabel(rows);
|
||||
const pillLabel =
|
||||
rows.length === 1
|
||||
? t("subagents.pillLabelOne")
|
||||
: t("subagents.pillLabelMany", { count: rows.length });
|
||||
const finishedCount = countFinishedSubagents(rows);
|
||||
const showArchiveFinished = finishedCount > 0 || isArchivingFinished || isArchiveFinishedFailed;
|
||||
|
||||
return (
|
||||
<View style={styles.outer} testID="subagents-track">
|
||||
<View style={styles.track}>
|
||||
<View style={surfaceStyle}>
|
||||
<View style={headerContainerStyle}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
accessibilityLabel={headerLabel}
|
||||
accessibilityState={headerAccessibilityState}
|
||||
testID="subagents-track-header"
|
||||
onPress={toggleExpanded}
|
||||
leftIcon={expanded ? ChevronDown : ChevronRight}
|
||||
style={styles.headerToggle}
|
||||
textStyle={styles.headerLabel}
|
||||
>
|
||||
{headerLabel}
|
||||
</Button>
|
||||
{showArchiveFinished && onArchiveFinished ? (
|
||||
<View style={styles.headerAction}>
|
||||
<SubagentActionButton
|
||||
accessibilityLabel={t("subagents.archiveFinishedAction")}
|
||||
testID="subagents-track-archive-finished"
|
||||
tooltipLabel={t("subagents.archiveFinishedTooltip")}
|
||||
icon="archive"
|
||||
visible
|
||||
disabled={isArchivingFinished}
|
||||
onPress={onArchiveFinished}
|
||||
/>
|
||||
{archiveFinishedStatus.kind === "archiving" ? (
|
||||
<Text
|
||||
style={styles.archiveFinishedStatus}
|
||||
testID="subagents-track-archive-progress"
|
||||
>
|
||||
{archiveFinishedStatus.completedCount}/{archiveFinishedStatus.totalCount}
|
||||
</Text>
|
||||
) : null}
|
||||
{archiveFinishedStatus.kind === "failed" ? (
|
||||
<Text
|
||||
style={styles.archiveFinishedStatus}
|
||||
testID="subagents-track-archive-failed"
|
||||
>
|
||||
{t("subagents.archiveFinishedRetry", {
|
||||
failed: archiveFinishedStatus.failedCount,
|
||||
total: archiveFinishedStatus.totalCount,
|
||||
})}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{expanded ? (
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
nestedScrollEnabled
|
||||
>
|
||||
{rows.map((row) => (
|
||||
<SubagentsTrackRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
onOpenSubagent={onOpenSubagent}
|
||||
onOpenProviderSubagent={onOpenProviderSubagent}
|
||||
onArchiveSubagent={onArchiveSubagent}
|
||||
onDetachSubagent={onDetachSubagent}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<ComposerTrackPill
|
||||
testID="subagents-track-header"
|
||||
label={pillLabel}
|
||||
panelTitle={t("subagents.title")}
|
||||
statusBucket={aggregateSubagentStatusBucket(rows)}
|
||||
>
|
||||
{rows.map((row) => (
|
||||
<SubagentsTrackRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
onOpenSubagent={onOpenSubagent}
|
||||
onOpenProviderSubagent={onOpenProviderSubagent}
|
||||
onArchiveSubagent={onArchiveSubagent}
|
||||
onDetachSubagent={onDetachSubagent}
|
||||
/>
|
||||
))}
|
||||
{showArchiveFinished && onArchiveFinished ? (
|
||||
<ArchiveFinishedRow
|
||||
status={archiveFinishedStatus}
|
||||
disabled={isArchivingFinished}
|
||||
onPress={onArchiveFinished}
|
||||
/>
|
||||
) : null}
|
||||
</ComposerTrackPill>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk archive, as a row at the foot of the panel rather than an icon next to the count. The
|
||||
* pill has no header to hang an icon off, and a destructive-ish action reads better with its
|
||||
* name attached.
|
||||
*/
|
||||
function ArchiveFinishedRow({
|
||||
status,
|
||||
disabled,
|
||||
onPress,
|
||||
}: {
|
||||
status: ArchiveFinishedStatus;
|
||||
disabled: boolean;
|
||||
onPress: () => void;
|
||||
}): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderRow = useCallback(
|
||||
({ active }: { active: boolean }) => (
|
||||
<>
|
||||
<ThemedArchive
|
||||
size={ROW_ICON_SIZE}
|
||||
uniProps={active ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||
/>
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{t("subagents.archiveFinishedAction")}
|
||||
</Text>
|
||||
{status.kind === "archiving" ? (
|
||||
<Text style={styles.rowTrailing} testID="subagents-track-archive-progress">
|
||||
{status.completedCount}/{status.totalCount}
|
||||
</Text>
|
||||
) : null}
|
||||
{status.kind === "failed" ? (
|
||||
<Text style={styles.rowTrailing} testID="subagents-track-archive-failed">
|
||||
{t("subagents.archiveFinishedRetry", {
|
||||
failed: status.failedCount,
|
||||
total: status.totalCount,
|
||||
})}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[status, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<ComposerTrackRow
|
||||
accessibilityLabel={t("subagents.archiveFinishedAction")}
|
||||
testID="subagents-track-archive-finished"
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
>
|
||||
{renderRow}
|
||||
</ComposerTrackRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +179,6 @@ function SubagentsTrackRow({
|
||||
}: SubagentsTrackRowProps): ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const presentation = useMemo(() => buildRowPresentation(row), [row]);
|
||||
const displayLabel =
|
||||
presentation.titleState === "loading" ? t("common.states.loading") : presentation.label;
|
||||
@@ -198,49 +195,51 @@ function SubagentsTrackRow({
|
||||
const handleDetachPress = useCallback(() => {
|
||||
onDetachSubagent?.(row.id);
|
||||
}, [onDetachSubagent, row.id]);
|
||||
const handlePointerEnter = useCallback(() => setHovered(true), []);
|
||||
const handlePointerLeave = useCallback(() => setHovered(false), []);
|
||||
const actionsAlwaysVisible = isNative || isCompact;
|
||||
const actionsVisible = actionsAlwaysVisible || hovered;
|
||||
|
||||
const renderRow = useCallback(
|
||||
({ active }: { active: boolean }) => (
|
||||
<>
|
||||
<WorkspaceTabIcon presentation={presentation} backdrop={active ? "surface2" : "surface1"} />
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{displayLabel}
|
||||
</Text>
|
||||
{presentation.subtitle ? (
|
||||
<Text style={styles.rowTrailing} numberOfLines={1}>
|
||||
{presentation.subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
{row.kind === "paseo" ? (
|
||||
<SubagentRowActions
|
||||
rowId={row.id}
|
||||
displayLabel={displayLabel}
|
||||
visible={actionsAlwaysVisible || active}
|
||||
onDetachPress={onDetachSubagent ? handleDetachPress : undefined}
|
||||
onArchivePress={handleArchivePress}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[
|
||||
actionsAlwaysVisible,
|
||||
displayLabel,
|
||||
handleArchivePress,
|
||||
handleDetachPress,
|
||||
onDetachSubagent,
|
||||
presentation,
|
||||
row.kind,
|
||||
row.id,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
// Wrapper View handles hover so moving the pointer between the row and
|
||||
// the archive button doesn't drop the hover state — the same pattern
|
||||
// used by sidebar workspace rows.
|
||||
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={displayLabel}
|
||||
testID={`subagents-track-row-${row.id}`}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{({ pressed }) => (
|
||||
<View style={hovered || pressed ? styles.rowActive : styles.row}>
|
||||
<WorkspaceTabIcon
|
||||
presentation={presentation}
|
||||
backdrop={hovered || pressed ? "surface2" : "surface1"}
|
||||
/>
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{displayLabel}
|
||||
</Text>
|
||||
{presentation.subtitle ? (
|
||||
<Text style={styles.rowSubtitle} numberOfLines={1}>
|
||||
{presentation.subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
{row.kind === "paseo" ? (
|
||||
<SubagentRowActions
|
||||
rowId={row.id}
|
||||
displayLabel={displayLabel}
|
||||
visible={actionsVisible}
|
||||
onDetachPress={onDetachSubagent ? handleDetachPress : undefined}
|
||||
onArchivePress={handleArchivePress}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
<ComposerTrackRow
|
||||
accessibilityLabel={displayLabel}
|
||||
testID={`subagents-track-row-${row.id}`}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{renderRow}
|
||||
</ComposerTrackRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -290,9 +289,9 @@ type SubagentActionIcon = "archive" | "detach";
|
||||
function renderSubagentActionIcon(icon: SubagentActionIcon, isActive: boolean): ReactElement {
|
||||
const uniProps = isActive ? foregroundColorMapping : foregroundMutedColorMapping;
|
||||
if (icon === "detach") {
|
||||
return <ThemedUnlink size={14} uniProps={uniProps} />;
|
||||
return <ThemedUnlink size={ROW_ICON_SIZE} uniProps={uniProps} />;
|
||||
}
|
||||
return <ThemedArchive size={14} uniProps={uniProps} />;
|
||||
return <ThemedArchive size={ROW_ICON_SIZE} uniProps={uniProps} />;
|
||||
}
|
||||
|
||||
function SubagentActionButton({
|
||||
@@ -301,7 +300,6 @@ function SubagentActionButton({
|
||||
tooltipLabel,
|
||||
icon,
|
||||
visible,
|
||||
disabled = false,
|
||||
onPress,
|
||||
}: {
|
||||
accessibilityLabel: string;
|
||||
@@ -309,17 +307,15 @@ function SubagentActionButton({
|
||||
tooltipLabel: string;
|
||||
icon: SubagentActionIcon;
|
||||
visible: boolean;
|
||||
disabled?: boolean;
|
||||
onPress: () => void;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger asChild disabled={!visible || disabled}>
|
||||
<TooltipTrigger asChild disabled={!visible}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
testID={testID}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={styles.actionButton}
|
||||
hitSlop={8}
|
||||
@@ -335,97 +331,22 @@ function SubagentActionButton({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
outer: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
track: {
|
||||
width: "100%",
|
||||
maxWidth: MAX_CONTENT_WIDTH,
|
||||
marginBottom: -theme.spacing[4],
|
||||
},
|
||||
surface: {
|
||||
alignSelf: "stretch",
|
||||
backgroundColor: theme.colors.surface1,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.borderAccent,
|
||||
borderBottomWidth: 0,
|
||||
borderTopLeftRadius: theme.borderRadius["2xl"],
|
||||
borderTopRightRadius: theme.borderRadius["2xl"],
|
||||
overflow: "hidden",
|
||||
},
|
||||
surfaceExpanded: {
|
||||
paddingBottom: theme.spacing[4],
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerToggle: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0,
|
||||
paddingLeft: theme.spacing[3],
|
||||
paddingRight: theme.spacing[1],
|
||||
paddingVertical: theme.spacing[2],
|
||||
},
|
||||
headerAction: {
|
||||
paddingRight: theme.spacing[2],
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
archiveFinishedStatus: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
headerCollapsed: {
|
||||
paddingBottom: theme.spacing[4],
|
||||
},
|
||||
headerDivider: {
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerLabel: {
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
scroll: {
|
||||
maxHeight: SUBAGENTS_LIST_MAX_HEIGHT,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
},
|
||||
rowActive: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
// `flexBasis: "auto"` rather than `flex: 1`: a zero-basis label contributes nothing to the row's
|
||||
// intrinsic width, so the panel measures itself at its floor and truncates every label at once.
|
||||
rowLabel: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexBasis: "auto",
|
||||
minWidth: 0,
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
// Keep provider context secondary and bounded so the task remains readable on compact screens.
|
||||
rowSubtitle: {
|
||||
flexShrink: 1,
|
||||
// Trailing metadata — provider context on a subagent row, progress on the archive row. No width
|
||||
// cap: the panel's own ceiling bounds it. It shrinks twice as fast as the label, so a wordy
|
||||
// provider subtitle gives way first instead of squeezing the thing that names the row.
|
||||
rowTrailing: {
|
||||
flexShrink: 2,
|
||||
minWidth: 0,
|
||||
maxWidth: "45%",
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user