👌 IMPROVE: outside folders chat

This commit is contained in:
kumarabhirup 2026-02-13 18:57:15 -08:00
parent 2f788ad4a2
commit ad0a4578f9
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
3 changed files with 75 additions and 23 deletions

View File

@ -426,6 +426,8 @@ export type ChatPanelHandle = {
export type FileContext = {
path: string;
filename: string;
/** When true the path refers to a directory rather than a file. */
isDirectory?: boolean;
};
type FileScopedSession = {
@ -897,7 +899,8 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
}
if (fileContext && isFirstFileMessageRef.current) {
messageText = `[Context: workspace file '${fileContext.path}']\n\n${messageText}`;
const label = fileContext.isDirectory ? "directory" : "file";
messageText = `[Context: workspace ${label} '${fileContext.path}']\n\n${messageText}`;
isFirstFileMessageRef.current = false;
}
@ -1387,10 +1390,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
onChange={(isEmpty) =>
setEditorEmpty(isEmpty)
}
placeholder={
compact && fileContext
? `Ask about ${fileContext.filename}...`
: attachedFiles.length >
placeholder={
compact && fileContext
? `Ask about ${fileContext.isDirectory ? "this folder" : fileContext.filename}...`
: attachedFiles.length >
0
? "Add a message or send files..."
: "Type @ to mention files..."

View File

@ -31,13 +31,17 @@ export function useWorkspaceWatcher() {
const mountedRef = useRef(true);
const retryDelayRef = useRef(1000);
// Version counter: prevents stale fetch responses from overwriting newer data.
// Each fetch increments the counter; only the latest version's response is applied.
const fetchVersionRef = useRef(0);
// Fetch the workspace tree from the tree API
const fetchWorkspaceTree = useCallback(async () => {
const version = ++fetchVersionRef.current;
try {
const res = await fetch("/api/workspace/tree");
const data = await res.json();
if (mountedRef.current) {
if (mountedRef.current && fetchVersionRef.current === version) {
setTree(data.tree ?? []);
setExists(data.exists ?? false);
setWorkspaceRoot(data.workspaceRoot ?? null);
@ -45,36 +49,45 @@ export function useWorkspaceWatcher() {
setLoading(false);
}
} catch {
if (mountedRef.current) {setLoading(false);}
if (mountedRef.current && fetchVersionRef.current === version) {setLoading(false);}
}
}, []);
// Fetch a directory listing from the browse API
const fetchBrowseTree = useCallback(async (dir: string) => {
const version = ++fetchVersionRef.current;
try {
setLoading(true);
const res = await fetch(`/api/workspace/browse?dir=${encodeURIComponent(dir)}`);
const data = await res.json();
if (mountedRef.current) {
if (mountedRef.current && fetchVersionRef.current === version) {
setTree(data.entries ?? []);
setParentDir(data.parentDir ?? null);
setExists(true);
setLoading(false);
}
} catch {
if (mountedRef.current) {setLoading(false);}
if (mountedRef.current && fetchVersionRef.current === version) {setLoading(false);}
}
}, []);
// Smart setBrowseDir: auto-return to workspace mode when navigating to the
// workspace root, so all virtual folders (Chats, Cron, etc.) and DuckDB
// object detection are restored.
const browseDirRef = useRef<string | null>(null);
const setBrowseDir = useCallback((dir: string | null) => {
let nextDir = dir;
if (dir != null && workspaceRoot && dir === workspaceRoot) {
setBrowseDirRaw(null);
} else {
setBrowseDirRaw(dir);
nextDir = null;
}
// Mark loading synchronously when entering a new browse directory so the
// very first render after navigation already shows the loading state
// (prevents a flash of stale tree data).
if (nextDir != null && nextDir !== browseDirRef.current) {
setLoading(true);
}
browseDirRef.current = nextDir;
setBrowseDirRaw(nextDir);
}, [workspaceRoot]);
// Expose the raw value for reads

View File

@ -251,8 +251,8 @@ function WorkspacePageInner() {
if (!activePath) {return undefined;}
if (isVirtualPath(activePath)) {return undefined;}
const filename = activePath.split("/").pop() || activePath;
return { path: activePath, filename };
}, [activePath]);
return { path: activePath, filename, isDirectory: content.kind === "directory" };
}, [activePath, content.kind]);
// Update content state when the agent edits the file (live reload)
const handleFileChanged = useCallback((newContent: string) => {
@ -413,9 +413,13 @@ function WorkspacePageInner() {
}
}
// Clicking a folder in browse mode → navigate into it so the tree
// is fetched fresh (avoids stale/empty children from depth limits).
// is fetched fresh, AND show it in the main panel with the chat sidebar.
// Children come from the live tree (same data source as the sidebar),
// not from the stale node snapshot.
if (node.type === "folder") {
setBrowseDir(node.path);
setActivePath(node.path);
setContent({ kind: "directory", node: { name: node.name, path: node.path, type: "folder" } });
return;
}
}
@ -541,8 +545,11 @@ function WorkspacePageInner() {
const handleFileSearchSelect = useCallback(
(item: { name: string; path: string; type: string }) => {
if (item.type === "folder") {
// Navigate the sidebar into the folder
// Navigate the sidebar into the folder and show it in the main panel.
// Children come from the live tree (same data source as the sidebar).
setBrowseDir(item.path);
setActivePath(item.path);
setContent({ kind: "directory", node: { name: item.name, path: item.path, type: "folder" } });
} else {
// Navigate the sidebar to the parent directory of the file
const parentOfFile = item.path.split("/").slice(0, -1).join("/") || "/";
@ -801,8 +808,8 @@ function WorkspacePageInner() {
<path d="m12 19-7-7 7-7" /><path d="M19 12H5" />
</svg>
</button>
{/* Chat sidebar toggle (hidden for reserved/virtual paths and directories) */}
{fileContext && content.kind !== "directory" && (
{/* Chat sidebar toggle (hidden for reserved/virtual paths) */}
{fileContext && (
<button
type="button"
onClick={() => setShowChatSidebar((v) => !v)}
@ -811,7 +818,7 @@ function WorkspacePageInner() {
color: showChatSidebar ? "var(--color-accent)" : "var(--color-text-muted)",
background: showChatSidebar ? "var(--color-accent-light)" : "transparent",
}}
title={showChatSidebar ? "Hide chat" : "Chat about this file"}
title={showChatSidebar ? "Hide chat" : fileContext.isDirectory ? "Chat about this folder" : "Chat about this file"}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
@ -844,6 +851,8 @@ function WorkspacePageInner() {
workspaceExists={workspaceExists}
tree={tree}
activePath={activePath}
browseDir={browseDir}
treeLoading={treeLoading}
members={context?.members}
onNodeSelect={handleNodeSelect}
onNavigateToObject={handleNavigateToObject}
@ -857,8 +866,8 @@ function WorkspacePageInner() {
/>
</div>
{/* Chat sidebar (file-scoped) — hidden for directories and reserved paths */}
{fileContext && showChatSidebar && content.kind !== "directory" && (
{/* Chat sidebar (file/folder-scoped) — hidden for reserved paths */}
{fileContext && showChatSidebar && (
<aside
className="flex-shrink-0 border-l"
style={{
@ -905,6 +914,8 @@ function ContentRenderer({
workspaceExists,
tree,
activePath,
browseDir,
treeLoading,
members,
onNodeSelect,
onNavigateToObject,
@ -920,6 +931,10 @@ function ContentRenderer({
workspaceExists: boolean;
tree: TreeNode[];
activePath: string | null;
/** Current browse directory (absolute path), or null in workspace mode. */
browseDir?: string | null;
/** Whether the tree is currently being fetched. */
treeLoading?: boolean;
members?: Array<{ id: string; name: string; email: string; role: string }>;
onNodeSelect: (node: TreeNode) => void;
onNavigateToObject: (objectName: string) => void;
@ -1011,13 +1026,34 @@ function ContentRenderer({
/>
);
case "directory":
case "directory": {
// In browse mode the top-level tree is the live listing of browseDir
// (same data source as the sidebar). Use it directly instead of the
// possibly-stale node.children stored in content state.
const isBrowseLive = browseDir != null && activePath === browseDir;
if (isBrowseLive && treeLoading) {
return (
<div className="flex items-center justify-center h-full">
<div
className="w-6 h-6 border-2 rounded-full animate-spin"
style={{
borderColor: "var(--color-border)",
borderTopColor: "var(--color-accent)",
}}
/>
</div>
);
}
const directoryNode = isBrowseLive
? { ...content.node, children: tree }
: content.node;
return (
<DirectoryListing
node={content.node}
node={directoryNode}
onNodeSelect={onNodeSelect}
/>
);
}
case "cron-dashboard":
return (