Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 10 additions & 32 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,22 +425,16 @@ describe("shouldShowEnvironmentIndicator", () => {
});

describe("shouldShowComposerContextStrip", () => {
it.each([false, true])(
"honors the active-thread preference with resting controls %s",
(hostsRestingComposerControls) => {
const input = {
isDraftHeroState: false,
hasActiveProject: true,
isGitRepo: true,
showEnvironmentIndicator: true,
hostsRestingComposerControls,
};
expect(shouldShowComposerContextStrip({ ...input, persistInActiveThreads: false })).toBe(
false,
);
expect(shouldShowComposerContextStrip({ ...input, persistInActiveThreads: true })).toBe(true);
},
);
it("honors the active-thread preference", () => {
const input = {
isDraftHeroState: false,
hasActiveProject: true,
isGitRepo: true,
showEnvironmentIndicator: true,
};
expect(shouldShowComposerContextStrip({ ...input, persistInActiveThreads: false })).toBe(false);
expect(shouldShowComposerContextStrip({ ...input, persistInActiveThreads: true })).toBe(true);
});

it("keeps the environment indicator visible for a non-Git project", () => {
expect(
Expand All @@ -450,7 +444,6 @@ describe("shouldShowComposerContextStrip", () => {
hasActiveProject: true,
isGitRepo: false,
showEnvironmentIndicator: true,
hostsRestingComposerControls: false,
}),
).toBe(true);
});
Expand All @@ -463,24 +456,10 @@ describe("shouldShowComposerContextStrip", () => {
hasActiveProject: true,
isGitRepo: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
}),
).toBe(false);
});

it("keeps the strip for visible resting composer controls in a non-Git thread", () => {
expect(
shouldShowComposerContextStrip({
isDraftHeroState: true,
persistInActiveThreads: false,
hasActiveProject: true,
isGitRepo: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: true,
}),
).toBe(true);
});

