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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,17 @@ jobs:
docker exec ${{ steps.postgres.outputs.container }} \
psql -U dispatch -d postgres -c "CREATE DATABASE dispatch_ci;"

- name: Pick E2E port
id: e2e
run: |
PORT=$((RANDOM % 10000 + 20000))
echo "port=$PORT" >> "$GITHUB_OUTPUT"

- name: E2E tests
run: npx playwright test
env:
CI: "true"
E2E_PORT: "8799"
E2E_PORT: "${{ steps.e2e.outputs.port }}"
DATABASE_URL: postgres://dispatch:[email protected]:${{ steps.postgres.outputs.port }}/dispatch_ci

- name: Upload Playwright report
Expand Down
5 changes: 3 additions & 2 deletions e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ test.describe("Settings pane", () => {

// Settings overlay should appear with a "Settings" heading and "Release" nav item
await expect(page.getByText("Settings").first()).toBeVisible({ timeout: 3_000 });
await expect(page.getByText("Release", { exact: true })).toBeVisible();
const releaseNav = page.getByRole("navigation").getByText("Release");
await expect(releaseNav).toBeVisible();

// Close it via the X button (sr-only "Close")
await page.getByRole("button", { name: "Close" }).click();

// The Release nav item should no longer be visible (pane is closed)
await expect(page.getByText("Release", { exact: true })).not.toBeVisible({ timeout: 3_000 });
await expect(releaseNav).not.toBeVisible({ timeout: 3_000 });
});
});
8 changes: 4 additions & 4 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1429,12 +1429,12 @@ export function App(): JSX.Element {
agents={agents}
selectedAgentId={selectedAgentId}
overflowAgentId={overflowAgentId}
onOpenCreateDialog={openCreateDialog}
onOpenEditWorktreeDialog={openEditWorktreeDialog}
onOpenSettings={() => setSettingsPaneOpen(true)}
onOpenCreateDialog={() => { setMobileLeftOpen(false); openCreateDialog(); }}
onOpenEditWorktreeDialog={(agent) => { setMobileLeftOpen(false); openEditWorktreeDialog(agent); }}
onOpenSettings={() => { setMobileLeftOpen(false); setSettingsPaneOpen(true); }}
setOverflowAgentId={setOverflowAgentId}
setDeleteTarget={setDeleteTarget}
setDeleteConfirmOpen={setDeleteConfirmOpen}
setDeleteConfirmOpen={(open) => { if (open) setMobileLeftOpen(false); setDeleteConfirmOpen(open); }}
agentVisualState={agentVisualState}
borderForAgentState={borderForAgentState}
toggleAgentDetails={toggleAgentDetails}
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/app/release-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ export function ReleaseManager(): JSX.Element {
const phaseIndex = job ? PHASES_ORDER.indexOf(job.phase) : -1;

return (
<div className="flex h-full min-h-0">
<div className="flex h-full min-h-0 flex-col md:flex-row">
{/* Left column — controls */}
<div className="flex w-[360px] shrink-0 flex-col gap-6 overflow-y-auto border-r border-border p-6">
<div className="flex md:w-[360px] shrink-0 flex-col gap-6 overflow-y-auto border-b md:border-b-0 md:border-r border-border p-4 md:p-6">

{/* Current version */}
<div>
Expand Down Expand Up @@ -428,7 +428,7 @@ export function ReleaseManager(): JSX.Element {
</div>

{/* Right column — log */}
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex min-h-0 min-w-0 flex-1 flex-col p-2">
{showLog ? (
<div
ref={logRef}
Expand Down
59 changes: 50 additions & 9 deletions web/src/components/app/settings-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Rocket, X } from "lucide-react";
import { ChevronRight, ArrowLeft, Rocket, X } from "lucide-react";

import { ReleaseManager } from "@/components/app/release-manager";
import { cn } from "@/lib/utils";
Expand All @@ -16,19 +17,41 @@ type SettingsPaneProps = {
};

export function SettingsPane({ open, onClose }: SettingsPaneProps): JSX.Element {
const activeSection: SettingsSection = "release";
const [activeSection, setActiveSection] = useState<SettingsSection | null>("release");

return (
<DialogPrimitive.Root open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogPrimitive.Root
open={open}
onOpenChange={(v) => {
if (!v) {
onClose();
setActiveSection("release");
}
}}
>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/70 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogPrimitive.Content className="fixed inset-4 z-50 flex flex-col overflow-hidden rounded-sm border border-border bg-card text-foreground shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
<DialogPrimitive.Overlay className="fixed inset-0 z-[70] bg-black/70 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogPrimitive.Content className="fixed inset-0 md:inset-4 z-[70] flex flex-col overflow-hidden rounded-none md:rounded-sm border border-border bg-card text-foreground shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
<DialogPrimitive.Title className="sr-only">Settings</DialogPrimitive.Title>
<DialogPrimitive.Description className="sr-only">Dispatch settings and release manager</DialogPrimitive.Description>

{/* Header */}
<div className="flex h-12 shrink-0 items-center border-b border-border px-5">
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Settings</span>
{/* Mobile back button when viewing a section */}
{activeSection !== null && (
<button
onClick={() => setActiveSection(null)}
className="mr-2 rounded-sm p-1 opacity-70 transition-opacity hover:opacity-100 md:hidden"
>
<ArrowLeft className="h-4 w-4" />
</button>
)}
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{activeSection !== null ? (
<span className="md:hidden">{SECTIONS.find((s) => s.id === activeSection)?.label ?? "Settings"}</span>
) : null}
<span className={activeSection !== null ? "hidden md:inline" : ""}>Settings</span>
</span>
<DialogPrimitive.Close className="ml-auto rounded-sm p-1 opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
Expand All @@ -37,11 +60,12 @@ export function SettingsPane({ open, onClose }: SettingsPaneProps): JSX.Element

{/* Body */}
<div className="flex min-h-0 flex-1">
{/* Nav */}
<nav className="flex w-40 shrink-0 flex-col border-r border-border py-2">
{/* Desktop nav — always visible */}
<nav className="hidden md:flex w-40 shrink-0 flex-col border-r border-border py-2">
{SECTIONS.map(({ id, label, icon: Icon }) => (
<div
key={id}
onClick={() => setActiveSection(id)}
className={cn(
"flex cursor-pointer items-center gap-2.5 px-4 py-2.5 text-sm transition-colors",
activeSection === id
Expand All @@ -55,8 +79,25 @@ export function SettingsPane({ open, onClose }: SettingsPaneProps): JSX.Element
))}
</nav>

{/* Mobile nav — section list, shown when no section selected */}
{activeSection === null && (
<nav className="flex flex-1 flex-col md:hidden">
{SECTIONS.map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setActiveSection(id)}
className="flex items-center gap-3 border-b border-border px-5 py-3.5 text-sm text-foreground transition-colors active:bg-muted"
>
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
{label}
<ChevronRight className="ml-auto h-4 w-4 text-muted-foreground" />
</button>
))}
</nav>
)}

{/* Content */}
<div className="min-h-0 min-w-0 flex-1">
<div className={cn("min-h-0 min-w-0 flex-1", activeSection === null && "hidden md:block")}>
{activeSection === "release" && <ReleaseManager />}
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-50 bg-black/70", className)} {...props} />
<DialogPrimitive.Overlay ref={ref} className={cn("fixed inset-0 z-[70] bg-black/70", className)} {...props} />
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

Expand All @@ -25,7 +25,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-[min(560px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl border border-border bg-card p-4 shadow-lg",
"fixed left-1/2 top-1/2 z-[70] grid w-[min(560px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl border border-border bg-card p-4 shadow-lg",
className
)}
{...props}
Expand Down
Loading