From 68f09660e9e8dd1d5bbfffd773874bda3aa07c07 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 19 Feb 2026 15:23:27 -0800 Subject: [PATCH 01/10] Fix: thinking UI just messed up with scroll and make the whole chat panel moved. --- apps/web/app/components/chat-panel.tsx | 128 +++++++++---------------- 1 file changed, 47 insertions(+), 81 deletions(-) diff --git a/apps/web/app/components/chat-panel.tsx b/apps/web/app/components/chat-panel.tsx index 9d52779cfbd..847367d53c7 100644 --- a/apps/web/app/components/chat-panel.tsx +++ b/apps/web/app/components/chat-panel.tsx @@ -1353,7 +1353,7 @@ export const ChatPanel = forwardRef( return (
{/* Header — sticky glass bar */} @@ -1603,12 +1603,11 @@ export const ChatPanel = forwardRef( >
{ if ( @@ -1786,56 +1785,50 @@ export const ChatPanel = forwardRef(
- {/* Send / Stop / Queue buttons */} + {/* Send / Stop button (single button, toggles role) */}
- {isStreaming && ( - + ) : ( + - )} - + + )}
@@ -1884,16 +1860,6 @@ export const ChatPanel = forwardRef( onSelect={handleFilesSelected} /> - {/* Drop highlight for sidebar drag-and-drop */} - ); }, From 027593b3502617dc62bdaa32346225c1d27ae26b Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 19 Feb 2026 15:42:23 -0800 Subject: [PATCH 02/10] Resizable sidebar --- .../workspace/chat-sessions-sidebar.tsx | 6 +- .../components/workspace/profile-switcher.tsx | 77 ++++-- .../workspace/workspace-sidebar.tsx | 95 ++++--- apps/web/app/workspace/page.tsx | 234 ++++++++++++++---- 4 files changed, 290 insertions(+), 122 deletions(-) diff --git a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx index 694785e5a9c..0aa8d09f94d 100644 --- a/apps/web/app/components/workspace/chat-sessions-sidebar.tsx +++ b/apps/web/app/components/workspace/chat-sessions-sidebar.tsx @@ -38,6 +38,8 @@ type ChatSessionsSidebarProps = { mobile?: boolean; /** Close the mobile drawer. */ onClose?: () => void; + /** Fixed width in px when not mobile (overrides default 260). */ + width?: number; }; /** Format a timestamp into a human-readable relative time string. */ @@ -123,6 +125,7 @@ export function ChatSessionsSidebar({ onSelectSubagent, mobile, onClose, + width: widthProp, }: ChatSessionsSidebarProps) { const [hoveredId, setHoveredId] = useState(null); @@ -160,11 +163,12 @@ export function ChatSessionsSidebar({ // Group sessions: today, yesterday, this week, this month, older const grouped = groupSessions(sessions); + const width = mobile ? "280px" : (widthProp ?? 260); const sidebar = ( ); diff --git a/apps/web/app/components/workspace/workspace-sidebar.tsx b/apps/web/app/components/workspace/workspace-sidebar.tsx index 0e009608bf3..d68bddec5a4 100644 --- a/apps/web/app/components/workspace/workspace-sidebar.tsx +++ b/apps/web/app/components/workspace/workspace-sidebar.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback } from "react"; import { FileManagerTree, type TreeNode } from "./file-manager-tree"; import { ProfileSwitcher } from "./profile-switcher"; import { CreateWorkspaceDialog } from "./create-workspace-dialog"; +import { UnicodeSpinner } from "../unicode-spinner"; /** Shape returned by /api/workspace/suggest-files */ type SuggestItem = { @@ -319,9 +320,10 @@ function FileSearch({ onSelect }: { onSelect: (item: SuggestItem) => void }) { /> {loading && ( -
)} @@ -411,9 +413,10 @@ export function WorkspaceSidebar({ const sidebar = ( @@ -1382,6 +1605,309 @@ function WorkspacePageInner() { ); } +function previewFileTypeBadge(filename: string): { label: string; color: string } { + const ext = filename.split(".").pop()?.toLowerCase() ?? ""; + if (ext === "pdf") {return { label: "PDF", color: "#ef4444" };} + if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "heic", "avif"].includes(ext)) {return { label: "Image", color: "#3b82f6" };} + if (["mp4", "webm", "mov", "avi", "mkv"].includes(ext)) {return { label: "Video", color: "#8b5cf6" };} + if (["mp3", "wav", "ogg", "m4a", "aac", "flac"].includes(ext)) {return { label: "Audio", color: "#f59e0b" };} + if (["md", "mdx"].includes(ext)) {return { label: "Markdown", color: "#10b981" };} + if (["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "rb", "swift", "kt", "c", "cpp", "h"].includes(ext)) {return { label: ext.toUpperCase(), color: "#3b82f6" };} + if (["json", "yaml", "yml", "toml", "xml", "csv"].includes(ext)) {return { label: ext.toUpperCase(), color: "#6b7280" };} + if (["duckdb", "sqlite", "sqlite3", "db"].includes(ext)) {return { label: "Database", color: "#6366f1" };} + return { label: ext.toUpperCase() || "File", color: "#6b7280" }; +} + +function shortenPreviewPath(p: string): string { + return p.replace(/^\/Users\/[^/]+/, "~").replace(/^\/home\/[^/]+/, "~"); +} + +function ChatSidebarPreview({ + preview, + onClose, +}: { + preview: ChatSidebarPreviewState; + onClose: () => void; +}) { + const badge = previewFileTypeBadge(preview.filename); + + const openInFinder = useCallback(async () => { + try { + await fetch("/api/workspace/open-file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: preview.path, reveal: true }), + }); + } catch { /* ignore */ } + }, [preview.path]); + + const openWithSystem = useCallback(async () => { + try { + await fetch("/api/workspace/open-file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: preview.path }), + }); + } catch { /* ignore */ } + }, [preview.path]); + + const downloadUrl = preview.status === "ready" && preview.content.kind === "media" + ? preview.content.url + : null; + + let body: React.ReactNode; + + if (preview.status === "loading") { + body = ( +
+ +

+ Loading preview... +

+
+ ); + } else if (preview.status === "error") { + body = ( +
+
+ + + + + +
+
+

+ Preview unavailable +

+

+ {preview.message} +

+
+
+ ); + } else { + const c = preview.content; + switch (c.kind) { + case "media": + if (c.mediaType === "pdf") { + // Hide the browser's built-in PDF toolbar for a cleaner look + const pdfUrl = c.url + (c.url.includes("#") ? "&" : "#") + "toolbar=0&navpanes=0&scrollbar=1"; + body = ( +