|
9 | 9 | import { homedir, tmpdir } from "node:os"; |
10 | 10 | import { join } from "node:path"; |
11 | 11 | import { MEMORY_SYSTEM_DIR } from "../../agent/memoryFilesystem"; |
| 12 | +import { getDirectoryLimits } from "../../utils/directoryLimits"; |
12 | 13 | import { parseFrontmatter } from "../../utils/frontmatter"; |
13 | 14 | import { type Line, linesToTranscript } from "./accumulator"; |
14 | 15 |
|
@@ -192,24 +193,96 @@ function buildParentMemoryTree(files: ParentMemoryFile[]): string { |
192 | 193 | }, |
193 | 194 | ); |
194 | 195 |
|
195 | | - const lines: string[] = ["/memory/"]; |
| 196 | + const limits = getDirectoryLimits(); |
| 197 | + const maxLines = Math.max(2, limits.memfsTreeMaxLines); |
| 198 | + const maxChars = Math.max(128, limits.memfsTreeMaxChars); |
| 199 | + const maxChildrenPerDir = Math.max(1, limits.memfsTreeMaxChildrenPerDir); |
196 | 200 |
|
197 | | - const render = (node: TreeNode, prefix: string) => { |
| 201 | + const rootLine = "/memory/"; |
| 202 | + const lines: string[] = [rootLine]; |
| 203 | + let totalChars = rootLine.length; |
| 204 | + |
| 205 | + const countTreeEntries = (node: TreeNode): number => { |
| 206 | + let total = 0; |
| 207 | + for (const [, child] of node.children) { |
| 208 | + total += 1; |
| 209 | + if (child.children.size > 0) { |
| 210 | + total += countTreeEntries(child); |
| 211 | + } |
| 212 | + } |
| 213 | + return total; |
| 214 | + }; |
| 215 | + |
| 216 | + const canAppendLine = (line: string): boolean => { |
| 217 | + const nextLineCount = lines.length + 1; |
| 218 | + const nextCharCount = totalChars + 1 + line.length; |
| 219 | + return nextLineCount <= maxLines && nextCharCount <= maxChars; |
| 220 | + }; |
| 221 | + |
| 222 | + const render = (node: TreeNode, prefix: string): boolean => { |
198 | 223 | const entries = sortedEntries(node); |
199 | | - for (const [index, [name, child]] of entries.entries()) { |
200 | | - const isLast = index === entries.length - 1; |
| 224 | + const visibleEntries = entries.slice(0, maxChildrenPerDir); |
| 225 | + const omittedEntries = Math.max(0, entries.length - visibleEntries.length); |
| 226 | + |
| 227 | + const renderItems: Array< |
| 228 | + | { kind: "entry"; name: string; child: TreeNode } |
| 229 | + | { kind: "omitted"; omittedCount: number } |
| 230 | + > = visibleEntries.map(([name, child]) => ({ |
| 231 | + kind: "entry", |
| 232 | + name, |
| 233 | + child, |
| 234 | + })); |
| 235 | + |
| 236 | + if (omittedEntries > 0) { |
| 237 | + renderItems.push({ kind: "omitted", omittedCount: omittedEntries }); |
| 238 | + } |
| 239 | + |
| 240 | + for (const [index, item] of renderItems.entries()) { |
| 241 | + const isLast = index === renderItems.length - 1; |
201 | 242 | const branch = isLast ? "└──" : "├──"; |
202 | | - const suffix = child.isFile ? "" : "/"; |
203 | | - const description = child.description ? ` (${child.description})` : ""; |
204 | | - lines.push(`${prefix}${branch} ${name}${suffix}${description}`); |
205 | | - if (child.children.size > 0) { |
| 243 | + const line = |
| 244 | + item.kind === "entry" |
| 245 | + ? `${prefix}${branch} ${item.name}${item.child.isFile ? "" : "/"}${item.child.description ? ` (${item.child.description})` : ""}` |
| 246 | + : `${prefix}${branch} … (${item.omittedCount.toLocaleString()} more entries)`; |
| 247 | + |
| 248 | + if (!canAppendLine(line)) { |
| 249 | + return false; |
| 250 | + } |
| 251 | + |
| 252 | + lines.push(line); |
| 253 | + totalChars += 1 + line.length; |
| 254 | + |
| 255 | + if (item.kind === "entry" && item.child.children.size > 0) { |
206 | 256 | const nextPrefix = `${prefix}${isLast ? " " : "│ "}`; |
207 | | - render(child, nextPrefix); |
| 257 | + if (!render(item.child, nextPrefix)) { |
| 258 | + return false; |
| 259 | + } |
208 | 260 | } |
209 | 261 | } |
| 262 | + |
| 263 | + return true; |
210 | 264 | }; |
211 | 265 |
|
212 | | - render(root, ""); |
| 266 | + const totalEntries = countTreeEntries(root); |
| 267 | + const fullyRendered = render(root, ""); |
| 268 | + |
| 269 | + if (!fullyRendered) { |
| 270 | + while (lines.length > 1) { |
| 271 | + const shownEntries = Math.max(0, lines.length - 1); |
| 272 | + const omittedEntries = Math.max(1, totalEntries - shownEntries); |
| 273 | + const notice = `[Tree truncated: showing ${shownEntries.toLocaleString()} of ${totalEntries.toLocaleString()} entries. ${omittedEntries.toLocaleString()} omitted.]`; |
| 274 | + |
| 275 | + if (canAppendLine(notice)) { |
| 276 | + lines.push(notice); |
| 277 | + break; |
| 278 | + } |
| 279 | + |
| 280 | + const removed = lines.pop(); |
| 281 | + if (removed) { |
| 282 | + totalChars -= 1 + removed.length; |
| 283 | + } |
| 284 | + } |
| 285 | + } |
213 | 286 |
|
214 | 287 | return lines.join("\n"); |
215 | 288 | } |
|
0 commit comments