| author | |
| committer | |
| log | d649ccf71598a38aac6ea0e7f2f9c034b1bc78b1 |
| tree | bb27d77bee4c461133a35b0f29ce62bdbae41407 |
| parent | f54f4385e518504f87001afef51bdf3b0be2a5be |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>13 files changed, 277 insertions(+), 65 deletions(-)
apps/web/index.html+4-1| ... | @@ -2,7 +2,10 @@ | ... | @@ -2,7 +2,10 @@ |
| 2 | <html lang="en"> | 2 | <html lang="en"> |
| 3 | <head> | 3 | <head> |
| 4 | <meta charset="UTF-8" /> | 4 | <meta charset="UTF-8" /> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | 5 | <meta |
| 6 | name="viewport" | ||
| 7 | content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" | ||
| 8 | /> | ||
| 6 | <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" /> | 9 | <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" /> |
| 7 | <meta name="theme-color" content="#161616" media="(prefers-color-scheme: dark)" /> | 10 | <meta name="theme-color" content="#161616" media="(prefers-color-scheme: dark)" /> |
| 8 | <meta name="theme-color" content="#161616" /> | 11 | <meta name="theme-color" content="#161616" /> |
apps/web/src/components/AppSidebarLayout.tsx+1-1| ... | @@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 54 | }, [navigate]); | 54 | }, [navigate]); |
| 55 | 55 | ||
| 56 | return ( | 56 | return ( |
| 57 | <SidebarProvider defaultOpen> | 57 | <SidebarProvider className="h-dvh! min-h-0!" defaultOpen> |
| 58 | <Sidebar | 58 | <Sidebar |
| 59 | side="left" | 59 | side="left" |
| 60 | collapsible="offcanvas" | 60 | collapsible="offcanvas" |
apps/web/src/components/BranchToolbar.tsx+178-18| ... | @@ -1,18 +1,41 @@ | ... | @@ -1,18 +1,41 @@ |
| 1 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime"; | 1 | import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime"; |
| 2 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; | 2 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; |
| 3 | import { | ||
| 4 | ChevronDownIcon, | ||
| 5 | CloudIcon, | ||
| 6 | FolderGit2Icon, | ||
| 7 | FolderGitIcon, | ||
| 8 | FolderIcon, | ||
| 9 | MonitorIcon, | ||
| 10 | } from "lucide-react"; | ||
| 3 | import { memo, useMemo } from "react"; | 11 | import { memo, useMemo } from "react"; |
| 4 | 12 | ||
| 5 | import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; | 13 | import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; |
| 14 | import { useIsMobile } from "../hooks/useMediaQuery"; | ||
| 6 | import { useStore } from "../store"; | 15 | import { useStore } from "../store"; |
| 7 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; | 16 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; |
| 8 | import { | 17 | import { |
| 9 | type EnvMode, | 18 | type EnvMode, |
| 10 | type EnvironmentOption, | 19 | type EnvironmentOption, |
| 20 | resolveCurrentWorkspaceLabel, | ||
| 21 | resolveEnvModeLabel, | ||
| 11 | resolveEffectiveEnvMode, | 22 | resolveEffectiveEnvMode, |
| 23 | resolveLockedWorkspaceLabel, | ||
| 12 | } from "./BranchToolbar.logic"; | 24 | } from "./BranchToolbar.logic"; |
| 13 | import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector"; | 25 | import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector"; |
| 14 | import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; | 26 | import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector"; |
| 15 | import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; | 27 | import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector"; |
| 28 | import { Button } from "./ui/button"; | ||
| 29 | import { | ||
| 30 | Menu, | ||
| 31 | MenuGroup, | ||
| 32 | MenuGroupLabel, | ||
| 33 | MenuPopup, | ||
| 34 | MenuRadioGroup, | ||
| 35 | MenuRadioItem, | ||
| 36 | MenuSeparator, | ||
| 37 | MenuTrigger, | ||
| 38 | } from "./ui/menu"; | ||
| 16 | import { Separator } from "./ui/separator"; | 39 | import { Separator } from "./ui/separator"; |
| 17 | 40 | ||
| 18 | interface BranchToolbarProps { | 41 | interface BranchToolbarProps { |
| ... | @@ -30,6 +53,126 @@ interface BranchToolbarProps { | ... | @@ -30,6 +53,126 @@ interface BranchToolbarProps { |
| 30 | onEnvironmentChange?: (environmentId: EnvironmentId) => void; | 53 | onEnvironmentChange?: (environmentId: EnvironmentId) => void; |
| 31 | } | 54 | } |
| 32 | 55 | ||
| 56 | interface MobileRunContextSelectorProps { | ||
| 57 | envLocked: boolean; | ||
| 58 | envModeLocked: boolean; | ||
| 59 | environmentId: EnvironmentId; | ||
| 60 | availableEnvironments: readonly EnvironmentOption[] | undefined; | ||
| 61 | showEnvironmentPicker: boolean; | ||
| 62 | onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined; | ||
| 63 | effectiveEnvMode: EnvMode; | ||
| 64 | activeWorktreePath: string | null; | ||
| 65 | onEnvModeChange: (mode: EnvMode) => void; | ||
| 66 | } | ||
| 67 | |||
| 68 | const MobileRunContextSelector = memo(function MobileRunContextSelector({ | ||
| 69 | envLocked, | ||
| 70 | envModeLocked, | ||
| 71 | environmentId, | ||
| 72 | availableEnvironments, | ||
| 73 | showEnvironmentPicker, | ||
| 74 | onEnvironmentChange, | ||
| 75 | effectiveEnvMode, | ||
| 76 | activeWorktreePath, | ||
| 77 | onEnvModeChange, | ||
| 78 | }: MobileRunContextSelectorProps) { | ||
| 79 | const activeEnvironment = useMemo( | ||
| 80 | () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, | ||
| 81 | [availableEnvironments, environmentId], | ||
| 82 | ); | ||
| 83 | const environmentLabel = activeEnvironment?.label ?? "Run on"; | ||
| 84 | const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon; | ||
| 85 | const WorkspaceIcon = | ||
| 86 | effectiveEnvMode === "worktree" | ||
| 87 | ? FolderGit2Icon | ||
| 88 | : activeWorktreePath | ||
| 89 | ? FolderGitIcon | ||
| 90 | : FolderIcon; | ||
| 91 | const workspaceLabel = envModeLocked | ||
| 92 | ? resolveLockedWorkspaceLabel(activeWorktreePath) | ||
| 93 | : effectiveEnvMode === "worktree" | ||
| 94 | ? resolveEnvModeLabel("worktree") | ||
| 95 | : resolveCurrentWorkspaceLabel(activeWorktreePath); | ||
| 96 | |||
| 97 | return ( | ||
| 98 | <Menu> | ||
| 99 | <MenuTrigger | ||
| 100 | render={<Button variant="ghost" size="xs" />} | ||
| 101 | className="min-w-0 max-w-[48%] flex-1 justify-start text-muted-foreground/70 hover:text-foreground/80 md:hidden" | ||
| 102 | > | ||
| 103 | {showEnvironmentPicker ? ( | ||
| 104 | <> | ||
| 105 | <EnvironmentIcon className="size-3 shrink-0" /> | ||
| 106 | <span className="min-w-0 truncate">{environmentLabel}</span> | ||
| 107 | </> | ||
| 108 | ) : ( | ||
| 109 | <> | ||
| 110 | <WorkspaceIcon className="size-3 shrink-0" /> | ||
| 111 | <span className="min-w-0 truncate">{workspaceLabel}</span> | ||
| 112 | </> | ||
| 113 | )} | ||
| 114 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> | ||
| 115 | </MenuTrigger> | ||
| 116 | <MenuPopup align="start" side="top" className="w-64"> | ||
| 117 | {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( | ||
| 118 | <> | ||
| 119 | <MenuGroup> | ||
| 120 | <MenuGroupLabel>Run on</MenuGroupLabel> | ||
| 121 | <MenuRadioGroup | ||
| 122 | value={environmentId} | ||
| 123 | onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)} | ||
| 124 | > | ||
| 125 | {availableEnvironments.map((env) => { | ||
| 126 | const Icon = env.isPrimary ? MonitorIcon : CloudIcon; | ||
| 127 | return ( | ||
| 128 | <MenuRadioItem | ||
| 129 | key={env.environmentId} | ||
| 130 | disabled={envLocked} | ||
| 131 | value={env.environmentId} | ||
| 132 | > | ||
| 133 | <span className="flex min-w-0 items-center gap-1.5"> | ||
| 134 | <Icon className="size-3" /> | ||
| 135 | <span className="min-w-0 truncate">{env.label}</span> | ||
| 136 | </span> | ||
| 137 | </MenuRadioItem> | ||
| 138 | ); | ||
| 139 | })} | ||
| 140 | </MenuRadioGroup> | ||
| 141 | </MenuGroup> | ||
| 142 | <MenuSeparator /> | ||
| 143 | </> | ||
| 144 | ) : null} | ||
| 145 | <MenuGroup> | ||
| 146 | <MenuGroupLabel>Workspace</MenuGroupLabel> | ||
| 147 | <MenuRadioGroup | ||
| 148 | value={effectiveEnvMode} | ||
| 149 | onValueChange={(value) => onEnvModeChange(value as EnvMode)} | ||
| 150 | > | ||
| 151 | <MenuRadioItem disabled={envModeLocked} value="local"> | ||
| 152 | <span className="flex min-w-0 items-center gap-1.5"> | ||
| 153 | {activeWorktreePath ? ( | ||
| 154 | <FolderGitIcon className="size-3" /> | ||
| 155 | ) : ( | ||
| 156 | <FolderIcon className="size-3" /> | ||
| 157 | )} | ||
| 158 | <span className="min-w-0 truncate"> | ||
| 159 | {resolveCurrentWorkspaceLabel(activeWorktreePath)} | ||
| 160 | </span> | ||
| 161 | </span> | ||
| 162 | </MenuRadioItem> | ||
| 163 | <MenuRadioItem disabled={envModeLocked} value="worktree"> | ||
| 164 | <span className="flex min-w-0 items-center gap-1.5"> | ||
| 165 | <FolderGit2Icon className="size-3" /> | ||
| 166 | <span className="min-w-0 truncate">{resolveEnvModeLabel("worktree")}</span> | ||
| 167 | </span> | ||
| 168 | </MenuRadioItem> | ||
| 169 | </MenuRadioGroup> | ||
| 170 | </MenuGroup> | ||
| 171 | </MenuPopup> | ||
| 172 | </Menu> | ||
| 173 | ); | ||
| 174 | }); | ||
| 175 | |||
| 33 | export const BranchToolbar = memo(function BranchToolbar({ | 176 | export const BranchToolbar = memo(function BranchToolbar({ |
| 34 | environmentId, | 177 | environmentId, |
| 35 | threadId, | 178 | threadId, |
| ... | @@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({ | ... | @@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({ |
| 74 | }); | 217 | }); |
| 75 | const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null); | 218 | const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null); |
| 76 | 219 | ||
| 77 | const showEnvironmentPicker = | 220 | const showEnvironmentPicker = Boolean( |
| 78 | availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange; | 221 | availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange, |
| 222 | ); | ||
| 223 | const isMobile = useIsMobile(); | ||
| 79 | 224 | ||
| 80 | if (!hasActiveThread || !activeProject) return null; | 225 | if (!hasActiveThread || !activeProject) return null; |
| 81 | 226 | ||
| 82 | return ( | 227 | return ( |
| 83 | <div className="mx-auto flex w-full max-w-208 items-center justify-between px-2.5 pb-3 pt-1 sm:px-3"> | 228 | <div className="mx-auto flex w-full max-w-208 items-center gap-2 px-2.5 pb-3 pt-1 sm:px-3"> |
| 84 | <div className="flex items-center gap-1"> | 229 | {isMobile ? ( |
| 85 | {showEnvironmentPicker && ( | 230 | <MobileRunContextSelector |
| 86 | <> | 231 | envLocked={envLocked} |
| 87 | <BranchToolbarEnvironmentSelector | 232 | envModeLocked={envModeLocked} |
| 88 | envLocked={envLocked} | 233 | environmentId={environmentId} |
| 89 | environmentId={environmentId} | 234 | availableEnvironments={availableEnvironments} |
| 90 | availableEnvironments={availableEnvironments} | 235 | showEnvironmentPicker={showEnvironmentPicker} |
| 91 | onEnvironmentChange={onEnvironmentChange} | 236 | onEnvironmentChange={onEnvironmentChange} |
| 92 | /> | ||
| 93 | <Separator orientation="vertical" className="mx-0.5 h-3.5!" /> | ||
| 94 | </> | ||
| 95 | )} | ||
| 96 | <BranchToolbarEnvModeSelector | ||
| 97 | envLocked={envModeLocked} | ||
| 98 | effectiveEnvMode={effectiveEnvMode} | 237 | effectiveEnvMode={effectiveEnvMode} |
| 99 | activeWorktreePath={activeWorktreePath} | 238 | activeWorktreePath={activeWorktreePath} |
| 100 | onEnvModeChange={onEnvModeChange} | 239 | onEnvModeChange={onEnvModeChange} |
| 101 | /> | 240 | /> |
| 102 | </div> | 241 | ) : ( |
| 242 | <div className="flex min-w-0 shrink-0 items-center gap-1"> | ||
| 243 | {showEnvironmentPicker && availableEnvironments && onEnvironmentChange && ( | ||
| 244 | <> | ||
| 245 | <BranchToolbarEnvironmentSelector | ||
| 246 | envLocked={envLocked} | ||
| 247 | environmentId={environmentId} | ||
| 248 | availableEnvironments={availableEnvironments} | ||
| 249 | onEnvironmentChange={onEnvironmentChange} | ||
| 250 | /> | ||
| 251 | <Separator orientation="vertical" className="mx-0.5 h-3.5!" /> | ||
| 252 | </> | ||
| 253 | )} | ||
| 254 | <BranchToolbarEnvModeSelector | ||
| 255 | envLocked={envModeLocked} | ||
| 256 | effectiveEnvMode={effectiveEnvMode} | ||
| 257 | activeWorktreePath={activeWorktreePath} | ||
| 258 | onEnvModeChange={onEnvModeChange} | ||
| 259 | /> | ||
| 260 | </div> | ||
| 261 | )} | ||
| 103 | 262 | ||
| 104 | <BranchToolbarBranchSelector | 263 | <BranchToolbarBranchSelector |
| 264 | className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none" | ||
| 105 | environmentId={environmentId} | 265 | environmentId={environmentId} |
| 106 | threadId={threadId} | 266 | threadId={threadId} |
| 107 | {...(draftId ? { draftId } : {})} | 267 | {...(draftId ? { draftId } : {})} |
apps/web/src/components/BranchToolbarBranchSelector.tsx+6-3| ... | @@ -19,6 +19,7 @@ import { readEnvironmentApi } from "../environmentApi"; | ... | @@ -19,6 +19,7 @@ import { readEnvironmentApi } from "../environmentApi"; |
| 19 | import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery"; | 19 | import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery"; |
| 20 | import { useGitStatus } from "../lib/gitStatusState"; | 20 | import { useGitStatus } from "../lib/gitStatusState"; |
| 21 | import { newCommandId } from "../lib/utils"; | 21 | import { newCommandId } from "../lib/utils"; |
| 22 | import { cn } from "../lib/utils"; | ||
| 22 | import { parsePullRequestReference } from "../pullRequestReference"; | 23 | import { parsePullRequestReference } from "../pullRequestReference"; |
| 23 | import { useStore } from "../store"; | 24 | import { useStore } from "../store"; |
| 24 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; | 25 | import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; |
| ... | @@ -45,6 +46,7 @@ import { | ... | @@ -45,6 +46,7 @@ import { |
| 45 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 46 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 46 | 47 | ||
| 47 | interface BranchToolbarBranchSelectorProps { | 48 | interface BranchToolbarBranchSelectorProps { |
| 49 | className?: string; | ||
| 48 | environmentId: EnvironmentId; | 50 | environmentId: EnvironmentId; |
| 49 | threadId: ThreadId; | 51 | threadId: ThreadId; |
| 50 | draftId?: DraftId; | 52 | draftId?: DraftId; |
| ... | @@ -76,6 +78,7 @@ function getBranchTriggerLabel(input: { | ... | @@ -76,6 +78,7 @@ function getBranchTriggerLabel(input: { |
| 76 | } | 78 | } |
| 77 | 79 | ||
| 78 | export function BranchToolbarBranchSelector({ | 80 | export function BranchToolbarBranchSelector({ |
| 81 | className, | ||
| 79 | environmentId, | 82 | environmentId, |
| 80 | threadId, | 83 | threadId, |
| 81 | draftId, | 84 | draftId, |
| ... | @@ -577,11 +580,11 @@ export function BranchToolbarBranchSelector({ | ... | @@ -577,11 +580,11 @@ export function BranchToolbarBranchSelector({ |
| 577 | > | 580 | > |
| 578 | <ComboboxTrigger | 581 | <ComboboxTrigger |
| 579 | render={<Button variant="ghost" size="xs" />} | 582 | render={<Button variant="ghost" size="xs" />} |
| 580 | className="text-muted-foreground/70 hover:text-foreground/80" | 583 | className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)} |
| 581 | disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending} | 584 | disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending} |
| 582 | > | 585 | > |
| 583 | <span className="max-w-[240px] truncate">{triggerLabel}</span> | 586 | <span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span> |
| 584 | <ChevronDownIcon /> | 587 | <ChevronDownIcon className="shrink-0" /> |
| 585 | </ComboboxTrigger> | 588 | </ComboboxTrigger> |
| 586 | <ComboboxPopup align="end" side="top" className="w-80"> | 589 | <ComboboxPopup align="end" side="top" className="w-80"> |
| 587 | <div className="border-b p-1"> | 590 | <div className="border-b p-1"> |
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+1| ... | @@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe | ... | @@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 58 | 58 | ||
| 59 | return ( | 59 | return ( |
| 60 | <Select | 60 | <Select |
| 61 | modal={false} | ||
| 61 | value={effectiveEnvMode} | 62 | value={effectiveEnvMode} |
| 62 | onValueChange={(value) => onEnvModeChange(value as EnvMode)} | 63 | onValueChange={(value) => onEnvModeChange(value as EnvMode)} |
| 63 | items={envModeItems} | 64 | items={envModeItems} |
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+1| ... | @@ -54,6 +54,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -54,6 +54,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 54 | 54 | ||
| 55 | return ( | 55 | return ( |
| 56 | <Select | 56 | <Select |
| 57 | modal={false} | ||
| 57 | value={environmentId} | 58 | value={environmentId} |
| 58 | onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)} | 59 | onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)} |
| 59 | items={environmentItems} | 60 | items={environmentItems} |
apps/web/src/components/ChatView.tsx+37-30| ... | @@ -3281,14 +3281,14 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3281,14 +3281,14 @@ export default function ChatView(props: ChatViewProps) { |
| 3281 | {/* Top bar */} | 3281 | {/* Top bar */} |
| 3282 | <header | 3282 | <header |
| 3283 | className={cn( | 3283 | className={cn( |
| 3284 | "border-b border-border px-3 sm:px-5", | 3284 | "border-b border-border", |
| 3285 | isElectron | 3285 | isElectron |
| 3286 | ? cn( | 3286 | ? cn( |
| 3287 | "drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]", | 3287 | "drag-region flex h-[52px] items-center px-3 sm:px-5 wco:h-[env(titlebar-area-height)]", |
| 3288 | reserveTitleBarControlInset && | 3288 | reserveTitleBarControlInset && |
| 3289 | "wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]", | 3289 | "wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]", |
| 3290 | ) | 3290 | ) |
| 3291 | : "py-2 sm:py-3", | 3291 | : "pb-2 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-2 sm:pb-3 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-3", |
| 3292 | )} | 3292 | )} |
| 3293 | > | 3293 | > |
| 3294 | <ChatHeader | 3294 | <ChatHeader |
| ... | @@ -3374,7 +3374,14 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3374,7 +3374,14 @@ export default function ChatView(props: ChatViewProps) { |
| 3374 | </div> | 3374 | </div> |
| 3375 | 3375 | ||
| 3376 | {/* Input bar */} | 3376 | {/* Input bar */} |
| 3377 | <div className={cn("px-3 pt-1.5 sm:px-5 sm:pt-2", isGitRepo ? "pb-1" : "pb-3 sm:pb-4")}> | 3377 | <div |
| 3378 | className={cn( | ||
| 3379 | "pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-1.5 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-2", | ||
| 3380 | isGitRepo | ||
| 3381 | ? "pb-[calc(env(safe-area-inset-bottom)+0.25rem)]" | ||
| 3382 | : "pb-[calc(env(safe-area-inset-bottom)+0.75rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1rem)]", | ||
| 3383 | )} | ||
| 3384 | > | ||
| 3378 | <ChatComposer | 3385 | <ChatComposer |
| 3379 | ref={composerRef} | 3386 | ref={composerRef} |
| 3380 | composerDraftTarget={composerDraftTarget} | 3387 | composerDraftTarget={composerDraftTarget} |
| ... | @@ -3443,34 +3450,34 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3443,34 +3450,34 @@ export default function ChatView(props: ChatViewProps) { |
| 3443 | setThreadError={setThreadError} | 3450 | setThreadError={setThreadError} |
| 3444 | onExpandImage={onExpandTimelineImage} | 3451 | onExpandImage={onExpandTimelineImage} |
| 3445 | /> | 3452 | /> |
| 3453 | {isGitRepo && ( | ||
| 3454 | <BranchToolbar | ||
| 3455 | environmentId={activeThread.environmentId} | ||
| 3456 | threadId={activeThread.id} | ||
| 3457 | {...(routeKind === "draft" && draftId ? { draftId } : {})} | ||
| 3458 | onEnvModeChange={onEnvModeChange} | ||
| 3459 | {...(canOverrideServerThreadEnvMode ? { effectiveEnvModeOverride: envMode } : {})} | ||
| 3460 | {...(canOverrideServerThreadEnvMode | ||
| 3461 | ? { | ||
| 3462 | activeThreadBranchOverride: activeThreadBranch, | ||
| 3463 | onActiveThreadBranchOverrideChange: setPendingServerThreadBranch, | ||
| 3464 | } | ||
| 3465 | : {})} | ||
| 3466 | envLocked={envLocked} | ||
| 3467 | onComposerFocusRequest={scheduleComposerFocus} | ||
| 3468 | {...(canCheckoutPullRequestIntoThread | ||
| 3469 | ? { onCheckoutPullRequestRequest: openPullRequestDialog } | ||
| 3470 | : {})} | ||
| 3471 | {...(hasMultipleEnvironments | ||
| 3472 | ? { | ||
| 3473 | availableEnvironments: logicalProjectEnvironments, | ||
| 3474 | onEnvironmentChange, | ||
| 3475 | } | ||
| 3476 | : {})} | ||
| 3477 | /> | ||
| 3478 | )} | ||
| 3446 | </div> | 3479 | </div> |
| 3447 | 3480 | ||
| 3448 | {isGitRepo && ( | ||
| 3449 | <BranchToolbar | ||
| 3450 | environmentId={activeThread.environmentId} | ||
| 3451 | threadId={activeThread.id} | ||
| 3452 | {...(routeKind === "draft" && draftId ? { draftId } : {})} | ||
| 3453 | onEnvModeChange={onEnvModeChange} | ||
| 3454 | {...(canOverrideServerThreadEnvMode ? { effectiveEnvModeOverride: envMode } : {})} | ||
| 3455 | {...(canOverrideServerThreadEnvMode | ||
| 3456 | ? { | ||
| 3457 | activeThreadBranchOverride: activeThreadBranch, | ||
| 3458 | onActiveThreadBranchOverrideChange: setPendingServerThreadBranch, | ||
| 3459 | } | ||
| 3460 | : {})} | ||
| 3461 | envLocked={envLocked} | ||
| 3462 | onComposerFocusRequest={scheduleComposerFocus} | ||
| 3463 | {...(canCheckoutPullRequestIntoThread | ||
| 3464 | ? { onCheckoutPullRequestRequest: openPullRequestDialog } | ||
| 3465 | : {})} | ||
| 3466 | {...(hasMultipleEnvironments | ||
| 3467 | ? { | ||
| 3468 | availableEnvironments: logicalProjectEnvironments, | ||
| 3469 | onEnvironmentChange, | ||
| 3470 | } | ||
| 3471 | : {})} | ||
| 3472 | /> | ||
| 3473 | )} | ||
| 3474 | {pullRequestDialogState ? ( | 3481 | {pullRequestDialogState ? ( |
| 3475 | <PullRequestThreadDialog | 3482 | <PullRequestThreadDialog |
| 3476 | key={pullRequestDialogState.key} | 3483 | key={pullRequestDialogState.key} |
apps/web/src/components/ComposerPromptEditor.tsx+3-3| ... | @@ -1490,7 +1490,7 @@ function ComposerPromptEditorInner({ | ... | @@ -1490,7 +1490,7 @@ function ComposerPromptEditorInner({ |
| 1490 | const rootElement = editor.getRootElement(); | 1490 | const rootElement = editor.getRootElement(); |
| 1491 | if (!rootElement) return; | 1491 | if (!rootElement) return; |
| 1492 | const boundedCursor = clampCollapsedComposerCursor(snapshotRef.current.value, nextCursor); | 1492 | const boundedCursor = clampCollapsedComposerCursor(snapshotRef.current.value, nextCursor); |
| 1493 | rootElement.focus(); | 1493 | rootElement.focus({ preventScroll: true }); |
| 1494 | editor.update(() => { | 1494 | editor.update(() => { |
| 1495 | $setSelectionAtComposerOffset(boundedCursor); | 1495 | $setSelectionAtComposerOffset(boundedCursor); |
| 1496 | }); | 1496 | }); |
| ... | @@ -1621,7 +1621,7 @@ function ComposerPromptEditorInner({ | ... | @@ -1621,7 +1621,7 @@ function ComposerPromptEditorInner({ |
| 1621 | contentEditable={ | 1621 | contentEditable={ |
| 1622 | <ContentEditable | 1622 | <ContentEditable |
| 1623 | className={cn( | 1623 | className={cn( |
| 1624 | "block max-h-[200px] min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent text-base sm:text-[14px] leading-relaxed text-foreground focus:outline-none", | 1624 | "block max-h-[200px] min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent text-[16px] leading-relaxed text-foreground focus:outline-none sm:text-[14px]", |
| 1625 | className, | 1625 | className, |
| 1626 | )} | 1626 | )} |
| 1627 | data-testid="composer-editor" | 1627 | data-testid="composer-editor" |
| ... | @@ -1632,7 +1632,7 @@ function ComposerPromptEditorInner({ | ... | @@ -1632,7 +1632,7 @@ function ComposerPromptEditorInner({ |
| 1632 | } | 1632 | } |
| 1633 | placeholder={ | 1633 | placeholder={ |
| 1634 | terminalContexts.length > 0 ? null : ( | 1634 | terminalContexts.length > 0 ? null : ( |
| 1635 | <div className="pointer-events-none absolute inset-0 text-base sm:text-[14px] leading-relaxed text-muted-foreground/35"> | 1635 | <div className="pointer-events-none absolute inset-0 text-[16px] leading-relaxed text-muted-foreground/35 sm:text-[14px]"> |
| 1636 | {placeholder} | 1636 | {placeholder} |
| 1637 | </div> | 1637 | </div> |
| 1638 | ) | 1638 | ) |
apps/web/src/components/chat/ChatHeader.tsx+6-1| ... | @@ -16,6 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr | ... | @@ -16,6 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr |
| 16 | import { Toggle } from "../ui/toggle"; | 16 | import { Toggle } from "../ui/toggle"; |
| 17 | import { SidebarTrigger } from "../ui/sidebar"; | 17 | import { SidebarTrigger } from "../ui/sidebar"; |
| 18 | import { OpenInPicker } from "./OpenInPicker"; | 18 | import { OpenInPicker } from "./OpenInPicker"; |
| 19 | import { usePrimaryEnvironmentId } from "../../environments/primary"; | ||
| 19 | 20 | ||
| 20 | interface ChatHeaderProps { | 21 | interface ChatHeaderProps { |
| 21 | activeThreadEnvironmentId: EnvironmentId; | 22 | activeThreadEnvironmentId: EnvironmentId; |
| ... | @@ -68,6 +69,10 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -68,6 +69,10 @@ export const ChatHeader = memo(function ChatHeader({ |
| 68 | onToggleTerminal, | 69 | onToggleTerminal, |
| 69 | onToggleDiff, | 70 | onToggleDiff, |
| 70 | }: ChatHeaderProps) { | 71 | }: ChatHeaderProps) { |
| 72 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | ||
| 73 | const isRemoteEnvironment = | ||
| 74 | primaryEnvironmentId !== null && activeThreadEnvironmentId !== primaryEnvironmentId; | ||
| 75 | |||
| 71 | return ( | 76 | return ( |
| 72 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"> | 77 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"> |
| 73 | <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> | 78 | <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3"> |
| ... | @@ -101,7 +106,7 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -101,7 +106,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 101 | onDeleteScript={onDeleteProjectScript} | 106 | onDeleteScript={onDeleteProjectScript} |
| 102 | /> | 107 | /> |
| 103 | )} | 108 | )} |
| 104 | {activeProjectName && ( | 109 | {activeProjectName && !isRemoteEnvironment && ( |
| 105 | <OpenInPicker | 110 | <OpenInPicker |
| 106 | keybindings={keybindings} | 111 | keybindings={keybindings} |
| 107 | availableEditors={availableEditors} | 112 | availableEditors={availableEditors} |
apps/web/src/components/ui/sidebar.tsx+8-1| ... | @@ -245,7 +245,14 @@ function Sidebar({ | ... | @@ -245,7 +245,14 @@ function Sidebar({ |
| 245 | <SheetTitle>Sidebar</SheetTitle> | 245 | <SheetTitle>Sidebar</SheetTitle> |
| 246 | <SheetDescription>Displays the mobile sidebar.</SheetDescription> | 246 | <SheetDescription>Displays the mobile sidebar.</SheetDescription> |
| 247 | </SheetHeader> | 247 | </SheetHeader> |
| 248 | <div className="flex h-full w-full flex-col">{children}</div> | 248 | <div |
| 249 | className={cn( | ||
| 250 | "flex h-full w-full flex-col pb-safe pt-safe", | ||
| 251 | side === "left" ? "pl-safe" : "pr-safe", | ||
| 252 | )} | ||
| 253 | > | ||
| 254 | {children} | ||
| 255 | </div> | ||
| 249 | </SheetPopup> | 256 | </SheetPopup> |
| 250 | </Sheet> | 257 | </Sheet> |
| 251 | </SidebarInstanceContext.Provider> | 258 | </SidebarInstanceContext.Provider> |
apps/web/src/index.css+28-3| ... | @@ -58,6 +58,22 @@ | ... | @@ -58,6 +58,22 @@ |
| 58 | } | 58 | } |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | /* Safe-area inset utilities for surfaces that opt into edge-to-edge rendering. | ||
| 62 | Insets resolve to 0 when the browser already constrains the layout viewport | ||
| 63 | to the safe area. */ | ||
| 64 | @utility pt-safe { | ||
| 65 | padding-top: max(env(safe-area-inset-top), 0px); | ||
| 66 | } | ||
| 67 | @utility pb-safe { | ||
| 68 | padding-bottom: max(env(safe-area-inset-bottom), 0px); | ||
| 69 | } | ||
| 70 | @utility pl-safe { | ||
| 71 | padding-left: max(env(safe-area-inset-left), 0px); | ||
| 72 | } | ||
| 73 | @utility pr-safe { | ||
| 74 | padding-right: max(env(safe-area-inset-right), 0px); | ||
| 75 | } | ||
| 76 | |||
| 61 | /* Suppress all transitions during theme changes */ | 77 | /* Suppress all transitions during theme changes */ |
| 62 | .no-transitions, | 78 | .no-transitions, |
| 63 | .no-transitions *, | 79 | .no-transitions *, |
| ... | @@ -138,19 +154,28 @@ body { | ... | @@ -138,19 +154,28 @@ body { |
| 138 | sans-serif; | 154 | sans-serif; |
| 139 | margin: 0; | 155 | margin: 0; |
| 140 | padding: 0; | 156 | padding: 0; |
| 141 | min-height: 100vh; | ||
| 142 | } | 157 | } |
| 143 | 158 | ||
| 144 | html, | 159 | html, |
| 145 | body, | 160 | body { |
| 161 | min-height: calc(100svh + env(safe-area-inset-top)); | ||
| 162 | overscroll-behavior: none; | ||
| 163 | } | ||
| 164 | |||
| 165 | body { | ||
| 166 | height: 100%; | ||
| 167 | overflow: hidden; | ||
| 168 | } | ||
| 169 | |||
| 146 | #root { | 170 | #root { |
| 147 | height: 100%; | 171 | height: 100%; |
| 148 | width: 100%; | 172 | width: 100%; |
| 149 | max-width: 100%; | 173 | max-width: 100%; |
| 150 | overflow-y: hidden; | ||
| 151 | overflow-x: hidden; | 174 | overflow-x: hidden; |
| 152 | overflow-x: clip; | 175 | overflow-x: clip; |
| 176 | overflow-y: hidden; | ||
| 153 | overscroll-behavior-y: none; | 177 | overscroll-behavior-y: none; |
| 178 | padding-top: max(env(safe-area-inset-top), 0px); | ||
| 154 | } | 179 | } |
| 155 | 180 | ||
| 156 | body::after { | 181 | body::after { |
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+2-2| ... | @@ -240,7 +240,7 @@ function ChatThreadRouteView() { | ... | @@ -240,7 +240,7 @@ function ChatThreadRouteView() { |
| 240 | if (!shouldUseDiffSheet) { | 240 | if (!shouldUseDiffSheet) { |
| 241 | return ( | 241 | return ( |
| 242 | <> | 242 | <> |
| 243 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 243 | <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh"> |
| 244 | <ChatView | 244 | <ChatView |
| 245 | environmentId={threadRef.environmentId} | 245 | environmentId={threadRef.environmentId} |
| 246 | threadId={threadRef.threadId} | 246 | threadId={threadRef.threadId} |
| ... | @@ -261,7 +261,7 @@ function ChatThreadRouteView() { | ... | @@ -261,7 +261,7 @@ function ChatThreadRouteView() { |
| 261 | 261 | ||
| 262 | return ( | 262 | return ( |
| 263 | <> | 263 | <> |
| 264 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 264 | <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh"> |
| 265 | <ChatView | 265 | <ChatView |
| 266 | environmentId={threadRef.environmentId} | 266 | environmentId={threadRef.environmentId} |
| 267 | threadId={threadRef.threadId} | 267 | threadId={threadRef.threadId} |
apps/web/src/routes/_chat.draft.$draftId.tsx+2-2| ... | @@ -55,7 +55,7 @@ function DraftChatThreadRouteView() { | ... | @@ -55,7 +55,7 @@ function DraftChatThreadRouteView() { |
| 55 | 55 | ||
| 56 | if (canonicalThreadRef) { | 56 | if (canonicalThreadRef) { |
| 57 | return ( | 57 | return ( |
| 58 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 58 | <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh"> |
| 59 | <ChatView | 59 | <ChatView |
| 60 | environmentId={canonicalThreadRef.environmentId} | 60 | environmentId={canonicalThreadRef.environmentId} |
| 61 | threadId={canonicalThreadRef.threadId} | 61 | threadId={canonicalThreadRef.threadId} |
| ... | @@ -70,7 +70,7 @@ function DraftChatThreadRouteView() { | ... | @@ -70,7 +70,7 @@ function DraftChatThreadRouteView() { |
| 70 | } | 70 | } |
| 71 | 71 | ||
| 72 | return ( | 72 | return ( |
| 73 | <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground"> | 73 | <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh"> |
| 74 | <ChatView | 74 | <ChatView |
| 75 | draftId={draftId} | 75 | draftId={draftId} |
| 76 | environmentId={draftSession.environmentId} | 76 | environmentId={draftSession.environmentId} |