Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/editor/components/AnnotateAgentTerminalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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();
Expand Down Expand Up @@ -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}
Expand All @@ -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) => {
Expand Down
31 changes: 31 additions & 0 deletions packages/server/reference-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
});
});
49 changes: 41 additions & 8 deletions packages/server/reference-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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);
}
}
}
Expand All @@ -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" },
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/reference-common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/reference-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
export const FILE_BROWSER_EXCLUDED = [
"node_modules/",
".git/",
".claude/",
".agents/",
"dist/",
"build/",
".next/",
Expand Down