From 3f7e732c3a1e8adc7f0671009badadb9b129d762 Mon Sep 17 00:00:00 2001 From: brsbl Date: Sat, 29 Aug 2026 01:18:06 -0700 Subject: [PATCH 01/19] Use Zap icon for native Claude Skill calls (#2609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Human comments ## What was wrong Native Claude `Skill` calls used the generic plugin Puzzle glyph instead of bb’s established skill glyph. ## What changed Maps native Claude `Skill` calls to the Zap glyph and adds focused provider and story coverage. ### Before — parent `main` at `93643a53` Native Claude Skill calls use the generic Puzzle glyph. ![Before — native Skill call with Puzzle icon](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2609-skill-before.png) ### After — PR head `d8a06e53` The same row uses the established Zap skill glyph. ![After — native Skill call with Zap icon](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2609-skill-after.png) ## How you verified - Exact parent/head Ladle states rendered in Chrome for Testing 149.0.7827.55. - The focused provider presentation test and app story coverage pass in remote CI. - All required CI checks are green. ## Fixes No linked GitHub issue; addresses the reported icon regression. BB-Thread-ID: thr_fdabesxhdr > AGENT GENERATED --- .../thread/timeline/rows/Tool.stories.tsx | 35 +++++++++++++++++++ .../src/presentation.test.ts | 13 +++++++ .../provider-claude-code/src/presentation.ts | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/apps/app/src/components/thread/timeline/rows/Tool.stories.tsx b/apps/app/src/components/thread/timeline/rows/Tool.stories.tsx index 3ddb7a4c62..3ac271f986 100644 --- a/apps/app/src/components/thread/timeline/rows/Tool.stories.tsx +++ b/apps/app/src/components/thread/timeline/rows/Tool.stories.tsx @@ -36,6 +36,26 @@ const toolSearchTool: TimelineRow = toolRow({ durationMs: 105, }); +const nativeSkillTool: TimelineRow = toolRow({ + id: "thr_skill_native:tool:toolu_skill_native", + threadId: "thr_skill_native", + turnId: "turn_skill_native_1", + sourceSeqStart: 1, + sourceSeqEnd: 2, + status: "completed", + callId: "toolu_skill_native", + toolName: "Skill", + toolArgs: { skill: "visual-qa-loop" }, + output: "Skill loaded", + approvalStatus: null, + durationMs: 120, + presentation: { + label: { pending: "Loading skill", completed: "Loaded skill" }, + icon: { glyph: "Zap" }, + title: "visual-qa-loop", + }, +}); + const longOutputTool: TimelineRow = toolRow({ id: "thr_tool_long_output:tool:toolu_long_output", threadId: "thr_tool_long_output", @@ -435,3 +455,18 @@ export function SkillReads() { ); } + +export function NativeSkillCall() { + return ( + + + + + + + + ); +} diff --git a/plugins/provider-claude-code/src/presentation.test.ts b/plugins/provider-claude-code/src/presentation.test.ts index c54abb0838..51232ab902 100644 --- a/plugins/provider-claude-code/src/presentation.test.ts +++ b/plugins/provider-claude-code/src/presentation.test.ts @@ -371,6 +371,19 @@ describe("claude item presentation", () => { }); }); + it("uses the established skill glyph for native Skill calls", () => { + const harness = createClaudeDeltaHarness(); + const events = harness.translate( + toolUse("skill-1", "Skill", { skill: "debugging" }), + ); + + expect(presentationOf(startedItems(events)[0])).toEqual({ + label: { pending: "Loading skill", completed: "Loaded skill" }, + icon: { glyph: "Zap" }, + title: "debugging", + }); + }); + it("maps MultiEdit and NotebookEdit to file changes with their own verbs", () => { const harness = createClaudeDeltaHarness(); const events = harness.translate({ diff --git a/plugins/provider-claude-code/src/presentation.ts b/plugins/provider-claude-code/src/presentation.ts index 3db8588d98..bb9fac0776 100644 --- a/plugins/provider-claude-code/src/presentation.ts +++ b/plugins/provider-claude-code/src/presentation.ts @@ -167,7 +167,7 @@ const BUILTIN_TOOL_PRESENTATIONS: Readonly< }, Skill: { label: { pending: "Loading skill", completed: "Loaded skill" }, - glyph: "Puzzle", + glyph: "Zap", titleField: "skill", }, StructuredOutput: { From 4af2920f8d048e47707e224a899a53c9b31726e1 Mon Sep 17 00:00:00 2001 From: brsbl Date: Sat, 29 Aug 2026 01:18:07 -0700 Subject: [PATCH 02/19] Keep composer metadata stable on hover (#2611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Human comments ## What was wrong Muted project, environment, and model controls changed foreground treatment on hover, creating a subtle visual jump in the composer metadata row. ## What changed Keeps the muted foreground token stable across hover while preserving the existing interactive background treatment. ### Before hover The composer metadata row is settled in its resting geometry. ![Before hover — settled composer metadata](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2611-hover-before.png) ### After hover Hovering the model control preserves the row geometry while exposing its normal interactive treatment. ![After hover — stable composer metadata geometry](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2611-hover-after.png) ## How you verified - Chrome for Testing 149.0.7827.55 exercised project, environment, and model controls in the real composer. - Element bounds remained unchanged across rest and hover; only the intended interactive background changed. - All required CI checks are green. ## Fixes No linked GitHub issue; addresses the reported hover regression. BB-Thread-ID: thr_fdabesxhdr > AGENT GENERATED --- packages/shared-ui/src/components/ui/option-display.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared-ui/src/components/ui/option-display.tsx b/packages/shared-ui/src/components/ui/option-display.tsx index 05e2da20ac..e95ba0f4d2 100644 --- a/packages/shared-ui/src/components/ui/option-display.tsx +++ b/packages/shared-ui/src/components/ui/option-display.tsx @@ -9,7 +9,7 @@ export const OPTION_CONTENT_CLASS_NAME = "flex min-w-0 items-center gap-1.5"; export const OPTION_TRIGGER_CONTENT_CLASS_NAME = "contents"; export const OPTION_MENU_CONTENT_CLASS_NAME = "w-max min-w-0 max-w-96"; export const OPTION_MUTED_CLASS_NAME = - "text-muted-foreground hover:text-foreground"; + "text-muted-foreground hover:text-muted-foreground"; export interface OptionDisplayProps { label: string; From fd8248acaa74e95e3641f9234afbe8135367ec43 Mon Sep 17 00:00:00 2001 From: brsbl Date: Sat, 29 Aug 2026 01:18:07 -0700 Subject: [PATCH 03/19] Shorten worktree thread action copy (#2612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Human comments ## What was wrong The worktree action tooltip said “Create new thread in this worktree,” which was longer than needed and inconsistent across the composer and metadata panel. ## What changed Uses “Create thread in worktree” for the tooltip and accessible label in both surfaces. ### Before — parent head `a1b2d5c8` ![Before — verbose worktree action tooltip](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2612-tooltip-before.png) ### After — PR head `738b5606` ![After — concise worktree action tooltip](https://raw.githubusercontent.com/brsbl/bb/a8df5aae7c5fb670a9ba5b3e13c600650d1c7e74/2612-tooltip-after.png) ## How you verified - The same isolated worktree thread and 1440×900 viewport were rendered against the exact parent and PR head in Chrome for Testing 149.0.7827.55. - The composer action exposed the expected visible tooltip and matching accessible label on each revision. - All required CI checks are green. ## Fixes No linked GitHub issue; addresses the reported tooltip copy regression. BB-Thread-ID: thr_fdabesxhdr > AGENT GENERATED --- .../promptbox/ThreadEnvironmentSummary.test.tsx | 4 ++-- .../promptbox/ThreadEnvironmentSummary.tsx | 4 ++-- .../secondary-panel/ThreadMetadataContent.test.tsx | 14 +++++--------- .../secondary-panel/ThreadMetadataContent.tsx | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx index 98f28956f0..ad6f750c7e 100644 --- a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx +++ b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx @@ -34,12 +34,12 @@ describe("ThreadEnvironmentSummary", () => { fireEvent.focus( screen.getByRole("button", { - name: "Create new thread in this worktree", + name: "Create thread in worktree", }), ); expect((await screen.findByRole("tooltip")).textContent).toBe( - "Create new thread in this worktree", + "Create thread in worktree", ); }); }); diff --git a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx index fdcbdd52b7..cadc8b4851 100644 --- a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx +++ b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx @@ -90,14 +90,14 @@ export const ThreadEnvironmentSummary = memo(function ThreadEnvironmentSummary({ - Create new thread in this worktree + Create thread in worktree ) : null} diff --git a/apps/app/src/components/secondary-panel/ThreadMetadataContent.test.tsx b/apps/app/src/components/secondary-panel/ThreadMetadataContent.test.tsx index 697466ef24..ee3a7730ec 100644 --- a/apps/app/src/components/secondary-panel/ThreadMetadataContent.test.tsx +++ b/apps/app/src/components/secondary-panel/ThreadMetadataContent.test.tsx @@ -118,7 +118,7 @@ describe("ThreadMetadataCard", () => { describe("EnvironmentRow", () => { it("shows the create-thread action for a provisioned worktree", () => { expect(renderEnvironmentRow(makeEnvironment())).toContain( - 'aria-label="Create new thread in this worktree"', + 'aria-label="Create thread in worktree"', ); }); @@ -137,12 +137,12 @@ describe("EnvironmentRow", () => { fireEvent.focus( screen.getByRole("button", { - name: "Create new thread in this worktree", + name: "Create thread in worktree", }), ); expect((await screen.findByRole("tooltip")).textContent).toBe( - "Create new thread in this worktree", + "Create thread in worktree", ); }); @@ -155,9 +155,7 @@ describe("EnvironmentRow", () => { }), ); - expect(markup).not.toContain( - 'aria-label="Create new thread in this worktree"', - ); + expect(markup).not.toContain('aria-label="Create thread in worktree"'); }); it("hides the create-thread action before a prepared worktree has a path", () => { @@ -168,9 +166,7 @@ describe("EnvironmentRow", () => { }), ); - expect(markup).not.toContain( - 'aria-label="Create new thread in this worktree"', - ); + expect(markup).not.toContain('aria-label="Create thread in worktree"'); }); }); diff --git a/apps/app/src/components/secondary-panel/ThreadMetadataContent.tsx b/apps/app/src/components/secondary-panel/ThreadMetadataContent.tsx index 496804a07a..e720e6fcf8 100644 --- a/apps/app/src/components/secondary-panel/ThreadMetadataContent.tsx +++ b/apps/app/src/components/secondary-panel/ThreadMetadataContent.tsx @@ -337,14 +337,14 @@ export function EnvironmentRow({ - Create new thread in this worktree + Create thread in worktree ) : null} From 0db384c6136b6165c065c4a3e02734b74346c531 Mon Sep 17 00:00:00 2001 From: brsbl Date: Sat, 29 Aug 2026 01:18:08 -0700 Subject: [PATCH 04/19] Clarify thread environment identity (#2472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Human comments ## What was wrong The composer reduced an unnamed worktree to the generic “Worktree” label, so users could see the checkout but not which host owned the environment. Environment type was also implicit, worktree rename copy used the broader “environment” concept, and provisioning could be presented as a workspace type instead of a lifecycle state. ## What changed The environment summary now preserves environment identity and checkout metadata as separate dimensions: - Direct environments and unnamed worktrees show their host name; named worktrees show their custom name. - The branch remains separately visible with the branch icon and copies on click, even if it matches a custom worktree name. - The environment icon tooltip distinguishes Local worktree, Remote worktree, Local, and Remote. - Provisioning takes precedence, uses the active loading spinner, and keeps the lifecycle label “Provisioning.” - Worktree actions and the rename dialog use worktree-specific language. A custom-named worktree shows its branch beneath the field, and “Clear custom name” restores the host as the environment identity. ### Composer identity **Before — the unnamed worktree is reduced to the generic “Worktree” label.** ![Before — generic Worktree environment label](https://github.com/user-attachments/assets/224b176f-5e3d-4cad-9b11-b626b58f9f6c) **After — the host identifies the environment while the branch remains visible as checkout metadata.** ![After — host environment identity with separate branch metadata](https://github.com/user-attachments/assets/90386fa6-c341-4bf8-acd9-342500fe1b4f) ### Rename flow **Before — generic environment terminology and no worktree context.** ![Before — generic rename environment dialog](https://github.com/user-attachments/assets/a927c09c-880d-4c6e-b8dd-3526f27df668) **After — worktree-specific terminology for an unnamed worktree.** ![After — rename worktree dialog](https://github.com/user-attachments/assets/bdd814b3-862b-40e8-a2e4-06fec2fce476) **Custom-name state — the branch remains visible and clearing the custom name restores host identity.** ![After — custom worktree name with branch context and Clear custom name action](https://github.com/user-attachments/assets/36dd5510-9e3b-45d6-a911-06b47f3ddfee) ## How you verified - Exact parent head `409ef81e9c36ef927e4af0a2bbff89c5001ed5c7` and exact PR head `78b589967f86c575066b2e8ef1d339c97f8c0b3e` were rendered in the branch web app with the same project, thread, environment, route, light theme, and 1440×900 viewport. - Chrome for Testing 150 exercised hard reload, host and branch rendering, worktree rename and clear, the Local worktree tooltip, and the Create thread in worktree tooltip. All passed with no runtime errors. - Remote CI covers the environment-state matrix, icon selection, branch presentation, rename copy, and component integration. ## Fixes No linked GitHub issue; addresses the reported environment-summary and worktree-naming regressions. BB-Thread-ID: thr_fdabesxhdr > AGENT GENERATED --- .../EnvironmentRenameDialog.stories.tsx | 30 ++- .../dialogs/EnvironmentRenameDialog.tsx | 15 +- .../src/components/dialogs/RenameDialog.tsx | 3 + .../plugin/PluginThreadChat.test.tsx | 1 + .../components/plugin/PluginThreadChat.tsx | 34 ++- .../promptbox/FollowUpPromptBox.stories.tsx | 224 +++++++++++++++++- .../ThreadEnvironmentSummary.test.tsx | 100 +++++++- .../promptbox/ThreadEnvironmentSummary.tsx | 102 +++++--- .../sidebar/ProjectRow.interactions.test.tsx | 11 +- .../app/src/components/sidebar/ProjectRow.tsx | 2 +- .../lib/environment-workspace-display.test.ts | 87 +++++++ .../src/lib/environment-workspace-display.ts | 69 ++++++ .../thread-detail/ThreadDetailPromptArea.tsx | 5 + .../views/thread-detail/ThreadDetailView.tsx | 38 +-- packages/core-ui/src/environment-display.ts | 9 + .../core-ui/test/environment-display.test.ts | 5 + .../src/components/ui/option-display.tsx | 21 +- 17 files changed, 663 insertions(+), 93 deletions(-) create mode 100644 apps/app/src/lib/environment-workspace-display.test.ts diff --git a/apps/app/src/components/dialogs/EnvironmentRenameDialog.stories.tsx b/apps/app/src/components/dialogs/EnvironmentRenameDialog.stories.tsx index 8b97c87fe0..2b021239be 100644 --- a/apps/app/src/components/dialogs/EnvironmentRenameDialog.stories.tsx +++ b/apps/app/src/components/dialogs/EnvironmentRenameDialog.stories.tsx @@ -26,11 +26,37 @@ const customNameTarget: EnvironmentRenameDialogTarget = { canClearName: true, }; +export function BranchContext() { + const inputRef = useRef(null); + return ( + + + + + + + + ); +} + export function Overview() { const inputRef = useRef(null); return ( - + diff --git a/apps/app/src/components/dialogs/EnvironmentRenameDialog.tsx b/apps/app/src/components/dialogs/EnvironmentRenameDialog.tsx index 38f3d1446a..121169b76d 100644 --- a/apps/app/src/components/dialogs/EnvironmentRenameDialog.tsx +++ b/apps/app/src/components/dialogs/EnvironmentRenameDialog.tsx @@ -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 { @@ -65,17 +65,24 @@ export function EnvironmentRenameDialogContent({ }: EnvironmentRenameDialogContentProps) { return ( + Branch: {target.branchName} +

+ ) : 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 diff --git a/apps/app/src/components/dialogs/RenameDialog.tsx b/apps/app/src/components/dialogs/RenameDialog.tsx index 36a7616112..31dff6372a 100644 --- a/apps/app/src/components/dialogs/RenameDialog.tsx +++ b/apps/app/src/components/dialogs/RenameDialog.tsx @@ -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; @@ -65,6 +66,7 @@ export function RenameDialogContent({ pending, errorMessage, placeholder, + inputDetails, maxLength, autoCapitalize, compact = false, @@ -119,6 +121,7 @@ export function RenameDialogContent({ clearMessage(); }} /> + {inputDetails} {displayedErrorMessage ? (

{displayedErrorMessage}

) : null} diff --git a/apps/app/src/components/plugin/PluginThreadChat.test.tsx b/apps/app/src/components/plugin/PluginThreadChat.test.tsx index 26e000ef2c..945b0c5313 100644 --- a/apps/app/src/components/plugin/PluginThreadChat.test.tsx +++ b/apps/app/src/components/plugin/PluginThreadChat.test.tsx @@ -30,6 +30,7 @@ vi.mock("@/lib/sdk", () => ({ })); vi.mock("@/hooks/useRealtimeSubscription", () => ({ + useHostListRealtimeSubscription: vi.fn(), useThreadDetailRealtimeSubscription: vi.fn(), useThreadListRealtimeSubscription: vi.fn(), useEnvironmentDetailRealtimeSubscription: vi.fn(), diff --git a/apps/app/src/components/plugin/PluginThreadChat.tsx b/apps/app/src/components/plugin/PluginThreadChat.tsx index b1ca9195ad..39c8db5202 100644 --- a/apps/app/src/components/plugin/PluginThreadChat.tsx +++ b/apps/app/src/components/plugin/PluginThreadChat.tsx @@ -22,10 +22,13 @@ import { useThreadTimelineNavigation } from "@/components/thread/timeline/Thread import { PluginContext } from "@/components/plugin/plugin-context"; import { ThreadProviderContext } from "@/components/thread/thread-provider-context"; import { useEnvironment } from "@/hooks/queries/environment-queries"; +import { useHosts } from "@/hooks/queries/host-queries"; import { useSystemProviderInfo } from "@/hooks/queries/system-queries"; import { useThread } from "@/hooks/queries/thread-queries"; import { useHostDaemon } from "@/hooks/useHostDaemon"; -import { getEnvironmentWorkspaceLabelIconName } from "@/lib/environment-workspace-display"; +import { + getEnvironmentWorkspaceSummaryDisplay, +} from "@/lib/environment-workspace-display"; import { formatWorkspaceCheckoutDisplay } from "@/lib/workspace-checkout-display"; import { BbHttpError } from "@/lib/sdk"; import { @@ -107,6 +110,11 @@ function PluginThreadChatBody({ const { isLocalDaemonHost } = useHostDaemon(); const environmentQuery = useEnvironment(thread?.environmentId ?? null); const environment = environmentQuery.data ?? null; + const hostsQuery = useHosts({ enabled: environment !== null }); + const environmentHostName = environment + ? (hostsQuery.data?.find((host) => host.id === environment.hostId)?.name ?? + null) + : null; const timelineNavigation = useThreadTimelineNavigation(); const canUseHostFileNavigation = thread !== undefined && @@ -171,25 +179,25 @@ function PluginThreadChatBody({ const environmentSummary = useMemo(() => { if (environment === null) { - return ( - - ); + return null; } const host: EnvironmentDisplayHostContext = { locality: isLocalDaemonHost(environment.hostId) ? "local" : "remote", identity: null, }; const display = formatEnvironmentDisplay({ environment, host }); + const summaryDisplay = getEnvironmentWorkspaceSummaryDisplay({ + display, + environmentName: environment.name, + locality: host.locality, + hostName: environmentHostName ?? undefined, + }); return ( ); - }, [environment, isLocalDaemonHost]); + }, [environment, environmentHostName, isLocalDaemonHost]); const isThreadMissing = threadQuery.error instanceof BbHttpError && diff --git a/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx b/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx index 31f6cd8ae9..9b575e44f7 100644 --- a/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx +++ b/apps/app/src/components/promptbox/FollowUpPromptBox.stories.tsx @@ -1,4 +1,10 @@ -import { useCallback, useMemo, useState, type ReactNode } from "react"; +import { + useCallback, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import type { Environment, PermissionMode, @@ -24,7 +30,9 @@ import { getFollowUpPromptPlaceholder, getCompactFollowUpPromptPlaceholder, } from "@/components/promptbox/follow-up-placeholder"; -import { getEnvironmentWorkspaceLabelIconName } from "@/lib/environment-workspace-display"; +import { + getEnvironmentWorkspaceSummaryDisplay, +} from "@/lib/environment-workspace-display"; import { INERT_TYPEAHEAD_COMMAND_CONFIG, type AttachmentsConfig, @@ -42,6 +50,7 @@ import { type QueuedMessageInlineEditor, } from "@/components/promptbox/banner/QueuedMessagesList"; import { ThreadEnvironmentSummary } from "@/components/promptbox/ThreadEnvironmentSummary"; +import { EnvironmentRenameDialogContent } from "@/components/dialogs/EnvironmentRenameDialog"; import { formatWorkspaceCheckoutDisplay, type WorkspaceCheckoutDisplay, @@ -49,6 +58,7 @@ import { import type { PickerOption } from "@/components/pickers/OptionPicker"; import { selectWorkspaceChangedFilesSection } from "@/components/workspace/workspace-change-summary"; import { StoryCard, StoryRow } from "../../../.ladle/story-card"; +import { DialogStage } from "../../../.ladle/story-dialog-stage"; import { makeEnvironment, makeExecutionControlsProps, @@ -168,6 +178,8 @@ const readOnlyPermission: ExecutionPermissionConfig = { interface EnvironmentSummaryArgs { environment: Environment; host: EnvironmentDisplayHostContext; + projectName?: string; + machineName?: string; branchName?: string; environmentCheckout?: WorkspaceCheckoutDisplay; onCreateNewThreadInWorktree?: () => void; @@ -176,6 +188,8 @@ interface EnvironmentSummaryArgs { function makeEnvironmentSummary({ environment, host, + projectName, + machineName, branchName, environmentCheckout, onCreateNewThreadInWorktree, @@ -184,6 +198,13 @@ function makeEnvironmentSummary({ environment, host, }); + const summaryDisplay = getEnvironmentWorkspaceSummaryDisplay({ + display, + environmentName: environment.name, + locality: host.locality, + hostName: machineName, + machinePrefix: machineName ? `${machineName} · ` : "", + }); const checkoutDisplay = environmentCheckout ?? (branchName @@ -197,11 +218,11 @@ function makeEnvironmentSummary({ : undefined); return ( @@ -226,6 +247,20 @@ const localEnvironmentSummary: ReactNode = makeEnvironmentSummary({ status: "ready", }), host: localEnvironmentDisplayHost, + machineName: "Bersabel's MacBook Pro", + branchName: STORY_BRANCH_NAME, +}); + +const longHostEnvironmentSummary: ReactNode = makeEnvironmentSummary({ + environment: makeEnvironment({ + managed: false, + isWorktree: false, + workspaceProvisionType: "unmanaged", + status: "ready", + }), + host: localEnvironmentDisplayHost, + projectName: "bb UI QA", + machineName: "Bersabel's MacBook Pro", branchName: STORY_BRANCH_NAME, }); @@ -237,6 +272,7 @@ const remoteEnvironmentSummary: ReactNode = makeEnvironmentSummary({ status: "ready", }), host: remoteEnvironmentDisplayHost, + machineName: "Build Mac mini", branchName: STORY_BRANCH_NAME, }); @@ -247,6 +283,31 @@ const worktreeEnvironmentSummary: ReactNode = makeEnvironmentSummary({ status: "ready", }), host: localEnvironmentDisplayHost, + machineName: "Bersabel's MacBook Pro", + branchName: STORY_BRANCH_NAME, + onCreateNewThreadInWorktree: noop, +}); + +const remoteWorktreeEnvironmentSummary: ReactNode = makeEnvironmentSummary({ + environment: makeEnvironment({ + isWorktree: true, + workspaceProvisionType: "managed-worktree", + status: "ready", + }), + host: remoteEnvironmentDisplayHost, + machineName: "Build Mac mini", + branchName: STORY_BRANCH_NAME, + onCreateNewThreadInWorktree: noop, +}); + +const namedWorktreeEnvironmentSummary: ReactNode = makeEnvironmentSummary({ + environment: makeEnvironment({ + name: "Design system polish", + isWorktree: true, + workspaceProvisionType: "managed-worktree", + status: "ready", + }), + host: localEnvironmentDisplayHost, branchName: STORY_BRANCH_NAME, onCreateNewThreadInWorktree: noop, }); @@ -969,6 +1030,33 @@ export function Overview() { environmentSummary={worktreeEnvironmentSummary} />
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +export function ProvisioningEnvironmentSummary() { + return ( + + +
+ {provisioningEnvironmentSummary} +
+
+
+ ); +} + +export function WorktreeNamingContract() { + const inputRef = useRef(null); + return ( + + + + + + + +
+ {worktreeEnvironmentSummary} +
+
+
+ ); +} + +export function WorktreeCopyAction() { + return ( + + + + + + ); +} + export function QueuedWorkspace() { return ( diff --git a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx index ad6f750c7e..32d1db1dc2 100644 --- a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx +++ b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.test.tsx @@ -1,27 +1,109 @@ // @vitest-environment jsdom -import { fireEvent, render, screen } from "@testing-library/react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { TooltipProvider } from "@bb/shared-ui/tooltip"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { ThreadEnvironmentSummary } from "./ThreadEnvironmentSummary"; +afterEach(cleanup); + describe("ThreadEnvironmentSummary", () => { it("uses a host-free environment label in compact prompt boxes", () => { - render( - , + const { container } = render( + + + , ); expect( - document.querySelector('[data-promptbox-full-label=""]')?.textContent, + container.querySelector('[data-promptbox-full-label=""]')?.textContent, ).toBe("Mac Studio · New worktree"); expect( - document.querySelector('[data-promptbox-compact-label=""]')?.textContent, + container.querySelector('[data-promptbox-compact-label=""]')?.textContent, ).toBe("Worktree"); }); + it("reveals the full host and mode when the environment label is constrained", async () => { + const { container } = render( + + + , + ); + + const environmentDisplay = container.querySelector( + '[data-option-display=""]', + ); + expect(environmentDisplay).not.toBeNull(); + expect(environmentDisplay!.className).not.toContain("max-w-[10rem]"); + fireEvent.focus(environmentDisplay!); + + expect((await screen.findByRole("tooltip")).textContent).toBe( + "Bersabel's MacBook Pro", + ); + }); + + it("keeps matching environment and branch labels visibly separate", () => { + render( + + + , + ); + + const copyButton = screen.getByRole("button", { + name: "bb/fix-environment-summary", + }); + expect(screen.getAllByText("bb/fix-environment-summary")).toHaveLength(3); + expect(copyButton.textContent).toBe("bb/fix-environment-summary"); + expect(copyButton.querySelector('[data-icon="GitBranch"]')).not.toBeNull(); + expect(copyButton.querySelector('[data-icon="Copy"]')).toBeNull(); + }); + + it.each(["Local worktree", "Remote worktree", "Local", "Remote"] as const)( + "shows the %s environment type from the environment icon", + async (environmentTypeLabel) => { + render( + + + , + ); + + fireEvent.focus( + screen.getByRole("img", { + name: `Environment type: ${environmentTypeLabel}`, + }), + ); + + expect((await screen.findByRole("tooltip")).textContent).toBe( + environmentTypeLabel, + ); + }, + ); + it("explains the create-thread action in a tooltip", async () => { render( diff --git a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx index cadc8b4851..4f40641ace 100644 --- a/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx +++ b/apps/app/src/components/promptbox/ThreadEnvironmentSummary.tsx @@ -3,6 +3,8 @@ import { OptionDisplay } from "@bb/shared-ui/option-display"; import { copyToClipboardWithToast } from "@/lib/clipboard"; import { Icon, type IconName } from "@bb/shared-ui/icon"; import { Tooltip, TooltipContent, TooltipTrigger } from "@bb/shared-ui/tooltip"; +import { cn } from "@bb/shared-ui/lib/utils"; +import type { EnvironmentWorkspaceTypeLabel } from "@/lib/environment-workspace-display"; import type { WorkspaceCheckoutDisplay } from "@/lib/workspace-checkout-display"; const CHECKOUT_CHIP_BASE_CLASS_NAME = @@ -14,6 +16,7 @@ interface ThreadEnvironmentSummaryProps { environmentLabel?: string; environmentCompactLabel?: string; environmentIcon?: IconName; + environmentTypeLabel?: EnvironmentWorkspaceTypeLabel; environmentCheckout?: WorkspaceCheckoutDisplay; onCreateNewThreadInWorktree?: () => void; } @@ -23,10 +26,16 @@ export const ThreadEnvironmentSummary = memo(function ThreadEnvironmentSummary({ environmentLabel, environmentCompactLabel, environmentIcon, + environmentTypeLabel, environmentCheckout, onCreateNewThreadInWorktree, }: ThreadEnvironmentSummaryProps) { - if (!environmentLabel) { + if ( + !projectName && + !environmentLabel && + !environmentCheckout && + !onCreateNewThreadInWorktree + ) { return null; } @@ -39,42 +48,69 @@ export const ThreadEnvironmentSummary = memo(function ThreadEnvironmentSummary({ value={projectName} compactValue={projectName} leading={} - className="h-6 max-w-[10rem] shrink-0" - title={`Project: ${projectName}`} + className="h-6 min-w-0 max-w-[10rem] shrink" + tooltip={`Project: ${projectName}`} muted /> ) : null} - - ) : null - } - className="h-6 max-w-[10rem] shrink-0" - title={`Environment: ${environmentLabel}`} - muted - /> + {environmentLabel ? ( +
+ {environmentIcon && environmentTypeLabel ? ( + + + + + + + {environmentTypeLabel} + + ) : environmentIcon ? ( + + ) : null} + +
+ ) : null} {environmentCheckout && checkoutCopyValue !== null ? ( - + + + + + {environmentCheckout.title} + ) : environmentCheckout ? ( { screen.getByRole("button", { name: "Worktree actions" }), { button: 0 }, ); - fireEvent.click(await screen.findByRole("menuitem", { name: "Rename" })); + fireEvent.click( + await screen.findByRole("menuitem", { name: "Rename worktree" }), + ); expect( - await screen.findByRole("dialog", { name: "Rename environment" }), + await screen.findByRole("dialog", { name: "Rename worktree" }), ).not.toBeNull(); + expect(screen.getByText("feat/menu-close")).not.toBeNull(); await waitFor(() => { - expect(screen.queryByRole("menuitem", { name: "Rename" })).toBeNull(); + expect( + screen.queryByRole("menuitem", { name: "Rename worktree" }), + ).toBeNull(); }); }); }); diff --git a/apps/app/src/components/sidebar/ProjectRow.tsx b/apps/app/src/components/sidebar/ProjectRow.tsx index a3026af04d..a81ad5949b 100644 --- a/apps/app/src/components/sidebar/ProjectRow.tsx +++ b/apps/app/src/components/sidebar/ProjectRow.tsx @@ -871,7 +871,7 @@ function EnvironmentThreadGroupHeaderActions({ }} >