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:
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveBottomOverlayTailInset } from "./bottom-overlay-inset";
|
||||
|
||||
describe("resolveBottomOverlayTailInset", () => {
|
||||
it("adds only the space missing after existing footer clearance", () => {
|
||||
expect(
|
||||
resolveBottomOverlayTailInset({
|
||||
overlayHeight: 44,
|
||||
clearance: 12,
|
||||
existingTailSpacing: 24,
|
||||
}),
|
||||
).toBe(32);
|
||||
});
|
||||
|
||||
it("reserves the full overlay and clearance when the tail has no spacing", () => {
|
||||
expect(
|
||||
resolveBottomOverlayTailInset({
|
||||
overlayHeight: 44,
|
||||
clearance: 12,
|
||||
existingTailSpacing: 0,
|
||||
}),
|
||||
).toBe(56);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export function resolveBottomOverlayTailInset({
|
||||
overlayHeight,
|
||||
clearance,
|
||||
existingTailSpacing,
|
||||
}: {
|
||||
overlayHeight: number;
|
||||
clearance: number;
|
||||
existingTailSpacing: number;
|
||||
}): number {
|
||||
return Math.max(0, overlayHeight + clearance - existingTailSpacing);
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("react-native", () => ({
|
||||
Platform: { OS: "web" },
|
||||
Platform: {
|
||||
OS: "web",
|
||||
select: (options: { web?: unknown; default?: unknown }) => options.web ?? options.default,
|
||||
},
|
||||
View: ({ children, testID }: { children?: React.ReactNode; testID?: string }) => (
|
||||
<div data-testid={testID}>{children}</div>
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { memo, useCallback, useMemo, type ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { SPACING, type Theme } from "@/styles/theme";
|
||||
import type { TurnTiming } from "@/timeline/turn-time";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
|
||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||
const workingIndicatorColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
export const TURN_FOOTER_BOTTOM_SPACING = SPACING[6];
|
||||
|
||||
export type TurnContentStrategy = StreamStrategy;
|
||||
export type AssistantTurnForkHandler = (input: {
|
||||
@@ -226,7 +227,7 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
alignItems: "center",
|
||||
alignSelf: "flex-start",
|
||||
minHeight: 24,
|
||||
paddingBottom: theme.spacing[6],
|
||||
paddingBottom: TURN_FOOTER_BOTTOM_SPACING,
|
||||
},
|
||||
turnFooterContent: {
|
||||
height: 24,
|
||||
|
||||
@@ -74,10 +74,12 @@ import { planTimelineTailFetch } from "@/timeline/timeline-sync-plan";
|
||||
import {
|
||||
CompletedTurnFooterRow,
|
||||
TurnFooter,
|
||||
TURN_FOOTER_BOTTOM_SPACING,
|
||||
type AssistantTurnForkHandler,
|
||||
type InFlightTurnForkHandler,
|
||||
type TurnContentStrategy,
|
||||
} from "./turn-footer";
|
||||
import { resolveBottomOverlayTailInset } from "./bottom-overlay-inset";
|
||||
import { layoutStream, type StreamLayoutItem } from "./layout";
|
||||
import {
|
||||
type BottomAnchorLocalRequest,
|
||||
@@ -106,8 +108,9 @@ import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
function renderLiveAuxiliaryNode(input: {
|
||||
pendingPermissions: ReactNode;
|
||||
turnFooter: ReactNode;
|
||||
bottomOverlayInset: number;
|
||||
}): ReactNode {
|
||||
if (!input.pendingPermissions && !input.turnFooter) {
|
||||
if (!input.pendingPermissions && !input.turnFooter && input.bottomOverlayInset === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -118,10 +121,18 @@ function renderLiveAuxiliaryNode(input: {
|
||||
<View style={stylesheet.listHeaderContent}>{input.pendingPermissions}</View>
|
||||
</View>
|
||||
) : null}
|
||||
{input.bottomOverlayInset > 0 ? (
|
||||
<BottomOverlayInset height={input.bottomOverlayInset} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomOverlayInset({ height }: { height: number }) {
|
||||
const style = useMemo(() => ({ height }), [height]);
|
||||
return <View style={style} />;
|
||||
}
|
||||
|
||||
function renderPendingPermissionsNode(input: {
|
||||
pendingPermissions: PendingPermission[];
|
||||
client: DaemonClient | null;
|
||||
@@ -245,6 +256,10 @@ export interface AgentStreamViewProps {
|
||||
turnPresentation: TurnPresentation;
|
||||
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
||||
isAuthoritativeHistoryReady?: boolean;
|
||||
/** Measured transparent overlay at the bottom edge, rendered by the owning panel. */
|
||||
bottomOverlayHeight?: number;
|
||||
/** Visible space kept between the final content and that overlay. */
|
||||
bottomOverlayClearance?: number;
|
||||
toast?: ToastApi | null;
|
||||
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
|
||||
readOnly?: boolean;
|
||||
@@ -293,6 +308,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
turnPresentation,
|
||||
routeBottomAnchorRequest = null,
|
||||
isAuthoritativeHistoryReady = true,
|
||||
bottomOverlayHeight = 0,
|
||||
bottomOverlayClearance = 0,
|
||||
toast,
|
||||
onOpenWorkspaceFile,
|
||||
readOnly = false,
|
||||
@@ -946,11 +963,24 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
}),
|
||||
);
|
||||
const renderLiveAuxiliary = useCallback<StreamSegmentRenderers["renderLiveAuxiliary"]>(() => {
|
||||
const existingTailSpacing =
|
||||
auxiliary.turnFooter && !auxiliary.pendingPermissions ? TURN_FOOTER_BOTTOM_SPACING : 0;
|
||||
const bottomOverlayInset = resolveBottomOverlayTailInset({
|
||||
overlayHeight: bottomOverlayHeight,
|
||||
clearance: bottomOverlayClearance,
|
||||
existingTailSpacing,
|
||||
});
|
||||
return renderLiveAuxiliaryNode({
|
||||
pendingPermissions: auxiliary.pendingPermissions,
|
||||
turnFooter: auxiliary.turnFooter,
|
||||
bottomOverlayInset,
|
||||
});
|
||||
}, [auxiliary.pendingPermissions, auxiliary.turnFooter]);
|
||||
}, [
|
||||
auxiliary.pendingPermissions,
|
||||
auxiliary.turnFooter,
|
||||
bottomOverlayClearance,
|
||||
bottomOverlayHeight,
|
||||
]);
|
||||
|
||||
const renderers = useMemo<StreamSegmentRenderers>(
|
||||
() => ({
|
||||
@@ -1508,7 +1538,7 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
syncingIndicatorText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
invertedWrapper: {
|
||||
transform: [{ scaleY: -1 }],
|
||||
@@ -1516,7 +1546,7 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
emptyStateText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
textAlign: "center",
|
||||
},
|
||||
scrollToBottomContainer: {
|
||||
@@ -1556,7 +1586,7 @@ const permissionStyles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
description: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
lineHeight: 20,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
@@ -1564,10 +1594,10 @@ const permissionStyles = StyleSheet.create((theme) => ({
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
question: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
marginTop: theme.spacing[1],
|
||||
marginBottom: theme.spacing[1],
|
||||
color: theme.colors.foregroundMuted,
|
||||
@@ -1603,7 +1633,7 @@ const permissionStyles = StyleSheet.create((theme) => ({
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
optionText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user