| author | |
| committer | |
| log | 7f3628e4153d5be4a241c4d54f73a6d7087014ce |
| tree | 17d83b919493113808f1bd676262451ec9b51edd |
| parent | 26285ab80513260d1ff29d2c3fbe18f21990cb62 |
| signature | Signed by PGP key B5690EEEBB952194 |
17 files changed, 376 insertions(+), 343 deletions(-)
apps/web/src/components/BranchToolbar.logic.test.ts-13| ... | @@ -13,7 +13,6 @@ import { | ... | @@ -13,7 +13,6 @@ import { |
| 13 | resolveBranchToolbarPrBranch, | 13 | resolveBranchToolbarPrBranch, |
| 14 | resolveBranchToolbarValue, | 14 | resolveBranchToolbarValue, |
| 15 | resolveLockedWorkspaceLabel, | 15 | resolveLockedWorkspaceLabel, |
| 16 | resolveWorkspaceDisplayName, | ||
| 17 | resolveLocalCheckoutBranchMismatch, | 16 | resolveLocalCheckoutBranchMismatch, |
| 18 | resolvePreviousWorktreeLabel, | 17 | resolvePreviousWorktreeLabel, |
| 19 | resolvePreviousWorktreeSeed, | 18 | resolvePreviousWorktreeSeed, |
| ... | @@ -568,18 +567,6 @@ describe("resolveLockedWorkspaceLabel", () => { | ... | @@ -568,18 +567,6 @@ describe("resolveLockedWorkspaceLabel", () => { |
| 568 | }); | 567 | }); |
| 569 | }); | 568 | }); |
| 570 | 569 | ||
| 571 | describe("resolveWorkspaceDisplayName", () => { | ||
| 572 | it("returns the final folder for POSIX and Windows paths", () => { | ||
| 573 | expect(resolveWorkspaceDisplayName("/repo/.t3/worktrees/feature-a")).toBe("feature-a"); | ||
| 574 | expect(resolveWorkspaceDisplayName("C:\\code\\project\\feature-b\\")).toBe("feature-b"); | ||
| 575 | }); | ||
| 576 | |||
| 577 | it("handles missing and root paths", () => { | ||
| 578 | expect(resolveWorkspaceDisplayName(null)).toBeNull(); | ||
| 579 | expect(resolveWorkspaceDisplayName("/")).toBe("/"); | ||
| 580 | }); | ||
| 581 | }); | ||
| 582 | |||
| 583 | describe("deriveLocalBranchNameFromRemoteRef", () => { | 570 | describe("deriveLocalBranchNameFromRemoteRef", () => { |
| 584 | it("strips the remote prefix from a remote ref", () => { | 571 | it("strips the remote prefix from a remote ref", () => { |
| 585 | expect(deriveLocalBranchNameFromRemoteRef("origin/feature/demo")).toBe("feature/demo"); | 572 | expect(deriveLocalBranchNameFromRemoteRef("origin/feature/demo")).toBe("feature/demo"); |
apps/web/src/components/BranchToolbar.logic.ts-7| ... | @@ -118,13 +118,6 @@ export function resolveLockedWorkspaceLabel( | ... | @@ -118,13 +118,6 @@ export function resolveLockedWorkspaceLabel( |
| 118 | return effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : "Local checkout"; | 118 | return effectiveEnvMode === "worktree" ? resolveEnvModeLabel("worktree") : "Local checkout"; |
| 119 | } | 119 | } |
| 120 | 120 | ||
| 121 | export function resolveWorkspaceDisplayName(path: string | null): string | null { | ||
| 122 | if (!path) return null; | ||
| 123 | const normalizedPath = path.replace(/[\\/]+$/, ""); | ||
| 124 | if (normalizedPath.length === 0) return path; | ||
| 125 | return normalizedPath.split(/[\\/]/).at(-1) ?? normalizedPath; | ||
| 126 | } | ||
| 127 | |||
| 128 | export interface PreviousWorktreeSeed { | 121 | export interface PreviousWorktreeSeed { |
| 129 | branch: string | null; | 122 | branch: string | null; |
| 130 | worktreePath: string; | 123 | worktreePath: string; |
apps/web/src/components/BranchToolbar.panel.test.tsx created+138| ... | @@ -0,0 +1,138 @@ | ||
| 1 | // @vitest-environment jsdom | ||
| 2 | |||
| 3 | import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; | ||
| 4 | import { act } from "react"; | ||
| 5 | import { createRoot } from "react-dom/client"; | ||
| 6 | import { expect, it, vi } from "vite-plus/test"; | ||
| 7 | |||
| 8 | const state = vi.hoisted(() => ({ | ||
| 9 | worktreePath: "/tmp/worktree" as string | null, | ||
| 10 | showContextMenu: vi.fn().mockResolvedValue("copy-path"), | ||
| 11 | writeTextToClipboard: vi.fn().mockResolvedValue(true), | ||
| 12 | })); | ||
| 13 | |||
| 14 | vi.mock("../localApi", () => ({ | ||
| 15 | readLocalApi: () => ({ contextMenu: { show: state.showContextMenu } }), | ||
| 16 | })); | ||
| 17 | vi.mock("../hooks/useCopyToClipboard", () => ({ | ||
| 18 | writeTextToClipboard: state.writeTextToClipboard, | ||
| 19 | })); | ||
| 20 | vi.mock("./BranchToolbarBranchSelector", () => ({ BranchToolbarBranchSelector: () => null })); | ||
| 21 | vi.mock("../composerDraftStore", () => ({ | ||
| 22 | useComposerDraftStore: (select: (store: unknown) => unknown) => | ||
| 23 | select({ getDraftThreadByRef: () => null, setDraftThreadContext: vi.fn() }), | ||
| 24 | })); | ||
| 25 | vi.mock("../state/entities", () => ({ | ||
| 26 | useThreadShell: () => ({ | ||
| 27 | environmentId: "local", | ||
| 28 | projectId: "project", | ||
| 29 | worktreePath: state.worktreePath, | ||
| 30 | }), | ||
| 31 | useProject: () => ({ workspaceRoot: "/tmp/project" }), | ||
| 32 | useThreadShellsForProjectRefs: () => [], | ||
| 33 | })); | ||
| 34 | |||
| 35 | import { BranchToolbar } from "./BranchToolbar"; | ||
| 36 | |||
| 37 | it("keeps machine choices usable when the combined row's workspace is locked", async () => { | ||
| 38 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | ||
| 39 | const container = document.createElement("div"); | ||
| 40 | document.body.append(container); | ||
| 41 | const root = createRoot(container); | ||
| 42 | const onEnvironmentChange = vi.fn(); | ||
| 43 | try { | ||
| 44 | await act(async () => { | ||
| 45 | root.render( | ||
| 46 | <BranchToolbar | ||
| 47 | layout="panel" | ||
| 48 | panelSection="workspace" | ||
| 49 | environmentId={EnvironmentId.make("local")} | ||
| 50 | threadId={ThreadId.make("thread")} | ||
| 51 | showGitControls | ||
| 52 | envMode="local" | ||
| 53 | envLocked={false} | ||
| 54 | startFromOrigin={false} | ||
| 55 | onStartFromOriginChange={vi.fn()} | ||
| 56 | onEnvModeChange={vi.fn()} | ||
| 57 | onEnvironmentChange={onEnvironmentChange} | ||
| 58 | availableEnvironments={["local", "remote"].map((id) => ({ | ||
| 59 | environmentId: EnvironmentId.make(id), | ||
| 60 | projectId: ProjectId.make("project"), | ||
| 61 | label: id, | ||
| 62 | isPrimary: id === "local", | ||
| 63 | machine: "server", | ||
| 64 | }))} | ||
| 65 | />, | ||
| 66 | ); | ||
| 67 | }); | ||
| 68 | const trigger = container.querySelector("button")!; | ||
| 69 | expect(trigger.textContent).toBe("local"); | ||
| 70 | expect(container.querySelectorAll("button")).toHaveLength(1); | ||
| 71 | await act(async () => { | ||
| 72 | trigger.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, ctrlKey: true })); | ||
| 73 | }); | ||
| 74 | expect(document.querySelector('[role="menu"]')).toBeNull(); | ||
| 75 | await act(async () => trigger.click()); | ||
| 76 | const items = [...document.querySelectorAll<HTMLElement>('[role="menuitemradio"]')]; | ||
| 77 | expect( | ||
| 78 | items.find((item) => item.textContent === "New worktree")?.getAttribute("aria-disabled"), | ||
| 79 | ).toBe("true"); | ||
| 80 | await act(async () => items.find((item) => item.textContent === "remote")!.click()); | ||
| 81 | expect(onEnvironmentChange).toHaveBeenCalledWith("remote"); | ||
| 82 | } finally { | ||
| 83 | await act(async () => root.unmount()); | ||
| 84 | container.remove(); | ||
| 85 | vi.unstubAllGlobals(); | ||
| 86 | } | ||
| 87 | }); | ||
| 88 | |||
| 89 | it.each([ | ||
| 90 | ["local", null, "/tmp/project"], | ||
| 91 | ["worktree", null, null], | ||
| 92 | ["worktree", "/tmp/worktree", "/tmp/worktree"], | ||
| 93 | ] as const)( | ||
| 94 | "copies only an existing workspace path with mode %s and worktree %s", | ||
| 95 | async (envMode, worktreePath, copiedPath) => { | ||
| 96 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | ||
| 97 | state.worktreePath = worktreePath; | ||
| 98 | state.showContextMenu.mockClear(); | ||
| 99 | state.writeTextToClipboard.mockClear(); | ||
| 100 | const container = document.createElement("div"); | ||
| 101 | document.body.append(container); | ||
| 102 | const root = createRoot(container); | ||
| 103 | try { | ||
| 104 | await act(async () => { | ||
| 105 | root.render( | ||
| 106 | <BranchToolbar | ||
| 107 | layout="panel" | ||
| 108 | panelSection="workspace" | ||
| 109 | environmentId={EnvironmentId.make("local")} | ||
| 110 | threadId={ThreadId.make("thread")} | ||
| 111 | showGitControls | ||
| 112 | envMode={envMode} | ||
| 113 | envLocked={false} | ||
| 114 | startFromOrigin={false} | ||
| 115 | onStartFromOriginChange={vi.fn()} | ||
| 116 | onEnvModeChange={vi.fn()} | ||
| 117 | />, | ||
| 118 | ); | ||
| 119 | }); | ||
| 120 | const event = new MouseEvent("contextmenu", { bubbles: true, cancelable: true }); | ||
| 121 | await act(async () => { | ||
| 122 | container.querySelector('[aria-label="Run context"]')!.dispatchEvent(event); | ||
| 123 | }); | ||
| 124 | expect(event.defaultPrevented).toBe(copiedPath !== null); | ||
| 125 | if (copiedPath === null) { | ||
| 126 | expect(state.showContextMenu).not.toHaveBeenCalled(); | ||
| 127 | expect(state.writeTextToClipboard).not.toHaveBeenCalled(); | ||
| 128 | } else { | ||
| 129 | expect(state.writeTextToClipboard).toHaveBeenCalledWith(copiedPath, "workspace path"); | ||
| 130 | } | ||
| 131 | } finally { | ||
| 132 | await act(async () => root.unmount()); | ||
| 133 | container.remove(); | ||
| 134 | state.worktreePath = "/tmp/worktree"; | ||
| 135 | vi.unstubAllGlobals(); | ||
| 136 | } | ||
| 137 | }, | ||
| 138 | ); | ||
apps/web/src/components/BranchToolbar.tsx+126-20| ... | @@ -1,3 +1,6 @@ | ... | @@ -1,3 +1,6 @@ |
| 1 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; | ||
| 2 | import { readLocalApi } from "../localApi"; | ||
| 3 | import { stackedThreadToast, toastManager } from "./ui/toast"; | ||
| 1 | import { ComposerContextLabel } from "./ComposerContextLabel"; | 4 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 2 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; | 5 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 3 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; | 6 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| ... | @@ -10,6 +13,7 @@ import { | ... | @@ -10,6 +13,7 @@ import { |
| 10 | } from "lucide-react"; | 13 | } from "lucide-react"; |
| 11 | import { | 14 | import { |
| 12 | type Ref, | 15 | type Ref, |
| 16 | type MouseEvent as ReactMouseEvent, | ||
| 13 | memo, | 17 | memo, |
| 14 | useImperativeHandle, | 18 | useImperativeHandle, |
| 15 | useCallback, | 19 | useCallback, |
| ... | @@ -41,6 +45,11 @@ import { | ... | @@ -41,6 +45,11 @@ import { |
| 41 | import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; | 45 | import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; |
| 42 | import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; | 46 | import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; |
| 43 | import { PreviousWorktreeItemContent } from "./PreviousWorktreeItemContent"; | 47 | import { PreviousWorktreeItemContent } from "./PreviousWorktreeItemContent"; |
| 48 | import { ThreadDetailsControl } from "./chat/ThreadDetailsControl"; | ||
| 49 | import { | ||
| 50 | THREAD_DETAILS_PANEL_ICON_CLASS, | ||
| 51 | THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 52 | } from "./chat/threadDetailsPanelStyles"; | ||
| 44 | import { ComposerControl } from "./chat/ComposerControl"; | 53 | import { ComposerControl } from "./chat/ComposerControl"; |
| 45 | import { | 54 | import { |
| 46 | Menu, | 55 | Menu, |
| ... | @@ -93,7 +102,44 @@ interface BranchToolbarProps { | ... | @@ -93,7 +102,44 @@ interface BranchToolbarProps { |
| 93 | contextStripVisible?: boolean; | 102 | contextStripVisible?: boolean; |
| 94 | } | 103 | } |
| 95 | 104 | ||
| 96 | interface MobileRunContextSelectorProps { | 105 | function showWorkspaceContextMenu(event: ReactMouseEvent, workspacePath: string) { |
| 106 | const api = readLocalApi(); | ||
| 107 | if (!api) return; | ||
| 108 | event.preventDefault(); | ||
| 109 | event.stopPropagation(); | ||
| 110 | void api.contextMenu | ||
| 111 | .show([{ id: "copy-path", label: "Copy full path", icon: "copy" }], { | ||
| 112 | x: event.clientX, | ||
| 113 | y: event.clientY, | ||
| 114 | }) | ||
| 115 | .then((action) => { | ||
| 116 | if (action !== "copy-path") return; | ||
| 117 | void writeTextToClipboard(workspacePath, "workspace path").then( | ||
| 118 | (didCopy) => { | ||
| 119 | if (didCopy) { | ||
| 120 | toastManager.add({ | ||
| 121 | type: "success", | ||
| 122 | title: "Path copied", | ||
| 123 | description: workspacePath, | ||
| 124 | }); | ||
| 125 | } | ||
| 126 | }, | ||
| 127 | (error: unknown) => { | ||
| 128 | toastManager.add( | ||
| 129 | stackedThreadToast({ | ||
| 130 | type: "error", | ||
| 131 | title: "Failed to copy path", | ||
| 132 | description: error instanceof Error ? error.message : "An error occurred.", | ||
| 133 | }), | ||
| 134 | ); | ||
| 135 | }, | ||
| 136 | ); | ||
| 137 | }); | ||
| 138 | } | ||
| 139 | |||
| 140 | interface RunContextSelectorProps { | ||
| 141 | displayMode?: "toolbar" | "panel"; | ||
| 142 | workspaceRoot?: string; | ||
| 97 | forceNewWorktree: boolean; | 143 | forceNewWorktree: boolean; |
| 98 | autoEnvironmentLabel?: string | undefined; | 144 | autoEnvironmentLabel?: string | undefined; |
| 99 | onAutoEnvironment?: (() => void) | undefined; | 145 | onAutoEnvironment?: (() => void) | undefined; |
| ... | @@ -112,7 +158,9 @@ interface MobileRunContextSelectorProps { | ... | @@ -112,7 +158,9 @@ interface MobileRunContextSelectorProps { |
| 112 | onUsePreviousWorktree: () => void; | 158 | onUsePreviousWorktree: () => void; |
| 113 | } | 159 | } |
| 114 | 160 | ||
| 115 | const MobileRunContextSelector = memo(function MobileRunContextSelector({ | 161 | const RunContextSelector = memo(function RunContextSelector({ |
| 162 | displayMode = "toolbar", | ||
| 163 | workspaceRoot, | ||
| 116 | forceNewWorktree, | 164 | forceNewWorktree, |
| 117 | autoEnvironmentLabel, | 165 | autoEnvironmentLabel, |
| 118 | onAutoEnvironment, | 166 | onAutoEnvironment, |
| ... | @@ -129,7 +177,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -129,7 +177,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 129 | previousWorktreeLabel, | 177 | previousWorktreeLabel, |
| 130 | previousWorktreeBranch, | 178 | previousWorktreeBranch, |
| 131 | onUsePreviousWorktree, | 179 | onUsePreviousWorktree, |
| 132 | }: MobileRunContextSelectorProps) { | 180 | }: RunContextSelectorProps) { |
| 133 | const composerFloatingLayerProps = useComposerMenuProps(); | 181 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 134 | const activeEnvironment = useMemo( | 182 | const activeEnvironment = useMemo( |
| 135 | () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, | 183 | () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, |
| ... | @@ -148,13 +196,35 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -148,13 +196,35 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 148 | : effectiveEnvMode === "worktree" | 196 | : effectiveEnvMode === "worktree" |
| 149 | ? resolveEnvModeLabel("worktree") | 197 | ? resolveEnvModeLabel("worktree") |
| 150 | : resolveCurrentWorkspaceLabel(activeWorktreePath); | 198 | : resolveCurrentWorkspaceLabel(activeWorktreePath); |
| 151 | const isLocked = envLocked || envModeLocked; | 199 | const isPanel = displayMode === "panel"; |
| 200 | const isLocked = envLocked || (envModeLocked && (!isPanel || !showEnvironmentPicker)); | ||
| 201 | const workspacePath = | ||
| 202 | forceNewWorktree || (effectiveEnvMode === "worktree" && !activeWorktreePath) | ||
| 203 | ? null | ||
| 204 | : (activeWorktreePath ?? workspaceRoot); | ||
| 205 | const handleContextMenu = (event: ReactMouseEvent) => { | ||
| 206 | if (isPanel && workspacePath) { | ||
| 207 | showWorkspaceContextMenu(event, workspacePath); | ||
| 208 | } | ||
| 209 | }; | ||
| 152 | const workspaceIcon = ( | 210 | const workspaceIcon = ( |
| 153 | <Tooltip> | 211 | <Tooltip> |
| 154 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> | 212 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> |
| 155 | <WorkspaceIcon className={cn("size-3 shrink-0", showEnvironmentIndicator && "mx-0!")} /> | 213 | <WorkspaceIcon |
| 214 | className={ | ||
| 215 | isPanel | ||
| 216 | ? THREAD_DETAILS_PANEL_ICON_CLASS | ||
| 217 | : cn("size-3 shrink-0", showEnvironmentIndicator && "mx-0!") | ||
| 218 | } | ||
| 219 | /> | ||
| 156 | </TooltipTrigger> | 220 | </TooltipTrigger> |
| 157 | <TooltipPopup>{workspaceLabel}</TooltipPopup> | 221 | <TooltipPopup> |
| 222 | {isPanel | ||
| 223 | ? forceNewWorktree | ||
| 224 | ? "Each model starts in its own worktree." | ||
| 225 | : (workspacePath ?? workspaceLabel) | ||
| 226 | : workspaceLabel} | ||
| 227 | </TooltipPopup> | ||
| 158 | </Tooltip> | 228 | </Tooltip> |
| 159 | ); | 229 | ); |
| 160 | const icon = showEnvironmentIndicator ? ( | 230 | const icon = showEnvironmentIndicator ? ( |
| ... | @@ -164,11 +234,14 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -164,11 +234,14 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 164 | <Tooltip> | 234 | <Tooltip> |
| 165 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> | 235 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> |
| 166 | {autoEnvironmentLabel ? ( | 236 | {autoEnvironmentLabel ? ( |
| 167 | <ScaleIcon className="size-3 shrink-0 mx-0!" aria-hidden="true" /> | 237 | <ScaleIcon |
| 238 | className={isPanel ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0 mx-0!"} | ||
| 239 | aria-hidden="true" | ||
| 240 | /> | ||
| 168 | ) : ( | 241 | ) : ( |
| 169 | <EnvironmentMachineIcon | 242 | <EnvironmentMachineIcon |
| 170 | kind={activeEnvironment?.machine ?? "server"} | 243 | kind={activeEnvironment?.machine ?? "server"} |
| 171 | className="size-3 shrink-0 mx-0!" | 244 | className={isPanel ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0 mx-0!"} |
| 172 | /> | 245 | /> |
| 173 | )} | 246 | )} |
| 174 | </TooltipTrigger> | 247 | </TooltipTrigger> |
| ... | @@ -182,7 +255,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -182,7 +255,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 182 | const triggerContent = ( | 255 | const triggerContent = ( |
| 183 | <> | 256 | <> |
| 184 | {icon} | 257 | {icon} |
| 185 | <ComposerContextLabel> | 258 | <ComposerContextLabel displayMode={displayMode}> |
| 186 | {autoEnvironmentLabel ?? | 259 | {autoEnvironmentLabel ?? |
| 187 | (showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)} | 260 | (showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)} |
| 188 | </ComposerContextLabel> | 261 | </ComposerContextLabel> |
| ... | @@ -192,7 +265,13 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -192,7 +265,13 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 192 | if (isLocked) { | 265 | if (isLocked) { |
| 193 | return ( | 266 | return ( |
| 194 | <span | 267 | <span |
| 195 | className="inline-flex h-7 min-w-0 max-w-[48%] flex-initial items-center justify-start gap-1 rounded-md border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6" | 268 | className={cn( |
| 269 | "inline-flex h-7 min-w-0 max-w-[48%] flex-initial items-center justify-start gap-1 rounded-md border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6", | ||
| 270 | isPanel && "max-w-full", | ||
| 271 | isPanel && THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 272 | )} | ||
| 273 | onContextMenu={handleContextMenu} | ||
| 274 | aria-label={isPanel ? "Run context" : undefined} | ||
| 196 | data-composer-context-control | 275 | data-composer-context-control |
| 197 | > | 276 | > |
| 198 | {triggerContent} | 277 | {triggerContent} |
| ... | @@ -203,8 +282,17 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -203,8 +282,17 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 203 | return ( | 282 | return ( |
| 204 | <Menu> | 283 | <Menu> |
| 205 | <MenuTrigger | 284 | <MenuTrigger |
| 206 | render={<ComposerControl size="xs" />} | 285 | render={isPanel ? <ThreadDetailsControl part="select" /> : <ComposerControl size="xs" />} |
| 207 | className="min-w-0 max-w-[48%] flex-initial justify-start" | 286 | className={isPanel ? undefined : "min-w-0 max-w-[48%] flex-initial justify-start"} |
| 287 | onContextMenu={handleContextMenu} | ||
| 288 | onMouseDownCapture={ | ||
| 289 | isPanel | ||
| 290 | ? (event) => { | ||
| 291 | if (event.button !== 0 || event.ctrlKey) event.stopPropagation(); | ||
| 292 | } | ||
| 293 | : undefined | ||
| 294 | } | ||
| 295 | aria-label={isPanel ? "Run context" : undefined} | ||
| 208 | data-composer-context-control | 296 | data-composer-context-control |
| 209 | data-composer-shortcut={[ | 297 | data-composer-shortcut={[ |
| 210 | showEnvironmentPicker && !envLocked ? "composer.host" : "", | 298 | showEnvironmentPicker && !envLocked ? "composer.host" : "", |
| ... | @@ -212,13 +300,19 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ... | @@ -212,13 +300,19 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 212 | ].join(" ")} | 300 | ].join(" ")} |
| 213 | > | 301 | > |
| 214 | {triggerContent} | 302 | {triggerContent} |
| 215 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> | 303 | {!isPanel ? <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> : null} |
| 216 | </MenuTrigger> | 304 | </MenuTrigger> |
| 217 | <MenuPopup | 305 | <MenuPopup |
| 218 | align="start" | 306 | align="start" |
| 219 | side="top" | 307 | side={isPanel ? "bottom" : "top"} |
| 220 | className={previousWorktreeLabel ? "w-[min(21rem,calc(100vw-2rem))]" : undefined} | 308 | className={ |
| 221 | {...composerFloatingLayerProps} | 309 | isPanel |
| 310 | ? "w-(--anchor-width)" | ||
| 311 | : previousWorktreeLabel | ||
| 312 | ? "w-[min(21rem,calc(100vw-2rem))]" | ||
| 313 | : undefined | ||
| 314 | } | ||
| 315 | {...(!isPanel ? composerFloatingLayerProps : {})} | ||
| 222 | > | 316 | > |
| 223 | {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( | 317 | {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( |
| 224 | <> | 318 | <> |
| ... | @@ -618,13 +712,25 @@ export const BranchToolbar = memo(function BranchToolbar({ | ... | @@ -618,13 +712,25 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 618 | return ( | 712 | return ( |
| 619 | <div className="flex w-full flex-col" data-thread-panel-run-context> | 713 | <div className="flex w-full flex-col" data-thread-panel-run-context> |
| 620 | {panelSection !== "branch" ? ( | 714 | {panelSection !== "branch" ? ( |
| 621 | <BranchToolbarEnvModeSelector | 715 | <RunContextSelector |
| 622 | displayMode="panel" | 716 | displayMode="panel" |
| 623 | envLocked={envModeLocked} | 717 | workspaceRoot={activeProject.workspaceRoot} |
| 718 | forceNewWorktree={forceNewWorktree} | ||
| 719 | autoEnvironmentLabel={autoEnvironmentLabel} | ||
| 720 | onAutoEnvironment={onAutoEnvironment} | ||
| 721 | envLocked={envLocked} | ||
| 722 | envModeLocked={envModeLocked} | ||
| 723 | environmentId={environmentId} | ||
| 724 | availableEnvironments={availableEnvironments} | ||
| 725 | showEnvironmentPicker={showEnvironmentPicker} | ||
| 726 | showEnvironmentIndicator={activeEnvironmentOption !== null} | ||
| 727 | onEnvironmentChange={onEnvironmentChange} | ||
| 624 | effectiveEnvMode={effectiveEnvMode} | 728 | effectiveEnvMode={effectiveEnvMode} |
| 625 | activeWorktreePath={activeWorktreePath} | 729 | activeWorktreePath={activeWorktreePath} |
| 626 | workspaceRoot={activeProject.workspaceRoot} | ||
| 627 | onEnvModeChange={onEnvModeChange} | 730 | onEnvModeChange={onEnvModeChange} |
| 731 | previousWorktreeLabel={previousWorktreeLabel} | ||
| 732 | previousWorktreeBranch={previousWorktreeSeed?.branch ?? null} | ||
| 733 | onUsePreviousWorktree={onUsePreviousWorktree} | ||
| 628 | /> | 734 | /> |
| 629 | ) : null} | 735 | ) : null} |
| 630 | {panelSection !== "workspace" ? ( | 736 | {panelSection !== "workspace" ? ( |
| ... | @@ -662,7 +768,7 @@ export const BranchToolbar = memo(function BranchToolbar({ | ... | @@ -662,7 +768,7 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 662 | > | 768 | > |
| 663 | {showGitControls ? ( | 769 | {showGitControls ? ( |
| 664 | <div className="contents @3xl/composer-surface:hidden"> | 770 | <div className="contents @3xl/composer-surface:hidden"> |
| 665 | <MobileRunContextSelector | 771 | <RunContextSelector |
| 666 | forceNewWorktree={forceNewWorktree} | 772 | forceNewWorktree={forceNewWorktree} |
| 667 | autoEnvironmentLabel={autoEnvironmentLabel} | 773 | autoEnvironmentLabel={autoEnvironmentLabel} |
| 668 | onAutoEnvironment={onAutoEnvironment} | 774 | onAutoEnvironment={onAutoEnvironment} |
apps/web/src/components/BranchToolbarBranchSelector.tsx+3-10| ... | @@ -42,10 +42,7 @@ import { threadEnvironment } from "../state/threads"; | ... | @@ -42,10 +42,7 @@ import { threadEnvironment } from "../state/threads"; |
| 42 | import { useAtomCommand } from "../state/use-atom-command"; | 42 | import { useAtomCommand } from "../state/use-atom-command"; |
| 43 | import { vcsEnvironment } from "../state/vcs"; | 43 | import { vcsEnvironment } from "../state/vcs"; |
| 44 | import { cn } from "../lib/utils"; | 44 | import { cn } from "../lib/utils"; |
| 45 | import { | 45 | import { THREAD_DETAILS_PANEL_ICON_CLASS } from "./chat/threadDetailsPanelStyles"; |
| 46 | THREAD_DETAILS_PANEL_CHEVRON_CLASS, | ||
| 47 | THREAD_DETAILS_PANEL_ICON_CLASS, | ||
| 48 | } from "./chat/threadDetailsPanelStyles"; | ||
| 49 | import { ThreadDetailsPrRows } from "./chat/ThreadDetailsPrRows"; | 46 | import { ThreadDetailsPrRows } from "./chat/ThreadDetailsPrRows"; |
| 50 | import { parsePullRequestReference } from "../pullRequestReference"; | 47 | import { parsePullRequestReference } from "../pullRequestReference"; |
| 51 | import { getSourceControlPresentation } from "../sourceControlPresentation"; | 48 | import { getSourceControlPresentation } from "../sourceControlPresentation"; |
| ... | @@ -784,13 +781,9 @@ export function BranchToolbarBranchSelector({ | ... | @@ -784,13 +781,9 @@ export function BranchToolbarBranchSelector({ |
| 784 | <ComposerContextLabel displayMode={displayMode}> | 781 | <ComposerContextLabel displayMode={displayMode}> |
| 785 | <MiddleTruncate value={triggerLabel} className="w-full" /> | 782 | <MiddleTruncate value={triggerLabel} className="w-full" /> |
| 786 | </ComposerContextLabel> | 783 | </ComposerContextLabel> |
| 787 | {displayMode === "panel" ? ( | 784 | {displayMode !== "panel" ? ( |
| 788 | <span data-slot="select-icon"> | ||
| 789 | <ChevronDownIcon className={THREAD_DETAILS_PANEL_CHEVRON_CLASS} /> | ||
| 790 | </span> | ||
| 791 | ) : ( | ||
| 792 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> | 785 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> |
| 793 | )} | 786 | ) : null} |
| 794 | </ComboboxTrigger> | 787 | </ComboboxTrigger> |
| 795 | </span> | 788 | </span> |
| 796 | {displayMode === "panel" && prNumber !== undefined && prUrl !== undefined ? ( | 789 | {displayMode === "panel" && prNumber !== undefined && prUrl !== undefined ? ( |
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+23-114| ... | @@ -1,20 +1,12 @@ | ... | @@ -1,20 +1,12 @@ |
| 1 | import { ThreadDetailsSelectControl } from "./chat/ThreadDetailsControl"; | 1 | import { ComposerSelectControl } from "./chat/ComposerControl"; |
| 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; | 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 3 | import { FolderGit2Icon, FolderGitIcon, FolderIcon } from "lucide-react"; | 3 | import { FolderGit2Icon, FolderGitIcon, FolderIcon } from "lucide-react"; |
| 4 | import { memo, useMemo, type MouseEvent as ReactMouseEvent } from "react"; | 4 | import { memo, useMemo, type MouseEvent as ReactMouseEvent } from "react"; |
| 5 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; | ||
| 6 | import { readLocalApi } from "../localApi"; | ||
| 7 | import { cn } from "../lib/utils"; | ||
| 8 | import { | ||
| 9 | THREAD_DETAILS_PANEL_ICON_CLASS, | ||
| 10 | THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 11 | } from "./chat/threadDetailsPanelStyles"; | ||
| 12 | 5 | ||
| 13 | import { | 6 | import { |
| 14 | resolveCurrentWorkspaceLabel, | 7 | resolveCurrentWorkspaceLabel, |
| 15 | resolveEnvModeLabel, | 8 | resolveEnvModeLabel, |
| 16 | resolveLockedWorkspaceLabel, | 9 | resolveLockedWorkspaceLabel, |
| 17 | resolveWorkspaceDisplayName, | ||
| 18 | type EnvMode, | 10 | type EnvMode, |
| 19 | } from "./BranchToolbar.logic"; | 11 | } from "./BranchToolbar.logic"; |
| 20 | import { useComposerMenuProps } from "./chat/composerEventScope"; | 12 | import { useComposerMenuProps } from "./chat/composerEventScope"; |
| ... | @@ -28,7 +20,6 @@ import { | ... | @@ -28,7 +20,6 @@ import { |
| 28 | SelectValue, | 20 | SelectValue, |
| 29 | } from "./ui/select"; | 21 | } from "./ui/select"; |
| 30 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 22 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 31 | import { stackedThreadToast, toastManager } from "./ui/toast"; | ||
| 32 | 23 | ||
| 33 | const PREVIOUS_WORKTREE_SELECT_VALUE = "previous-worktree"; | 24 | const PREVIOUS_WORKTREE_SELECT_VALUE = "previous-worktree"; |
| 34 | 25 | ||
| ... | @@ -37,9 +28,7 @@ interface BranchToolbarEnvModeSelectorProps { | ... | @@ -37,9 +28,7 @@ interface BranchToolbarEnvModeSelectorProps { |
| 37 | envLocked: boolean; | 28 | envLocked: boolean; |
| 38 | effectiveEnvMode: EnvMode; | 29 | effectiveEnvMode: EnvMode; |
| 39 | activeWorktreePath: string | null; | 30 | activeWorktreePath: string | null; |
| 40 | workspaceRoot?: string | null; | ||
| 41 | onEnvModeChange: (mode: EnvMode) => void; | 31 | onEnvModeChange: (mode: EnvMode) => void; |
| 42 | displayMode?: "toolbar" | "panel"; | ||
| 43 | previousWorktreeLabel?: string | null; | 32 | previousWorktreeLabel?: string | null; |
| 44 | previousWorktreeBranch?: string | null; | 33 | previousWorktreeBranch?: string | null; |
| 45 | onUsePreviousWorktree?: () => void; | 34 | onUsePreviousWorktree?: () => void; |
| ... | @@ -50,72 +39,27 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe | ... | @@ -50,72 +39,27 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 50 | envLocked, | 39 | envLocked, |
| 51 | effectiveEnvMode, | 40 | effectiveEnvMode, |
| 52 | activeWorktreePath, | 41 | activeWorktreePath, |
| 53 | workspaceRoot = null, | ||
| 54 | onEnvModeChange, | 42 | onEnvModeChange, |
| 55 | displayMode = "toolbar", | ||
| 56 | previousWorktreeLabel, | 43 | previousWorktreeLabel, |
| 57 | previousWorktreeBranch = null, | 44 | previousWorktreeBranch = null, |
| 58 | onUsePreviousWorktree, | 45 | onUsePreviousWorktree, |
| 59 | }: BranchToolbarEnvModeSelectorProps) { | 46 | }: BranchToolbarEnvModeSelectorProps) { |
| 60 | const workspacePath = displayMode === "panel" ? (activeWorktreePath ?? workspaceRoot) : null; | ||
| 61 | const workspaceDisplayName = resolveWorkspaceDisplayName(workspacePath); | ||
| 62 | // The panel names the workspace kind only when it is not the project folder. | ||
| 63 | const workspaceKind = activeWorktreePath ? "Worktree" : null; | ||
| 64 | const lockedWorkspaceKind = forceNewWorktree ? "Worktree" : workspaceKind; | ||
| 65 | const selectWorkspaceKind = | ||
| 66 | effectiveEnvMode === "worktree" && !activeWorktreePath ? "Create" : workspaceKind; | ||
| 67 | const composerFloatingLayerProps = useComposerMenuProps(); | 47 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 68 | const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree); | 48 | const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree); |
| 69 | const envModeItems = useMemo( | 49 | const envModeItems = useMemo( |
| 70 | () => [ | 50 | () => [ |
| 71 | { | 51 | { |
| 72 | value: "local", | 52 | value: "local", |
| 73 | label: workspaceDisplayName ?? resolveCurrentWorkspaceLabel(activeWorktreePath), | 53 | label: resolveCurrentWorkspaceLabel(activeWorktreePath), |
| 74 | }, | 54 | }, |
| 75 | { value: "worktree", label: resolveEnvModeLabel("worktree") }, | 55 | { value: "worktree", label: resolveEnvModeLabel("worktree") }, |
| 76 | ...(showPreviousWorktree && previousWorktreeLabel | 56 | ...(showPreviousWorktree && previousWorktreeLabel |
| 77 | ? [{ value: PREVIOUS_WORKTREE_SELECT_VALUE, label: previousWorktreeLabel }] | 57 | ? [{ value: PREVIOUS_WORKTREE_SELECT_VALUE, label: previousWorktreeLabel }] |
| 78 | : []), | 58 | : []), |
| 79 | ], | 59 | ], |
| 80 | [activeWorktreePath, previousWorktreeLabel, showPreviousWorktree, workspaceDisplayName], | 60 | [activeWorktreePath, previousWorktreeLabel, showPreviousWorktree], |
| 81 | ); | 61 | ); |
| 82 | 62 | ||
| 83 | const handleWorkspaceContextMenu = (event: ReactMouseEvent) => { | ||
| 84 | if (!workspacePath || forceNewWorktree) return; | ||
| 85 | const api = readLocalApi(); | ||
| 86 | if (!api) return; | ||
| 87 | event.preventDefault(); | ||
| 88 | event.stopPropagation(); | ||
| 89 | void api.contextMenu | ||
| 90 | .show([{ id: "copy-path", label: "Copy full path", icon: "copy" }], { | ||
| 91 | x: event.clientX, | ||
| 92 | y: event.clientY, | ||
| 93 | }) | ||
| 94 | .then((action) => { | ||
| 95 | if (action !== "copy-path") return; | ||
| 96 | void writeTextToClipboard(workspacePath, "workspace path").then( | ||
| 97 | (didCopy) => { | ||
| 98 | if (didCopy) { | ||
| 99 | toastManager.add({ | ||
| 100 | type: "success", | ||
| 101 | title: "Path copied", | ||
| 102 | description: workspacePath, | ||
| 103 | }); | ||
| 104 | } | ||
| 105 | }, | ||
| 106 | (error: unknown) => { | ||
| 107 | toastManager.add( | ||
| 108 | stackedThreadToast({ | ||
| 109 | type: "error", | ||
| 110 | title: "Failed to copy path", | ||
| 111 | description: error instanceof Error ? error.message : "An error occurred.", | ||
| 112 | }), | ||
| 113 | ); | ||
| 114 | }, | ||
| 115 | ); | ||
| 116 | }); | ||
| 117 | }; | ||
| 118 | |||
| 119 | const stopContextMenuMouseDown = (event: ReactMouseEvent) => { | 63 | const stopContextMenuMouseDown = (event: ReactMouseEvent) => { |
| 120 | if (event.button !== 0 || event.ctrlKey) { | 64 | if (event.button !== 0 || event.ctrlKey) { |
| 121 | event.stopPropagation(); | 65 | event.stopPropagation(); |
| ... | @@ -125,47 +69,31 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe | ... | @@ -125,47 +69,31 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 125 | if (envLocked || forceNewWorktree) { | 69 | if (envLocked || forceNewWorktree) { |
| 126 | const lockedRow = ( | 70 | const lockedRow = ( |
| 127 | <span | 71 | <span |
| 128 | className={cn( | 72 | className="inline-flex h-7 min-w-0 items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6" |
| 129 | "inline-flex h-7 min-w-0 items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6", | ||
| 130 | displayMode === "panel" && THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 131 | )} | ||
| 132 | data-composer-context-control | 73 | data-composer-context-control |
| 133 | onContextMenu={handleWorkspaceContextMenu} | ||
| 134 | > | 74 | > |
| 135 | {activeWorktreePath ? ( | 75 | {activeWorktreePath ? ( |
| 136 | <FolderGitIcon | 76 | <FolderGitIcon className="size-3" /> |
| 137 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 138 | /> | ||
| 139 | ) : effectiveEnvMode === "worktree" ? ( | 77 | ) : effectiveEnvMode === "worktree" ? ( |
| 140 | <FolderGit2Icon | 78 | <FolderGit2Icon className="size-3" /> |
| 141 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 142 | /> | ||
| 143 | ) : ( | 79 | ) : ( |
| 144 | <FolderIcon | 80 | <FolderIcon className="size-3" /> |
| 145 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 146 | /> | ||
| 147 | )} | 81 | )} |
| 148 | <ComposerContextLabel displayMode={displayMode}> | 82 | <ComposerContextLabel> |
| 149 | {forceNewWorktree | 83 | {forceNewWorktree |
| 150 | ? resolveEnvModeLabel("worktree") | 84 | ? resolveEnvModeLabel("worktree") |
| 151 | : (workspaceDisplayName ?? | 85 | : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} |
| 152 | resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} | ||
| 153 | </ComposerContextLabel> | 86 | </ComposerContextLabel> |
| 154 | {displayMode === "panel" && lockedWorkspaceKind ? ( | ||
| 155 | <span className="shrink-0 text-3xs font-normal text-muted-foreground/70"> | ||
| 156 | {lockedWorkspaceKind} | ||
| 157 | </span> | ||
| 158 | ) : null} | ||
| 159 | </span> | 87 | </span> |
| 160 | ); | 88 | ); |
| 161 | 89 | ||
| 162 | return ( | 90 | return ( |
| 163 | <Tooltip> | 91 | <Tooltip> |
| 164 | <TooltipTrigger render={lockedRow} /> | 92 | <TooltipTrigger render={lockedRow} /> |
| 165 | <TooltipPopup side={displayMode === "panel" ? "left" : undefined}> | 93 | <TooltipPopup> |
| 166 | {forceNewWorktree | 94 | {forceNewWorktree |
| 167 | ? "Each model starts in its own worktree." | 95 | ? "Each model starts in its own worktree." |
| 168 | : (workspacePath ?? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} | 96 | : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} |
| 169 | </TooltipPopup> | 97 | </TooltipPopup> |
| 170 | </Tooltip> | 98 | </Tooltip> |
| 171 | ); | 99 | ); |
| ... | @@ -187,56 +115,37 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe | ... | @@ -187,56 +115,37 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 187 | <Tooltip> | 115 | <Tooltip> |
| 188 | <TooltipTrigger | 116 | <TooltipTrigger |
| 189 | render={ | 117 | render={ |
| 190 | <ThreadDetailsSelectControl | 118 | <ComposerSelectControl |
| 191 | panel={displayMode === "panel"} | 119 | size="xs" |
| 192 | className="min-w-0 shrink" | 120 | className="min-w-0 shrink" |
| 193 | aria-label="Workspace" | 121 | aria-label="Workspace" |
| 194 | data-composer-shortcut="composer.workspace" | 122 | data-composer-shortcut="composer.workspace" |
| 195 | data-composer-context-control | 123 | data-composer-context-control |
| 196 | onMouseDownCapture={stopContextMenuMouseDown} | 124 | onMouseDownCapture={stopContextMenuMouseDown} |
| 197 | onContextMenu={handleWorkspaceContextMenu} | ||
| 198 | /> | 125 | /> |
| 199 | } | 126 | } |
| 200 | > | 127 | > |
| 201 | {effectiveEnvMode === "worktree" ? ( | 128 | {effectiveEnvMode === "worktree" ? ( |
| 202 | <FolderGit2Icon | 129 | <FolderGit2Icon className="size-3" /> |
| 203 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 204 | /> | ||
| 205 | ) : activeWorktreePath ? ( | 130 | ) : activeWorktreePath ? ( |
| 206 | <FolderGitIcon | 131 | <FolderGitIcon className="size-3" /> |
| 207 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 208 | /> | ||
| 209 | ) : ( | 132 | ) : ( |
| 210 | <FolderIcon | 133 | <FolderIcon className="size-3" /> |
| 211 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | ||
| 212 | /> | ||
| 213 | )} | 134 | )} |
| 214 | <ComposerContextLabel displayMode={displayMode}> | 135 | <ComposerContextLabel> |
| 215 | <SelectValue /> | 136 | <SelectValue /> |
| 216 | </ComposerContextLabel> | 137 | </ComposerContextLabel> |
| 217 | {displayMode === "panel" && selectWorkspaceKind ? ( | ||
| 218 | <span className="shrink-0 text-3xs font-normal text-muted-foreground/70"> | ||
| 219 | {selectWorkspaceKind} | ||
| 220 | </span> | ||
| 221 | ) : null} | ||
| 222 | </TooltipTrigger> | 138 | </TooltipTrigger> |
| 223 | <TooltipPopup side={displayMode === "panel" ? "left" : undefined}> | 139 | <TooltipPopup> |
| 224 | {workspacePath ?? | 140 | {effectiveEnvMode === "worktree" |
| 225 | (effectiveEnvMode === "worktree" | 141 | ? resolveEnvModeLabel("worktree") |
| 226 | ? resolveEnvModeLabel("worktree") | 142 | : resolveCurrentWorkspaceLabel(activeWorktreePath)} |
| 227 | : resolveCurrentWorkspaceLabel(activeWorktreePath))} | ||
| 228 | </TooltipPopup> | 143 | </TooltipPopup> |
| 229 | </Tooltip> | 144 | </Tooltip> |
| 230 | <SelectPopup | 145 | <SelectPopup |
| 231 | alignItemWithTrigger={false} | 146 | alignItemWithTrigger={false} |
| 232 | {...(displayMode === "toolbar" ? composerFloatingLayerProps : {})} | 147 | {...composerFloatingLayerProps} |
| 233 | className={ | 148 | className={showPreviousWorktree ? "w-[min(21rem,calc(100vw-2rem))]" : undefined} |
| 234 | displayMode === "panel" | ||
| 235 | ? "w-(--anchor-width)" | ||
| 236 | : showPreviousWorktree | ||
| 237 | ? "w-[min(21rem,calc(100vw-2rem))]" | ||
| 238 | : undefined | ||
| 239 | } | ||
| 240 | > | 149 | > |
| 241 | <SelectGroup> | 150 | <SelectGroup> |
| 242 | <SelectGroupLabel>Workspace</SelectGroupLabel> | 151 | <SelectGroupLabel>Workspace</SelectGroupLabel> |
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+10-37| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { ThreadDetailsSelectControl } from "./chat/ThreadDetailsControl"; | 1 | import { ComposerSelectControl } from "./chat/ComposerControl"; |
| 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; | 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 3 | import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip"; | 3 | import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip"; |
| 4 | import type { EnvironmentId } from "@t3tools/contracts"; | 4 | import type { EnvironmentId } from "@t3tools/contracts"; |
| ... | @@ -6,11 +6,6 @@ import { ScaleIcon } from "lucide-react"; | ... | @@ -6,11 +6,6 @@ import { ScaleIcon } from "lucide-react"; |
| 6 | import { memo, useMemo } from "react"; | 6 | import { memo, useMemo } from "react"; |
| 7 | 7 | ||
| 8 | import type { EnvironmentOption } from "./BranchToolbar.logic"; | 8 | import type { EnvironmentOption } from "./BranchToolbar.logic"; |
| 9 | import { cn } from "../lib/utils"; | ||
| 10 | import { | ||
| 11 | THREAD_DETAILS_PANEL_ICON_CLASS, | ||
| 12 | THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 13 | } from "./chat/threadDetailsPanelStyles"; | ||
| 14 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | 9 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| 15 | import { useComposerMenuProps } from "./chat/composerEventScope"; | 10 | import { useComposerMenuProps } from "./chat/composerEventScope"; |
| 16 | import { | 11 | import { |
| ... | @@ -29,7 +24,6 @@ interface BranchToolbarEnvironmentSelectorProps { | ... | @@ -29,7 +24,6 @@ interface BranchToolbarEnvironmentSelectorProps { |
| 29 | environmentId: EnvironmentId; | 24 | environmentId: EnvironmentId; |
| 30 | availableEnvironments: readonly EnvironmentOption[]; | 25 | availableEnvironments: readonly EnvironmentOption[]; |
| 31 | onEnvironmentChange?: (environmentId: EnvironmentId) => void; | 26 | onEnvironmentChange?: (environmentId: EnvironmentId) => void; |
| 32 | displayMode?: "toolbar" | "panel"; | ||
| 33 | } | 27 | } |
| 34 | 28 | ||
| 35 | export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({ | 29 | export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({ |
| ... | @@ -39,7 +33,6 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -39,7 +33,6 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 39 | environmentId, | 33 | environmentId, |
| 40 | availableEnvironments, | 34 | availableEnvironments, |
| 41 | onEnvironmentChange, | 35 | onEnvironmentChange, |
| 42 | displayMode = "toolbar", | ||
| 43 | }: BranchToolbarEnvironmentSelectorProps) { | 36 | }: BranchToolbarEnvironmentSelectorProps) { |
| 44 | const composerFloatingLayerProps = useComposerMenuProps(); | 37 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 45 | const activeEnvironment = useMemo(() => { | 38 | const activeEnvironment = useMemo(() => { |
| ... | @@ -67,19 +60,14 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -67,19 +60,14 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 67 | if (envLocked || onEnvironmentChange === undefined) { | 60 | if (envLocked || onEnvironmentChange === undefined) { |
| 68 | const lockedRow = ( | 61 | const lockedRow = ( |
| 69 | <span | 62 | <span |
| 70 | className={cn( | 63 | className="inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6" |
| 71 | "inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6", | ||
| 72 | displayMode === "panel" && THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, | ||
| 73 | )} | ||
| 74 | data-composer-context-control | 64 | data-composer-context-control |
| 75 | > | 65 | > |
| 76 | <EnvironmentMachineIcon | 66 | <EnvironmentMachineIcon |
| 77 | kind={activeEnvironment?.machine ?? "server"} | 67 | kind={activeEnvironment?.machine ?? "server"} |
| 78 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0"} | 68 | className="size-3 shrink-0" |
| 79 | /> | 69 | /> |
| 80 | <ComposerContextLabel displayMode={displayMode}> | 70 | <ComposerContextLabel>{activeEnvironment?.label ?? "Run on"}</ComposerContextLabel> |
| 81 | {activeEnvironment?.label ?? "Run on"} | ||
| 82 | </ComposerContextLabel> | ||
| 83 | </span> | 71 | </span> |
| 84 | ); | 72 | ); |
| 85 | return ( | 73 | return ( |
| ... | @@ -102,8 +90,8 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -102,8 +90,8 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 102 | <Tooltip> | 90 | <Tooltip> |
| 103 | <TooltipTrigger | 91 | <TooltipTrigger |
| 104 | render={ | 92 | render={ |
| 105 | <ThreadDetailsSelectControl | 93 | <ComposerSelectControl |
| 106 | panel={displayMode === "panel"} | 94 | size="xs" |
| 107 | className="min-w-0 max-w-full" | 95 | className="min-w-0 max-w-full" |
| 108 | aria-label="Run on" | 96 | aria-label="Run on" |
| 109 | data-composer-shortcut="composer.host" | 97 | data-composer-shortcut="composer.host" |
| ... | @@ -112,35 +100,20 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -112,35 +100,20 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 112 | } | 100 | } |
| 113 | > | 101 | > |
| 114 | {autoEnvironmentLabel ? ( | 102 | {autoEnvironmentLabel ? ( |
| 115 | <ScaleIcon | 103 | <ScaleIcon className="size-3 shrink-0" aria-hidden="true" /> |
| 116 | className={ | ||
| 117 | displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0" | ||
| 118 | } | ||
| 119 | aria-hidden="true" | ||
| 120 | /> | ||
| 121 | ) : ( | 104 | ) : ( |
| 122 | <EnvironmentMachineIcon | 105 | <EnvironmentMachineIcon |
| 123 | kind={activeEnvironment?.machine ?? "server"} | 106 | kind={activeEnvironment?.machine ?? "server"} |
| 124 | className={ | 107 | className="size-3 shrink-0" |
| 125 | displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0" | ||
| 126 | } | ||
| 127 | /> | 108 | /> |
| 128 | )} | 109 | )} |
| 129 | <ComposerContextLabel displayMode={displayMode}> | 110 | <ComposerContextLabel> |
| 130 | <SelectValue /> | 111 | <SelectValue /> |
| 131 | </ComposerContextLabel> | 112 | </ComposerContextLabel> |
| 132 | </TooltipTrigger> | 113 | </TooltipTrigger> |
| 133 | <TooltipPopup>{autoEnvironmentLabel ?? activeEnvironment?.label ?? "Run on"}</TooltipPopup> | 114 | <TooltipPopup>{autoEnvironmentLabel ?? activeEnvironment?.label ?? "Run on"}</TooltipPopup> |
| 134 | </Tooltip> | 115 | </Tooltip> |
| 135 | <SelectPopup | 116 | <SelectPopup alignItemWithTrigger={false} {...composerFloatingLayerProps}> |
| 136 | alignItemWithTrigger={false} | ||
| 137 | {...(displayMode === "toolbar" ? composerFloatingLayerProps : {})} | ||
| 138 | {...(displayMode === "panel" | ||
| 139 | ? { | ||
| 140 | className: "w-(--anchor-width)", | ||
| 141 | } | ||
| 142 | : {})} | ||
| 143 | > | ||
| 144 | <SelectGroup> | 117 | <SelectGroup> |
| 145 | <SelectGroupLabel>Run on</SelectGroupLabel> | 118 | <SelectGroupLabel>Run on</SelectGroupLabel> |
| 146 | {onAutoEnvironment && ( | 119 | {onAutoEnvironment && ( |
apps/web/src/components/ChatView.tsx-16| ... | @@ -3069,11 +3069,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3069,11 +3069,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3069 | hasMultipleRegisteredEnvironments && activeThread | 3069 | hasMultipleRegisteredEnvironments && activeThread |
| 3070 | ? `${environmentById.get(activeThread.environmentId)?.label ?? serverConfig?.environment.label ?? activeThread.environmentId} server` | 3070 | ? `${environmentById.get(activeThread.environmentId)?.label ?? serverConfig?.environment.label ?? activeThread.environmentId} server` |
| 3071 | : "server"; | 3071 | : "server"; |
| 3072 | const handleDismissVersionMismatch = useCallback(() => { | ||
| 3073 | if (!versionMismatchDismissKey) return; | ||
| 3074 | dismissVersionMismatch(versionMismatchDismissKey); | ||
| 3075 | setDismissedVersionMismatchKey(versionMismatchDismissKey); | ||
| 3076 | }, [setDismissedVersionMismatchKey, versionMismatchDismissKey]); | ||
| 3077 | const serverUpdateEnvironmentId = activeThread?.environmentId ?? null; | 3072 | const serverUpdateEnvironmentId = activeThread?.environmentId ?? null; |
| 3078 | const versionMismatchSelfUpdate = resolveServerSelfUpdateCapability(serverConfig); | 3073 | const versionMismatchSelfUpdate = resolveServerSelfUpdateCapability(serverConfig); |
| 3079 | const versionMismatchInstallation = serverConfig?.environment.capabilities.serverInstallation; | 3074 | const versionMismatchInstallation = serverConfig?.environment.capabilities.serverInstallation; |
| ... | @@ -11078,15 +11073,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -11078,15 +11073,6 @@ export default function ChatView(props: ChatViewProps) { |
| 11078 | : {}), | 11073 | : {}), |
| 11079 | onComposerFocusRequest: scheduleComposerFocus, | 11074 | onComposerFocusRequest: scheduleComposerFocus, |
| 11080 | ...(isServerThread && isGitRepo ? { onOpenChanges: openChangesFromThreadPanel } : {}), | 11075 | ...(isServerThread && isGitRepo ? { onOpenChanges: openChangesFromThreadPanel } : {}), |
| 11081 | versionMismatch: | ||
| 11082 | showVersionMismatchBanner && versionMismatch | ||
| 11083 | ? { | ||
| 11084 | clientVersion: versionMismatch.clientVersion, | ||
| 11085 | serverVersion: versionMismatch.serverVersion, | ||
| 11086 | serverLabel: versionMismatchServerLabel, | ||
| 11087 | } | ||
| 11088 | : null, | ||
| 11089 | onDismissVersionMismatch: handleDismissVersionMismatch, | ||
| 11090 | onRunProjectScript: runProjectScript, | 11076 | onRunProjectScript: runProjectScript, |
| 11091 | onAddProjectScript: saveProjectScript, | 11077 | onAddProjectScript: saveProjectScript, |
| 11092 | onUpdateProjectScript: updateProjectScript, | 11078 | onUpdateProjectScript: updateProjectScript, |
| ... | @@ -11100,8 +11086,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -11100,8 +11086,6 @@ export default function ChatView(props: ChatViewProps) { |
| 11100 | threadPanelPresentation, | 11086 | threadPanelPresentation, |
| 11101 | threadPanelPopoverHandle, | 11087 | threadPanelPopoverHandle, |
| 11102 | threadPanelShortcutLabel: shortcutLabelForCommand(keybindings, "threadPanel.toggle"), | 11088 | threadPanelShortcutLabel: shortcutLabelForCommand(keybindings, "threadPanel.toggle"), |
| 11103 | threadPanelHasAttention: | ||
| 11104 | activeEnvironmentUnavailableState !== null || showVersionMismatchBanner, | ||
| 11105 | rightPanelAvailable: activeProject !== null, | 11089 | rightPanelAvailable: activeProject !== null, |
| 11106 | rightPanelOpen, | 11090 | rightPanelOpen, |
| 11107 | rightPanelShortcutLabel: shortcutLabelForCommand(keybindings, "rightPanel.toggle"), | 11091 | rightPanelShortcutLabel: shortcutLabelForCommand(keybindings, "rightPanel.toggle"), |
apps/web/src/components/chat/PanelLayoutControls.tsx-8| ... | @@ -19,7 +19,6 @@ export interface PanelLayoutControlsProps { | ... | @@ -19,7 +19,6 @@ export interface PanelLayoutControlsProps { |
| 19 | threadPanelPresentation: ThreadPanelPresentation; | 19 | threadPanelPresentation: ThreadPanelPresentation; |
| 20 | threadPanelPopoverHandle?: ReturnType<typeof PopoverCreateHandle>; | 20 | threadPanelPopoverHandle?: ReturnType<typeof PopoverCreateHandle>; |
| 21 | threadPanelShortcutLabel: string | null; | 21 | threadPanelShortcutLabel: string | null; |
| 22 | threadPanelHasAttention: boolean; | ||
| 23 | rightPanelAvailable: boolean; | 22 | rightPanelAvailable: boolean; |
| 24 | rightPanelOpen: boolean; | 23 | rightPanelOpen: boolean; |
| 25 | rightPanelShortcutLabel: string | null; | 24 | rightPanelShortcutLabel: string | null; |
| ... | @@ -40,7 +39,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ | ... | @@ -40,7 +39,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 40 | threadPanelPresentation, | 39 | threadPanelPresentation, |
| 41 | threadPanelPopoverHandle, | 40 | threadPanelPopoverHandle, |
| 42 | threadPanelShortcutLabel, | 41 | threadPanelShortcutLabel, |
| 43 | threadPanelHasAttention, | ||
| 44 | rightPanelAvailable, | 42 | rightPanelAvailable, |
| 45 | rightPanelOpen, | 43 | rightPanelOpen, |
| 46 | rightPanelShortcutLabel, | 44 | rightPanelShortcutLabel, |
| ... | @@ -58,12 +56,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ | ... | @@ -58,12 +56,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 58 | size="sm" | 56 | size="sm" |
| 59 | > | 57 | > |
| 60 | <SquareMenuIcon className="size-4" /> | 58 | <SquareMenuIcon className="size-4" /> |
| 61 | {threadPanelHasAttention ? ( | ||
| 62 | <span | ||
| 63 | className="absolute right-1 top-1 size-1.5 rounded-full bg-warning ring-2 ring-background" | ||
| 64 | aria-hidden="true" | ||
| 65 | /> | ||
| 66 | ) : null} | ||
| 67 | </Toggle> | 59 | </Toggle> |
| 68 | ); | 60 | ); |
| 69 | const threadPanelTooltip = (trigger: ReactElement) => ( | 61 | const threadPanelTooltip = (trigger: ReactElement) => ( |
apps/web/src/components/chat/ThreadDetailsControl.test.tsx created+38| ... | @@ -0,0 +1,38 @@ | ||
| 1 | // @vitest-environment jsdom | ||
| 2 | |||
| 3 | import { act } from "react"; | ||
| 4 | import { createRoot } from "react-dom/client"; | ||
| 5 | import { expect, it, vi } from "vite-plus/test"; | ||
| 6 | |||
| 7 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; | ||
| 8 | import { ThreadDetailsControl } from "./ThreadDetailsControl"; | ||
| 9 | |||
| 10 | it("keeps the row label and one chevron when used as a menu trigger", async () => { | ||
| 11 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | ||
| 12 | const container = document.createElement("div"); | ||
| 13 | document.body.append(container); | ||
| 14 | const root = createRoot(container); | ||
| 15 | try { | ||
| 16 | await act(async () => { | ||
| 17 | root.render( | ||
| 18 | <Menu> | ||
| 19 | <MenuTrigger render={<ThreadDetailsControl part="select" />}> | ||
| 20 | Run on this machine | ||
| 21 | </MenuTrigger> | ||
| 22 | <MenuPopup> | ||
| 23 | <MenuItem>New worktree</MenuItem> | ||
| 24 | </MenuPopup> | ||
| 25 | </Menu>, | ||
| 26 | ); | ||
| 27 | }); | ||
| 28 | const trigger = container.querySelector("button")!; | ||
| 29 | expect(trigger.textContent).toBe("Run on this machine"); | ||
| 30 | expect(trigger.querySelectorAll(".lucide-chevron-down")).toHaveLength(1); | ||
| 31 | await act(async () => trigger.click()); | ||
| 32 | expect(document.querySelector('[role="menuitem"]')?.textContent).toBe("New worktree"); | ||
| 33 | } finally { | ||
| 34 | await act(async () => root.unmount()); | ||
| 35 | container.remove(); | ||
| 36 | vi.unstubAllGlobals(); | ||
| 37 | } | ||
| 38 | }); | ||
apps/web/src/components/chat/ThreadDetailsControl.tsx+17-32| ... | @@ -1,17 +1,14 @@ | ... | @@ -1,17 +1,14 @@ |
| 1 | import { mergeProps } from "@base-ui/react/merge-props"; | 1 | import { mergeProps } from "@base-ui/react/merge-props"; |
| 2 | import { Select as SelectPrimitive } from "@base-ui/react/select"; | ||
| 3 | import { useRender } from "@base-ui/react/use-render"; | 2 | import { useRender } from "@base-ui/react/use-render"; |
| 4 | import type { ComponentProps } from "react"; | 3 | import type { ComponentProps } from "react"; |
| 5 | 4 | ||
| 6 | import { cn } from "~/lib/utils"; | 5 | import { cn } from "~/lib/utils"; |
| 7 | import { Button } from "../ui/button"; | 6 | import { Button } from "../ui/button"; |
| 8 | import { ComposerSelectControl } from "./ComposerControl"; | ||
| 9 | import { | 7 | import { |
| 10 | THREAD_DETAILS_PANEL_ROW_CLASS, | 8 | THREAD_DETAILS_PANEL_ROW_CLASS, |
| 11 | THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, | 9 | THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, |
| 12 | THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, | 10 | THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, |
| 13 | THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, | 11 | THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, |
| 14 | THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS, | ||
| 15 | THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, | 12 | THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, |
| 16 | THREAD_DETAILS_PANEL_META_ACTION_CLASS, | 13 | THREAD_DETAILS_PANEL_META_ACTION_CLASS, |
| 17 | THREAD_DETAILS_PANEL_CHEVRON_CLASS, | 14 | THREAD_DETAILS_PANEL_CHEVRON_CLASS, |
| ... | @@ -20,11 +17,9 @@ import { ChevronDownIcon } from "lucide-react"; | ... | @@ -20,11 +17,9 @@ import { ChevronDownIcon } from "lucide-react"; |
| 20 | 17 | ||
| 21 | const parts = { | 18 | const parts = { |
| 22 | row: THREAD_DETAILS_PANEL_ROW_CLASS, | 19 | row: THREAD_DETAILS_PANEL_ROW_CLASS, |
| 23 | "group-row": `${THREAD_DETAILS_PANEL_ROW_CLASS} hover:!bg-transparent dark:hover:!bg-transparent`, | ||
| 24 | select: THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, | 20 | select: THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, |
| 25 | primary: THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, | 21 | primary: THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, |
| 26 | secondary: THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, | 22 | secondary: THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, |
| 27 | "link-primary": THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS, | ||
| 28 | icon: THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, | 23 | icon: THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, |
| 29 | meta: THREAD_DETAILS_PANEL_META_ACTION_CLASS, | 24 | meta: THREAD_DETAILS_PANEL_META_ACTION_CLASS, |
| 30 | }; | 25 | }; |
| ... | @@ -36,6 +31,7 @@ export function ThreadDetailsControl({ | ... | @@ -36,6 +31,7 @@ export function ThreadDetailsControl({ |
| 36 | multiline = false, | 31 | multiline = false, |
| 37 | tone = "default", | 32 | tone = "default", |
| 38 | className, | 33 | className, |
| 34 | children, | ||
| 39 | size = "default", | 35 | size = "default", |
| 40 | variant = "default", | 36 | variant = "default", |
| 41 | render, | 37 | render, |
| ... | @@ -64,6 +60,19 @@ export function ThreadDetailsControl({ | ... | @@ -64,6 +60,19 @@ export function ThreadDetailsControl({ |
| 64 | ), | 60 | ), |
| 65 | }, | 61 | }, |
| 66 | props, | 62 | props, |
| 63 | { | ||
| 64 | children: | ||
| 65 | part === "select" ? ( | ||
| 66 | <> | ||
| 67 | {children} | ||
| 68 | <span data-slot="select-icon"> | ||
| 69 | <ChevronDownIcon className={THREAD_DETAILS_PANEL_CHEVRON_CLASS} /> | ||
| 70 | </span> | ||
| 71 | </> | ||
| 72 | ) : ( | ||
| 73 | children | ||
| 74 | ), | ||
| 75 | }, | ||
| 67 | ), | 76 | ), |
| 68 | }); | 77 | }); |
| 69 | if (!panel) { | 78 | if (!panel) { |
| ... | @@ -74,34 +83,10 @@ export function ThreadDetailsControl({ | ... | @@ -74,34 +83,10 @@ export function ThreadDetailsControl({ |
| 74 | size={multiline ? "sm-multiline" : size} | 83 | size={multiline ? "sm-multiline" : size} |
| 75 | variant={variant} | 84 | variant={variant} |
| 76 | className={className} | 85 | className={className} |
| 77 | /> | 86 | > |
| 78 | ); | ||
| 79 | } | ||
| 80 | return control; | ||
| 81 | } | ||
| 82 | |||
| 83 | export function ThreadDetailsSelectControl({ | ||
| 84 | panel, | ||
| 85 | children, | ||
| 86 | className, | ||
| 87 | ...props | ||
| 88 | }: Omit<SelectPrimitive.Trigger.Props, "className"> & { panel: boolean; className?: string }) { | ||
| 89 | if (!panel) { | ||
| 90 | return ( | ||
| 91 | <ComposerSelectControl {...props} size="xs" className={className}> | ||
| 92 | {children} | 87 | {children} |
| 93 | </ComposerSelectControl> | 88 | </Button> |
| 94 | ); | 89 | ); |
| 95 | } | 90 | } |
| 96 | return ( | 91 | return control; |
| 97 | <SelectPrimitive.Trigger | ||
| 98 | {...props} | ||
| 99 | render={<ThreadDetailsControl part="select" className={className} />} | ||
| 100 | > | ||
| 101 | {children} | ||
| 102 | <SelectPrimitive.Icon data-slot="select-icon"> | ||
| 103 | <ChevronDownIcon className={THREAD_DETAILS_PANEL_CHEVRON_CLASS} /> | ||
| 104 | </SelectPrimitive.Icon> | ||
| 105 | </SelectPrimitive.Trigger> | ||
| 106 | ); | ||
| 107 | } | 92 | } |
apps/web/src/components/chat/ThreadDetailsPanel.test.tsx-2| ... | @@ -74,8 +74,6 @@ describe("ThreadDetailsPanel", () => { | ... | @@ -74,8 +74,6 @@ describe("ThreadDetailsPanel", () => { |
| 74 | startFromOrigin: false, | 74 | startFromOrigin: false, |
| 75 | onStartFromOriginChange: vi.fn(), | 75 | onStartFromOriginChange: vi.fn(), |
| 76 | onComposerFocusRequest: vi.fn(), | 76 | onComposerFocusRequest: vi.fn(), |
| 77 | versionMismatch: null, | ||
| 78 | onDismissVersionMismatch: vi.fn(), | ||
| 79 | onRunProjectScript: vi.fn(), | 77 | onRunProjectScript: vi.fn(), |
| 80 | onAddProjectScript: vi.fn() as ThreadDetailsPanelProps["onAddProjectScript"], | 78 | onAddProjectScript: vi.fn() as ThreadDetailsPanelProps["onAddProjectScript"], |
| 81 | onUpdateProjectScript: vi.fn() as ThreadDetailsPanelProps["onUpdateProjectScript"], | 79 | onUpdateProjectScript: vi.fn() as ThreadDetailsPanelProps["onUpdateProjectScript"], |
apps/web/src/components/chat/ThreadDetailsPanel.tsx+8-58| ... | @@ -5,23 +5,16 @@ import type { | ... | @@ -5,23 +5,16 @@ import type { |
| 5 | ResolvedKeybindingsConfig, | 5 | ResolvedKeybindingsConfig, |
| 6 | ThreadId, | 6 | ThreadId, |
| 7 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 8 | import { AlertTriangleIcon, XIcon } from "lucide-react"; | ||
| 9 | 8 | ||
| 10 | import type { DraftId } from "../../composerDraftStore"; | 9 | import type { DraftId } from "../../composerDraftStore"; |
| 11 | import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts"; | 10 | import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts"; |
| 12 | import { | 11 | import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic"; |
| 13 | shouldShowEnvironmentIndicator, | ||
| 14 | type EnvMode, | ||
| 15 | type EnvironmentOption, | ||
| 16 | } from "../BranchToolbar.logic"; | ||
| 17 | import { BranchToolbar } from "../BranchToolbar"; | 12 | import { BranchToolbar } from "../BranchToolbar"; |
| 18 | import { BranchToolbarEnvironmentSelector } from "../BranchToolbarEnvironmentSelector"; | ||
| 19 | import GitActionsControl from "../GitActionsControl"; | 13 | import GitActionsControl from "../GitActionsControl"; |
| 20 | import ProjectScriptsControl, { | 14 | import ProjectScriptsControl, { |
| 21 | type NewProjectScriptInput, | 15 | type NewProjectScriptInput, |
| 22 | type ProjectScriptActionResult, | 16 | type ProjectScriptActionResult, |
| 23 | } from "../ProjectScriptsControl"; | 17 | } from "../ProjectScriptsControl"; |
| 24 | import { Button } from "../ui/button"; | ||
| 25 | import type { ComponentProps } from "react"; | 18 | import type { ComponentProps } from "react"; |
| 26 | import { ThreadDetailsCard } from "./ThreadDetailsCard"; | 19 | import { ThreadDetailsCard } from "./ThreadDetailsCard"; |
| 27 | import { OpenInPicker } from "./OpenInPicker"; | 20 | import { OpenInPicker } from "./OpenInPicker"; |
| ... | @@ -29,12 +22,6 @@ import { ThreadDetailsSection } from "./ThreadDetailsSection"; | ... | @@ -29,12 +22,6 @@ import { ThreadDetailsSection } from "./ThreadDetailsSection"; |
| 29 | import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; | 22 | import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; |
| 30 | import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl"; | 23 | import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl"; |
| 31 | 24 | ||
| 32 | interface VersionMismatchIssue { | ||
| 33 | readonly clientVersion: string; | ||
| 34 | readonly serverVersion: string; | ||
| 35 | readonly serverLabel: string; | ||
| 36 | } | ||
| 37 | |||
| 38 | export interface ThreadDetailsPanelProps extends Pick< | 25 | export interface ThreadDetailsPanelProps extends Pick< |
| 39 | ComponentProps<typeof ThreadDetailsCard>, | 26 | ComponentProps<typeof ThreadDetailsCard>, |
| 40 | "anchor" | "handle" | "onPresentationChange" | 27 | "anchor" | "handle" | "onPresentationChange" |
| ... | @@ -66,8 +53,6 @@ export interface ThreadDetailsPanelProps extends Pick< | ... | @@ -66,8 +53,6 @@ export interface ThreadDetailsPanelProps extends Pick< |
| 66 | onCheckoutPullRequestRequest?: (reference: string) => void; | 53 | onCheckoutPullRequestRequest?: (reference: string) => void; |
| 67 | onComposerFocusRequest: () => void; | 54 | onComposerFocusRequest: () => void; |
| 68 | onOpenChanges?: () => void; | 55 | onOpenChanges?: () => void; |
| 69 | versionMismatch: VersionMismatchIssue | null; | ||
| 70 | onDismissVersionMismatch: () => void; | ||
| 71 | onRunProjectScript: (script: ProjectScript) => void; | 56 | onRunProjectScript: (script: ProjectScript) => void; |
| 72 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; | 57 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; |
| 73 | onUpdateProjectScript: ( | 58 | onUpdateProjectScript: ( |
| ... | @@ -82,14 +67,6 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { | ... | @@ -82,14 +67,6 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 82 | props.environmentId, | 67 | props.environmentId, |
| 83 | props.activeProjectScripts ? props.gitCwd : null, | 68 | props.activeProjectScripts ? props.gitCwd : null, |
| 84 | ); | 69 | ); |
| 85 | // Same rule as the composer strip: a lone remote machine still gets a row, | ||
| 86 | // shown as a static label because there is nothing to pick. | ||
| 87 | const canPickEnvironment = props.availableEnvironments.length > 1; | ||
| 88 | const showEnvironment = shouldShowEnvironmentIndicator({ | ||
| 89 | activeEnvironment: | ||
| 90 | props.availableEnvironments.find((env) => env.environmentId === props.environmentId) ?? null, | ||
| 91 | canPickEnvironment, | ||
| 92 | }); | ||
| 93 | const branchToolbarProps = { | 70 | const branchToolbarProps = { |
| 94 | showGitControls: props.isGitRepo, | 71 | showGitControls: props.isGitRepo, |
| 95 | environmentId: props.environmentId, | 72 | environmentId: props.environmentId, |
| ... | @@ -128,46 +105,19 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { | ... | @@ -128,46 +105,19 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 128 | separated={false} | 105 | separated={false} |
| 129 | showHeading={false} | 106 | showHeading={false} |
| 130 | > | 107 | > |
| 131 | {props.versionMismatch ? ( | ||
| 132 | <div className="mx-1 mb-2 flex gap-2 rounded-xl border border-warning/30 bg-warning/6 p-3"> | ||
| 133 | <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0 text-warning" /> | ||
| 134 | <div className="min-w-0 flex-1"> | ||
| 135 | <p className="text-xs font-medium">Client and server versions differ</p> | ||
| 136 | <p className="mt-1 text-2xs leading-relaxed text-muted-foreground"> | ||
| 137 | Client {props.versionMismatch.clientVersion} ·{" "} | ||
| 138 | {props.versionMismatch.serverLabel} {props.versionMismatch.serverVersion} | ||
| 139 | </p> | ||
| 140 | </div> | ||
| 141 | <Button | ||
| 142 | size="icon-xs" | ||
| 143 | variant="ghost" | ||
| 144 | aria-label="Dismiss version mismatch warning" | ||
| 145 | onClick={props.onDismissVersionMismatch} | ||
| 146 | > | ||
| 147 | <XIcon className="size-3.5" /> | ||
| 148 | </Button> | ||
| 149 | </div> | ||
| 150 | ) : null} | ||
| 151 | |||
| 152 | <div className="flex flex-col"> | 108 | <div className="flex flex-col"> |
| 153 | {density === "full" && showEnvironment ? ( | 109 | {density === "full" ? ( |
| 154 | <BranchToolbarEnvironmentSelector | 110 | <BranchToolbar |
| 155 | displayMode="panel" | 111 | layout="panel" |
| 112 | panelSection="workspace" | ||
| 113 | availableEnvironments={props.availableEnvironments} | ||
| 114 | onEnvironmentChange={props.onEnvironmentChange} | ||
| 156 | autoEnvironmentLabel={props.autoEnvironmentLabel} | 115 | autoEnvironmentLabel={props.autoEnvironmentLabel} |
| 157 | onAutoEnvironment={props.onAutoEnvironment} | 116 | onAutoEnvironment={props.onAutoEnvironment} |
| 158 | envLocked={props.envLocked} | 117 | {...branchToolbarProps} |
| 159 | environmentId={props.environmentId} | ||
| 160 | availableEnvironments={props.availableEnvironments} | ||
| 161 | {...(canPickEnvironment | ||
| 162 | ? { onEnvironmentChange: props.onEnvironmentChange } | ||
| 163 | : {})} | ||
| 164 | /> | 118 | /> |
| 165 | ) : null} | 119 | ) : null} |
| 166 | 120 | ||
| 167 | {density === "full" ? ( | ||
| 168 | <BranchToolbar layout="panel" panelSection="workspace" {...branchToolbarProps} /> | ||
| 169 | ) : null} | ||
| 170 | |||
| 171 | {density !== "essential" && props.showOpenInPicker ? ( | 121 | {density !== "essential" && props.showOpenInPicker ? ( |
| 172 | <OpenInPicker | 122 | <OpenInPicker |
| 173 | keybindings={props.keybindings} | 123 | keybindings={props.keybindings} |
apps/web/src/components/chat/ThreadDetailsPrRow.tsx+4-4| ... | @@ -75,7 +75,7 @@ import { Button } from "../ui/button"; | ... | @@ -75,7 +75,7 @@ import { Button } from "../ui/button"; |
| 75 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 75 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 76 | import { | 76 | import { |
| 77 | THREAD_DETAILS_PANEL_ICON_CLASS, | 77 | THREAD_DETAILS_PANEL_ICON_CLASS, |
| 78 | THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS, | 78 | THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, |
| 79 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, | 79 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, |
| 80 | } from "./threadDetailsPanelStyles"; | 80 | } from "./threadDetailsPanelStyles"; |
| 81 | 81 | ||
| ... | @@ -383,7 +383,7 @@ export function ThreadDetailsPrRow({ | ... | @@ -383,7 +383,7 @@ export function ThreadDetailsPrRow({ |
| 383 | </Tooltip> | 383 | </Tooltip> |
| 384 | ) : null; | 384 | ) : null; |
| 385 | 385 | ||
| 386 | const openRow = (text: string, part: "row" | "link-primary") => ( | 386 | const openRow = (text: string, part: "row" | "primary") => ( |
| 387 | <Tooltip> | 387 | <Tooltip> |
| 388 | <TooltipTrigger | 388 | <TooltipTrigger |
| 389 | render={ | 389 | render={ |
| ... | @@ -450,8 +450,8 @@ export function ThreadDetailsPrRow({ | ... | @@ -450,8 +450,8 @@ export function ThreadDetailsPrRow({ |
| 450 | </div> | 450 | </div> |
| 451 | </div> | 451 | </div> |
| 452 | ) : watching ? ( | 452 | ) : watching ? ( |
| 453 | <div className={THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS}> | 453 | <div className={THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS}> |
| 454 | {openRow(label, "link-primary")} | 454 | {openRow(label, "primary")} |
| 455 | <span aria-hidden="true" className={THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS} /> | 455 | <span aria-hidden="true" className={THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS} /> |
| 456 | {watchControl("secondary")} | 456 | {watchControl("secondary")} |
| 457 | </div> | 457 | </div> |
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+5-8| ... | @@ -59,7 +59,7 @@ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; | ... | @@ -59,7 +59,7 @@ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; |
| 59 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 59 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 60 | import { toastManager } from "../ui/toast"; | 60 | import { toastManager } from "../ui/toast"; |
| 61 | import { | 61 | import { |
| 62 | THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS, | 62 | THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, |
| 63 | THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS, | 63 | THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS, |
| 64 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, | 64 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, |
| 65 | } from "./threadDetailsPanelStyles"; | 65 | } from "./threadDetailsPanelStyles"; |
| ... | @@ -461,12 +461,9 @@ export function ThreadRelationshipsPanel(props: { | ... | @@ -461,12 +461,9 @@ export function ThreadRelationshipsPanel(props: { |
| 461 | </> | 461 | </> |
| 462 | ); | 462 | ); |
| 463 | return ( | 463 | return ( |
| 464 | <li | 464 | <li key={threadId} className="group relative flex h-8 items-center rounded-lg"> |
| 465 | key={threadId} | ||
| 466 | className={`group relative flex h-8 items-center rounded-lg ${canStop ? THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS : ""}`} | ||
| 467 | > | ||
| 468 | {isMergeTarget ? ( | 465 | {isMergeTarget ? ( |
| 469 | <div className={THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS}> | 466 | <div className={THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS}> |
| 470 | <Tooltip> | 467 | <Tooltip> |
| 471 | <TooltipTrigger | 468 | <TooltipTrigger |
| 472 | delay={200} | 469 | delay={200} |
| ... | @@ -474,7 +471,7 @@ export function ThreadRelationshipsPanel(props: { | ... | @@ -474,7 +471,7 @@ export function ThreadRelationshipsPanel(props: { |
| 474 | <ThreadDetailsControl | 471 | <ThreadDetailsControl |
| 475 | size="sm" | 472 | size="sm" |
| 476 | variant="ghost" | 473 | variant="ghost" |
| 477 | part="link-primary" | 474 | part="primary" |
| 478 | aria-label={`${threadTitle} ${threadRelationshipStatusLabel(status)}`} | 475 | aria-label={`${threadTitle} ${threadRelationshipStatusLabel(status)}`} |
| 479 | disabled={node?.missing === true} | 476 | disabled={node?.missing === true} |
| 480 | onClick={() => openThread(threadId)} | 477 | onClick={() => openThread(threadId)} |
| ... | @@ -534,7 +531,7 @@ export function ThreadRelationshipsPanel(props: { | ... | @@ -534,7 +531,7 @@ export function ThreadRelationshipsPanel(props: { |
| 534 | variant="ghost" | 531 | variant="ghost" |
| 535 | disabled={node?.missing === true} | 532 | disabled={node?.missing === true} |
| 536 | onClick={() => openThread(threadId)} | 533 | onClick={() => openThread(threadId)} |
| 537 | part={canStop ? "group-row" : "row"} | 534 | part="row" |
| 538 | /> | 535 | /> |
| 539 | } | 536 | } |
| 540 | > | 537 | > |
apps/web/src/components/chat/threadDetailsPanelStyles.ts+4-13| ... | @@ -6,29 +6,20 @@ | ... | @@ -6,29 +6,20 @@ |
| 6 | const THREAD_DETAILS_PANEL_RESTING_BUTTON_SURFACE_CLASS = "bg-transparent shadow-none"; | 6 | const THREAD_DETAILS_PANEL_RESTING_BUTTON_SURFACE_CLASS = "bg-transparent shadow-none"; |
| 7 | 7 | ||
| 8 | const THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS = | 8 | const THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS = |
| 9 | "hover:!bg-black/[0.055] data-pressed:!bg-black/[0.055] dark:hover:!bg-white/[0.075] dark:data-pressed:!bg-white/[0.075]"; | 9 | "hover:!bg-black/[0.035] data-pressed:!bg-black/[0.035] dark:hover:!bg-white/[0.08] dark:data-pressed:!bg-white/[0.08]"; |
| 10 | 10 | ||
| 11 | const THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_RESTING_BUTTON_SURFACE_CLASS} ${THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS}`; | 11 | const THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_RESTING_BUTTON_SURFACE_CLASS} ${THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS}`; |
| 12 | const THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS} data-popup-open:!bg-black/[0.035] dark:data-popup-open:!bg-white/[0.08]`; | ||
| 13 | const THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS} has-[[data-popup-open]]:bg-black/[0.035] dark:has-[[data-popup-open]]:bg-white/[0.08]`; | ||
| 12 | 14 | ||
| 13 | export const THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS = "gap-2.5 px-2.5 text-left"; | 15 | export const THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS = "gap-2.5 px-2.5 text-left"; |
| 14 | 16 | ||
| 15 | // The row supplies the first tint; the hovered or open segment adds a second tint. | ||
| 16 | const THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS} data-popup-open:!bg-black/[0.055] dark:data-popup-open:!bg-white/[0.075]`; | ||
| 17 | |||
| 18 | const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-8 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`; | 17 | const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-8 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`; |
| 19 | const THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS} has-[[data-popup-open]]:bg-black/[0.055] dark:has-[[data-popup-open]]:bg-white/[0.075]`; | ||
| 20 | 18 | ||
| 21 | export const THREAD_DETAILS_PANEL_ROW_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} w-full justify-start ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`; | 19 | export const THREAD_DETAILS_PANEL_ROW_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} w-full justify-start ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`; |
| 22 | 20 | ||
| 23 | export const THREAD_DETAILS_PANEL_SELECT_ROW_CLASS = `${THREAD_DETAILS_PANEL_ROW_CLASS} pe-0 [&_[data-slot=select-icon]]:-me-px [&_[data-slot=select-icon]]:relative [&_[data-slot=select-icon]]:flex [&_[data-slot=select-icon]]:h-full [&_[data-slot=select-icon]]:w-8 [&_[data-slot=select-icon]]:shrink-0 [&_[data-slot=select-icon]]:items-center [&_[data-slot=select-icon]]:justify-center [&_[data-slot=select-icon]]:before:absolute [&_[data-slot=select-icon]]:before:-left-px [&_[data-slot=select-icon]]:before:top-1/2 [&_[data-slot=select-icon]]:before:h-4 [&_[data-slot=select-icon]]:before:w-px [&_[data-slot=select-icon]]:before:-translate-y-1/2 [&_[data-slot=select-icon]]:before:bg-border/65 [&_[data-slot=select-icon]>svg]:me-0 [&_[data-slot=select-icon]>svg]:size-4 [&_[data-slot=select-icon]>svg]:text-muted-foreground [&_[data-slot=select-icon]>svg]:opacity-100`; | 21 | export const THREAD_DETAILS_PANEL_SELECT_ROW_CLASS = `${THREAD_DETAILS_PANEL_ROW_CLASS} pe-0 [&_[data-slot=select-icon]]:-me-px [&_[data-slot=select-icon]]:relative [&_[data-slot=select-icon]]:flex [&_[data-slot=select-icon]]:h-full [&_[data-slot=select-icon]]:w-8 [&_[data-slot=select-icon]]:shrink-0 [&_[data-slot=select-icon]]:items-center [&_[data-slot=select-icon]]:justify-center [&_[data-slot=select-icon]]:before:absolute [&_[data-slot=select-icon]]:before:-left-px [&_[data-slot=select-icon]]:before:top-1/2 [&_[data-slot=select-icon]]:before:h-4 [&_[data-slot=select-icon]]:before:w-px [&_[data-slot=select-icon]]:before:-translate-y-1/2 [&_[data-slot=select-icon]]:before:bg-border/65 [&_[data-slot=select-icon]>svg]:me-0 [&_[data-slot=select-icon]>svg]:size-4 [&_[data-slot=select-icon]>svg]:text-muted-foreground [&_[data-slot=select-icon]>svg]:opacity-100`; |
| 24 | 22 | ||
| 25 | // No transition on the group: the halves are Buttons whose background snaps (their base only | ||
| 26 | // transitions shadow), so an eased group tint would land frames later and the hover would | ||
| 27 | // visibly commit in two steps. | ||
| 28 | export const THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS = `group/thread-details-link flex w-full items-center rounded-lg ${THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS}`; | ||
| 29 | |||
| 30 | export const THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} flex-1 justify-start rounded-e-none ${THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS}`; | ||
| 31 | |||
| 32 | export const THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS = `h-8 w-full min-w-0 justify-start rounded-lg border border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80 sm:h-8 sm:text-[13px]`; | 23 | export const THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS = `h-8 w-full min-w-0 justify-start rounded-lg border border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80 sm:h-8 sm:text-[13px]`; |
| 33 | 24 | ||
| 34 | export const THREAD_DETAILS_PANEL_ICON_CLASS = "size-4 shrink-0 text-muted-foreground"; | 25 | export const THREAD_DETAILS_PANEL_ICON_CLASS = "size-4 shrink-0 text-muted-foreground"; |
| ... | @@ -40,7 +31,7 @@ export const THREAD_DETAILS_PANEL_ICON_ACTION_CLASS = `size-6 justify-center rou | ... | @@ -40,7 +31,7 @@ export const THREAD_DETAILS_PANEL_ICON_ACTION_CLASS = `size-6 justify-center rou |
| 40 | /** A small text control on a row's second line, such as a pull request's checks or next action. */ | 31 | /** A small text control on a row's second line, such as a pull request's checks or next action. */ |
| 41 | export const THREAD_DETAILS_PANEL_META_ACTION_CLASS = `h-6 gap-1 rounded-md border-transparent px-1.5 text-xs font-medium text-muted-foreground ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`; | 32 | export const THREAD_DETAILS_PANEL_META_ACTION_CLASS = `h-6 gap-1 rounded-md border-transparent px-1.5 text-xs font-medium text-muted-foreground ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`; |
| 42 | 33 | ||
| 43 | export const THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS = `group/thread-details-action flex w-full items-center rounded-lg ${THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS}`; | 34 | export const THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS = `flex w-full items-center rounded-lg ${THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS}`; |
| 44 | 35 | ||
| 45 | export const THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} flex-1 justify-start rounded-e-none ${THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS}`; | 36 | export const THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} flex-1 justify-start rounded-e-none ${THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS}`; |
| 46 | 37 |
apps/web/src/routes/_chat.pull-requests.tsx-1| ... | @@ -1843,7 +1843,6 @@ function PullRequestsRouteView() { | ... | @@ -1843,7 +1843,6 @@ function PullRequestsRouteView() { |
| 1843 | threadPanelOpen={false} | 1843 | threadPanelOpen={false} |
| 1844 | threadPanelPresentation="inline" | 1844 | threadPanelPresentation="inline" |
| 1845 | threadPanelShortcutLabel={null} | 1845 | threadPanelShortcutLabel={null} |
| 1846 | threadPanelHasAttention={false} | ||
| 1847 | onToggleThreadPanel={() => undefined} | 1846 | onToggleThreadPanel={() => undefined} |
| 1848 | rightPanelAvailable={rightPanelAvailable} | 1847 | rightPanelAvailable={rightPanelAvailable} |
| 1849 | rightPanelOpen={rightPanelState.isOpen} | 1848 | rightPanelOpen={rightPanelState.isOpen} |