| 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 | 13 | resolveBranchToolbarPrBranch, |
| 14 | 14 | resolveBranchToolbarValue, |
| 15 | 15 | resolveLockedWorkspaceLabel, |
| 16 | resolveWorkspaceDisplayName, | |
| 17 | 16 | resolveLocalCheckoutBranchMismatch, |
| 18 | 17 | resolvePreviousWorktreeLabel, |
| 19 | 18 | resolvePreviousWorktreeSeed, |
| ... | ... | @@ -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 | 570 | describe("deriveLocalBranchNameFromRemoteRef", () => { |
| 584 | 571 | it("strips the remote prefix from a remote ref", () => { |
| 585 | 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 | 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 | 121 | export interface PreviousWorktreeSeed { |
| 129 | 122 | branch: string | null; |
| 130 | 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 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; | |
| 2 | import { readLocalApi } from "../localApi"; | |
| 3 | import { stackedThreadToast, toastManager } from "./ui/toast"; | |
| 1 | 4 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 2 | 5 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 3 | 6 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| ... | ... | @@ -10,6 +13,7 @@ import { |
| 10 | 13 | } from "lucide-react"; |
| 11 | 14 | import { |
| 12 | 15 | type Ref, |
| 16 | type MouseEvent as ReactMouseEvent, | |
| 13 | 17 | memo, |
| 14 | 18 | useImperativeHandle, |
| 15 | 19 | useCallback, |
| ... | ... | @@ -41,6 +45,11 @@ import { |
| 41 | 45 | import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; |
| 42 | 46 | import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; |
| 43 | 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 | 53 | import { ComposerControl } from "./chat/ComposerControl"; |
| 45 | 54 | import { |
| 46 | 55 | Menu, |
| ... | ... | @@ -93,7 +102,44 @@ interface BranchToolbarProps { |
| 93 | 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 | 143 | forceNewWorktree: boolean; |
| 98 | 144 | autoEnvironmentLabel?: string | undefined; |
| 99 | 145 | onAutoEnvironment?: (() => void) | undefined; |
| ... | ... | @@ -112,7 +158,9 @@ interface MobileRunContextSelectorProps { |
| 112 | 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 | 164 | forceNewWorktree, |
| 117 | 165 | autoEnvironmentLabel, |
| 118 | 166 | onAutoEnvironment, |
| ... | ... | @@ -129,7 +177,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 129 | 177 | previousWorktreeLabel, |
| 130 | 178 | previousWorktreeBranch, |
| 131 | 179 | onUsePreviousWorktree, |
| 132 | }: MobileRunContextSelectorProps) { | |
| 180 | }: RunContextSelectorProps) { | |
| 133 | 181 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 134 | 182 | const activeEnvironment = useMemo( |
| 135 | 183 | () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, |
| ... | ... | @@ -148,13 +196,35 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 148 | 196 | : effectiveEnvMode === "worktree" |
| 149 | 197 | ? resolveEnvModeLabel("worktree") |
| 150 | 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 | 210 | const workspaceIcon = ( |
| 153 | 211 | <Tooltip> |
| 154 | 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 | 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 | 228 | </Tooltip> |
| 159 | 229 | ); |
| 160 | 230 | const icon = showEnvironmentIndicator ? ( |
| ... | ... | @@ -164,11 +234,14 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 164 | 234 | <Tooltip> |
| 165 | 235 | <TooltipTrigger render={<span className="inline-flex shrink-0" />}> |
| 166 | 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 | 242 | <EnvironmentMachineIcon |
| 170 | 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 | 247 | </TooltipTrigger> |
| ... | ... | @@ -182,7 +255,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 182 | 255 | const triggerContent = ( |
| 183 | 256 | <> |
| 184 | 257 | {icon} |
| 185 | <ComposerContextLabel> | |
| 258 | <ComposerContextLabel displayMode={displayMode}> | |
| 186 | 259 | {autoEnvironmentLabel ?? |
| 187 | 260 | (showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)} |
| 188 | 261 | </ComposerContextLabel> |
| ... | ... | @@ -192,7 +265,13 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 192 | 265 | if (isLocked) { |
| 193 | 266 | return ( |
| 194 | 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 | 275 | data-composer-context-control |
| 197 | 276 | > |
| 198 | 277 | {triggerContent} |
| ... | ... | @@ -203,8 +282,17 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 203 | 282 | return ( |
| 204 | 283 | <Menu> |
| 205 | 284 | <MenuTrigger |
| 206 | render={<ComposerControl size="xs" />} | |
| 207 | className="min-w-0 max-w-[48%] flex-initial justify-start" | |
| 285 | render={isPanel ? <ThreadDetailsControl part="select" /> : <ComposerControl size="xs" />} | |
| 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 | 296 | data-composer-context-control |
| 209 | 297 | data-composer-shortcut={[ |
| 210 | 298 | showEnvironmentPicker && !envLocked ? "composer.host" : "", |
| ... | ... | @@ -212,13 +300,19 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 212 | 300 | ].join(" ")} |
| 213 | 301 | > |
| 214 | 302 | {triggerContent} |
| 215 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> | |
| 303 | {!isPanel ? <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> : null} | |
| 216 | 304 | </MenuTrigger> |
| 217 | 305 | <MenuPopup |
| 218 | 306 | align="start" |
| 219 | side="top" | |
| 220 | className={previousWorktreeLabel ? "w-[min(21rem,calc(100vw-2rem))]" : undefined} | |
| 221 | {...composerFloatingLayerProps} | |
| 307 | side={isPanel ? "bottom" : "top"} | |
| 308 | className={ | |
| 309 | isPanel | |
| 310 | ? "w-(--anchor-width)" | |
| 311 | : previousWorktreeLabel | |
| 312 | ? "w-[min(21rem,calc(100vw-2rem))]" | |
| 313 | : undefined | |
| 314 | } | |
| 315 | {...(!isPanel ? composerFloatingLayerProps : {})} | |
| 222 | 316 | > |
| 223 | 317 | {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( |
| 224 | 318 | <> |
| ... | ... | @@ -618,13 +712,25 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 618 | 712 | return ( |
| 619 | 713 | <div className="flex w-full flex-col" data-thread-panel-run-context> |
| 620 | 714 | {panelSection !== "branch" ? ( |
| 621 | <BranchToolbarEnvModeSelector | |
| 715 | <RunContextSelector | |
| 622 | 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 | 728 | effectiveEnvMode={effectiveEnvMode} |
| 625 | 729 | activeWorktreePath={activeWorktreePath} |
| 626 | workspaceRoot={activeProject.workspaceRoot} | |
| 627 | 730 | onEnvModeChange={onEnvModeChange} |
| 731 | previousWorktreeLabel={previousWorktreeLabel} | |
| 732 | previousWorktreeBranch={previousWorktreeSeed?.branch ?? null} | |
| 733 | onUsePreviousWorktree={onUsePreviousWorktree} | |
| 628 | 734 | /> |
| 629 | 735 | ) : null} |
| 630 | 736 | {panelSection !== "workspace" ? ( |
| ... | ... | @@ -662,7 +768,7 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 662 | 768 | > |
| 663 | 769 | {showGitControls ? ( |
| 664 | 770 | <div className="contents @3xl/composer-surface:hidden"> |
| 665 | <MobileRunContextSelector | |
| 771 | <RunContextSelector | |
| 666 | 772 | forceNewWorktree={forceNewWorktree} |
| 667 | 773 | autoEnvironmentLabel={autoEnvironmentLabel} |
| 668 | 774 | onAutoEnvironment={onAutoEnvironment} |
apps/web/src/components/BranchToolbarBranchSelector.tsx+3-10| ... | ... | @@ -42,10 +42,7 @@ import { threadEnvironment } from "../state/threads"; |
| 42 | 42 | import { useAtomCommand } from "../state/use-atom-command"; |
| 43 | 43 | import { vcsEnvironment } from "../state/vcs"; |
| 44 | 44 | import { cn } from "../lib/utils"; |
| 45 | import { | |
| 46 | THREAD_DETAILS_PANEL_CHEVRON_CLASS, | |
| 47 | THREAD_DETAILS_PANEL_ICON_CLASS, | |
| 48 | } from "./chat/threadDetailsPanelStyles"; | |
| 45 | import { THREAD_DETAILS_PANEL_ICON_CLASS } from "./chat/threadDetailsPanelStyles"; | |
| 49 | 46 | import { ThreadDetailsPrRows } from "./chat/ThreadDetailsPrRows"; |
| 50 | 47 | import { parsePullRequestReference } from "../pullRequestReference"; |
| 51 | 48 | import { getSourceControlPresentation } from "../sourceControlPresentation"; |
| ... | ... | @@ -784,13 +781,9 @@ export function BranchToolbarBranchSelector({ |
| 784 | 781 | <ComposerContextLabel displayMode={displayMode}> |
| 785 | 782 | <MiddleTruncate value={triggerLabel} className="w-full" /> |
| 786 | 783 | </ComposerContextLabel> |
| 787 | {displayMode === "panel" ? ( | |
| 788 | <span data-slot="select-icon"> | |
| 789 | <ChevronDownIcon className={THREAD_DETAILS_PANEL_CHEVRON_CLASS} /> | |
| 790 | </span> | |
| 791 | ) : ( | |
| 784 | {displayMode !== "panel" ? ( | |
| 792 | 785 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> |
| 793 | )} | |
| 786 | ) : null} | |
| 794 | 787 | </ComboboxTrigger> |
| 795 | 788 | </span> |
| 796 | 789 | {displayMode === "panel" && prNumber !== undefined && prUrl !== undefined ? ( |
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+23-114| ... | ... | @@ -1,20 +1,12 @@ |
| 1 | import { ThreadDetailsSelectControl } from "./chat/ThreadDetailsControl"; | |
| 1 | import { ComposerSelectControl } from "./chat/ComposerControl"; | |
| 2 | 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 3 | 3 | import { FolderGit2Icon, FolderGitIcon, FolderIcon } from "lucide-react"; |
| 4 | 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 | 6 | import { |
| 14 | 7 | resolveCurrentWorkspaceLabel, |
| 15 | 8 | resolveEnvModeLabel, |
| 16 | 9 | resolveLockedWorkspaceLabel, |
| 17 | resolveWorkspaceDisplayName, | |
| 18 | 10 | type EnvMode, |
| 19 | 11 | } from "./BranchToolbar.logic"; |
| 20 | 12 | import { useComposerMenuProps } from "./chat/composerEventScope"; |
| ... | ... | @@ -28,7 +20,6 @@ import { |
| 28 | 20 | SelectValue, |
| 29 | 21 | } from "./ui/select"; |
| 30 | 22 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 31 | import { stackedThreadToast, toastManager } from "./ui/toast"; | |
| 32 | 23 | |
| 33 | 24 | const PREVIOUS_WORKTREE_SELECT_VALUE = "previous-worktree"; |
| 34 | 25 | |
| ... | ... | @@ -37,9 +28,7 @@ interface BranchToolbarEnvModeSelectorProps { |
| 37 | 28 | envLocked: boolean; |
| 38 | 29 | effectiveEnvMode: EnvMode; |
| 39 | 30 | activeWorktreePath: string | null; |
| 40 | workspaceRoot?: string | null; | |
| 41 | 31 | onEnvModeChange: (mode: EnvMode) => void; |
| 42 | displayMode?: "toolbar" | "panel"; | |
| 43 | 32 | previousWorktreeLabel?: string | null; |
| 44 | 33 | previousWorktreeBranch?: string | null; |
| 45 | 34 | onUsePreviousWorktree?: () => void; |
| ... | ... | @@ -50,72 +39,27 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 50 | 39 | envLocked, |
| 51 | 40 | effectiveEnvMode, |
| 52 | 41 | activeWorktreePath, |
| 53 | workspaceRoot = null, | |
| 54 | 42 | onEnvModeChange, |
| 55 | displayMode = "toolbar", | |
| 56 | 43 | previousWorktreeLabel, |
| 57 | 44 | previousWorktreeBranch = null, |
| 58 | 45 | onUsePreviousWorktree, |
| 59 | 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 | 47 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 68 | 48 | const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree); |
| 69 | 49 | const envModeItems = useMemo( |
| 70 | 50 | () => [ |
| 71 | 51 | { |
| 72 | 52 | value: "local", |
| 73 | label: workspaceDisplayName ?? resolveCurrentWorkspaceLabel(activeWorktreePath), | |
| 53 | label: resolveCurrentWorkspaceLabel(activeWorktreePath), | |
| 74 | 54 | }, |
| 75 | 55 | { value: "worktree", label: resolveEnvModeLabel("worktree") }, |
| 76 | 56 | ...(showPreviousWorktree && previousWorktreeLabel |
| 77 | 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 | 63 | const stopContextMenuMouseDown = (event: ReactMouseEvent) => { |
| 120 | 64 | if (event.button !== 0 || event.ctrlKey) { |
| 121 | 65 | event.stopPropagation(); |
| ... | ... | @@ -125,47 +69,31 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 125 | 69 | if (envLocked || forceNewWorktree) { |
| 126 | 70 | const lockedRow = ( |
| 127 | 71 | <span |
| 128 | className={cn( | |
| 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 | )} | |
| 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" | |
| 132 | 73 | data-composer-context-control |
| 133 | onContextMenu={handleWorkspaceContextMenu} | |
| 134 | 74 | > |
| 135 | 75 | {activeWorktreePath ? ( |
| 136 | <FolderGitIcon | |
| 137 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 138 | /> | |
| 76 | <FolderGitIcon className="size-3" /> | |
| 139 | 77 | ) : effectiveEnvMode === "worktree" ? ( |
| 140 | <FolderGit2Icon | |
| 141 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 142 | /> | |
| 78 | <FolderGit2Icon className="size-3" /> | |
| 143 | 79 | ) : ( |
| 144 | <FolderIcon | |
| 145 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 146 | /> | |
| 80 | <FolderIcon className="size-3" /> | |
| 147 | 81 | )} |
| 148 | <ComposerContextLabel displayMode={displayMode}> | |
| 82 | <ComposerContextLabel> | |
| 149 | 83 | {forceNewWorktree |
| 150 | 84 | ? resolveEnvModeLabel("worktree") |
| 151 | : (workspaceDisplayName ?? | |
| 152 | resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} | |
| 85 | : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} | |
| 153 | 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 | 87 | </span> |
| 160 | 88 | ); |
| 161 | 89 | |
| 162 | 90 | return ( |
| 163 | 91 | <Tooltip> |
| 164 | 92 | <TooltipTrigger render={lockedRow} /> |
| 165 | <TooltipPopup side={displayMode === "panel" ? "left" : undefined}> | |
| 93 | <TooltipPopup> | |
| 166 | 94 | {forceNewWorktree |
| 167 | 95 | ? "Each model starts in its own worktree." |
| 168 | : (workspacePath ?? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} | |
| 96 | : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)} | |
| 169 | 97 | </TooltipPopup> |
| 170 | 98 | </Tooltip> |
| 171 | 99 | ); |
| ... | ... | @@ -187,56 +115,37 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 187 | 115 | <Tooltip> |
| 188 | 116 | <TooltipTrigger |
| 189 | 117 | render={ |
| 190 | <ThreadDetailsSelectControl | |
| 191 | panel={displayMode === "panel"} | |
| 118 | <ComposerSelectControl | |
| 119 | size="xs" | |
| 192 | 120 | className="min-w-0 shrink" |
| 193 | 121 | aria-label="Workspace" |
| 194 | 122 | data-composer-shortcut="composer.workspace" |
| 195 | 123 | data-composer-context-control |
| 196 | 124 | onMouseDownCapture={stopContextMenuMouseDown} |
| 197 | onContextMenu={handleWorkspaceContextMenu} | |
| 198 | 125 | /> |
| 199 | 126 | } |
| 200 | 127 | > |
| 201 | 128 | {effectiveEnvMode === "worktree" ? ( |
| 202 | <FolderGit2Icon | |
| 203 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 204 | /> | |
| 129 | <FolderGit2Icon className="size-3" /> | |
| 205 | 130 | ) : activeWorktreePath ? ( |
| 206 | <FolderGitIcon | |
| 207 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 208 | /> | |
| 131 | <FolderGitIcon className="size-3" /> | |
| 209 | 132 | ) : ( |
| 210 | <FolderIcon | |
| 211 | className={displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3"} | |
| 212 | /> | |
| 133 | <FolderIcon className="size-3" /> | |
| 213 | 134 | )} |
| 214 | <ComposerContextLabel displayMode={displayMode}> | |
| 135 | <ComposerContextLabel> | |
| 215 | 136 | <SelectValue /> |
| 216 | 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 | 138 | </TooltipTrigger> |
| 223 | <TooltipPopup side={displayMode === "panel" ? "left" : undefined}> | |
| 224 | {workspacePath ?? | |
| 225 | (effectiveEnvMode === "worktree" | |
| 226 | ? resolveEnvModeLabel("worktree") | |
| 227 | : resolveCurrentWorkspaceLabel(activeWorktreePath))} | |
| 139 | <TooltipPopup> | |
| 140 | {effectiveEnvMode === "worktree" | |
| 141 | ? resolveEnvModeLabel("worktree") | |
| 142 | : resolveCurrentWorkspaceLabel(activeWorktreePath)} | |
| 228 | 143 | </TooltipPopup> |
| 229 | 144 | </Tooltip> |
| 230 | 145 | <SelectPopup |
| 231 | 146 | alignItemWithTrigger={false} |
| 232 | {...(displayMode === "toolbar" ? composerFloatingLayerProps : {})} | |
| 233 | className={ | |
| 234 | displayMode === "panel" | |
| 235 | ? "w-(--anchor-width)" | |
| 236 | : showPreviousWorktree | |
| 237 | ? "w-[min(21rem,calc(100vw-2rem))]" | |
| 238 | : undefined | |
| 239 | } | |
| 147 | {...composerFloatingLayerProps} | |
| 148 | className={showPreviousWorktree ? "w-[min(21rem,calc(100vw-2rem))]" : undefined} | |
| 240 | 149 | > |
| 241 | 150 | <SelectGroup> |
| 242 | 151 | <SelectGroupLabel>Workspace</SelectGroupLabel> |
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+10-37| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import { ThreadDetailsSelectControl } from "./chat/ThreadDetailsControl"; | |
| 1 | import { ComposerSelectControl } from "./chat/ComposerControl"; | |
| 2 | 2 | import { ComposerContextLabel } from "./ComposerContextLabel"; |
| 3 | 3 | import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip"; |
| 4 | 4 | import type { EnvironmentId } from "@t3tools/contracts"; |
| ... | ... | @@ -6,11 +6,6 @@ import { ScaleIcon } from "lucide-react"; |
| 6 | 6 | import { memo, useMemo } from "react"; |
| 7 | 7 | |
| 8 | 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 | 9 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| 15 | 10 | import { useComposerMenuProps } from "./chat/composerEventScope"; |
| 16 | 11 | import { |
| ... | ... | @@ -29,7 +24,6 @@ interface BranchToolbarEnvironmentSelectorProps { |
| 29 | 24 | environmentId: EnvironmentId; |
| 30 | 25 | availableEnvironments: readonly EnvironmentOption[]; |
| 31 | 26 | onEnvironmentChange?: (environmentId: EnvironmentId) => void; |
| 32 | displayMode?: "toolbar" | "panel"; | |
| 33 | 27 | } |
| 34 | 28 | |
| 35 | 29 | export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({ |
| ... | ... | @@ -39,7 +33,6 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 39 | 33 | environmentId, |
| 40 | 34 | availableEnvironments, |
| 41 | 35 | onEnvironmentChange, |
| 42 | displayMode = "toolbar", | |
| 43 | 36 | }: BranchToolbarEnvironmentSelectorProps) { |
| 44 | 37 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 45 | 38 | const activeEnvironment = useMemo(() => { |
| ... | ... | @@ -67,19 +60,14 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 67 | 60 | if (envLocked || onEnvironmentChange === undefined) { |
| 68 | 61 | const lockedRow = ( |
| 69 | 62 | <span |
| 70 | className={cn( | |
| 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 | )} | |
| 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" | |
| 74 | 64 | data-composer-context-control |
| 75 | 65 | > |
| 76 | 66 | <EnvironmentMachineIcon |
| 77 | 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}> | |
| 81 | {activeEnvironment?.label ?? "Run on"} | |
| 82 | </ComposerContextLabel> | |
| 70 | <ComposerContextLabel>{activeEnvironment?.label ?? "Run on"}</ComposerContextLabel> | |
| 83 | 71 | </span> |
| 84 | 72 | ); |
| 85 | 73 | return ( |
| ... | ... | @@ -102,8 +90,8 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 102 | 90 | <Tooltip> |
| 103 | 91 | <TooltipTrigger |
| 104 | 92 | render={ |
| 105 | <ThreadDetailsSelectControl | |
| 106 | panel={displayMode === "panel"} | |
| 93 | <ComposerSelectControl | |
| 94 | size="xs" | |
| 107 | 95 | className="min-w-0 max-w-full" |
| 108 | 96 | aria-label="Run on" |
| 109 | 97 | data-composer-shortcut="composer.host" |
| ... | ... | @@ -112,35 +100,20 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 112 | 100 | } |
| 113 | 101 | > |
| 114 | 102 | {autoEnvironmentLabel ? ( |
| 115 | <ScaleIcon | |
| 116 | className={ | |
| 117 | displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0" | |
| 118 | } | |
| 119 | aria-hidden="true" | |
| 120 | /> | |
| 103 | <ScaleIcon className="size-3 shrink-0" aria-hidden="true" /> | |
| 121 | 104 | ) : ( |
| 122 | 105 | <EnvironmentMachineIcon |
| 123 | 106 | kind={activeEnvironment?.machine ?? "server"} |
| 124 | className={ | |
| 125 | displayMode === "panel" ? THREAD_DETAILS_PANEL_ICON_CLASS : "size-3 shrink-0" | |
| 126 | } | |
| 107 | className="size-3 shrink-0" | |
| 127 | 108 | /> |
| 128 | 109 | )} |
| 129 | <ComposerContextLabel displayMode={displayMode}> | |
| 110 | <ComposerContextLabel> | |
| 130 | 111 | <SelectValue /> |
| 131 | 112 | </ComposerContextLabel> |
| 132 | 113 | </TooltipTrigger> |
| 133 | 114 | <TooltipPopup>{autoEnvironmentLabel ?? activeEnvironment?.label ?? "Run on"}</TooltipPopup> |
| 134 | 115 | </Tooltip> |
| 135 | <SelectPopup | |
| 136 | alignItemWithTrigger={false} | |
| 137 | {...(displayMode === "toolbar" ? composerFloatingLayerProps : {})} | |
| 138 | {...(displayMode === "panel" | |
| 139 | ? { | |
| 140 | className: "w-(--anchor-width)", | |
| 141 | } | |
| 142 | : {})} | |
| 143 | > | |
| 116 | <SelectPopup alignItemWithTrigger={false} {...composerFloatingLayerProps}> | |
| 144 | 117 | <SelectGroup> |
| 145 | 118 | <SelectGroupLabel>Run on</SelectGroupLabel> |
| 146 | 119 | {onAutoEnvironment && ( |
apps/web/src/components/ChatView.tsx-16| ... | ... | @@ -3069,11 +3069,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3069 | 3069 | hasMultipleRegisteredEnvironments && activeThread |
| 3070 | 3070 | ? `${environmentById.get(activeThread.environmentId)?.label ?? serverConfig?.environment.label ?? activeThread.environmentId} server` |
| 3071 | 3071 | : "server"; |
| 3072 | const handleDismissVersionMismatch = useCallback(() => { | |
| 3073 | if (!versionMismatchDismissKey) return; | |
| 3074 | dismissVersionMismatch(versionMismatchDismissKey); | |
| 3075 | setDismissedVersionMismatchKey(versionMismatchDismissKey); | |
| 3076 | }, [setDismissedVersionMismatchKey, versionMismatchDismissKey]); | |
| 3077 | 3072 | const serverUpdateEnvironmentId = activeThread?.environmentId ?? null; |
| 3078 | 3073 | const versionMismatchSelfUpdate = resolveServerSelfUpdateCapability(serverConfig); |
| 3079 | 3074 | const versionMismatchInstallation = serverConfig?.environment.capabilities.serverInstallation; |
| ... | ... | @@ -11078,15 +11073,6 @@ export default function ChatView(props: ChatViewProps) { |
| 11078 | 11073 | : {}), |
| 11079 | 11074 | onComposerFocusRequest: scheduleComposerFocus, |
| 11080 | 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 | 11076 | onRunProjectScript: runProjectScript, |
| 11091 | 11077 | onAddProjectScript: saveProjectScript, |
| 11092 | 11078 | onUpdateProjectScript: updateProjectScript, |
| ... | ... | @@ -11100,8 +11086,6 @@ export default function ChatView(props: ChatViewProps) { |
| 11100 | 11086 | threadPanelPresentation, |
| 11101 | 11087 | threadPanelPopoverHandle, |
| 11102 | 11088 | threadPanelShortcutLabel: shortcutLabelForCommand(keybindings, "threadPanel.toggle"), |
| 11103 | threadPanelHasAttention: | |
| 11104 | activeEnvironmentUnavailableState !== null || showVersionMismatchBanner, | |
| 11105 | 11089 | rightPanelAvailable: activeProject !== null, |
| 11106 | 11090 | rightPanelOpen, |
| 11107 | 11091 | rightPanelShortcutLabel: shortcutLabelForCommand(keybindings, "rightPanel.toggle"), |
apps/web/src/components/chat/PanelLayoutControls.tsx-8| ... | ... | @@ -19,7 +19,6 @@ export interface PanelLayoutControlsProps { |
| 19 | 19 | threadPanelPresentation: ThreadPanelPresentation; |
| 20 | 20 | threadPanelPopoverHandle?: ReturnType<typeof PopoverCreateHandle>; |
| 21 | 21 | threadPanelShortcutLabel: string | null; |
| 22 | threadPanelHasAttention: boolean; | |
| 23 | 22 | rightPanelAvailable: boolean; |
| 24 | 23 | rightPanelOpen: boolean; |
| 25 | 24 | rightPanelShortcutLabel: string | null; |
| ... | ... | @@ -40,7 +39,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 40 | 39 | threadPanelPresentation, |
| 41 | 40 | threadPanelPopoverHandle, |
| 42 | 41 | threadPanelShortcutLabel, |
| 43 | threadPanelHasAttention, | |
| 44 | 42 | rightPanelAvailable, |
| 45 | 43 | rightPanelOpen, |
| 46 | 44 | rightPanelShortcutLabel, |
| ... | ... | @@ -58,12 +56,6 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 58 | 56 | size="sm" |
| 59 | 57 | > |
| 60 | 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 | 59 | </Toggle> |
| 68 | 60 | ); |
| 69 | 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 | 1 | import { mergeProps } from "@base-ui/react/merge-props"; |
| 2 | import { Select as SelectPrimitive } from "@base-ui/react/select"; | |
| 3 | 2 | import { useRender } from "@base-ui/react/use-render"; |
| 4 | 3 | import type { ComponentProps } from "react"; |
| 5 | 4 | |
| 6 | 5 | import { cn } from "~/lib/utils"; |
| 7 | 6 | import { Button } from "../ui/button"; |
| 8 | import { ComposerSelectControl } from "./ComposerControl"; | |
| 9 | 7 | import { |
| 10 | 8 | THREAD_DETAILS_PANEL_ROW_CLASS, |
| 11 | 9 | THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, |
| 12 | 10 | THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, |
| 13 | 11 | THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, |
| 14 | THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS, | |
| 15 | 12 | THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, |
| 16 | 13 | THREAD_DETAILS_PANEL_META_ACTION_CLASS, |
| 17 | 14 | THREAD_DETAILS_PANEL_CHEVRON_CLASS, |
| ... | ... | @@ -20,11 +17,9 @@ import { ChevronDownIcon } from "lucide-react"; |
| 20 | 17 | |
| 21 | 18 | const parts = { |
| 22 | 19 | row: THREAD_DETAILS_PANEL_ROW_CLASS, |
| 23 | "group-row": `${THREAD_DETAILS_PANEL_ROW_CLASS} hover:!bg-transparent dark:hover:!bg-transparent`, | |
| 24 | 20 | select: THREAD_DETAILS_PANEL_SELECT_ROW_CLASS, |
| 25 | 21 | primary: THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, |
| 26 | 22 | secondary: THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, |
| 27 | "link-primary": THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS, | |
| 28 | 23 | icon: THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, |
| 29 | 24 | meta: THREAD_DETAILS_PANEL_META_ACTION_CLASS, |
| 30 | 25 | }; |
| ... | ... | @@ -36,6 +31,7 @@ export function ThreadDetailsControl({ |
| 36 | 31 | multiline = false, |
| 37 | 32 | tone = "default", |
| 38 | 33 | className, |
| 34 | children, | |
| 39 | 35 | size = "default", |
| 40 | 36 | variant = "default", |
| 41 | 37 | render, |
| ... | ... | @@ -64,6 +60,19 @@ export function ThreadDetailsControl({ |
| 64 | 60 | ), |
| 65 | 61 | }, |
| 66 | 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 | 78 | if (!panel) { |
| ... | ... | @@ -74,34 +83,10 @@ export function ThreadDetailsControl({ |
| 74 | 83 | size={multiline ? "sm-multiline" : size} |
| 75 | 84 | variant={variant} |
| 76 | 85 | className={className} |
| 77 | /> | |
| 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}> | |
| 86 | > | |
| 92 | 87 | {children} |
| 93 | </ComposerSelectControl> | |
| 88 | </Button> | |
| 94 | 89 | ); |
| 95 | 90 | } |
| 96 | return ( | |
| 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 | ); | |
| 91 | return control; | |
| 107 | 92 | } |
apps/web/src/components/chat/ThreadDetailsPanel.test.tsx-2| ... | ... | @@ -74,8 +74,6 @@ describe("ThreadDetailsPanel", () => { |
| 74 | 74 | startFromOrigin: false, |
| 75 | 75 | onStartFromOriginChange: vi.fn(), |
| 76 | 76 | onComposerFocusRequest: vi.fn(), |
| 77 | versionMismatch: null, | |
| 78 | onDismissVersionMismatch: vi.fn(), | |
| 79 | 77 | onRunProjectScript: vi.fn(), |
| 80 | 78 | onAddProjectScript: vi.fn() as ThreadDetailsPanelProps["onAddProjectScript"], |
| 81 | 79 | onUpdateProjectScript: vi.fn() as ThreadDetailsPanelProps["onUpdateProjectScript"], |
apps/web/src/components/chat/ThreadDetailsPanel.tsx+8-58| ... | ... | @@ -5,23 +5,16 @@ import type { |
| 5 | 5 | ResolvedKeybindingsConfig, |
| 6 | 6 | ThreadId, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | import { AlertTriangleIcon, XIcon } from "lucide-react"; | |
| 9 | 8 | |
| 10 | 9 | import type { DraftId } from "../../composerDraftStore"; |
| 11 | 10 | import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts"; |
| 12 | import { | |
| 13 | shouldShowEnvironmentIndicator, | |
| 14 | type EnvMode, | |
| 15 | type EnvironmentOption, | |
| 16 | } from "../BranchToolbar.logic"; | |
| 11 | import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic"; | |
| 17 | 12 | import { BranchToolbar } from "../BranchToolbar"; |
| 18 | import { BranchToolbarEnvironmentSelector } from "../BranchToolbarEnvironmentSelector"; | |
| 19 | 13 | import GitActionsControl from "../GitActionsControl"; |
| 20 | 14 | import ProjectScriptsControl, { |
| 21 | 15 | type NewProjectScriptInput, |
| 22 | 16 | type ProjectScriptActionResult, |
| 23 | 17 | } from "../ProjectScriptsControl"; |
| 24 | import { Button } from "../ui/button"; | |
| 25 | 18 | import type { ComponentProps } from "react"; |
| 26 | 19 | import { ThreadDetailsCard } from "./ThreadDetailsCard"; |
| 27 | 20 | import { OpenInPicker } from "./OpenInPicker"; |
| ... | ... | @@ -29,12 +22,6 @@ import { ThreadDetailsSection } from "./ThreadDetailsSection"; |
| 29 | 22 | import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; |
| 30 | 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 | 25 | export interface ThreadDetailsPanelProps extends Pick< |
| 39 | 26 | ComponentProps<typeof ThreadDetailsCard>, |
| 40 | 27 | "anchor" | "handle" | "onPresentationChange" |
| ... | ... | @@ -66,8 +53,6 @@ export interface ThreadDetailsPanelProps extends Pick< |
| 66 | 53 | onCheckoutPullRequestRequest?: (reference: string) => void; |
| 67 | 54 | onComposerFocusRequest: () => void; |
| 68 | 55 | onOpenChanges?: () => void; |
| 69 | versionMismatch: VersionMismatchIssue | null; | |
| 70 | onDismissVersionMismatch: () => void; | |
| 71 | 56 | onRunProjectScript: (script: ProjectScript) => void; |
| 72 | 57 | onAddProjectScript: (input: NewProjectScriptInput) => Promise<ProjectScriptActionResult>; |
| 73 | 58 | onUpdateProjectScript: ( |
| ... | ... | @@ -82,14 +67,6 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 82 | 67 | props.environmentId, |
| 83 | 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 | 70 | const branchToolbarProps = { |
| 94 | 71 | showGitControls: props.isGitRepo, |
| 95 | 72 | environmentId: props.environmentId, |
| ... | ... | @@ -128,46 +105,19 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 128 | 105 | separated={false} |
| 129 | 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 | 108 | <div className="flex flex-col"> |
| 153 | {density === "full" && showEnvironment ? ( | |
| 154 | <BranchToolbarEnvironmentSelector | |
| 155 | displayMode="panel" | |
| 109 | {density === "full" ? ( | |
| 110 | <BranchToolbar | |
| 111 | layout="panel" | |
| 112 | panelSection="workspace" | |
| 113 | availableEnvironments={props.availableEnvironments} | |
| 114 | onEnvironmentChange={props.onEnvironmentChange} | |
| 156 | 115 | autoEnvironmentLabel={props.autoEnvironmentLabel} |
| 157 | 116 | onAutoEnvironment={props.onAutoEnvironment} |
| 158 | envLocked={props.envLocked} | |
| 159 | environmentId={props.environmentId} | |
| 160 | availableEnvironments={props.availableEnvironments} | |
| 161 | {...(canPickEnvironment | |
| 162 | ? { onEnvironmentChange: props.onEnvironmentChange } | |
| 163 | : {})} | |
| 117 | {...branchToolbarProps} | |
| 164 | 118 | /> |
| 165 | 119 | ) : null} |
| 166 | 120 | |
| 167 | {density === "full" ? ( | |
| 168 | <BranchToolbar layout="panel" panelSection="workspace" {...branchToolbarProps} /> | |
| 169 | ) : null} | |
| 170 | ||
| 171 | 121 | {density !== "essential" && props.showOpenInPicker ? ( |
| 172 | 122 | <OpenInPicker |
| 173 | 123 | keybindings={props.keybindings} |
apps/web/src/components/chat/ThreadDetailsPrRow.tsx+4-4| ... | ... | @@ -75,7 +75,7 @@ import { Button } from "../ui/button"; |
| 75 | 75 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 76 | 76 | import { |
| 77 | 77 | THREAD_DETAILS_PANEL_ICON_CLASS, |
| 78 | THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS, | |
| 78 | THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, | |
| 79 | 79 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, |
| 80 | 80 | } from "./threadDetailsPanelStyles"; |
| 81 | 81 | |
| ... | ... | @@ -383,7 +383,7 @@ export function ThreadDetailsPrRow({ |
| 383 | 383 | </Tooltip> |
| 384 | 384 | ) : null; |
| 385 | 385 | |
| 386 | const openRow = (text: string, part: "row" | "link-primary") => ( | |
| 386 | const openRow = (text: string, part: "row" | "primary") => ( | |
| 387 | 387 | <Tooltip> |
| 388 | 388 | <TooltipTrigger |
| 389 | 389 | render={ |
| ... | ... | @@ -450,8 +450,8 @@ export function ThreadDetailsPrRow({ |
| 450 | 450 | </div> |
| 451 | 451 | </div> |
| 452 | 452 | ) : watching ? ( |
| 453 | <div className={THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS}> | |
| 454 | {openRow(label, "link-primary")} | |
| 453 | <div className={THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS}> | |
| 454 | {openRow(label, "primary")} | |
| 455 | 455 | <span aria-hidden="true" className={THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS} /> |
| 456 | 456 | {watchControl("secondary")} |
| 457 | 457 | </div> |
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+5-8| ... | ... | @@ -59,7 +59,7 @@ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; |
| 59 | 59 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 60 | 60 | import { toastManager } from "../ui/toast"; |
| 61 | 61 | import { |
| 62 | THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS, | |
| 62 | THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, | |
| 63 | 63 | THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS, |
| 64 | 64 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, |
| 65 | 65 | } from "./threadDetailsPanelStyles"; |
| ... | ... | @@ -461,12 +461,9 @@ export function ThreadRelationshipsPanel(props: { |
| 461 | 461 | </> |
| 462 | 462 | ); |
| 463 | 463 | return ( |
| 464 | <li | |
| 465 | key={threadId} | |
| 466 | className={`group relative flex h-8 items-center rounded-lg ${canStop ? THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS : ""}`} | |
| 467 | > | |
| 464 | <li key={threadId} className="group relative flex h-8 items-center rounded-lg"> | |
| 468 | 465 | {isMergeTarget ? ( |
| 469 | <div className={THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS}> | |
| 466 | <div className={THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS}> | |
| 470 | 467 | <Tooltip> |
| 471 | 468 | <TooltipTrigger |
| 472 | 469 | delay={200} |
| ... | ... | @@ -474,7 +471,7 @@ export function ThreadRelationshipsPanel(props: { |
| 474 | 471 | <ThreadDetailsControl |
| 475 | 472 | size="sm" |
| 476 | 473 | variant="ghost" |
| 477 | part="link-primary" | |
| 474 | part="primary" | |
| 478 | 475 | aria-label={`${threadTitle} ${threadRelationshipStatusLabel(status)}`} |
| 479 | 476 | disabled={node?.missing === true} |
| 480 | 477 | onClick={() => openThread(threadId)} |
| ... | ... | @@ -534,7 +531,7 @@ export function ThreadRelationshipsPanel(props: { |
| 534 | 531 | variant="ghost" |
| 535 | 532 | disabled={node?.missing === true} |
| 536 | 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 | 6 | const THREAD_DETAILS_PANEL_RESTING_BUTTON_SURFACE_CLASS = "bg-transparent shadow-none"; |
| 7 | 7 | |
| 8 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 31 | /** A small text control on a row's second line, such as a pull request's checks or next action. */ |
| 41 | 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 | 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 | 1843 | threadPanelOpen={false} |
| 1844 | 1844 | threadPanelPresentation="inline" |
| 1845 | 1845 | threadPanelShortcutLabel={null} |
| 1846 | threadPanelHasAttention={false} | |
| 1847 | 1846 | onToggleThreadPanel={() => undefined} |
| 1848 | 1847 | rightPanelAvailable={rightPanelAvailable} |
| 1849 | 1848 | rightPanelOpen={rightPanelState.isOpen} |