Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
/>
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#161616" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#161616" />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
}, [navigate]);

return (
<SidebarProvider defaultOpen>
<SidebarProvider className="h-dvh! min-h-0!" defaultOpen>
<Sidebar
side="left"
collapsible="offcanvas"
Expand Down
196 changes: 178 additions & 18 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import {
ChevronDownIcon,
CloudIcon,
FolderGit2Icon,
FolderGitIcon,
FolderIcon,
MonitorIcon,
} from "lucide-react";
import { memo, useMemo } from "react";

import { useComposerDraftStore, type DraftId } from "../composerDraftStore";
import { useIsMobile } from "../hooks/useMediaQuery";
import { useStore } from "../store";
import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";
import {
type EnvMode,
type EnvironmentOption,
resolveCurrentWorkspaceLabel,
resolveEnvModeLabel,
resolveEffectiveEnvMode,
resolveLockedWorkspaceLabel,
} from "./BranchToolbar.logic";
import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";
import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";
import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector";
import { Button } from "./ui/button";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuTrigger,
} from "./ui/menu";
import { Separator } from "./ui/separator";

interface BranchToolbarProps {
Expand All @@ -30,6 +53,126 @@ interface BranchToolbarProps {
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
}

interface MobileRunContextSelectorProps {
envLocked: boolean;
envModeLocked: boolean;
environmentId: EnvironmentId;
availableEnvironments: readonly EnvironmentOption[] | undefined;
showEnvironmentPicker: boolean;
onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined;
effectiveEnvMode: EnvMode;
activeWorktreePath: string | null;
onEnvModeChange: (mode: EnvMode) => void;
}

const MobileRunContextSelector = memo(function MobileRunContextSelector({
envLocked,
envModeLocked,
environmentId,
availableEnvironments,
showEnvironmentPicker,
onEnvironmentChange,
effectiveEnvMode,
activeWorktreePath,
onEnvModeChange,
}: MobileRunContextSelectorProps) {
const activeEnvironment = useMemo(
() => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null,
[availableEnvironments, environmentId],
);
const environmentLabel = activeEnvironment?.label ?? "Run on";
const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon;
const WorkspaceIcon =
effectiveEnvMode === "worktree"
? FolderGit2Icon
: activeWorktreePath
? FolderGitIcon
: FolderIcon;
const workspaceLabel = envModeLocked
? resolveLockedWorkspaceLabel(activeWorktreePath)
: effectiveEnvMode === "worktree"
? resolveEnvModeLabel("worktree")
: resolveCurrentWorkspaceLabel(activeWorktreePath);

return (
<Menu>
<MenuTrigger
render={<Button variant="ghost" size="xs" />}
className="min-w-0 max-w-[48%] flex-1 justify-start text-muted-foreground/70 hover:text-foreground/80 md:hidden"
>
{showEnvironmentPicker ? (
<>
<EnvironmentIcon className="size-3 shrink-0" />
<span className="min-w-0 truncate">{environmentLabel}</span>
</>
) : (
<>
<WorkspaceIcon className="size-3 shrink-0" />
<span className="min-w-0 truncate">{workspaceLabel}</span>
</>
)}
<ChevronDownIcon className="size-3 shrink-0 opacity-50" />
</MenuTrigger>
<MenuPopup align="start" side="top" className="w-64">
{showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? (
<>
<MenuGroup>
<MenuGroupLabel>Run on</MenuGroupLabel>
<MenuRadioGroup
value={environmentId}
onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}
>
{availableEnvironments.map((env) => {
const Icon = env.isPrimary ? MonitorIcon : CloudIcon;
return (
<MenuRadioItem
key={env.environmentId}
disabled={envLocked}
value={env.environmentId}
>
<span className="flex min-w-0 items-center gap-1.5">
<Icon className="size-3" />
<span className="min-w-0 truncate">{env.label}</span>
</span>
</MenuRadioItem>
);
})}
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator />
</>
) : null}
<MenuGroup>
<MenuGroupLabel>Workspace</MenuGroupLabel>
<MenuRadioGroup
value={effectiveEnvMode}
onValueChange={(value) => onEnvModeChange(value as EnvMode)}
>
<MenuRadioItem disabled={envModeLocked} value="local">
<span className="flex min-w-0 items-center gap-1.5">
{activeWorktreePath ? (
<FolderGitIcon className="size-3" />
) : (
<FolderIcon className="size-3" />
)}
<span className="min-w-0 truncate">
{resolveCurrentWorkspaceLabel(activeWorktreePath)}
</span>
</span>
</MenuRadioItem>
<MenuRadioItem disabled={envModeLocked} value="worktree">
<span className="flex min-w-0 items-center gap-1.5">
<FolderGit2Icon className="size-3" />
<span className="min-w-0 truncate">{resolveEnvModeLabel("worktree")}</span>
</span>
</MenuRadioItem>
</MenuRadioGroup>
</MenuGroup>
</MenuPopup>
</Menu>
);
});

