Queue items (edit, send now, delete)
This commit is contained in:
parent
a0ba55feec
commit
fcbec6c4d6
@ -691,7 +691,7 @@ export function ChainOfThought({ parts, isStreaming }: { parts: ChainPart[]; isS
|
||||
className="flex items-start gap-2.5 py-1.5"
|
||||
>
|
||||
<div
|
||||
className="relative z-10 flex-shrink-0 w-5 h-5 mt-0.5 flex items-center justify-center rounded-full"
|
||||
className="relative z-10 flex-shrink-0 w-[18px] h-[18px] flex items-center justify-center rounded-full"
|
||||
style={{
|
||||
background: "var(--color-bg)",
|
||||
}}
|
||||
@ -800,20 +800,12 @@ function FetchGroup({ items }: { items: ToolPart[] }) {
|
||||
return (
|
||||
<div className="flex items-start gap-2.5 py-1.5">
|
||||
<div
|
||||
className="relative z-10 flex-shrink-0 w-5 h-5 mt-0.5 flex items-center justify-center rounded-full"
|
||||
className="relative z-10 flex-shrink-0 w-[18px] h-[18px] flex items-center justify-center rounded-full"
|
||||
style={{ background: "var(--color-bg)" }}
|
||||
>
|
||||
{anyRunning ? (
|
||||
<span
|
||||
className="w-4 h-4 border-[1.5px] rounded-full animate-spin"
|
||||
style={{
|
||||
borderColor: "var(--color-border-strong)",
|
||||
borderTopColor: "var(--color-accent)",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className={anyRunning ? "animate-pulse" : ""}>
|
||||
<StepIcon kind="fetch" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
@ -824,7 +816,7 @@ function FetchGroup({ items }: { items: ToolPart[] }) {
|
||||
: "var(--color-text-secondary)",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className={anyRunning ? "animate-pulse" : ""}>
|
||||
{anyRunning
|
||||
? `Fetching ${items.length} sources...`
|
||||
: `Fetched ${items.length} sources`}
|
||||
@ -984,18 +976,10 @@ function MediaGroup({
|
||||
return (
|
||||
<div className="flex items-start gap-2.5 py-1.5">
|
||||
<div
|
||||
className="relative z-10 flex-shrink-0 w-5 h-5 mt-0.5 flex items-center justify-center rounded-full"
|
||||
className="relative z-10 flex-shrink-0 w-[18px] h-[18px] flex items-center justify-center rounded-full"
|
||||
style={{ background: "var(--color-bg)" }}
|
||||
>
|
||||
{anyRunning ? (
|
||||
<span
|
||||
className="w-4 h-4 border-[1.5px] rounded-full animate-spin"
|
||||
style={{
|
||||
borderColor: "var(--color-border-strong)",
|
||||
borderTopColor: "var(--color-accent)",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className={anyRunning ? "animate-pulse" : ""}>
|
||||
<StepIcon
|
||||
kind={
|
||||
mediaKind === "image"
|
||||
@ -1003,11 +987,11 @@ function MediaGroup({
|
||||
: "read"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-[13px] leading-snug mb-1.5"
|
||||
className={`text-[13px] leading-snug mb-1.5${anyRunning ? " animate-pulse" : ""}`}
|
||||
style={{
|
||||
color: anyRunning
|
||||
? "var(--color-text)"
|
||||
@ -1038,15 +1022,9 @@ function MediaGroup({
|
||||
border: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="w-4 h-4 border-[1.5px] rounded-full animate-spin"
|
||||
style={{
|
||||
borderColor:
|
||||
"var(--color-border-strong)",
|
||||
borderTopColor:
|
||||
"var(--color-accent)",
|
||||
}}
|
||||
/>
|
||||
<span className="animate-pulse" style={{ color: "var(--color-text-muted)" }}>
|
||||
<StepIcon kind="image" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasMore && (
|
||||
@ -1259,27 +1237,21 @@ function ToolStep({
|
||||
return (
|
||||
<div className="flex items-start gap-2.5 py-1.5">
|
||||
<div
|
||||
className="relative z-10 flex-shrink-0 w-5 h-5 mt-0.5 flex items-center justify-center rounded-full"
|
||||
className="relative z-10 flex-shrink-0 w-[18px] h-[18px] flex items-center justify-center rounded-full"
|
||||
style={{ background: "var(--color-bg)" }}
|
||||
>
|
||||
{status === "running" ? (
|
||||
<span
|
||||
className="w-4 h-4 border-[1.5px] rounded-full animate-spin"
|
||||
style={{
|
||||
borderColor: "var(--color-border-strong)",
|
||||
borderTopColor: "var(--color-accent)",
|
||||
}}
|
||||
/>
|
||||
) : status === "error" ? (
|
||||
{status === "error" ? (
|
||||
<ErrorCircleIcon />
|
||||
) : (
|
||||
<StepIcon kind={kind} />
|
||||
<span className={status === "running" ? "animate-pulse" : ""}>
|
||||
<StepIcon kind={kind} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-[13px] leading-snug flex items-start gap-2 flex-wrap"
|
||||
className="text-[13px] leading-snug flex items-center gap-2 flex-wrap"
|
||||
style={{
|
||||
color:
|
||||
status === "running"
|
||||
@ -1287,7 +1259,7 @@ function ToolStep({
|
||||
: "var(--color-text-secondary)",
|
||||
}}
|
||||
>
|
||||
<span className="break-all">{label}</span>
|
||||
<span className={`break-all${status === "running" ? " animate-pulse" : ""}`}>{label}</span>
|
||||
{/* Exit code badge for exec tools */}
|
||||
{kind === "exec" && status === "done" && output?.exitCode !== undefined && (
|
||||
<span
|
||||
|
||||
@ -155,6 +155,131 @@ function FileTypeIcon({ category }: { category: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
function QueueItem({
|
||||
msg,
|
||||
idx,
|
||||
onEdit,
|
||||
onSendNow,
|
||||
onRemove,
|
||||
}: {
|
||||
msg: QueuedMessage;
|
||||
idx: number;
|
||||
onEdit: (id: string, text: string) => void;
|
||||
onSendNow: (id: string) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(msg.text);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const autoResize = () => {
|
||||
const el = inputRef.current;
|
||||
if (!el) {return;}
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
inputRef.current?.focus();
|
||||
const len = inputRef.current?.value.length ?? 0;
|
||||
inputRef.current?.setSelectionRange(len, len);
|
||||
autoResize();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
const commitEdit = () => {
|
||||
const trimmed = draft.trim();
|
||||
if (trimmed && trimmed !== msg.text) {
|
||||
onEdit(msg.id, trimmed);
|
||||
} else {
|
||||
setDraft(msg.text);
|
||||
}
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-start gap-2.5 group py-2 ${idx > 0 ? "border-t" : ""}`}
|
||||
style={idx > 0 ? { borderColor: "var(--color-border)" } : undefined}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 mt-px text-[11px] font-medium tabular-nums w-4"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{idx + 1}
|
||||
</span>
|
||||
{editing ? (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className="flex-1 text-[13px] leading-[1.45] min-w-0 resize-none rounded-md px-2 py-1 outline-none"
|
||||
style={{
|
||||
color: "var(--color-text-secondary)",
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-border)",
|
||||
}}
|
||||
rows={1}
|
||||
value={draft}
|
||||
onChange={(e) => { setDraft(e.target.value); autoResize(); }}
|
||||
onBlur={commitEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); commitEdit(); }
|
||||
if (e.key === "Escape") { setDraft(msg.text); setEditing(false); }
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className="flex-1 text-[13px] leading-[1.45] line-clamp-2 min-w-0"
|
||||
style={{ color: "var(--color-text-secondary)", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{msg.text || (msg.attachedFiles.length > 0 ? `${msg.attachedFiles.length} file(s)` : "")}
|
||||
</p>
|
||||
)}
|
||||
{!editing && (
|
||||
<div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{/* Edit */}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-1 transition-colors hover:bg-stone-100 dark:hover:bg-stone-800"
|
||||
title="Edit message"
|
||||
onClick={() => { setDraft(msg.text); setEditing(true); }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-stone-400">
|
||||
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Send now */}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-1 transition-colors hover:bg-stone-100 dark:hover:bg-stone-800"
|
||||
title="Send now"
|
||||
onClick={() => onSendNow(msg.id)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-stone-400">
|
||||
<path d="M12 19V5" />
|
||||
<path d="m5 12 7-7 7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Delete */}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-1 transition-colors hover:bg-stone-100 dark:hover:bg-stone-800"
|
||||
title="Remove from queue"
|
||||
onClick={() => onRemove(msg.id)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-stone-400">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentStrip({
|
||||
files,
|
||||
compact,
|
||||
@ -1272,6 +1397,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
setQueuedMessages((prev) => prev.filter((m) => m.id !== id));
|
||||
}, []);
|
||||
|
||||
const updateQueuedMessageText = useCallback((id: string, text: string) => {
|
||||
setQueuedMessages((prev) => prev.map((m) => m.id === id ? { ...m, text } : m));
|
||||
}, []);
|
||||
|
||||
/** Force-send: stop the agent, then immediately submit this queued message. */
|
||||
const forceSendQueuedMessage = useCallback(
|
||||
async (id: string) => {
|
||||
@ -1361,13 +1490,12 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="h-full overflow-y-auto [scrollbar-gutter:stable]"
|
||||
className="h-full flex flex-col"
|
||||
style={{ background: "var(--color-main-bg)" }}
|
||||
>
|
||||
<div className="flex flex-col min-h-full">
|
||||
{/* Header — sticky glass bar */}
|
||||
<header
|
||||
className={`${compact ? "px-3 py-2" : "px-3 py-2 md:px-6 md:py-3"} flex items-center justify-between sticky top-0 z-20 backdrop-blur-md`}
|
||||
className={`${compact ? "px-3 py-2" : "px-3 py-2 md:px-6 md:py-3"} flex items-center justify-between z-20`}
|
||||
style={{
|
||||
background: "var(--color-bg-glass)",
|
||||
}}
|
||||
@ -1424,6 +1552,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
variant="destructive"
|
||||
onSelect={() => onDeleteSession(currentSessionId)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@ -1460,7 +1589,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
{/* File-scoped session tabs (compact mode) */}
|
||||
{compact && fileContext && fileSessions.length > 0 && (
|
||||
<div
|
||||
className="px-2 py-1.5 border-b flex gap-1 overflow-x-auto sticky top-[41px] z-20 backdrop-blur-md"
|
||||
className="px-2 py-1.5 border-b flex gap-1 overflow-x-auto z-20"
|
||||
style={{
|
||||
borderColor: "var(--color-border)",
|
||||
background: "var(--color-bg-glass)",
|
||||
@ -1497,9 +1626,13 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto min-h-0"
|
||||
>
|
||||
{/* Messages */}
|
||||
<div
|
||||
className={`flex-1 ${compact ? "px-3" : "px-6"}`}
|
||||
className={`${compact ? "px-3" : "px-6"}`}
|
||||
>
|
||||
{loadingSession ? (
|
||||
<div className="flex items-center justify-center h-full min-h-[60vh]">
|
||||
@ -1620,10 +1753,11 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
<p className="text-xs">{error.message}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input — sticky glass bar at bottom */}
|
||||
{/* Input bar at bottom */}
|
||||
<div
|
||||
className={`${compact ? "px-3 py-2" : "px-3 pb-3 pt-0 md:px-6 md:pb-5"} sticky bottom-0 z-20 backdrop-blur-md`}
|
||||
className={`${compact ? "px-3 py-2" : "px-3 pb-3 pt-0 md:px-6 md:pb-5"} z-20`}
|
||||
style={{ background: "var(--color-bg-glass)" }}
|
||||
>
|
||||
<div
|
||||
@ -1700,47 +1834,14 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
</div>
|
||||
<div className="flex flex-col p-2">
|
||||
{queuedMessages.map((msg, idx) => (
|
||||
<div
|
||||
<QueueItem
|
||||
key={msg.id}
|
||||
className={`flex items-start gap-2.5 group py-2 ${idx > 0 ? "border-t" : ""}`}
|
||||
style={idx > 0 ? { borderColor: "var(--color-border)" } : undefined}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 mt-px text-[11px] font-medium tabular-nums w-4"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<p
|
||||
className="flex-1 text-[13px] leading-[1.45] line-clamp-2 min-w-0"
|
||||
style={{ color: "var(--color-text-secondary)", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{msg.text || (msg.attachedFiles.length > 0 ? `${msg.attachedFiles.length} file(s)` : "")}
|
||||
</p>
|
||||
<div className="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md px-1.5 py-0.5 text-[11px] font-medium transition-colors"
|
||||
style={{ color: "var(--color-accent)", background: "var(--color-accent-light)" }}
|
||||
title="Stop agent and send this message now"
|
||||
onClick={() => forceSendQueuedMessage(msg.id)}
|
||||
>
|
||||
Send now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-0.5 transition-colors hover:opacity-80"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
title="Remove from queue"
|
||||
onClick={() => removeQueuedMessage(msg.id)}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
msg={msg}
|
||||
idx={idx}
|
||||
onEdit={updateQueuedMessageText}
|
||||
onSendNow={forceSendQueuedMessage}
|
||||
onRemove={removeQueuedMessage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -1814,9 +1915,9 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* Send / Stop button (single button, toggles role) */}
|
||||
{/* Send / Stop / Queue buttons */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isStreaming ? (
|
||||
{isStreaming && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStop()}
|
||||
@ -1836,6 +1937,37 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
<rect width="10" height="10" rx="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editorRef.current?.submit();
|
||||
}}
|
||||
disabled={
|
||||
(editorEmpty &&
|
||||
attachedFiles.length === 0) ||
|
||||
loadingSession
|
||||
}
|
||||
className="h-7 px-3 rounded-full flex items-center gap-1.5 text-[12px] font-medium disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background:
|
||||
!editorEmpty || attachedFiles.length > 0
|
||||
? "var(--color-accent)"
|
||||
: "var(--color-surface-hover)",
|
||||
color:
|
||||
!editorEmpty || attachedFiles.length > 0
|
||||
? "white"
|
||||
: "var(--color-text-muted)",
|
||||
}}
|
||||
title="Add to queue"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 10 4 15 9 20" />
|
||||
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
|
||||
</svg>
|
||||
Queue
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
@ -1881,7 +2013,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File picker modal */}
|
||||
<FilePickerModal
|
||||
|
||||
@ -22,10 +22,15 @@ function DropdownMenuPortal({
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
||||
<MenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
className={cn("cursor-pointer outline-none ring-0 border-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,9 +58,8 @@ function DropdownMenuContent({
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"min-w-32 rounded-lg p-1 shadow-md duration-100 overflow-x-hidden overflow-y-auto outline-none",
|
||||
"bg-[var(--color-surface)] text-[var(--color-text)] border border-[var(--color-border)]",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"bg-neutral-100/[0.67] border border-white backdrop-blur-md text-[var(--color-text)] z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-3xl p-1 shadow-[0_0_25px_0_rgba(0,0,0,0.16)] outline-none",
|
||||
"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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -85,8 +89,8 @@ function DropdownMenuLabel({
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-1.5 py-1 text-xs font-medium text-[var(--color-text-muted)]",
|
||||
inset && "pl-7",
|
||||
"px-2 py-1.5 text-sm font-medium",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -116,11 +120,10 @@ function DropdownMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"gap-1.5 rounded-md px-1.5 py-1 text-sm relative flex cursor-default items-center outline-none select-none",
|
||||
"focus:bg-[var(--color-surface-hover)] focus:text-[var(--color-text)]",
|
||||
"data-[variant=destructive]:text-[var(--color-error)] data-[variant=destructive]:focus:bg-[var(--color-error)]/10 data-[variant=destructive]:focus:text-[var(--color-error)]",
|
||||
inset && "pl-7",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"bg-transparent hover:bg-neutral-400/15 text-sm transition-all relative flex cursor-pointer items-center gap-2 rounded-full px-2 py-1.5 outline-none ring-0 border-none select-none",
|
||||
"data-[variant=destructive]:text-[var(--color-error)] data-[variant=destructive]:hover:bg-[var(--color-error)]/10 data-[variant=destructive]:hover:text-[var(--color-error)]",
|
||||
inset && "pl-8",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
@ -151,10 +154,9 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-[var(--color-surface-hover)] focus:text-[var(--color-text)] data-open:bg-[var(--color-surface-hover)] data-open:text-[var(--color-text)]",
|
||||
"gap-1.5 rounded-md px-1.5 py-1 text-sm flex cursor-default items-center outline-none select-none",
|
||||
inset && "pl-7",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"bg-transparent hover:bg-neutral-400/15 focus:bg-neutral-400/15 data-open:bg-neutral-400/15 flex cursor-pointer items-center gap-2 rounded-full px-2 py-1.5 text-sm outline-none select-none transition-all focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:outline-none",
|
||||
inset && "pl-8",
|
||||
"[&_svg:not([class*='text-'])]:text-[var(--color-text-muted)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -200,20 +202,16 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-[var(--color-surface-hover)] focus:text-[var(--color-text)]",
|
||||
"gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm relative flex cursor-default items-center outline-none select-none",
|
||||
inset && "pl-7",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"hover:bg-neutral-400/15 focus:bg-neutral-400/15 relative flex cursor-pointer items-center gap-2 rounded-full py-1.5 pr-2 pl-8 text-sm outline-none select-none transition-colors",
|
||||
inset && "pl-8",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
@ -247,19 +245,15 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-[var(--color-surface-hover)] focus:text-[var(--color-text)]",
|
||||
"gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm relative flex cursor-default items-center outline-none select-none",
|
||||
inset && "pl-7",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"hover:bg-neutral-400/15 focus:bg-neutral-400/15 relative flex cursor-pointer items-center gap-2 rounded-full py-1.5 pr-2 pl-8 text-sm outline-none select-none transition-colors",
|
||||
inset && "pl-8",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
@ -277,7 +271,7 @@ function DropdownMenuSeparator({
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn(
|
||||
"bg-[var(--color-border)] -mx-1 my-1 h-px",
|
||||
"bg-neutral-400/15 -mx-1 my-1 h-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -347,6 +347,7 @@ export function ChatSessionsSidebar({
|
||||
variant="destructive"
|
||||
onSelect={() => handleDeleteSession(session.id)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /></svg>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
/* Background / Surface */
|
||||
--color-bg: #f5f5f4;
|
||||
--color-surface: #ffffff;
|
||||
--color-sidebar-bg: #fafaf9;
|
||||
--color-main-bg: rgba(250, 250, 249, 0.5);
|
||||
--color-surface-hover: #f5f4f1;
|
||||
--color-surface-raised: #ffffff;
|
||||
|
||||
@ -43,7 +45,7 @@
|
||||
/* Glassmorphism */
|
||||
--color-glass: rgba(255, 255, 255, 0.72);
|
||||
--color-glass-border: rgba(255, 255, 255, 0.85);
|
||||
--color-bg-glass: rgba(245, 245, 244, 0.8);
|
||||
--color-bg-glass: rgba(250, 250, 249, 0.8);
|
||||
|
||||
/* Object type chips */
|
||||
--color-chip-object: rgba(37, 99, 235, 0.08);
|
||||
@ -74,6 +76,8 @@
|
||||
/* Background / Surface */
|
||||
--color-bg: #0c0c0b;
|
||||
--color-surface: #161615;
|
||||
--color-sidebar-bg: #141413;
|
||||
--color-main-bg: #161615;
|
||||
--color-surface-hover: #1e1e1c;
|
||||
--color-surface-raised: #1a1a18;
|
||||
|
||||
@ -240,6 +244,21 @@ a,
|
||||
Scrollbar
|
||||
============================================================ */
|
||||
|
||||
/* Base UI menu — remove default focus outlines */
|
||||
[data-slot="dropdown-menu-content"],
|
||||
[data-slot="dropdown-menu-item"],
|
||||
[data-slot="dropdown-menu-sub-content"],
|
||||
[data-slot="dropdown-menu-checkbox-item"],
|
||||
[data-slot="dropdown-menu-radio-item"],
|
||||
[data-slot="dropdown-menu-sub-trigger"] {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
[data-slot="dropdown-menu-content"] {
|
||||
border: 1px solid white !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
@ -223,8 +223,8 @@ function ResizeHandle({
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
onMouseDown={onMouseDown}
|
||||
className={`shrink-0 w-1 cursor-col-resize flex justify-center transition-colors ${showHover ? "bg-blue-600/30" : "hover:bg-blue-600/30"}`}
|
||||
style={{ minWidth: 4 }}
|
||||
className={`cursor-col-resize flex justify-center transition-colors ${showHover ? "bg-blue-600/30" : "hover:bg-blue-600/30"}`}
|
||||
style={{ position: "absolute", [mode === "left" ? "right" : "left"]: -2, top: 0, bottom: 0, width: 4, zIndex: 20 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1253,7 +1253,7 @@ function WorkspacePageInner() {
|
||||
<div
|
||||
ref={layoutRef}
|
||||
className="flex h-screen"
|
||||
style={{ background: "var(--color-bg)" }}
|
||||
style={{ background: "var(--color-main-bg)" }}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{/* Left sidebar — static on desktop (resizable), drawer overlay on mobile */}
|
||||
@ -1283,9 +1283,16 @@ function WorkspacePageInner() {
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="flex shrink-0 flex-col"
|
||||
className="flex shrink-0 flex-col relative"
|
||||
style={{ width: leftSidebarWidth, minWidth: leftSidebarWidth }}
|
||||
>
|
||||
<ResizeHandle
|
||||
mode="left"
|
||||
containerRef={layoutRef}
|
||||
min={LEFT_SIDEBAR_MIN}
|
||||
max={LEFT_SIDEBAR_MAX}
|
||||
onResize={setLeftSidebarWidth}
|
||||
/>
|
||||
<WorkspaceSidebar
|
||||
tree={enhancedTree}
|
||||
activePath={activePath}
|
||||
@ -1306,18 +1313,11 @@ function WorkspacePageInner() {
|
||||
width={leftSidebarWidth}
|
||||
/>
|
||||
</div>
|
||||
<ResizeHandle
|
||||
mode="left"
|
||||
containerRef={layoutRef}
|
||||
min={LEFT_SIDEBAR_MIN}
|
||||
max={LEFT_SIDEBAR_MAX}
|
||||
onResize={setLeftSidebarWidth}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden" style={{ background: "var(--color-main-bg)" }}>
|
||||
{/* Mobile top bar — always visible on mobile */}
|
||||
{isMobile && (
|
||||
<div
|
||||
@ -1426,7 +1426,7 @@ function WorkspacePageInner() {
|
||||
{showMainChat ? (
|
||||
/* Main chat view (default when no file is selected) */
|
||||
<>
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex-1 flex flex-col min-w-0" style={{ background: "var(--color-main-bg)" }}>
|
||||
{activeSubagent ? (
|
||||
<SubagentPanel
|
||||
sessionKey={activeSubagent.childSessionKey}
|
||||
@ -1483,17 +1483,17 @@ function WorkspacePageInner() {
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
<div
|
||||
className="flex shrink-0 flex-col"
|
||||
style={{ width: rightSidebarWidth, minWidth: rightSidebarWidth }}
|
||||
className="flex shrink-0 flex-col relative"
|
||||
style={{ width: rightSidebarWidth, minWidth: rightSidebarWidth, background: "var(--color-sidebar-bg)" }}
|
||||
>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
{chatSidebarPreview ? (
|
||||
<ChatSidebarPreview
|
||||
preview={chatSidebarPreview}
|
||||
@ -1556,21 +1556,21 @@ function WorkspacePageInner() {
|
||||
{/* Chat sidebar (file/folder-scoped) — hidden for reserved paths, hidden on mobile */}
|
||||
{!isMobile && fileContext && showChatSidebar && (
|
||||
<>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
<aside
|
||||
className="flex-shrink-0 border-l flex flex-col"
|
||||
className="flex-shrink-0 border-l flex flex-col relative"
|
||||
style={{
|
||||
width: rightSidebarWidth,
|
||||
borderColor: "var(--color-border)",
|
||||
background: "var(--color-bg)",
|
||||
}}
|
||||
>
|
||||
<ResizeHandle
|
||||
mode="right"
|
||||
containerRef={layoutRef}
|
||||
min={RIGHT_SIDEBAR_MIN}
|
||||
max={RIGHT_SIDEBAR_MAX}
|
||||
onResize={setRightSidebarWidth}
|
||||
/>
|
||||
<ChatPanel
|
||||
ref={compactChatRef}
|
||||
compact
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user