@pi-harness/pi-harness 0.1.85 → 0.1.87

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.
Files changed (41) hide show
  1. package/apps/web/dist/assets/de-DhpdIJOo.js +1 -0
  2. package/apps/web/dist/assets/en-Dxf8FQYK.js +1 -0
  3. package/apps/web/dist/assets/es-CltpT46t.js +1 -0
  4. package/apps/web/dist/assets/fr-BeFnZlsQ.js +1 -0
  5. package/apps/web/dist/assets/{index--BtqJS5O.css → index-CYoyyJnL.css} +1 -1
  6. package/apps/web/dist/assets/index-DdCC9J2I.js +29 -0
  7. package/apps/web/dist/assets/ja-Dl0FYuCd.js +1 -0
  8. package/apps/web/dist/assets/ko-C3paKqYe.js +1 -0
  9. package/apps/web/dist/assets/pt-BR-CuPyhsku.js +1 -0
  10. package/apps/web/dist/assets/ru-UnUgv8E7.js +1 -0
  11. package/apps/web/dist/assets/zh-TW-CG6IjP89.js +1 -0
  12. package/apps/web/dist/index.html +2 -2
  13. package/apps/web/package.json +1 -1
  14. package/apps/web/src/style.css +3 -0
  15. package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
  16. package/node_modules/@pi-harness/api-gateway/dist/index.js +144 -1
  17. package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
  18. package/node_modules/@pi-harness/api-gateway/package.json +1 -1
  19. package/node_modules/@pi-harness/cli/package.json +1 -1
  20. package/node_modules/@pi-harness/host-webserver/package.json +1 -1
  21. package/node_modules/@pi-harness/web-app/package.json +1 -1
  22. package/package.json +6 -6
  23. package/packages/api-gateway/dist/index.d.ts.map +1 -1
  24. package/packages/api-gateway/dist/index.js +144 -1
  25. package/packages/api-gateway/dist/index.js.map +1 -1
  26. package/packages/api-gateway/package.json +1 -1
  27. package/packages/api-gateway/src/index.ts +138 -1
  28. package/packages/api-gateway/test/index.test.ts +104 -3
  29. package/packages/bundle-web-app/package.json +1 -1
  30. package/packages/cli/package.json +1 -1
  31. package/packages/host-webserver/package.json +1 -1
  32. package/apps/web/dist/assets/de-D-l2rv3V.js +0 -1
  33. package/apps/web/dist/assets/en-Dr2zqUBB.js +0 -1
  34. package/apps/web/dist/assets/es-DE_jy231.js +0 -1
  35. package/apps/web/dist/assets/fr-BY56gaJw.js +0 -1
  36. package/apps/web/dist/assets/index-Qupnc3-4.js +0 -29
  37. package/apps/web/dist/assets/ja-CVgWx-2k.js +0 -1
  38. package/apps/web/dist/assets/ko-BpKji4q-.js +0 -1
  39. package/apps/web/dist/assets/pt-BR-Cpdxbmiq.js +0 -1
  40. package/apps/web/dist/assets/ru-D66z4xXo.js +0 -1
  41. package/apps/web/dist/assets/zh-TW-CX8lrXBT.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { existsSync, writeFileSync } from "node:fs";
3
- import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, opendir, readFile, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { SessionManager } from "@earendil-works/pi-coding-agent";
@@ -23,6 +23,12 @@ const PROCESS_FAILURE_TEXT_LIMIT = 400;
23
23
  const PROCESS_TIMEOUT_MS = 120_000;
24
24
  // Mutating commands run user hooks (pre-commit, lint-staged, test suites) and may stage large trees; killing them part-way leaves index.lock and a half-finished operation behind, so they get a far more generous bound than read-only queries.
25
25
  const GIT_MUTATION_TIMEOUT_MS = 120_000;
26
+ const MAX_WORKSPACE_FILES = 5_000;
27
+ const MAX_WORKSPACE_FILE_CANDIDATES = 20_000;
28
+ const MAX_WORKSPACE_DIRECTORIES = 2_000;
29
+ const MAX_WORKSPACE_DEPTH = 16;
30
+ const WORKSPACE_FILE_CACHE_MS = 1_000;
31
+ const IGNORED_WORKSPACE_DIRECTORIES = new Set([".git", "node_modules", ".pi", "dist", "build"]);
26
32
  class PayloadTooLargeError extends Error {
27
33
  constructor() {
28
34
  super("Request body is too large");
@@ -124,6 +130,7 @@ async function createSessionSnapshot(services, events, logger, messages = servic
124
130
  sessionId: session.sessionId,
125
131
  sessionFile: session.sessionFile,
126
132
  name: typeof manager?.getSessionName === "function" ? (manager.getSessionName() ?? undefined) : undefined,
133
+ forked: typeof manager?.getHeader === "function" && typeof manager.getHeader()?.parentSession === "string",
127
134
  messages,
128
135
  entries: typeof manager?.getEntries === "function" ? manager.getEntries() : [],
129
136
  events,
@@ -673,6 +680,106 @@ function gitCommand(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
673
680
  });
674
681
  });
