aicodeman 1.48.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/config/cli-registry/schema.d.ts +1 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +5 -1
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +71 -5
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +4 -4
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/foreign-tmux.d.ts +48 -0
- package/dist/config/foreign-tmux.d.ts.map +1 -0
- package/dist/config/foreign-tmux.js +48 -0
- package/dist/config/foreign-tmux.js.map +1 -0
- package/dist/custom-model-injection-apply.d.ts +4 -2
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +35 -21
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/docker-hosts.d.ts +8 -1
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +5 -5
- package/dist/docker-hosts.js.map +1 -1
- package/dist/foreign-tmux-discovery.d.ts +140 -0
- package/dist/foreign-tmux-discovery.d.ts.map +1 -0
- package/dist/foreign-tmux-discovery.js +366 -0
- package/dist/foreign-tmux-discovery.js.map +1 -0
- package/dist/foreign-tmux.d.ts +381 -0
- package/dist/foreign-tmux.d.ts.map +1 -0
- package/dist/foreign-tmux.js +775 -0
- package/dist/foreign-tmux.js.map +1 -0
- package/dist/git-fetch-scheduler.d.ts +2 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -1
- package/dist/git-fetch-scheduler.js +1 -1
- package/dist/git-fetch-scheduler.js.map +1 -1
- package/dist/git-fetch.d.ts +1 -1
- package/dist/git-fetch.d.ts.map +1 -1
- package/dist/git-workspace-status.d.ts +2 -2
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/hooks-config.d.ts +2 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +2 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/live-worktree.d.ts +53 -0
- package/dist/live-worktree.d.ts.map +1 -0
- package/dist/live-worktree.js +97 -0
- package/dist/live-worktree.js.map +1 -0
- package/dist/mux-interface.d.ts +12 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/reboot-restore.d.ts +1 -1
- package/dist/reboot-restore.d.ts.map +1 -1
- package/dist/reboot-restore.js +9 -0
- package/dist/reboot-restore.js.map +1 -1
- package/dist/remote-files.d.ts +150 -5
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +505 -7
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-hosts.d.ts +15 -9
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +42 -20
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +19 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-env-clamp.d.ts +1 -1
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +4 -3
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session.d.ts +91 -21
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +190 -33
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +74 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +256 -41
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/foreign-tmux.d.ts +101 -0
- package/dist/types/foreign-tmux.d.ts.map +1 -0
- package/dist/types/foreign-tmux.js +27 -0
- package/dist/types/foreign-tmux.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +106 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/update.d.ts +4 -1
- package/dist/types/update.d.ts.map +1 -1
- package/dist/web/edit-history-sync.d.ts +27 -7
- package/dist/web/edit-history-sync.d.ts.map +1 -1
- package/dist/web/edit-history-sync.js +35 -19
- package/dist/web/edit-history-sync.js.map +1 -1
- package/dist/web/launch-defaults.d.ts +1 -1
- package/dist/web/launch-defaults.js +2 -2
- package/dist/web/launch-defaults.js.map +1 -1
- package/dist/web/linked-cases-writer.d.ts +43 -0
- package/dist/web/linked-cases-writer.d.ts.map +1 -0
- package/dist/web/linked-cases-writer.js +107 -0
- package/dist/web/linked-cases-writer.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +4 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +4 -2
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.963b5ba8.js → app.9ea37070.js} +6 -6
- package/dist/web/public/app.9ea37070.js.br +0 -0
- package/dist/web/public/app.9ea37070.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +27 -14
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/foreign-sessions.js +476 -0
- package/dist/web/public/foreign-sessions.js.br +0 -0
- package/dist/web/public/foreign-sessions.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +5 -3
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.a5e82c95.js +2 -0
- package/dist/web/public/i18n.a5e82c95.js.br +0 -0
- package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +36 -17
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.d4ec5957.js → keyboard-accessory.b941676c.js} +3 -1
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/{keyboard-accessory.d4ec5957.js.gz → keyboard-accessory.b941676c.js.gz} +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +10 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/{mobile.5ef2ce85.css → mobile.67c8eb5b.css} +1 -1
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/{mobile.5ef2ce85.css.gz → mobile.67c8eb5b.css.gz} +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.b100727d.js → panels-ui.8bcd4473.js} +4 -4
- package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
- package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.5ac82f2c.js → session-ui.a7c8c095.js} +11 -11
- package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
- package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
- package/dist/web/public/{settings-ui.d1d4fb99.js → settings-ui.18f92052.js} +3 -3
- package/dist/web/public/settings-ui.18f92052.js.br +0 -0
- package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/{styles.77dc538f.css → styles.6a718331.css} +1 -1
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/{styles.77dc538f.css.gz → styles.6a718331.css.gz} +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -2
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/{terminal-ui.d7c75ce8.js → terminal-ui.a7883640.js} +2 -2
- package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
- package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +72 -53
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.js +1 -1
- package/dist/web/routes/discord-routes.js.map +1 -1
- package/dist/web/routes/edit-history-routes.d.ts +2 -2
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -1
- package/dist/web/routes/edit-history-routes.js +4 -3
- package/dist/web/routes/edit-history-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +213 -102
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +7 -2
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/mux-routes.d.ts +10 -3
- package/dist/web/routes/mux-routes.d.ts.map +1 -1
- package/dist/web/routes/mux-routes.js +59 -1
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +22 -0
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +3 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +24 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +11 -8
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +262 -29
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +3 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +24 -1
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +34 -9
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/self-update.d.ts +9 -0
- package/dist/web/self-update.d.ts.map +1 -1
- package/dist/web/self-update.js +19 -3
- package/dist/web/self-update.js.map +1 -1
- package/dist/web/server.d.ts +16 -3
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +84 -24
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +13 -0
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +16 -8
- package/dist/web/session-wait-registry.js.map +1 -1
- package/package.json +2 -2
- package/skills/codeman/SKILL.md +20 -4
- package/dist/web/public/app.963b5ba8.js.br +0 -0
- package/dist/web/public/app.963b5ba8.js.gz +0 -0
- package/dist/web/public/i18n.33f930d7.js +0 -2
- package/dist/web/public/i18n.33f930d7.js.br +0 -0
- package/dist/web/public/i18n.33f930d7.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.d4ec5957.js.br +0 -0
- package/dist/web/public/mobile.5ef2ce85.css.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.br +0 -0
- package/dist/web/public/panels-ui.b100727d.js.gz +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.br +0 -0
- package/dist/web/public/session-ui.5ac82f2c.js.gz +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.br +0 -0
- package/dist/web/public/settings-ui.d1d4fb99.js.gz +0 -0
- package/dist/web/public/styles.77dc538f.css.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.br +0 -0
- package/dist/web/public/terminal-ui.d7c75ce8.js.gz +0 -0
|
@@ -523,6 +523,85 @@ export function isPathWithinRoot(root, candidate) {
|
|
|
523
523
|
const rel = relative(root, candidate);
|
|
524
524
|
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
|
|
525
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Folder entries one Files panel listing may read, summed over the folders it opens. The budget of
|
|
528
|
+
* the walk caps what a listing RETURNS, but taking turns means opening every folder found so far,
|
|
529
|
+
* so this is what caps the reading: past it, folders not opened yet stay unread and are reported as
|
|
530
|
+
* unfinished. It also bounds what the walk holds, since a folder keeps only entries it has read. A
|
|
531
|
+
* folder is read whole, so the one that crosses this line overshoots by its own size.
|
|
532
|
+
*/
|
|
533
|
+
const MAX_WALK_ENTRIES_READ = 250_000;
|
|
534
|
+
/**
|
|
535
|
+
* First turn of a folder: reads it and sets `cursor.entries` to the part the listing could use,
|
|
536
|
+
* which is the entries it lists, sorted, up to what is left of the budget. Leaves `entries` unset
|
|
537
|
+
* for a folder it could not read, and sets `unanswered` when that was a probe that established
|
|
538
|
+
* nothing. Answers the number of entries read, for the walk's read ceiling.
|
|
539
|
+
*/
|
|
540
|
+
async function readWalkFolder(cursor, budgetLeft, isListed) {
|
|
541
|
+
// The walk's root is the session's own folder. Below it any folder can be a mount that stopped
|
|
542
|
+
// answering, and a bulk scan keeps the stall cap (#516): an unbounded readdir there would hold a
|
|
543
|
+
// threadpool worker for good. `unknown` is never `absent`, so only a folder the filesystem says
|
|
544
|
+
// is gone drops out quietly: one that did not answer goes on the walk's unfinished list.
|
|
545
|
+
if (cursor.depth > 1) {
|
|
546
|
+
const kind = await probePathKind(cursor.dirPath);
|
|
547
|
+
cursor.unanswered = kind === 'unknown';
|
|
548
|
+
if (kind !== 'directory')
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
let read;
|
|
552
|
+
try {
|
|
553
|
+
read = await fs.readdir(cursor.dirPath, { withFileTypes: true });
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Can't read directory (permission denied, etc.).
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const listed = read.filter(isListed).sort((a, b) => {
|
|
560
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
561
|
+
return -1;
|
|
562
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
563
|
+
return 1;
|
|
564
|
+
return a.name.localeCompare(b.name);
|
|
565
|
+
});
|
|
566
|
+
cursor.more = listed.length > budgetLeft;
|
|
567
|
+
cursor.entries = cursor.more ? listed.slice(0, budgetLeft) : listed;
|
|
568
|
+
return read.length;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Where a folder's turn ends, given a `share` of the budget. Folders sort first, and each one found
|
|
572
|
+
* here waits for a later round, by which time a folder that spent the rest of its share on its own
|
|
573
|
+
* files has left nothing to read it with. So those files count as one more folder: they take an
|
|
574
|
+
* equal part of what the subfolders left and the remainder stays in the budget. A turn that found
|
|
575
|
+
* no subfolder has nothing to hold back for, and a turn always covers at least one entry.
|
|
576
|
+
*/
|
|
577
|
+
function walkTurnEnd(entries, start, share) {
|
|
578
|
+
const end = Math.min(start + share, entries.length);
|
|
579
|
+
let firstFile = start;
|
|
580
|
+
while (firstFile < end && entries[firstFile].isDirectory())
|
|
581
|
+
firstFile++;
|
|
582
|
+
return firstFile + Math.floor((end - firstFile) / (firstFile - start + 1));
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Whether a `root=live` listing may start at a session's `liveWorktree` (#765). `findLiveWorktree()`
|
|
586
|
+
* judged that path lexically when the pane moved, and a symlink can take its place, or a parent's,
|
|
587
|
+
* without moving the pane. The probe and `readdir` both follow one, and the walk's `path`s only look
|
|
588
|
+
* relative to `workingDir`, so this is the request-time half of the rule, the same one
|
|
589
|
+
* `validateSessionFilePath()` applies to a file: resolved, the worktree must lie strictly inside the
|
|
590
|
+
* resolved `workingDir`. Only the CHECK uses the resolved paths: the walk keeps the session's spelling,
|
|
591
|
+
* or a workspace reached through a symlink would stop producing paths relative to `workingDir`.
|
|
592
|
+
* Probed first (#516); both realpaths then walk the components that probe has just answered for.
|
|
593
|
+
*/
|
|
594
|
+
async function canWalkLiveWorktree(workingDir, worktreePath) {
|
|
595
|
+
if ((await probePathKind(worktreePath, { pastCap: true })) !== 'directory')
|
|
596
|
+
return false;
|
|
597
|
+
try {
|
|
598
|
+
const [realRoot, realWorktree] = await Promise.all([fs.realpath(workingDir), fs.realpath(worktreePath)]);
|
|
599
|
+
return realWorktree !== realRoot && isPathWithinRoot(realRoot, realWorktree);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
526
605
|
function findMatchingPickerRoot(roots, candidate) {
|
|
527
606
|
return roots
|
|
528
607
|
.filter((root) => isPathWithinRoot(root.path, candidate))
|
|
@@ -1130,11 +1209,14 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1130
1209
|
}
|
|
1131
1210
|
// The cases directories (shared, and the caller's own in multi-user mode) are what GET /api/cases lists as
|
|
1132
1211
|
// local cases: a folder made there is a case with no scaffold, which Link then refuses by name and which
|
|
1133
|
-
// deleting would take the linked folder with. A case there is made through Add Case > New
|
|
1212
|
+
// deleting would take the linked folder with. A case there is made through Add Case > Create New with no
|
|
1213
|
+
// custom folder. The refusal has to read right from every picker that offers New folder: a Create New or
|
|
1214
|
+
// Clone Repo user is already on that tab, with the custom folder ticked, and this is where Browse opens
|
|
1215
|
+
// while the field is empty.
|
|
1134
1216
|
const casesDirs = await Promise.all([...new Set([CASES_DIR, resolveCasesDir(getAuthUser(req))])].map((dir) => fs.realpath(dir).catch(() => resolve(dir))));
|
|
1135
1217
|
if (casesDirs.includes(resolvedPath)) {
|
|
1136
1218
|
reply.code(400);
|
|
1137
|
-
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'This is a cases folder: every folder in it is a case.
|
|
1219
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'This is a cases folder: every folder in it is a case. Pick a folder outside it, or make the case with Add Case > Create New or Clone Repo (no custom folder).');
|
|
1138
1220
|
}
|
|
1139
1221
|
// Like browse, a file is not a place to create in.
|
|
1140
1222
|
try {
|
|
@@ -1216,11 +1298,20 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1216
1298
|
// File tree listing
|
|
1217
1299
|
app.get('/api/sessions/:id/files', async (req) => {
|
|
1218
1300
|
const { id } = req.params;
|
|
1219
|
-
const { depth, showHidden, q } = req.query;
|
|
1301
|
+
const { depth, showHidden, q, root } = req.query;
|
|
1220
1302
|
const session = findSessionOrFail(ctx, id, req);
|
|
1221
1303
|
const maxDepth = Math.min(parseInt(depth || '5', 10), 10);
|
|
1222
1304
|
const includeHidden = showHidden === 'true';
|
|
1223
1305
|
const workingDir = session.workingDir;
|
|
1306
|
+
// `root=live` starts the walk at the linked worktree the pane is in (#765), once that folder
|
|
1307
|
+
// proves to be inside `workingDir` right now, and every `path` below stays relative to
|
|
1308
|
+
// `workingDir`, so file-content, file-raw and the edit route need nothing new and judge these
|
|
1309
|
+
// paths against the same root as any other. Only the search predicate reads a path relative
|
|
1310
|
+
// to the listing, since that is the tree the person is looking at.
|
|
1311
|
+
const liveWorktree = session.liveWorktree;
|
|
1312
|
+
const walkRoot = root === 'live' && liveWorktree && (await canWalkLiveWorktree(workingDir, liveWorktree.path))
|
|
1313
|
+
? liveWorktree.path
|
|
1314
|
+
: workingDir;
|
|
1224
1315
|
// null for an empty/whitespace query, which is what keeps the default
|
|
1225
1316
|
// tree response byte-identical when no search is requested.
|
|
1226
1317
|
const matcher = compileFileQuery(q ?? '');
|
|
@@ -1245,6 +1336,8 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1245
1336
|
let totalDirectories = 0;
|
|
1246
1337
|
let truncated = false;
|
|
1247
1338
|
const maxFiles = 5000;
|
|
1339
|
+
let entriesRead = 0;
|
|
1340
|
+
const isListed = (entry) => (includeHidden || !entry.name.startsWith('.')) && !(entry.isDirectory() && excludeDirs.has(entry.name));
|
|
1248
1341
|
// ===== Search mode =====
|
|
1249
1342
|
// A query turns this endpoint into a FLAT match list rather than a nested
|
|
1250
1343
|
// tree. It recurses past non-matching directories on purpose — the whole
|
|
@@ -1254,71 +1347,84 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1254
1347
|
if (matcher) {
|
|
1255
1348
|
const matches = [];
|
|
1256
1349
|
const maxMatches = 1000;
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
1272
|
-
return -1;
|
|
1273
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
1274
|
-
return 1;
|
|
1275
|
-
return a.name.localeCompare(b.name);
|
|
1276
|
-
});
|
|
1277
|
-
for (const entry of entries) {
|
|
1278
|
-
if (totalFiles + totalDirectories > maxFiles || matches.length >= maxMatches) {
|
|
1279
|
-
truncated = true;
|
|
1350
|
+
const queue = [{ dirPath: walkRoot, depth: 1, index: 0 }];
|
|
1351
|
+
// Folders the read ceiling kept shut or that did not answer. What was already read still gets its turns.
|
|
1352
|
+
const unread = [];
|
|
1353
|
+
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
|
1354
|
+
// Per-directory round-robin (breadth-first): each queued directory gets an equal share of what
|
|
1355
|
+
// is left before any of them gets more, so results arrive shallow-first. Fair per directory,
|
|
1356
|
+
// not per subtree: a folder of thousands of small subfolders queues thousands of directories
|
|
1357
|
+
// and can still take most of the budget one level down. Reading a folder costs the same
|
|
1358
|
+
// whatever its share, so this never bounds the reading: MAX_WALK_ENTRIES_READ does.
|
|
1359
|
+
const directoriesInRound = queue.length;
|
|
1360
|
+
const nodesPerDirectory = Math.max(1, Math.floor((maxFiles - totalFiles - totalDirectories) / directoriesInRound));
|
|
1361
|
+
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
|
1362
|
+
// Spent part way through a round: the rest stay queued unread rather than sorted for nothing.
|
|
1363
|
+
if (totalFiles + totalDirectories >= maxFiles || matches.length >= maxMatches)
|
|
1280
1364
|
break;
|
|
1281
|
-
|
|
1282
|
-
if (!
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1365
|
+
const cursor = queue.shift();
|
|
1366
|
+
if (!cursor)
|
|
1367
|
+
break;
|
|
1368
|
+
if (cursor.depth > maxDepth) {
|
|
1369
|
+
truncated = true;
|
|
1285
1370
|
continue;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
matches.push({ name: entry.name, path: relativePath, type: 'directory' });
|
|
1371
|
+
}
|
|
1372
|
+
if (!cursor.entries) {
|
|
1373
|
+
if (entriesRead >= MAX_WALK_ENTRIES_READ) {
|
|
1374
|
+
unread.push(cursor);
|
|
1375
|
+
continue;
|
|
1292
1376
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1377
|
+
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
|
1378
|
+
if (cursor.unanswered)
|
|
1379
|
+
unread.push(cursor);
|
|
1380
|
+
if (!cursor.entries)
|
|
1381
|
+
continue;
|
|
1295
1382
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1383
|
+
const endIndex = walkTurnEnd(cursor.entries, cursor.index, nodesPerDirectory);
|
|
1384
|
+
while (cursor.index < endIndex && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
|
1385
|
+
const entry = cursor.entries[cursor.index++];
|
|
1386
|
+
const fullPath = join(cursor.dirPath, entry.name);
|
|
1387
|
+
const relativePath = relative(workingDir, fullPath);
|
|
1388
|
+
const listedPath = relative(walkRoot, fullPath);
|
|
1389
|
+
if (entry.isDirectory()) {
|
|
1390
|
+
totalDirectories++;
|
|
1391
|
+
if (matcher(entry.name, listedPath)) {
|
|
1392
|
+
matches.push({ name: entry.name, path: relativePath, type: 'directory' });
|
|
1302
1393
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1394
|
+
// Queued, not entered: it waits for a later round with every other directory found.
|
|
1395
|
+
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, index: 0 });
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
totalFiles++;
|
|
1399
|
+
if (matcher(entry.name, listedPath)) {
|
|
1400
|
+
let size;
|
|
1401
|
+
try {
|
|
1402
|
+
size = (await fs.stat(fullPath)).size;
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
// Skip size if we can't stat the match.
|
|
1406
|
+
}
|
|
1407
|
+
matches.push({
|
|
1408
|
+
name: entry.name,
|
|
1409
|
+
path: relativePath,
|
|
1410
|
+
type: 'file',
|
|
1411
|
+
size,
|
|
1412
|
+
extension: entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : undefined,
|
|
1413
|
+
});
|
|
1305
1414
|
}
|
|
1306
|
-
matches.push({
|
|
1307
|
-
name: entry.name,
|
|
1308
|
-
path: relativePath,
|
|
1309
|
-
type: 'file',
|
|
1310
|
-
size,
|
|
1311
|
-
extension: entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : undefined,
|
|
1312
|
-
});
|
|
1313
1415
|
}
|
|
1314
1416
|
}
|
|
1417
|
+
if (cursor.index < cursor.entries.length || cursor.more)
|
|
1418
|
+
queue.push(cursor);
|
|
1315
1419
|
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1420
|
+
}
|
|
1421
|
+
if (queue.length > 0 || unread.length > 0)
|
|
1422
|
+
truncated = true;
|
|
1318
1423
|
return {
|
|
1319
1424
|
success: true,
|
|
1320
1425
|
data: {
|
|
1321
|
-
root:
|
|
1426
|
+
root: walkRoot,
|
|
1427
|
+
...(liveWorktree && { worktree: liveWorktree }),
|
|
1322
1428
|
tree: [],
|
|
1323
1429
|
matches,
|
|
1324
1430
|
totalFiles,
|
|
@@ -1330,44 +1436,50 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1330
1436
|
},
|
|
1331
1437
|
};
|
|
1332
1438
|
}
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
//
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
// Skip hidden files unless requested
|
|
1355
|
-
if (!includeHidden && entry.name.startsWith('.'))
|
|
1439
|
+
const tree = [];
|
|
1440
|
+
const queue = [{ dirPath: walkRoot, depth: 1, nodes: tree, index: 0 }];
|
|
1441
|
+
const unread = [];
|
|
1442
|
+
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles) {
|
|
1443
|
+
// The same per-directory round-robin as the search walk above, with the same limits.
|
|
1444
|
+
const directoriesInRound = queue.length;
|
|
1445
|
+
const nodesPerDirectory = Math.max(1, Math.floor((maxFiles - totalFiles - totalDirectories) / directoriesInRound));
|
|
1446
|
+
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
|
1447
|
+
// Before the shift, so a cursor this skips stays queued and is flagged below.
|
|
1448
|
+
if (totalFiles + totalDirectories >= maxFiles)
|
|
1449
|
+
break;
|
|
1450
|
+
const cursor = queue.shift();
|
|
1451
|
+
if (!cursor)
|
|
1452
|
+
break;
|
|
1453
|
+
if (cursor.depth > maxDepth) {
|
|
1454
|
+
truncated = true;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (!cursor.entries) {
|
|
1458
|
+
if (entriesRead >= MAX_WALK_ENTRIES_READ) {
|
|
1459
|
+
unread.push(cursor);
|
|
1356
1460
|
continue;
|
|
1357
|
-
|
|
1358
|
-
|
|
1461
|
+
}
|
|
1462
|
+
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
|
1463
|
+
if (cursor.unanswered)
|
|
1464
|
+
unread.push(cursor);
|
|
1465
|
+
if (!cursor.entries)
|
|
1359
1466
|
continue;
|
|
1360
|
-
|
|
1467
|
+
}
|
|
1468
|
+
const endIndex = walkTurnEnd(cursor.entries, cursor.index, nodesPerDirectory);
|
|
1469
|
+
while (cursor.index < endIndex && totalFiles + totalDirectories < maxFiles) {
|
|
1470
|
+
const entry = cursor.entries[cursor.index++];
|
|
1471
|
+
const fullPath = join(cursor.dirPath, entry.name);
|
|
1361
1472
|
const relativePath = fullPath.slice(workingDir.length + 1);
|
|
1362
1473
|
if (entry.isDirectory()) {
|
|
1363
1474
|
totalDirectories++;
|
|
1364
|
-
const
|
|
1365
|
-
nodes.push({
|
|
1475
|
+
const node = {
|
|
1366
1476
|
name: entry.name,
|
|
1367
1477
|
path: relativePath,
|
|
1368
1478
|
type: 'directory',
|
|
1369
|
-
children,
|
|
1370
|
-
}
|
|
1479
|
+
children: [],
|
|
1480
|
+
};
|
|
1481
|
+
cursor.nodes.push(node);
|
|
1482
|
+
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, node, nodes: node.children, index: 0 });
|
|
1371
1483
|
}
|
|
1372
1484
|
else {
|
|
1373
1485
|
totalFiles++;
|
|
@@ -1378,29 +1490,28 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1378
1490
|
size = stat.size;
|
|
1379
1491
|
}
|
|
1380
1492
|
catch {
|
|
1381
|
-
// Skip if can't stat
|
|
1493
|
+
// Skip if can't stat.
|
|
1382
1494
|
}
|
|
1383
|
-
nodes.push({
|
|
1384
|
-
name: entry.name,
|
|
1385
|
-
path: relativePath,
|
|
1386
|
-
type: 'file',
|
|
1387
|
-
size,
|
|
1388
|
-
extension: ext,
|
|
1389
|
-
});
|
|
1495
|
+
cursor.nodes.push({ name: entry.name, path: relativePath, type: 'file', size, extension: ext });
|
|
1390
1496
|
}
|
|
1391
1497
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
catch {
|
|
1395
|
-
// Can't read directory (permission denied, etc.)
|
|
1396
|
-
return [];
|
|
1498
|
+
if (cursor.index < cursor.entries.length || cursor.more)
|
|
1499
|
+
queue.push(cursor);
|
|
1397
1500
|
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1501
|
+
}
|
|
1502
|
+
// Every directory still queued is unread or half read, and an empty `children` would read as an
|
|
1503
|
+
// empty folder. The root has no node to flag: `truncated` says the same for it.
|
|
1504
|
+
for (const cursor of [...queue, ...unread]) {
|
|
1505
|
+
if (cursor.node)
|
|
1506
|
+
cursor.node.incomplete = true;
|
|
1507
|
+
}
|
|
1508
|
+
if (queue.length > 0 || unread.length > 0)
|
|
1509
|
+
truncated = true;
|
|
1400
1510
|
return {
|
|
1401
1511
|
success: true,
|
|
1402
1512
|
data: {
|
|
1403
|
-
root:
|
|
1513
|
+
root: walkRoot,
|
|
1514
|
+
...(liveWorktree && { worktree: liveWorktree }),
|
|
1404
1515
|
tree,
|
|
1405
1516
|
totalFiles,
|
|
1406
1517
|
totalDirectories,
|