diff --git a/packages/editor/components/AnnotateAgentTerminalPanel.tsx b/packages/editor/components/AnnotateAgentTerminalPanel.tsx index b08477457..73d78493e 100644 --- a/packages/editor/components/AnnotateAgentTerminalPanel.tsx +++ b/packages/editor/components/AnnotateAgentTerminalPanel.tsx @@ -251,12 +251,8 @@ export const AnnotateAgentTerminalPanel = forwardRef< stopRequestedRef.current = true; const session = sessionRef.current; if (!session) { - if (startedAgentId) { - closeAfterStopRef.current = closeAfterStop; - setStatus("stopping"); - return; - } clearTimers(); + closeAfterStopRef.current = false; stopRequestedRef.current = false; setStartedAgentId(null); setStatus("idle"); @@ -276,7 +272,7 @@ export const AnnotateAgentTerminalPanel = forwardRef< sessionRef.current?.pty.kill(); if (closeAfterStopRef.current) onClose(); }, 1400)); - }, [clearTimers, onClose, onSessionActiveChange, onSessionReadyChange, startedAgentId]); + }, [clearTimers, onClose, onSessionActiveChange, onSessionReadyChange]); const sendMessage = useCallback((message: string) => { const text = message.trim(); @@ -348,7 +344,12 @@ export const AnnotateAgentTerminalPanel = forwardRef< -
+
+ {status === "starting" && ( +
+ Starting terminal... +
+ )} { diff --git a/packages/server/reference-handlers.test.ts b/packages/server/reference-handlers.test.ts index 389e05374..2e3413982 100644 --- a/packages/server/reference-handlers.test.ts +++ b/packages/server/reference-handlers.test.ts @@ -219,4 +219,35 @@ describe("handleFileBrowserFiles", () => { expect(data.workspaceStatus.totals.files).toBe(0); expect(data.workspaceStatus.files).toEqual({}); }); + + test("caps large folder walks", async () => { + const root = makeTempDir("plannotator-files-cap-"); + writeTempFile(root, "docs/a.md", "a\n"); + writeTempFile(root, "docs/b.md", "b\n"); + writeTempFile(root, "docs/c.md", "c\n"); + const previousLimit = process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES; + process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES = "2"; + + try { + const url = new URL("http://localhost/api/reference/files"); + url.searchParams.set("dirPath", root); + const res = await handleFileBrowserFiles(new Request(url.toString())); + const data = await res.json() as { + tree: VaultNode[]; + truncated: boolean; + fileLimit: number; + }; + + expect(res.status).toBe(200); + expect(flattenTree(data.tree)).toHaveLength(2); + expect(data.truncated).toBe(true); + expect(data.fileLimit).toBe(2); + } finally { + if (previousLimit === undefined) { + delete process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES; + } else { + process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES = previousLimit; + } + } + }); }); diff --git a/packages/server/reference-handlers.ts b/packages/server/reference-handlers.ts index 53c4676d3..a3332c6ef 100644 --- a/packages/server/reference-handlers.ts +++ b/packages/server/reference-handlers.ts @@ -543,12 +543,34 @@ export async function handleObsidianDoc(req: Request): Promise { // --- File Browser --- const FILE_BROWSER_EXTENSIONS = /\.(mdx?|txt|html?)$/i; +const DEFAULT_FILE_BROWSER_MAX_FILES = 5_000; function includeWorkspaceFile(relativePath: string, _change: WorkspaceFileChange): boolean { return FILE_BROWSER_EXTENSIONS.test(relativePath) && !isFileBrowserExcludedPath(relativePath); } -async function walkFileBrowserFiles(dir: string, root: string, files: Set): Promise { +function getFileBrowserMaxFiles(): number { + const value = Number.parseInt(process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES ?? "", 10); + return Number.isFinite(value) && value > 0 ? value : DEFAULT_FILE_BROWSER_MAX_FILES; +} + +type FileBrowserWalkState = { + files: Set; + limit: number; + truncated: boolean; +}; + +function addFileBrowserFile(state: FileBrowserWalkState, relativePath: string): void { + if (state.files.has(relativePath)) return; + if (state.files.size >= state.limit) { + state.truncated = true; + return; + } + state.files.add(relativePath); +} + +async function walkFileBrowserFiles(dir: string, root: string, state: FileBrowserWalkState): Promise { + if (state.truncated) return; let entries; try { entries = await readdir(dir, { withFileTypes: true }); @@ -557,14 +579,15 @@ async function walkFileBrowserFiles(dir: string, root: string, files: Set { } try { - const files = new Set(); - await walkFileBrowserFiles(resolvedDir, resolvedDir, files); + const state: FileBrowserWalkState = { + files: new Set(), + limit: getFileBrowserMaxFiles(), + truncated: false, + }; + await walkFileBrowserFiles(resolvedDir, resolvedDir, state); const workspaceStatus = filterWorkspaceStatusForDirectory(await getWorkspaceStatusForDirectory(resolvedDir), resolvedDir, includeWorkspaceFile); for (const match of getWorkspaceStatusRelativePaths(workspaceStatus, resolvedDir, includeWorkspaceFile)) { - files.add(match); + addFileBrowserFile(state, match); + if (state.truncated) break; } - const sortedFiles = [...files].sort(); + const sortedFiles = [...state.files].sort(); const tree = buildFileTree(sortedFiles); - return Response.json({ tree, workspaceStatus }); + return Response.json({ + tree, + workspaceStatus, + truncated: state.truncated, + fileLimit: state.limit, + }); } catch { return Response.json( { error: "Failed to list directory files" }, diff --git a/packages/shared/reference-common.test.ts b/packages/shared/reference-common.test.ts index 4217506ad..14e3012de 100644 --- a/packages/shared/reference-common.test.ts +++ b/packages/shared/reference-common.test.ts @@ -9,6 +9,8 @@ describe("isFileBrowserExcludedPath", () => { expect(isFileBrowserExcludedPath("docs/node_modules/pkg/readme.md")).toBe(true); expect(isFileBrowserExcludedPath("docs/dist")).toBe(true); expect(isFileBrowserExcludedPath("docs/dist/generated.md")).toBe(true); + expect(isFileBrowserExcludedPath(".claude/worktrees/session/plan.md")).toBe(true); + expect(isFileBrowserExcludedPath("packages/app/.agents/skills/review/SKILL.md")).toBe(true); }); test("matches exact path segments only", () => { diff --git a/packages/shared/reference-common.ts b/packages/shared/reference-common.ts index de8430742..ce5557282 100644 --- a/packages/shared/reference-common.ts +++ b/packages/shared/reference-common.ts @@ -3,6 +3,8 @@ export const FILE_BROWSER_EXCLUDED = [ "node_modules/", ".git/", + ".claude/", + ".agents/", "dist/", "build/", ".next/",