675
682
  }
683
+ function ignoredWorkspaceDirectory(name) {
684
+ return IGNORED_WORKSPACE_DIRECTORIES.has(process.platform === "win32" || process.platform === "darwin" ? name.toLowerCase() : name);
685
+ }
686
+ async function existingWorkspaceFiles(root, candidates, truncated) {
687
+ const canonicalRoot = await realpath(resolve(root));
688
+ const paths = [];
689
+ for (let start = 0; start < candidates.length && paths.length < MAX_WORKSPACE_FILES; start += 64) {
690
+ const batch = candidates.slice(start, start + 64);
691
+ const existing = await Promise.all(batch.map(async (path) => {
692
+ const target = resolve(canonicalRoot, path);
693
+ if (escapesRoot(relative(canonicalRoot, target)))
694
+ return undefined;
695
+ try {
696
+ const canonical = await realpath(target);
697
+ if (escapesRoot(relative(canonicalRoot, canonical)) || !(await stat(canonical)).isFile())
698
+ return undefined;
699
+ return path;
700
+ }
701
+ catch {
702
+ return undefined;
703
+ }
704
+ }));
705
+ for (const path of existing) {
706
+ if (path !== undefined)
707
+ paths.push(path);
708
+ if (paths.length >= MAX_WORKSPACE_FILES)
709
+ break;
710
+ }
711
+ }
712
+ return { paths: paths.sort(), truncated: truncated || paths.length >= MAX_WORKSPACE_FILES };
713
+ }
714
+ async function gitWorkspaceFiles(root) {
715
+ const result = await gitCommand(root, ["ls-files", "--cached", "--others", "--exclude-standard", "--deduplicate", "-z"]);
716
+ if (result.code !== 0 && result.terminated !== "output-limit") {
717
+ if (/not a git repository/iu.test(result.stderr))
718
+ return undefined;
719
+ throw new Error(result.terminated ? gitTerminationMessage("ls-files", result.terminated) : result.stderr.trim() || "Unable to list workspace files");
720
+ }
721
+ const lastBoundary = result.stdout.lastIndexOf("\0");
722
+ const completeOutput = result.terminated === "output-limit" ? result.stdout.slice(0, lastBoundary + 1) : result.stdout;
723
+ const allCandidates = [...new Set(completeOutput.split("\0").filter((path) => path !== "" && !path.includes("\uFFFD")))];
724
+ const candidates = allCandidates.slice(0, MAX_WORKSPACE_FILE_CANDIDATES);
725
+ return existingWorkspaceFiles(root, candidates, result.terminated === "output-limit" || allCandidates.length > candidates.length);
726
+ }
727
+ async function walkedWorkspaceFiles(root) {
728
+ const canonicalRoot = await realpath(resolve(root));
729
+ const pending = [{ directory: canonicalRoot, depth: 0 }];
730
+ const paths = [];
731
+ let directories = 0;
732
+ let scanned = 0;
733
+ let truncated = false;
734
+ while (pending.length > 0 && paths.length < MAX_WORKSPACE_FILES && directories < MAX_WORKSPACE_DIRECTORIES && scanned < MAX_WORKSPACE_FILE_CANDIDATES) {
735
+ const current = pending.shift();
736
+ if (current === undefined)
737
+ break;
738
+ directories += 1;
739
+ try {
740
+ const handle = await opendir(current.directory);
741
+ for await (const entry of handle) {
742
+ scanned += 1;
743
+ if (scanned > MAX_WORKSPACE_FILE_CANDIDATES) {
744
+ truncated = true;
745
+ break;
746
+ }
747
+ const name = entry.name;
748
+ if (name.includes("\uFFFD")) {
749
+ truncated = true;
750
+ continue;
751
+ }
752
+ const target = join(current.directory, name);
753
+ if (entry.isSymbolicLink())
754
+ continue;
755
+ if (entry.isFile()) {
756
+ paths.push(relative(canonicalRoot, target).split(sep).join("/"));
757
+ if (paths.length >= MAX_WORKSPACE_FILES) {
758
+ truncated = true;
759
+ break;
760
+ }
761
+ }
762
+ else if (entry.isDirectory() && !ignoredWorkspaceDirectory(name)) {
763
+ if (current.depth >= MAX_WORKSPACE_DEPTH)
764
+ truncated = true;
765
+ else
766
+ pending.push({ directory: target, depth: current.depth + 1 });
767
+ }
768
+ }
769
+ }
770
+ catch (error) {
771
+ if (current.directory === canonicalRoot)
772
+ throw error;
773
+ truncated = true;
774
+ }
775
+ }
776
+ if (pending.length > 0 || directories >= MAX_WORKSPACE_DIRECTORIES || scanned >= MAX_WORKSPACE_FILE_CANDIDATES)
777
+ truncated = true;
778
+ return { paths: paths.sort(), truncated };
779
+ }
780
+ async function workspaceFiles(root) {
781
+ return (await gitWorkspaceFiles(root)) ?? walkedWorkspaceFiles(root);
782
+ }
676
783
  function probeEveryApiCliAuth() {
677
784
  const executable = process.env.EVERYAPI_CLI_PATH?.trim() || "everyapi";
678
785
  return new Promise((resolveStatus) => {
@@ -774,6 +881,25 @@ export default {
774
881
  };
775
882
  const disposePluginPanels = registerPluginPanels(context, services);
776
883
  let busy = false;
884
+ let workspaceFileCache;
885
+ let workspaceFileRequest;
886
+ const readWorkspaceFiles = (cwd) => {
887
+ if (workspaceFileCache?.cwd === cwd && workspaceFileCache.expiresAt > Date.now())
888
+ return Promise.resolve(workspaceFileCache.catalogue);
889
+ if (workspaceFileRequest?.cwd === cwd)
890
+ return workspaceFileRequest.promise;
891
+ const promise = workspaceFiles(cwd)
892
+ .then((catalogue) => {
893
+ workspaceFileCache = { cwd, expiresAt: Date.now() + WORKSPACE_FILE_CACHE_MS, catalogue };
894
+ return catalogue;
895
+ })
896
+ .finally(() => {
897
+ if (workspaceFileRequest?.promise === promise)
898
+ workspaceFileRequest = undefined;
899
+ });
900
+ workspaceFileRequest = { cwd, promise };
901
+ return promise;
902
+ };
777
903
  const events = [];
778
904
  const eventClients = new Set();
779
905
  const initialRunAt = Date.now();
@@ -1537,6 +1663,21 @@ export default {
1537
1663
  sendJson(response, 200, { items, ...(status.truncated ? { truncated: true } : {}) });
1538
1664
  },
1539
1665
  });
1666
+ const disposeWorkspaceFiles = services.webServer.register({
1667
+ path: "/api/workspace/files",
1668
+ async handler(_request, response) {
1669
+ try {
1670
+ const catalogue = await readWorkspaceFiles(activeCwd(services));
1671
+ sendJson(response, 200, {
1672
+ items: catalogue.paths.map((path) => ({ path, status: "", label: "workspace" })),
1673
+ truncated: catalogue.truncated,
1674
+ });
1675
+ }
1676
+ catch (error) {
1677
+ sendJson(response, 500, { error: errorText(error) });
1678
+ }
1679
+ },
1680
+ });
1540
1681
  const disposeFileDiff = services.webServer.register({
1541
1682
  path: "/api/files/diff",
1542
1683
  async handler(request, response) {
@@ -2233,6 +2374,7 @@ export default {
2233
2374
  modified: item.modified,
2234
2375
  messageCount: item.messageCount,
2235
2376
  firstMessage: item.firstMessage,
2377
+ forked: typeof item.parentSessionPath === "string" && item.parentSessionPath !== "",
2236
2378
  archived: metadata[item.path]?.archived === true,
2237
2379
  pinned: metadata[item.path]?.pinned === true,
2238
2380
  })),
@@ -2270,6 +2412,7 @@ export default {
2270
2412
  disposeWorkspaces();
2271
2413
  disposePickDirectory();
2272
2414
  disposeFiles();
2415
+ disposeWorkspaceFiles();
2273
2416
  disposeFileDiff();
2274
2417
  disposeFileCommit();
2275
2418
  disposeFileRevert();