Skip to content
Merged
4 changes: 3 additions & 1 deletion studio/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ tasks:
generates:
- node_modules/.package-lock.json
cmds:
- npm install
# Pinned via npx: a system npm 11 rewrites the lockfile in a shape
# npm 10 (what CI's Node 22 ships) rejects, breaking npm ci.
- npx -y npm@10.9.4 install

dev:
desc: Start Studio + its mecated supervisor in the background (runs install if needed)
Expand Down
1 change: 1 addition & 0 deletions studio/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const config: KnipConfig = {
"@radix-ui/react-checkbox",
"@radix-ui/react-popover",
"@radix-ui/react-progress",
"@radix-ui/react-separator",
"@radix-ui/react-toggle",
"@radix-ui/react-toggle-group",
],
Expand Down
2 changes: 1 addition & 1 deletion studio/src/app/workspace/_components/resize-handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function ResizeHandle({
width,
onWidthChange,
min = 200,
max = 500,
max = 720,
}: ResizeHandleProps) {
const isDraggingRef = useRef(false);

Expand Down
6 changes: 4 additions & 2 deletions studio/src/app/workspace/chat/_components/approval-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ export function ApprovalPanel({
<p className="mb-2 text-sm">{approval.description}</p>
{actions.length > 0 && (
<div className="mb-2 flex flex-wrap gap-1.5">
{actions.map((a) => (
{/* Parsed actions can repeat (or parse without a verb), so keys
carry the position to stay unique. */}
{actions.map((a, index) => (
<Badge
key={`${a.connector}-${a.verb}`}
key={`${index}:${a.connector}-${a.verb}`}
variant="secondary"
className={cn(
"gap-1 border-transparent font-mono text-xs",
Expand Down
96 changes: 59 additions & 37 deletions studio/src/app/workspace/chat/_components/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
Loader2,
MessageCircle,
MessageSquareText,
PanelLeftClose,
PanelLeftOpen,
PanelRightClose,
PanelRightOpen,
Pencil,
RotateCcw,
Expand Down Expand Up @@ -41,6 +43,7 @@ import type {
ClarificationRequest,
} from "@/features/agent";
import { useIsMobile } from "@/hooks/use-mobile";
import type { SessionListSide } from "@/lib/profile-preferences";
import { cn } from "@/lib/utils";
import { ChatInput } from "../../_components/chat-input";
import { ApprovalPanel } from "./approval-panel";
Expand Down Expand Up @@ -211,7 +214,6 @@ function ThreadPanel({
maximized={maximized}
onToggleMaximize={onToggleMaximize}
onClose={onClose}
initialWidth={440}
minWidth={340}
>
{/* Body: root message, replies, composer */}
Expand Down Expand Up @@ -356,6 +358,7 @@ export function ChatView({
error,
onRetry,
sidebarOpen,
sidebarSide,
onToggleSidebar,
pendingApproval,
onRespondApproval,
Expand All @@ -379,6 +382,7 @@ export function ChatView({
error?: string | null;
onRetry?: () => void;
sidebarOpen: boolean;
sidebarSide: SessionListSide;
onToggleSidebar: () => void;
pendingApproval: ApprovalRequest | null;
onRespondApproval: (choice: ApprovalChoice) => void;
Expand Down Expand Up @@ -431,6 +435,37 @@ export function ChatView({
onSidePanelOpenChange?.(sidePanelOpen);
}, [sidePanelOpen, onSidePanelOpenChange]);

// The sidebar toggle renders on the header edge nearest the panel it
// controls: leading when the session list docks left, trailing when right.
const sidebarToggle = !isMobile && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 shrink-0 text-muted-foreground"
onClick={onToggleSidebar}
aria-label={sidebarOpen ? "Hide sidebar" : "Show sidebar"}
>
{sidebarOpen ? (
sidebarSide === "left" ? (
<PanelLeftClose className="size-4" />
) : (
<PanelRightClose className="size-4" />
)
) : sidebarSide === "left" ? (
<PanelLeftOpen className="size-4" />
) : (
<PanelRightOpen className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{sidebarOpen ? "Hide sidebar" : "Show sidebar"}
</TooltipContent>
</Tooltip>
);

return (
<div className="flex h-full">
<div
Expand All @@ -439,39 +474,25 @@ export function ChatView({
panelMaximized && "hidden",
)}
>
<div className="flex h-[60px] lg:h-[65px] items-center gap-2 lg:gap-3 border-b border-border px-3 lg:px-6">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:size-9 shrink-0 text-muted-foreground"
onClick={onToggleSidebar}
aria-label={
isMobile
? "Back to chats"
: sidebarOpen
? "Hide sidebar"
: "Show sidebar"
}
>
{isMobile ? (
<ArrowLeft className="size-4" />
) : sidebarOpen ? (
<PanelRightOpen className="size-4" />
) : (
<PanelLeftOpen className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{isMobile
? "Back to chats"
: sidebarOpen
? "Hide sidebar"
: "Show sidebar"}
</TooltipContent>
</Tooltip>
<div className="flex h-16 items-center gap-2 lg:gap-3 border-b border-border px-3 lg:px-6">
{isMobile && (
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0 text-muted-foreground"
onClick={onToggleSidebar}
aria-label="Back to chats"
>
<ArrowLeft className="size-4" />
</Button>
)}
{sidebarSide === "left" && sidebarToggle}
{isStreaming && (
<Loader2
aria-label="Generating a response"
className="size-4 shrink-0 animate-spin text-brand"
/>
)}
<h2
className="min-w-0 flex-1 truncate text-sm font-semibold select-none"
onDoubleClick={onRename}
Expand All @@ -485,13 +506,14 @@ export function ChatView({
<ContextWindowIndicator usage={usage} />
</div>
)}
{sidebarSide === "right" && sidebarToggle}
{!isMobile && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 shrink-0 text-muted-foreground ml-2 lg:ml-4"
className="size-8 shrink-0 text-muted-foreground"
>
<Ellipsis className="size-4" />
</Button>
Expand Down Expand Up @@ -539,7 +561,7 @@ export function ChatView({
onAddToChat={(text) => setAppendText(text)}
onAskInSideChat={(text) => setAppendText(text)}
/>
<div className="flex-1 min-w-0 flex flex-col gap-0 max-w-3xl">
<div className="flex-1 min-w-0 flex flex-col gap-0 w-full max-w-[768px]">
{messages.map((msg) => (
<MessageBubble
key={msg.id}
Expand Down Expand Up @@ -579,7 +601,7 @@ export function ChatView({
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 px-3 lg:px-6 pb-4 lg:pb-6">
<div className="max-w-3xl space-y-1.5">
<div className="max-w-[768px] space-y-1.5">
{error && (
<div className="flex items-center gap-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2">
<AlertCircle className="size-4 shrink-0 text-destructive" />
Expand Down
Loading
Loading