it("shows Git controls without requiring an environment indicator", () => {
expect(
shouldShowComposerContextStrip({
Expand All @@ -489,7 +468,6 @@ describe("shouldShowComposerContextStrip", () => {
hasActiveProject: true,
isGitRepo: true,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
}),
).toBe(true);
});
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,11 @@ export function shouldShowComposerContextStrip(input: {
hasActiveProject: boolean;
isGitRepo: boolean;
showEnvironmentIndicator: boolean;
/** A collapsed composer's controls currently fit in their measured strip host. */
hostsRestingComposerControls: boolean;
}): boolean {
return (
input.hasActiveProject &&
(input.isDraftHeroState || input.persistInActiveThreads) &&
(input.isGitRepo || input.showEnvironmentIndicator || input.hostsRestingComposerControls)
(input.isGitRepo || input.showEnvironmentIndicator)
);
}

Expand Down
43 changes: 3 additions & 40 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { MiddleTruncate } from "./ui/middle-truncate";
import { ComposerSurface } from "./chat/ComposerSurface";
import { useComposerMenuProps } from "./chat/composerEventScope";
import { measureRestingComposerControls } from "./chat/restingComposerControlsMeasurement";
import { resolveRestingComposerControlsNaturalWidth } from "./composerFooterLayout";
import { cn } from "~/lib/utils";

export interface BranchToolbarHandle {
Expand Down Expand Up @@ -89,8 +87,6 @@ interface BranchToolbarProps {
onComposerFocusRequest?: () => void;
availableEnvironments?: readonly EnvironmentOption[];
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
composerControlsHostRef?: (element: HTMLDivElement | null) => void;
contextStripVisible?: boolean;
}

interface MobileRunContextSelectorProps {
Expand Down Expand Up @@ -384,20 +380,7 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean {
let groups = 0;
for (const child of current.children) {
if (!(child instanceof HTMLElement)) continue;
// The host itself flexes into all remaining room. Reserve the natural
// width of the controls inside it, blocks in overflow included, so Git
// labels compact before squeezing out the model picker. Reserving only
// the visible controls would let the labels expand into room the
// composer just freed, shrink the host, and hide the controls again.
const hostedControls = child.matches('[data-chat-resting-composer-controls-host="true"]')
? child.querySelector<HTMLElement>('[data-chat-composer-resting-controls="true"]')
: null;
const hostedMeasurement = hostedControls
? measureRestingComposerControls(hostedControls)
: null;
const width = hostedMeasurement
? resolveRestingComposerControlsNaturalWidth(hostedMeasurement)
: contentWidth(hostedControls ?? child);
const width = contentWidth(child);
if (width <= 1) continue;
groups += 1;
needed += width;
Expand Down Expand Up @@ -520,8 +503,6 @@ export const BranchToolbar = memo(function BranchToolbar({
onComposerFocusRequest,
availableEnvironments,
onEnvironmentChange,
composerControlsHostRef,
contextStripVisible = true,
}: BranchToolbarProps) {
const branchSelectorRef = useRef<BranchToolbarBranchSelectorHandle>(null);
const threadRef = useMemo(
Expand Down Expand Up @@ -652,13 +633,7 @@ export const BranchToolbar = memo(function BranchToolbar({
<ComposerSurface.ContextStrip
ref={setStripElement}
data-compact={labelsOverflow ? "" : undefined}
className={cn(
"gap-1 text-xs font-normal text-muted-foreground/70",
// A non-Git strip with no visible composer controls should occupy no
// space, but its host must retain a prospective width so controls can
// become visible again when the chat view grows.
!contextStripVisible && "pointer-events-none invisible absolute inset-x-0 top-full",
)}
className="gap-1 text-xs font-normal text-muted-foreground/70"
>
{showGitControls ? (
<div className="contents @3xl/composer-surface:hidden">
Expand Down Expand Up @@ -687,7 +662,7 @@ export const BranchToolbar = memo(function BranchToolbar({
className={cn(
"min-h-7 min-w-10 items-center gap-1 sm:min-h-6",
showGitControls ? "hidden @3xl/composer-surface:flex" : "flex",
composerControlsHostRef ? "shrink" : "flex-1",
"flex-1",
)}
>
{showEnvironmentIndicator && availableEnvironments && (
Expand Down Expand Up @@ -724,18 +699,6 @@ export const BranchToolbar = memo(function BranchToolbar({
</div>
) : null}

{composerControlsHostRef ? (
// The host takes whatever the workspace and branch controls leave
// over, in both strip layouts, so a collapsed composer can show its
// model and mode controls wherever they fit.
<div
ref={composerControlsHostRef}
data-composer-context-control
data-chat-resting-composer-controls-host="true"
className="flex min-w-0 flex-1 items-center justify-start overflow-x-clip overflow-y-visible"
/>
) : null}

{showGitControls ? (
<BranchToolbarBranchSelector
forceNewWorktree={forceNewWorktree}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ export function BranchToolbarBranchSelector({

useEffect(() => {
if (
hasServerThread ||
effectiveEnvMode !== "worktree" ||
activeWorktreePath ||
activeThreadBranch ||
Expand All @@ -520,6 +521,7 @@ export function BranchToolbarBranchSelector({
activeThreadBranch,
activeWorktreePath,
effectiveEnvMode,
hasServerThread,
setThreadBranch,
worktreeBaseBranchCandidate,
]);
Expand Down
63 changes: 3 additions & 60 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1788,9 +1788,6 @@ export default function ChatView(props: ChatViewProps) {
const composerRef = useComposerHandleContext() ?? localComposerRef;
const branchToolbarRef = useRef<BranchToolbarHandle>(null);
const pasteAsTextShortcutUntilRef = useRef(0);
const [restingComposerControlsHost, setRestingComposerControlsHost] =
useState<HTMLDivElement | null>(null);
const [restingComposerControlsVisible, setRestingComposerControlsVisible] = useState(false);
const citeAssistantText = useCallback(
(citation: AssistantCitation, sourceAnchor: AssistantCitationSourceAnchor) => {
const inserted = composerRef.current?.citeAssistantText(citation, sourceAnchor) ?? false;
Expand Down Expand Up @@ -4178,9 +4175,6 @@ export default function ChatView(props: ChatViewProps) {
}
}, [environmentId, gitStatusCwd, liveIsGitRepo]);
const isGitRepo = liveIsGitRepo ?? recallCheckoutIsRepo(environmentId, gitStatusCwd) ?? true;
// When context is enabled, keep a hidden, off-flow strip mounted so the composer
// can measure whether its relocated controls fit. The visible chrome remains
// content-driven: Git/environment context or controls that actually fit.
// A provider-native subagent cannot take messages: a status bar replaces the
// composer and its strips. Its approvals and questions are asked on the
// top-level parent thread.
Expand Down Expand Up @@ -4209,25 +4203,14 @@ export default function ChatView(props: ChatViewProps) {
providerSubagentModels,
reportedModelSelection,
);
const mountComposerContextStrip = shouldShowComposerContextStrip({
isDraftHeroState,
persistInActiveThreads: settings.persistComposerContextStrip,
hasActiveProject: activeProject !== null && !showProviderSubagentBar,
isGitRepo,
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server",
});

const showComposerContextStrip = shouldShowComposerContextStrip({
isDraftHeroState,
persistInActiveThreads: settings.persistComposerContextStrip,
hasActiveProject: activeProject !== null && !showProviderSubagentBar,
isGitRepo,
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,
});
const mountComposerModelStrip =
routeKind === "server" && !mountComposerContextStrip && !showProviderSubagentBar;
const showComposerModelStrip = mountComposerModelStrip && restingComposerControlsVisible;
const terminalShortcutLabelOptions = useMemo(
() => ({
context: {
Expand Down Expand Up @@ -6776,23 +6759,10 @@ export default function ChatView(props: ChatViewProps) {
composerMounted: composerMountedRef.current,
composerReportedResting: composerRestingRef.current,
});
const modelStrip = composerOverlayElement?.querySelector<HTMLElement>(
'[data-composer-model-strip="true"]',
);
// The model-only strip disappears on expansion; counting it again would
// add 32px of timeline padding as soon as the empty composer collapses.
const restingOnlyHeight =
modelStrip && isResting
? Math.max(
0,
modelStrip.offsetHeight + Number.parseFloat(getComputedStyle(modelStrip).marginTop),
)
: 0;
const nextInset = resolveComposerTimelineInset({
currentInset: composerTimelineInsetRef.current,
overlayHeight: nextHeight,
isResting,
restingOnlyHeight,
});
if (composerTimelineInsetRef.current !== nextInset) {
composerTimelineInsetRef.current = nextInset;
Expand Down Expand Up @@ -11229,9 +11199,7 @@ export default function ChatView(props: ChatViewProps) {
: undefined
}
>
<ComposerSurface.Shell
contextStrip={showComposerContextStrip || showComposerModelStrip}
>
<ComposerSurface.Shell contextStrip={showComposerContextStrip}>
<ComposerSurface.Host
inert={isSavingQueuedEdit}
aria-busy={isSavingQueuedEdit}
Expand Down Expand Up @@ -11383,12 +11351,6 @@ export default function ChatView(props: ChatViewProps) {
pullRequestRepository={
supportsPullRequests ? activeProjectRepository : null
}
restingControlsHost={restingComposerControlsHost}
restingControlsHaveLeadingContext={
mountComposerContextStrip &&
(isGitRepo || showComposerEnvironmentIndicator)
}
onRestingControlsVisibilityChange={setRestingComposerControlsVisible}
getTimelineScrollableNode={getTimelineScrollableNode}
isTimelineAtLogicalEnd={isTimelineAtLogicalEnd}
timelineOverflows={timelineOverflows}
Expand Down Expand Up @@ -11440,24 +11402,7 @@ export default function ChatView(props: ChatViewProps) {
data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}
className="relative z-0"
>
{mountComposerModelStrip ? (
<ComposerSurface.ContextStrip
data-composer-model-strip="true"
aria-hidden={showComposerModelStrip ? undefined : true}
inert={showComposerModelStrip ? undefined : true}
className={cn(
"ps-2 group-data-model-strip-transition/composer-surface:before:backdrop-blur-(--glass-blur) group-data-model-strip-transition/composer-surface:before:bg-(--chat-composer-glass-surface)/(--glass-opacity)",
!showComposerModelStrip &&
"pointer-events-none invisible absolute inset-x-0 top-full",
)}
>
<div
ref={setRestingComposerControlsHost}
className="min-w-0 flex-1"
/>
</ComposerSurface.ContextStrip>
) : null}
{mountComposerContextStrip && (
{showComposerContextStrip && (
<div className="pointer-events-auto">
<BranchToolbar
forceNewWorktree={multipleModelSelections !== null}
Expand Down Expand Up @@ -11493,8 +11438,6 @@ export default function ChatView(props: ChatViewProps) {
: undefined
}
availableEnvironments={logicalProjectEnvironments}
composerControlsHostRef={setRestingComposerControlsHost}
contextStripVisible={showComposerContextStrip}
/>
</div>
)}
Expand Down
Loading
Loading