export const BranchToolbar = memo(function BranchToolbar({
environmentId,
threadId,
Expand Down Expand Up @@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({
});
const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null);

const showEnvironmentPicker =
availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange;
const showEnvironmentPicker = Boolean(
availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange,
);
const isMobile = useIsMobile();

if (!hasActiveThread || !activeProject) return null;

return (
<div className="mx-auto flex w-full max-w-208 items-center justify-between px-2.5 pb-3 pt-1 sm:px-3">
<div className="flex items-center gap-1">
{showEnvironmentPicker && (
<>
<BranchToolbarEnvironmentSelector
envLocked={envLocked}
environmentId={environmentId}
availableEnvironments={availableEnvironments}
onEnvironmentChange={onEnvironmentChange}
/>
<Separator orientation="vertical" className="mx-0.5 h-3.5!" />
</>
)}
<BranchToolbarEnvModeSelector
envLocked={envModeLocked}
<div className="mx-auto flex w-full max-w-208 items-center gap-2 px-2.5 pb-3 pt-1 sm:px-3">
{isMobile ? (
<MobileRunContextSelector
envLocked={envLocked}
envModeLocked={envModeLocked}
environmentId={environmentId}
availableEnvironments={availableEnvironments}
showEnvironmentPicker={showEnvironmentPicker}
onEnvironmentChange={onEnvironmentChange}
effectiveEnvMode={effectiveEnvMode}
activeWorktreePath={activeWorktreePath}
onEnvModeChange={onEnvModeChange}
/>
</div>
) : (
<div className="flex min-w-0 shrink-0 items-center gap-1">
{showEnvironmentPicker && availableEnvironments && onEnvironmentChange && (
<>
<BranchToolbarEnvironmentSelector
envLocked={envLocked}
environmentId={environmentId}
availableEnvironments={availableEnvironments}
onEnvironmentChange={onEnvironmentChange}
/>
<Separator orientation="vertical" className="mx-0.5 h-3.5!" />
</>
)}
<BranchToolbarEnvModeSelector
envLocked={envModeLocked}
effectiveEnvMode={effectiveEnvMode}
activeWorktreePath={activeWorktreePath}
onEnvModeChange={onEnvModeChange}
/>
</div>
)}

<BranchToolbarBranchSelector
className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"
environmentId={environmentId}
threadId={threadId}
{...(draftId ? { draftId } : {})}
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { readEnvironmentApi } from "../environmentApi";
import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery";
import { useGitStatus } from "../lib/gitStatusState";
import { newCommandId } from "../lib/utils";
import { cn } from "../lib/utils";
import { parsePullRequestReference } from "../pullRequestReference";
import { useStore } from "../store";
import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";
Expand All @@ -45,6 +46,7 @@ import {
import { stackedThreadToast, toastManager } from "./ui/toast";

interface BranchToolbarBranchSelectorProps {
className?: string;
environmentId: EnvironmentId;
threadId: ThreadId;
draftId?: DraftId;
Expand Down Expand Up @@ -76,6 +78,7 @@ function getBranchTriggerLabel(input: {
}

export function BranchToolbarBranchSelector({
className,
environmentId,
threadId,
draftId,
Expand Down Expand Up @@ -577,11 +580,11 @@ export function BranchToolbarBranchSelector({
>
<ComboboxTrigger
render={<Button variant="ghost" size="xs" />}
className="text-muted-foreground/70 hover:text-foreground/80"
className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)}
disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending}
>
<span className="max-w-[240px] truncate">{triggerLabel}</span>
<ChevronDownIcon />
<span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span>
<ChevronDownIcon className="shrink-0" />
</ComboboxTrigger>
<ComboboxPopup align="end" side="top" className="w-80">
<div className="border-b p-1">
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/BranchToolbarEnvModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe

return (
<Select
modal={false}
value={effectiveEnvMode}
onValueChange={(value) => onEnvModeChange(value as EnvMode)}
items={envModeItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir

return (
<Select
modal={false}
value={environmentId}
onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}
items={environmentItems}
Expand Down
Loading
Loading