Skip to content
Merged
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
12 changes: 6 additions & 6 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showsGitControls: false,
showEnvironmentIndicator: true,
hostsRestingComposerControls: false,
hasCapacityReading: false,
Expand All @@ -440,7 +440,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showsGitControls: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
hasCapacityReading: false,
Expand All @@ -452,7 +452,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showsGitControls: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: true,
hasCapacityReading: false,
Expand All @@ -464,7 +464,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: true,
showsGitControls: true,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
hasCapacityReading: false,
Expand All @@ -478,7 +478,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showsGitControls: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
hasCapacityReading: true,
Expand All @@ -490,7 +490,7 @@ describe("shouldShowComposerContextStrip", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: false,
isGitRepo: true,
showsGitControls: true,
showEnvironmentIndicator: true,
hostsRestingComposerControls: true,
hasCapacityReading: true,
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ export function shouldShowEnvironmentIndicator(input: {

export function shouldShowComposerContextStrip(input: {
hasActiveProject: boolean;
isGitRepo: boolean;
/**
* The Git controls the strip will actually draw — not merely whether the
* project is a repository. A rollback suppresses them while the repository
* is still there, and a strip kept open for controls nobody renders is an
* empty bar.
*/
showsGitControls: boolean;
showEnvironmentIndicator: boolean;
/** A collapsed composer's controls currently fit in their measured strip host. */
hostsRestingComposerControls: boolean;
Expand All @@ -71,7 +77,7 @@ export function shouldShowComposerContextStrip(input: {
}): boolean {
return (
input.hasActiveProject &&
(input.isGitRepo ||
(input.showsGitControls ||
input.showEnvironmentIndicator ||
input.hostsRestingComposerControls ||
input.hasCapacityReading)
Expand Down
61 changes: 61 additions & 0 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
getAntigravitySendBlockReason,
waitForRevertedMessage,
prepareRevertedMessageAttachments,
resolveComposerContextStripVisibility,
} from "./ChatView.logic";

describe("agent browser close confirmation", () => {
Expand Down Expand Up @@ -2752,3 +2753,63 @@ describe("draft retention through worktree setup", () => {
expect(canFinalizePromotedDraft({ ...preparing, latestTurn: completedTurn })).toBe(true);
});
});

describe("resolveComposerContextStripVisibility", () => {
// A thread on a Git repo, at rest, with nothing else to show.
const base = {
hasActiveProject: true,
showGitControls: true,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
restingComposerControlsVisible: false,
hasCapacityReading: false,
};

it("shows the strip for the Git controls it will draw", () => {
expect(resolveComposerContextStripVisibility(base)).toEqual({ mount: true, visible: true });
});

// The repository is still there during a rollback, but every Git control is
// suppressed, so keeping the strip open would leave an empty bar.
it("hides the strip when a rollback suppresses the Git controls", () => {
expect(resolveComposerContextStripVisibility({ ...base, showGitControls: false })).toEqual({
mount: false,
visible: false,
});
});

it("keeps the strip during a rollback when capacity still has something to say", () => {
expect(
resolveComposerContextStripVisibility({
...base,
showGitControls: false,
hasCapacityReading: true,
}),
).toEqual({ mount: true, visible: true });
});

// The reason the strip is mounted at all on a route that hosts relocated
// controls: it has to be measurable before anyone knows whether they fit.
it("mounts but hides the measuring strip until the relocated controls fit", () => {
const input = {
...base,
showGitControls: false,
hostsRestingComposerControls: true,
restingComposerControlsVisible: false,
};
expect(resolveComposerContextStripVisibility(input)).toEqual({ mount: true, visible: false });
expect(
resolveComposerContextStripVisibility({ ...input, restingComposerControlsVisible: true }),
).toEqual({ mount: true, visible: true });
});

it("shows nothing without a project to describe", () => {
expect(
resolveComposerContextStripVisibility({
...base,
hasActiveProject: false,
hasCapacityReading: true,
}),
).toEqual({ mount: false, visible: false });
});
});
40 changes: 40 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { shouldShowComposerContextStrip } from "./BranchToolbar.logic";
import {
ANTIGRAVITY_DEFAULT_MODEL,
type AssetCreateUrlInput,
Expand Down Expand Up @@ -1471,3 +1472,42 @@ export function restorePlanFollowUpComposer(input: {
detectTrigger: true,
});
}

/**
* Whether the composer's context strip is mounted, and whether it is visible.
*
* The two differ on purpose: an existing thread keeps a hidden, off-flow strip
* mounted so the composer can measure whether its relocated controls fit there,
* while the visible chrome stays content-driven. Assembled here rather than
* inline in the view so the inputs — the part that has twice been wrong — are
* testable without rendering a chat.
*/
export function resolveComposerContextStripVisibility(input: {
readonly hasActiveProject: boolean;
/** What `BranchToolbar` will actually draw, so a rollback cannot leave an empty bar. */
readonly showGitControls: boolean;
readonly showEnvironmentIndicator: boolean;
/** This route hosts the collapsed composer's relocated controls. */
readonly hostsRestingComposerControls: boolean;
/** Those controls currently fit, so they are really in the strip. */
readonly restingComposerControlsVisible: boolean;
readonly hasCapacityReading: boolean;
}): { readonly mount: boolean; readonly visible: boolean } {
const common = {
hasActiveProject: input.hasActiveProject,
showsGitControls: input.showGitControls,
showEnvironmentIndicator: input.showEnvironmentIndicator,
hasCapacityReading: input.hasCapacityReading,
};
return {
mount: shouldShowComposerContextStrip({
...common,
hostsRestingComposerControls: input.hostsRestingComposerControls,
}),
visible: shouldShowComposerContextStrip({
...common,
hostsRestingComposerControls:
input.hostsRestingComposerControls && input.restingComposerControlsVisible,
}),
};
}
33 changes: 16 additions & 17 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,6 @@ import {
type EnvironmentOption,
resolveEffectiveEnvMode,
resolveLocalCheckoutBranchMismatch,
shouldShowComposerContextStrip,
shouldShowEnvironmentIndicator,
} from "./BranchToolbar.logic";
import {
Expand Down Expand Up @@ -522,6 +521,7 @@ import {
toolGroupConsumesUpwardNavigation,
waitForStartedServerThread,
shouldRefocusComposerOnWindowFocus,
resolveComposerContextStripVisibility,
} from "./ChatView.logic";
import type { ThreadSyncPhase } from "../threadSync";
import { useLocalStorage } from "~/hooks/useLocalStorage";
Expand Down Expand Up @@ -3945,7 +3945,11 @@ export default function ChatView(props: ChatViewProps) {
}
}, [environmentId, gitStatusCwd, liveIsGitRepo]);
const isGitRepo = liveIsGitRepo ?? recallCheckoutIsRepo(environmentId, gitStatusCwd) ?? true;

// One source for the Git run: what the strip shows must be what it draws.
const showComposerGitControls = isGitRepo && !rollbackActive;
const initialDiffPanelGitScope =
gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
const terminalShortcutLabelOptions = useMemo(
() => ({
context: {
Expand Down Expand Up @@ -6357,20 +6361,15 @@ export default function ChatView(props: ChatViewProps) {
const composerUsageHasContent =
hasComposerUsageContent(composerUsage) ||
(settings.showProviderUsageInContextPopover && serverConfig === null);
const mountComposerContextStrip = shouldShowComposerContextStrip({
hasActiveProject: activeProject !== null,
isGitRepo,
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server",
hasCapacityReading: composerUsageHasContent,
});
const showComposerContextStrip = shouldShowComposerContextStrip({
hasActiveProject: activeProject !== null,
isGitRepo,
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,
hasCapacityReading: composerUsageHasContent,
});
const { mount: mountComposerContextStrip, visible: showComposerContextStrip } =
resolveComposerContextStripVisibility({
hasActiveProject: activeProject !== null,
showGitControls: showComposerGitControls,
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server",
restingComposerControlsVisible,
hasCapacityReading: composerUsageHasContent,
});
// The strip dims a reading the server should already have replaced, so the
// bound follows the server's own poll rather than a fixed number.
const composerUsageStaleAfterMs = useMemo(
Expand Down Expand Up @@ -10798,7 +10797,7 @@ export default function ChatView(props: ChatViewProps) {
ref={branchToolbarRef}
environmentId={activeThread.environmentId}
threadId={activeThread.id}
showGitControls={isGitRepo && !rollbackActive}
showGitControls={showComposerGitControls}
{...(routeKind === "draft" && draftId ? { draftId } : {})}
onEnvModeChange={onEnvModeChange}
startFromOrigin={startFromOrigin}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ComposerUsageIndicator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function renderUsage(
// Exercise the gate ChatView uses when there are no other reasons to show the strip.
const showStrip = shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showsGitControls: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
hasCapacityReading: hasComposerUsageContent(usage),
Expand Down
Loading