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: 3 additions & 2 deletions chat-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ const router = createBrowserRouter(
{
element: <Layout />,
children: [
{ index: true, element: <ChatRoute /> },
{ path: "chat", element: <ChatRoute /> },
{ path: "chat/s/:sessionId", element: <SessionRoute /> },
{ path: "chat/new", element: <NewChatRoute /> },
{ path: "s/:sessionId", element: <SessionRoute /> },
{ path: "new", element: <NewChatRoute /> },
{ path: "*", element: <NotFoundRoute /> },
],
},
],
{ basename: "/chat" },
);

export function App() {
Expand Down
19 changes: 12 additions & 7 deletions chat-ui/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useKeyboard } from "@/hooks/use-keyboard";
import { useSessions } from "@/hooks/use-sessions";
import { useTheme } from "@/hooks/use-theme";
import { useIsMobile } from "@/hooks/use-mobile";
import { CHAT_ROOT_PATH, chatSessionPath } from "@/lib/routes";
import { CommandPalette } from "./command-palette";
import { DeleteSessionDialog } from "./delete-session-dialog";
import { KeyboardHelpSheet } from "./keyboard-help-sheet";
Expand All @@ -15,7 +16,7 @@ import { SidebarPanel } from "./sidebar-panel";
export function AppShell({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const { sessionId } = useParams<{ sessionId: string }>();
const { sessions, isLoading, createSession, deleteSession, updateSession } =
const { sessions, isLoading, refresh, createSession, deleteSession, updateSession } =
useSessions();
const { toggleTheme } = useTheme();
const isMobile = useIsMobile();
Expand All @@ -32,6 +33,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
} | null>(null);
const [helpOpen, setHelpOpen] = useState(false);

useEffect(() => {
if (sessionId) refresh();
}, [sessionId, refresh]);

// Update the browser tab title once we know the agent name. Picks up
// cached name immediately on reload and refreshes when fresh data lands.
useEffect(() => {
Expand Down Expand Up @@ -70,12 +75,12 @@ export function AppShell({ children }: { children: React.ReactNode }) {

const handleNewSession = useCallback(async () => {
const id = await createSession();
navigate(`/s/${id}`);
navigate(chatSessionPath(id));
}, [createSession, navigate]);

const handleSessionClick = useCallback(
(id: string) => {
navigate(`/s/${id}`);
navigate(chatSessionPath(id));
if (isMobile) setSidebarOpen(false);
},
[navigate, isMobile],
Expand All @@ -101,7 +106,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
deleteSession(deleteTarget.id);
setDeleteTarget(null);
if (deleteTarget.id === sessionId) {
navigate("/");
navigate(CHAT_ROOT_PATH);
}
}, [deleteTarget, deleteSession, sessionId, navigate]);

Expand Down Expand Up @@ -147,7 +152,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
</a>

{sidebarOpen && (
<div className="w-64 shrink-0 border-r border-border">
<div className="w-64 shrink-0 border-r border-border/70">
<SidebarPanel
sessions={sessions}
isLoading={isLoading}
Expand All @@ -161,11 +166,11 @@ export function AppShell({ children }: { children: React.ReactNode }) {
)}

<div className="flex min-w-0 flex-1 flex-col">
<header className="flex h-12 items-center border-b border-border px-4">
<header className="flex h-12 shrink-0 items-center border-b border-border/70 bg-background/95 px-4 backdrop-blur">
<button
type="button"
onClick={() => setSidebarOpen(!sidebarOpen)}
className="mr-3 text-muted-foreground hover:text-foreground"
className="mr-3 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted/45 hover:text-foreground"
aria-label={sidebarOpen ? "Close sidebar" : "Open sidebar"}
>
<PanelLeft className="h-4 w-4" />
Expand Down
6 changes: 3 additions & 3 deletions chat-ui/src/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export function ChatInput({
const hasAttachments = (attachments?.length ?? 0) > 0;

return (
<div className="border-t border-border bg-background px-4 py-3">
<div className="border-t border-border/70 bg-background/95 px-4 py-3 backdrop-blur">
<div className="mx-auto max-w-3xl">
<div className="flex flex-col rounded-xl border border-border bg-card">
<div className="flex flex-col rounded-xl border border-border/80 bg-card/95 shadow-sm shadow-black/5 transition-colors focus-within:border-primary/45 focus-within:ring-4 focus-within:ring-primary/10">
{hasAttachments && attachments && onRemoveFile && (
<div className="pt-2">
<AttachmentStrip files={attachments} onRemove={onRemoveFile} />
Expand Down Expand Up @@ -149,7 +149,7 @@ export function ChatInput({
size="icon"
onClick={handleSend}
disabled={!text.trim() || disabled || isSubmitting}
className="h-8 w-8 shrink-0 rounded-lg bg-primary text-primary-content hover:bg-primary/90 disabled:opacity-50"
className="h-8 w-8 shrink-0 rounded-lg bg-primary text-primary-content shadow-sm shadow-primary/15 hover:bg-primary/90 disabled:opacity-50"
aria-label="Send message"
>
<ArrowUp className="h-4 w-4" />
Expand Down
29 changes: 22 additions & 7 deletions chat-ui/src/components/notification-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// "Want a ping when your task is done?" with Enable and Dismiss buttons.
// Dismiss hides for 24 hours via localStorage.

import { useCallback, useState } from "react";
import { useBootstrap } from "@/hooks/use-bootstrap";
import { useNotifications } from "@/hooks/use-notifications";
import { Button } from "@/ui/button";
import { BellRing, X } from "lucide-react";
import { useCallback, useState } from "react";

const DISMISS_KEY = "phantom_notification_banner_dismissed_at";
const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
Expand All @@ -25,7 +27,9 @@ export function NotificationBanner({
}: {
visible: boolean;
}) {
const { permission, subscribed, subscribe } = useNotifications();
const { data } = useBootstrap();
const notificationsEnabled = data?.push_notifications_enabled === true;
const { permission, subscribed, subscribe } = useNotifications({ enabled: notificationsEnabled });
const [dismissed, setDismissed] = useState(isDismissed);
const [enabling, setEnabling] = useState(false);

Expand All @@ -51,6 +55,7 @@ export function NotificationBanner({
// permission denied, or browser doesn't support it
if (
!visible ||
!notificationsEnabled ||
subscribed ||
dismissed ||
permission === "denied" ||
Expand All @@ -60,20 +65,30 @@ export function NotificationBanner({
}

return (
<div className="mx-auto mb-3 flex w-full max-w-2xl items-center gap-3 rounded-lg border border-border bg-card px-4 py-3">
<p className="flex-1 text-sm text-muted-foreground">
Want a ping when your task is done?
<div className="mx-auto mb-3 flex w-full max-w-2xl items-center gap-2 rounded-lg border border-border/70 bg-card/95 px-3 py-2 shadow-sm shadow-black/5">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/60 bg-muted/40 text-muted-foreground">
<BellRing className="h-3.5 w-3.5" />
</span>
<p className="min-w-0 flex-1 truncate text-sm text-muted-foreground">
Notify me when long tasks finish.
</p>
<Button
size="sm"
variant="default"
onClick={handleEnable}
disabled={enabling}
className="h-8 shrink-0"
>
{enabling ? "Enabling..." : "Enable"}
</Button>
<Button size="sm" variant="ghost" onClick={handleDismiss}>
Not now
<Button
size="icon"
variant="ghost"
onClick={handleDismiss}
className="h-8 w-8 shrink-0"
aria-label="Dismiss notification prompt"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
);
Expand Down
8 changes: 4 additions & 4 deletions chat-ui/src/components/run-activity-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ export function RunActivityRow({

return (
<div className="flex justify-start">
<section className="max-w-[92%] min-w-0 py-1.5" aria-label="Run activity">
<section className="w-full max-w-[92%] min-w-0 py-1.5" aria-label="Run activity">
<div className="relative pl-5">
<div className="absolute bottom-2 left-[7px] top-3 w-px bg-border" />
<div className="absolute bottom-2 left-[7px] top-3 w-px bg-border/70" />
<div className="relative flex min-w-0 items-start gap-3">
<div className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background">
<Icon className={cn("h-4 w-4", className)} />
Expand Down Expand Up @@ -212,7 +212,7 @@ export function RunActivityRow({
{facts.map((fact) => (
<span
key={fact}
className="max-w-full truncate rounded border border-border bg-muted/45 px-2 py-0.5"
className="max-w-full truncate rounded-md border border-border/70 bg-muted/35 px-2 py-0.5"
>
{fact}
</span>
Expand Down Expand Up @@ -253,7 +253,7 @@ export function RunActivityRow({
</div>

{toolCalls.length > 0 && (
<div className="mt-2 space-y-2">
<div className="mt-2 space-y-1.5">
{toolCalls.map((tool) => (
<ToolCallCard key={tool.id} tool={tool} />
))}
Expand Down
4 changes: 2 additions & 2 deletions chat-ui/src/components/sidebar-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function SidebarPanel({
}) {
return (
<div className="flex h-full flex-col bg-sidebar">
<div className="flex items-center justify-between border-b border-sidebar-border px-3 py-3">
<div className="flex h-12 items-center justify-between border-b border-sidebar-border/70 px-3">
<span className="text-sm font-semibold text-sidebar-foreground">
Conversations
</span>
Expand All @@ -32,7 +32,7 @@ export function SidebarPanel({
size="icon"
onClick={onNewSession}
aria-label="New conversation"
className="h-7 w-7"
className="h-8 w-8 rounded-md"
>
<Plus className="h-4 w-4" />
</Button>
Expand Down
93 changes: 72 additions & 21 deletions chat-ui/src/components/tool-call-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,67 @@ type StateStyle = {
border: string;
icon: typeof Terminal;
iconClass: string;
showSpinner?: boolean;
badgeClass: string;
};

function getStateStyle(state: ToolCallState["state"]): StateStyle {
switch (state) {
case "pending":
return { border: "border-muted", icon: Terminal, iconClass: "animate-pulse text-muted-foreground" };
return {
border: "border-border/60",
icon: Terminal,
iconClass: "animate-pulse text-muted-foreground",
badgeClass: "border-border bg-muted/55 text-muted-foreground",
};
case "input_streaming":
return { border: "border-primary/50 animate-pulse", icon: Terminal, iconClass: "text-primary" };
return {
border: "border-primary/35 animate-pulse",
icon: Terminal,
iconClass: "text-primary",
badgeClass: "border-primary/25 bg-primary/10 text-primary",
};
case "input_complete":
return { border: "border-border", icon: Terminal, iconClass: "text-foreground" };
return {
border: "border-border/70",
icon: Terminal,
iconClass: "text-foreground",
badgeClass: "border-border bg-muted/45 text-muted-foreground",
};
case "running":
return { border: "border-primary/40", icon: Loader2, iconClass: "text-primary animate-spin", showSpinner: true };
return {
border: "border-primary/35",
icon: Loader2,
iconClass: "text-primary animate-spin",
badgeClass: "border-primary/25 bg-primary/10 text-primary",
};
case "result":
return { border: "border-border", icon: Check, iconClass: "text-success" };
return {
border: "border-border/70",
icon: Check,
iconClass: "text-success",
badgeClass: "border-success/20 bg-success/10 text-success",
};
case "error":
return { border: "border-error", icon: XCircle, iconClass: "text-error" };
return {
border: "border-error/70",
icon: XCircle,
iconClass: "text-error",
badgeClass: "border-error/25 bg-error/10 text-error",
};
case "aborted":
return { border: "border-muted", icon: AlertCircle, iconClass: "text-muted-foreground line-through" };
return {
border: "border-border/60",
icon: AlertCircle,
iconClass: "text-muted-foreground line-through",
badgeClass: "border-border bg-muted/45 text-muted-foreground",
};
case "blocked":
return { border: "border-warning", icon: Shield, iconClass: "text-warning" };
return {
border: "border-warning/70",
icon: Shield,
iconClass: "text-warning",
badgeClass: "border-warning/25 bg-warning/10 text-warning",
};
}
}

Expand All @@ -170,25 +210,35 @@ export function ToolCallCard({ tool }: { tool: ToolCallState }) {

const isOpen = disclosure.isOpen;
const hasBody = Boolean(output || tool.error || tool.blockReason || inputDetails || tool.fullRef);
const detailLabel = isOpen ? "Hide details" : "View details";

return (
<div className={cn("my-2 overflow-hidden rounded border bg-card transition-colors", style.border)}>
<div
className={cn(
"my-1.5 overflow-hidden rounded-lg border bg-card/80 shadow-sm shadow-black/5 transition-colors",
style.border,
)}
>
<button
type="button"
onClick={() => hasBody && setDisclosure((current) => toggleToolDisclosure(current))}
className="flex min-h-11 w-full items-center gap-2 px-3 py-2 text-sm"
className="flex min-h-11 w-full items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-muted/35 disabled:hover:bg-transparent"
disabled={!hasBody}
aria-expanded={hasBody ? isOpen : undefined}
aria-controls={hasBody ? bodyId : undefined}
>
<Icon className={cn("h-4 w-4 shrink-0", style.iconClass)} />
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background/70">
<Icon className={cn("h-4 w-4", style.iconClass)} />
</span>
<div className="min-w-0 flex-1 text-left">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
<span className="font-medium text-foreground">{tool.toolName}</span>
{subtitle && <span className="min-w-0 max-w-full truncate text-muted-foreground">{subtitle}</span>}
</div>
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span>{stateLabel(tool)}</span>
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
<span className={cn("rounded border px-1.5 py-0.5 font-medium", style.badgeClass)}>
{stateLabel(tool)}
</span>
{tool.durationMs != null && <span>{durationLabel(tool.durationMs)}</span>}
{tool.outputTruncated && <span>Output truncated</span>}
{tool.fullRef && <span>Full output saved</span>}
Expand All @@ -204,21 +254,22 @@ export function ToolCallCard({ tool }: { tool: ToolCallState }) {
)}
{tool.state === "running" && <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />}
{hasBody && (
<ChevronDown
className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform", isOpen && "rotate-180")}
/>
<span className="inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-background/70">
<span className="hidden sm:inline">{detailLabel}</span>
<ChevronDown className={cn("h-3.5 w-3.5 transition-transform", isOpen && "rotate-180")} />
</span>
)}
</div>
</button>

{isOpen && hasBody && (
<div id={bodyId} className="space-y-3 border-t border-border bg-muted/25 px-3 py-3">
<div id={bodyId} className="space-y-3 border-t border-border/70 bg-muted/20 px-3 py-3">
{inputDetails && (
<div className="space-y-1">
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{inputDetails.label}
</div>
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded bg-background px-3 py-2 font-mono text-xs text-foreground">
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/50 bg-background px-3 py-2 font-mono text-xs text-foreground">
{inputDetails.value}
</pre>
</div>
Expand All @@ -230,15 +281,15 @@ export function ToolCallCard({ tool }: { tool: ToolCallState }) {
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
Full output path
</div>
<div className="rounded bg-background px-3 py-2 font-mono text-xs text-muted-foreground">
<div className="rounded-md border border-border/50 bg-background px-3 py-2 font-mono text-xs text-muted-foreground">
{redactSensitiveText(tool.fullRef)}
</div>
</div>
)}
{output && (
<div className="space-y-1">
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Output</div>
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded bg-background px-3 py-2 font-mono text-xs text-foreground">
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/50 bg-background px-3 py-2 font-mono text-xs text-foreground">
{output}
</pre>
</div>
Expand Down
Loading
Loading