Fix annotate terminal startup in large folders

Avoid starving folder annotate sessions by excluding agent work directories and capping folder file-browser scans. Improve the annotate WebTUI panel startup/stop behavior.
This commit is contained in:
Michael Ramos
2026-07-08 12:33:12 -07:00
committed by GitHub
parent e20fd97be7
commit d3c9de1ef0
5 changed files with 85 additions and 16 deletions
@@ -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<
</button>
</div>
</div>
<div className="min-h-0 flex-1" style={terminalTheme.shellStyle}>
<div className="relative min-h-0 flex-1" style={terminalTheme.shellStyle}>
{status === "starting" && (
<div className="pointer-events-none absolute left-3 top-3 z-10 rounded border border-border/50 bg-card/95 px-2 py-1 text-[11px] text-muted-foreground shadow-sm">
Starting terminal...
</div>
)}
<WebTuiTerminal
key={startedAgentId}
backend={backend}
@@ -357,7 +358,7 @@ export const AnnotateAgentTerminalPanel = forwardRef<
prompt={null}
terminalOptions={terminalOptions}
terminalColorScheme={terminalTheme.colorScheme}
terminalGpuAcceleration="auto"
terminalGpuAcceleration="off"
fontZoom={AGENT_TERMINAL_FONT_ZOOM}
className="h-full border-0"
onReady={(session) => {
@@ -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;
}
}
});
});
+41 -8
View File
@@ -543,12 +543,34 @@ export async function handleObsidianDoc(req: Request): Promise<Response> {
// --- 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<string>): Promise<void> {
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<string>;
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<void> {
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<string
}
for (const entry of entries) {
if (state.truncated) return;
const fullPath = join(dir, entry.name);
const relativePath = relative(root, fullPath).replace(/\\/g, "/");
if (entry.isDirectory()) {
if (isFileBrowserExcludedPath(relativePath)) continue;
await walkFileBrowserFiles(fullPath, root, files);
await walkFileBrowserFiles(fullPath, root, state);
} else if (entry.isFile() && FILE_BROWSER_EXTENSIONS.test(entry.name)) {
if (isFileBrowserExcludedPath(relativePath)) continue;
files.add(relativePath);
addFileBrowserFile(state, relativePath);
}
}
}
@@ -586,16 +609,26 @@ export async function handleFileBrowserFiles(req: Request): Promise<Response> {
}
try {
const files = new Set<string>();
await walkFileBrowserFiles(resolvedDir, resolvedDir, files);
const state: FileBrowserWalkState = {
files: new Set<string>(),
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" },
+2
View File
@@ -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", () => {
+2
View File
@@ -3,6 +3,8 @@
export const FILE_BROWSER_EXCLUDED = [
"node_modules/",
".git/",
".claude/",
".agents/",
"dist/",
"build/",
".next/",