feat: add workspace directory handling and integrate into chat interface

This commit is contained in:
Xi Zhang
2026-06-27 20:44:40 +01:00
parent f762831d02
commit df64ba5605
3 changed files with 69 additions and 47 deletions
+6 -1
View File
@@ -161,7 +161,12 @@ export async function GET(request: NextRequest) {
? relPath.replace(/\/+$/, "").split("/").slice(0, -1).join("/")
: null;
return NextResponse.json({ path: relPath, parent, entries });
return NextResponse.json({
path: relPath,
parent,
entries,
dir: workspaceDir,
});
} catch (error) {
return NextResponse.json(
{
+49 -45
View File
@@ -80,7 +80,6 @@ import {
import { lastTextOf, type SubAgentStep } from "@/lib/subAgentActivity";
import { useStickToBottom } from "use-stick-to-bottom";
import { FilesPopover } from "@/app/components/TasksFilesSidebar";
import { WorkspacePanel } from "@/app/components/WorkspacePanel";
import {
Dialog,
DialogContent,
@@ -119,6 +118,8 @@ interface ChatInterfaceProps {
onNavigate?: (target: DashboardNavTarget) => void;
// Open a pinned thread from the empty-state dashboard.
onOpenThread?: (id: string) => void;
// Whether the workspace inspector is currently visible.
workspaceOpen?: boolean;
// Register a "submit a message on THIS (main) thread" function up to page so
// the Agents board can loop an async result back to the main agent. Returns
// false if the main chat is mid-run (can't take a turn). Cleared on unmount.
@@ -256,10 +257,15 @@ const getStatusIcon = (status: TodoItem["status"], className?: string) => {
};
export const ChatInterface = React.memo<ChatInterfaceProps>(
({ assistant, onShowAgents, onNotifyReady, onNavigate, onOpenThread }) => {
const [metaOpen, setMetaOpen] = useState<
"tasks" | "files" | "workspace" | null
>(null);
({
assistant,
onShowAgents,
onNotifyReady,
onNavigate,
onOpenThread,
workspaceOpen,
}) => {
const [metaOpen, setMetaOpen] = useState<"tasks" | "files" | null>(null);
const tasksContainerRef = useRef<HTMLDivElement | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null);
@@ -269,6 +275,20 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
[]
);
const [isUploadingFiles, setIsUploadingFiles] = useState(false);
const [workspaceDir, setWorkspaceDir] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetch("/api/workspace")
.then((r) => (r.ok ? r.json() : null))
.then((d: { dir?: string } | null) => {
if (!cancelled && d?.dir) setWorkspaceDir(d.dir);
})
.catch(() => {});
return () => {
cancelled = true;
};
}, []);
// Messages typed while the agent is busy (Claude Code-style queue). They
// drain one-per-idle-window into the thread once it's free. A ref mirrors the
// latest queue so event handlers (key ↑, edit) read current state without
@@ -1791,8 +1811,6 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
"focus-within:ring-2 focus-within:ring-ring"
)}
>
{/* Always rendered: the Workspace tab is available even with no tasks
or state files yet. */}
{
<div className="flex max-h-60 flex-col overflow-y-auto border-b border-border bg-sidebar empty:hidden sm:max-h-72">
{!metaOpen && (
@@ -1901,31 +1919,12 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
);
})();
const workspaceTrigger = (
<button
type="button"
onClick={() =>
setMetaOpen((prev) =>
prev === "workspace" ? null : "workspace"
)
}
className="flex flex-shrink-0 cursor-pointer items-center gap-2 px-3 py-2.5 text-left text-sm sm:px-4"
aria-expanded={metaOpen === "workspace"}
aria-label="Open workspace"
>
<FolderOpen
size={16}
aria-hidden="true"
/>
<span>Workspace</span>
</button>
);
if (!hasTasks && !hasFiles) return null;
return (
<div className="flex items-center">
<div className="min-w-0 flex-1">{tasksTrigger}</div>
{filesTrigger}
{workspaceTrigger}
</div>
);
})()}
@@ -1966,18 +1965,6 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
</span>
</button>
)}
<button
type="button"
className="inline-flex items-center gap-2 py-2.5 pr-4 first:pl-3 aria-expanded:font-semibold sm:first:pl-4"
onClick={() =>
setMetaOpen((prev) =>
prev === "workspace" ? null : "workspace"
)
}
aria-expanded={metaOpen === "workspace"}
>
Workspace
</button>
<button
aria-label="Close"
className="flex-1"
@@ -2031,12 +2018,6 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
/>
</div>
)}
{metaOpen === "workspace" && (
<div className="mb-6 pt-2">
<WorkspacePanel />
</div>
)}
</div>
</>
)}
@@ -2214,6 +2195,29 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
{autoApprove ? "Auto-approve On" : "Auto-approve"}
</span>
</button>
{onNavigate && workspaceDir && (
<button
type="button"
onClick={() => onNavigate({ view: "workspace" })}
title={`${
workspaceOpen ? "Close" : "Open"
} workspace: ${workspaceDir}`}
aria-label={`${
workspaceOpen ? "Close" : "Open"
} workspace: ${workspaceDir}`}
aria-pressed={Boolean(workspaceOpen)}
className="inline-flex min-w-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
<FolderOpen
className="size-3.5 flex-shrink-0"
aria-hidden="true"
/>
<span className="hidden max-w-[140px] truncate font-mono sm:inline lg:max-w-[220px]">
{workspaceDir.split("/").filter(Boolean).pop() ||
workspaceDir}
</span>
</button>
)}
</div>
<div className="flex justify-end gap-2">
<Button
+14 -1
View File
@@ -55,7 +55,7 @@ function HomePageInner({
const [memoryObs, setMemoryObs] = useQueryState("memoryObs");
const [memoryExec, setMemoryExec] = useQueryState("memoryExec");
const [inspector, setInspector] = useQueryState("inspector");
const [, setInspectorTab] = useQueryState("inspectorTab");
const [inspectorTab, setInspectorTab] = useQueryState("inspectorTab");
const [mutateThreads, setMutateThreads] = useState<(() => void) | null>(null);
const [interruptCount, setInterruptCount] = useState(0);
@@ -212,16 +212,26 @@ function HomePageInner({
} else if (target.view === "schedule") {
setView("schedule");
} else {
if (inspector && inspectorTab !== "agents") {
closeInspector();
return;
}
if (isDesktopLayout === false) setSidebar(null);
setInspectorTab(null);
setInspector("1");
}
},
[
closeInspector,
inspector,
inspectorTab,
isDesktopLayout,
setInspector,
setInspectorTab,
setMemoryExec,
setMemoryObs,
setMemoryTab,
setSidebar,
setView,
]
);
@@ -454,6 +464,9 @@ function HomePageInner({
onNotifyReady={(fn) => setNotifyMainChat(() => fn)}
onNavigate={handleDashboardNav}
onOpenThread={selectThread}
workspaceOpen={Boolean(
inspector && inspectorTab !== "agents"
)}
/>
</ChatProvider>
</div>