Skip to content
Open
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
27 changes: 25 additions & 2 deletions apps/app/.ladle/settings-story-fixtures.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import { QueryClientProvider } from "@tanstack/react-query";
import { PERSONAL_PROJECT_ID } from "@bb/domain";
import { PERSONAL_PROJECT_ID, type ProviderInfo } from "@bb/domain";
import { UPDATE_ACTION_ICON } from "@bb/domain/update-state";
import type {
SidebarBootstrapResponse,
Expand All @@ -15,6 +15,7 @@ import {
pluginMarketplacesQueryKey,
sidebarNavigationQueryKey,
systemConfigQueryKey,
systemProvidersQueryKey,
systemVersionQueryKey,
} from "../src/hooks/queries/query-keys";
import {
Expand All @@ -23,6 +24,7 @@ import {
} from "../src/hooks/useUpdateInventory";
import { createAppQueryClient } from "../src/lib/query-client";
import { makeSystemConfig } from "../src/test/fixtures/system-config";
import { makeProviderInfo } from "../src/test/provider-info-fixture";
import { getSettingsRoutePath } from "../src/lib/route-paths";
import {
BbAppUpdateRows,
Expand All @@ -39,6 +41,9 @@ import {
makeProject,
makeProviderCliStatus,
} from "./story-fixtures";
import codexLogoUrl from "../../../plugins/provider-codex/icons/codex.svg";
import claudeCodeLogoUrl from "../../../plugins/provider-claude-code/icons/claude-code.svg";
import cursorLogoUrl from "../../../plugins/provider-acp/icons/cursor.svg";

const SETTINGS_STORY_NOW = Date.parse("2026-08-19T08:00:00.000Z");

Expand Down Expand Up @@ -140,6 +145,24 @@ const systemVersion = {
upgradeCommand: "npx bb-app@latest",
} satisfies SystemVersionResponse;

const systemProviders = [
makeProviderInfo({
id: "codex",
displayName: "Codex",
logoUrl: codexLogoUrl,
}),
makeProviderInfo({
id: "claude-code",
displayName: "Claude Code",
logoUrl: claudeCodeLogoUrl,
}),
makeProviderInfo({
id: "acp-cursor",
displayName: "Cursor",
logoUrl: cursorLogoUrl,
}),
] satisfies ProviderInfo[];

const settingsUpdateMachine = {
host: SETTINGS_STORY_PRIMARY_HOST,
isPrimary: true,
Expand Down Expand Up @@ -167,7 +190,6 @@ export function SettingsUpdatesStory() {
label="Update all 1 CLI tool"
tooltipLabel="Update all"
icon={UPDATE_ACTION_ICON}
iconPosition="end"
visibleLabel="Update all"
variant="default"
onClick={noop}
Expand Down Expand Up @@ -208,6 +230,7 @@ function createSettingsStoryQueryClient() {
});
queryClient.setQueryData(hostsQueryKey(), SETTINGS_STORY_HOSTS);
queryClient.setQueryData(systemConfigQueryKey(), systemConfig);
queryClient.setQueryData(systemProvidersQueryKey(), systemProviders);
queryClient.setQueryData(systemVersionQueryKey(), systemVersion);
queryClient.setQueryData(sidebarNavigationQueryKey(), sidebarNavigation);
queryClient.setQueryData(pluginMarketplacesQueryKey(), []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,37 @@ const customNameTarget: EnvironmentRenameDialogTarget = {
canClearName: true,
};

export function BranchContext() {
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<StoryCard>
<StoryRow
label="branch context"
hint="current branch beneath the custom worktree name"
>
<DialogStage>
<EnvironmentRenameDialogContent
target={{
id: "env_named",
currentName: "Design system polish",
branchName: "bb/design-system-polish",
canClearName: true,
}}
pending={false}
onRename={noop}
inputRef={inputRef}
/>
</DialogStage>
</StoryRow>
</StoryCard>
);
}

export function Overview() {
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<StoryCard>
<StoryRow label="branch placeholder" hint="unnamed environment">
<StoryRow label="branch placeholder" hint="unnamed worktree">
<DialogStage>
<EnvironmentRenameDialogContent
target={unnamedTarget}
Expand Down Expand Up @@ -65,7 +91,7 @@ export function Overview() {
<EnvironmentRenameDialogContent
target={customNameTarget}
pending={false}
errorMessage="Environment name must be 80 characters or fewer."
errorMessage="Worktree name must be 80 characters or fewer."
onRename={noop}
inputRef={inputRef}
/>
Expand Down
15 changes: 11 additions & 4 deletions apps/app/src/components/dialogs/EnvironmentRenameDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const ENVIRONMENT_NAME_MAX_LENGTH = 80;

const ENVIRONMENT_NAME_LENGTH_RULE = {
limit: ENVIRONMENT_NAME_MAX_LENGTH,
message: `Environment name must be ${ENVIRONMENT_NAME_MAX_LENGTH} characters or fewer.`,
message: `Worktree name must be ${ENVIRONMENT_NAME_MAX_LENGTH} characters or fewer.`,
};

export interface EnvironmentRenameDialogTarget {
Expand Down Expand Up @@ -65,17 +65,24 @@ export function EnvironmentRenameDialogContent({
}: EnvironmentRenameDialogContentProps) {
return (
<RenameDialogContent
entityLabel="environment"
entityLabel="worktree"
initialName={target.currentName}
pending={pending}
errorMessage={errorMessage}
placeholder={target.branchName ?? "Environment name"}
placeholder={target.branchName ?? "Worktree name"}
inputDetails={
target.canClearName && target.branchName ? (
<p className="truncate text-xs text-muted-foreground">
Branch: <span className="font-mono">{target.branchName}</span>
</p>
) : undefined
}
maxLength={ENVIRONMENT_NAME_LENGTH_RULE}
autoCapitalize="sentences"
clearAction={
target.canClearName
? {
label: "Use branch name",
label: "Clear custom name",
onClear: () => onRename(target.id, null),
}
: undefined
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/components/dialogs/RenameDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface RenameDialogContentProps {
pending: boolean;
errorMessage?: string | null;
placeholder?: string;
inputDetails?: ReactNode;
maxLength?: { limit: number; message: string };
autoCapitalize: "words" | "sentences";
compact?: boolean;
Expand All @@ -65,6 +66,7 @@ export function RenameDialogContent({
pending,
errorMessage,
placeholder,
inputDetails,
maxLength,
autoCapitalize,
compact = false,
Expand Down Expand Up @@ -119,6 +121,7 @@ export function RenameDialogContent({
clearMessage();
}}
/>
{inputDetails}
{displayedErrorMessage ? (
<p className="text-sm text-destructive">{displayedErrorMessage}</p>
) : null}
Expand Down
15 changes: 15 additions & 0 deletions apps/app/src/components/pickers/ModelReasoningPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ function renderPicker({
modelLoadError = null,
compact = false,
splitPane = false,
muted = false,
}: {
onSelectedProviderChange?: ((value: string) => void) | null;
onModelChange?: (value: string) => void;
Expand All @@ -176,6 +177,7 @@ function renderPicker({
modelLoadError?: SystemExecutionOptionsModelLoadError | null;
compact?: boolean;
splitPane?: boolean;
muted?: boolean;
} = {}) {
const { queryClient, wrapper } = createQueryClientTestHarness();
queryClient.setQueryData(
Expand Down Expand Up @@ -215,6 +217,7 @@ function renderPicker({
fastModeEnabled={false}
onFastModeChange={vi.fn()}
showFastModeToggle={false}
muted={muted}
modal={false}
/>
<button type="button">Composer action</button>
Expand Down Expand Up @@ -248,6 +251,18 @@ afterEach(() => {
});

describe("ModelReasoningPicker", () => {
it("uses the lower-emphasis chrome token for the composer caret", () => {
renderPicker({ muted: true });

const trigger = screen.getByRole("button", {
name: "Provider, model and reasoning",
});
expect(
trigger.querySelector('[data-icon="ChevronDown"]')?.classList,
).toContain("text-subtle-foreground/75");
expect(trigger.classList).toContain("font-normal");
});

it("gives a non-SVG provider mark the same 16px trigger size as button SVGs", () => {
renderPicker({
pickerProviderOptions: [
Expand Down
6 changes: 5 additions & 1 deletion apps/app/src/components/pickers/ModelReasoningPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ export function ModelReasoningPicker({
OPTION_INTERACTIVE_CLASS_NAME,
LIST_HOVER_TRANSITION,
muted && OPTION_MUTED_CLASS_NAME,
muted && "font-normal",
disabled && "cursor-default disabled:opacity-100",
className,
)}
Expand Down Expand Up @@ -855,7 +856,10 @@ export function ModelReasoningPicker({
{disabled ? null : (
<Icon
name="ChevronDown"
className="size-3.5 shrink-0 text-muted-foreground"
className={cn(
"size-3.5 shrink-0",
muted ? "text-subtle-foreground/75" : "text-muted-foreground",
)}
/>
)}
<AppCommandShortcutHint
Expand Down
9 changes: 4 additions & 5 deletions apps/app/src/components/pickers/OptionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
const OPTION_WARNING_TEXT_CLASS_NAME = "text-warning-text";
const OPTION_WARNING_INTERACTIVE_CLASS_NAME =
"hover:text-warning-text data-[state=open]:text-warning-text";
const OPTION_WARNING_ICON_CLASS_NAME = "text-warning-text";

export interface PickerOption<T extends string> {
value: T;
Expand All @@ -41,6 +40,7 @@ interface OptionPickerProps<T extends string> {
options: readonly PickerOption<T>[];
onChange: (value: T) => void;
className?: string;
caretClassName?: string;
contentClassName?: string;
muted?: boolean;
defaultOpen?: boolean;
Expand All @@ -62,6 +62,7 @@ export function OptionPicker<T extends string>({
options,
onChange,
className,
caretClassName,
contentClassName,
muted,
defaultOpen,
Expand Down Expand Up @@ -124,10 +125,8 @@ export function OptionPicker<T extends string>({
<Icon
name="ChevronDown"
className={cn(
"size-3.5 shrink-0",
selectedIsWarning
? OPTION_WARNING_ICON_CLASS_NAME
: "text-muted-foreground",
"size-3.5 shrink-0 text-muted-foreground",
caretClassName,
)}
/>
)}
Expand Down
16 changes: 16 additions & 0 deletions apps/app/src/components/pickers/PermissionModePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ afterEach(() => {
});

describe("PermissionModePicker", () => {
it("keeps the warning mode caret aligned with the other prompt box carets", () => {
const { container } = render(
<PermissionModePicker
value="full"
options={permissionOptions}
onChange={vi.fn()}
supported
/>,
);

const caret = container.querySelector('[data-icon="ChevronDown"]');
expect(caret).not.toBeNull();
expect(caret!.classList).toContain("text-subtle-foreground/75");
expect(caret!.classList).not.toContain("text-warning-text");
});

it("can show an effective display override without changing the selected value", () => {
const onChange = vi.fn();
render(
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/components/pickers/PermissionModePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function PermissionModePicker({
options={compactOptions}
onChange={onChange}
className={cn(LIST_HOVER_TRANSITION, className)}
caretClassName="text-subtle-foreground/75"
contentClassName="max-w-72"
muted={muted}
defaultOpen={defaultOpen}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useEffect, useRef } from "react";
import type { ExperimentalFileOpenOptions } from "@get-bb/plugin-sdk";
import type { ExperimentalResolvedFileOpenOptions } from "@get-bb/plugin-sdk";
import { appToast } from "@/components/ui/app-toast";
import { useLocalOpenTargets } from "@/hooks/useLocalOpenTargets";
import { useResolvedLiveFileTarget } from "@/hooks/useResolvedLiveFileTarget";
import { getExperimentalFileLocationStart } from "@/lib/live-file-navigation";
import {
getExperimentalFileLocationStart,
liveFileTargetFromIdentity,
} from "@/lib/live-file-navigation";

export function AppFileExternalNavigationDispatcher({
intent,
onSettled,
}: {
intent: ExperimentalFileOpenOptions;
intent: ExperimentalResolvedFileOpenOptions;
onSettled: () => void;
}) {
const didSettleRef = useRef(false);
const resolvedTarget = useResolvedLiveFileTarget(intent.target, {
enabled: true,
});
const resolvedTarget = useResolvedLiveFileTarget(
liveFileTargetFromIdentity(intent.identity),
{
enabled: true,
},
);
const { isLoading: areLocalTargetsLoading, openPathInPreferredFileTarget } =
useLocalOpenTargets({
enabled: resolvedTarget.status === "available",
Expand All @@ -40,14 +46,14 @@ export function AppFileExternalNavigationDispatcher({
});
return;
}
const location = getExperimentalFileLocationStart(intent.location);
const location = getExperimentalFileLocationStart(intent.identity.location);
void openPathInPreferredFileTarget({
columnNumber: location.columnNumber,
lineNumber: location.lineNumber,
path: resolvedTarget.absolutePath,
});
}, [
intent.location,
intent.identity.location,
areLocalTargetsLoading,
openPathInPreferredFileTarget,
onSettled,
Expand Down
Loading