pipane 0.1.5 → 0.1.7

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 (138) hide show
  1. package/README.md +45 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/bin/pipane-rendezvous.js +21 -0
  4. package/bin/pipane.js +21 -1
  5. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/dist/client/assets/__vite-browser-external-DtLbhLGx.js +1 -0
  65. package/dist/client/assets/app-runtime-Rw-O-AwW.js +1 -0
  66. package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +1 -0
  67. package/dist/client/assets/index-DwbBcYUf.js +2 -0
  68. package/dist/client/assets/index-iblQYBAl.css +1 -0
  69. package/dist/client/assets/main-LGItV9Aj.js +2807 -0
  70. package/dist/client/assets/pairing-page-D-WxlWZR.js +1 -0
  71. package/dist/client/assets/remote-backend-manager-BWuGh30I.js +1 -0
  72. package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +4 -0
  73. package/dist/client/assets/webrtc-frame-transport-BijNnA7N.js +1 -0
  74. package/dist/client/assets/ws-agent-adapter-BRysf-Y7.js +6 -0
  75. package/dist/client/index.html +2 -2
  76. package/dist/server/rendezvous/rendezvous-hub.js +426 -0
  77. package/dist/server/rendezvous/server.js +247 -0
  78. package/dist/server/rendezvous/trust-store.js +432 -0
  79. package/dist/server/server/auth-guard.js +61 -0
  80. package/dist/server/server/backend-connection-authorizer.js +29 -0
  81. package/dist/server/server/backend-identity.js +167 -0
  82. package/dist/server/server/backend-protocol-handler.js +132 -0
  83. package/dist/server/server/backend-trust-store.js +157 -0
  84. package/dist/server/server/backend-webrtc.js +239 -0
  85. package/dist/server/server/bundled-extensions.js +6 -0
  86. package/dist/server/server/conversation-file-access.js +97 -0
  87. package/dist/server/server/extension-status.js +31 -0
  88. package/dist/server/server/frame-connection.js +48 -0
  89. package/dist/server/server/frame-router.js +45 -0
  90. package/dist/server/server/ice-servers.js +24 -0
  91. package/dist/server/server/local-backend-api.js +389 -0
  92. package/dist/server/server/local-settings.js +28 -0
  93. package/dist/server/server/pi-rpc-protocol.js +407 -0
  94. package/dist/server/server/pi-runtime.js +1 -1
  95. package/dist/server/server/process-pool.js +179 -136
  96. package/dist/server/server/rendezvous-client.js +282 -0
  97. package/dist/server/server/rest-api.js +121 -231
  98. package/dist/server/server/server.js +188 -134
  99. package/dist/server/server/session-actor.js +202 -0
  100. package/dist/server/server/session-index.js +111 -18
  101. package/dist/server/server/session-jsonl.js +128 -8
  102. package/dist/server/server/session-path.js +145 -0
  103. package/dist/server/server/session-registry.js +67 -0
  104. package/dist/server/server/update-api.js +28 -0
  105. package/dist/server/server/update-check.js +33 -13
  106. package/dist/server/server/update-manager.js +231 -0
  107. package/dist/server/server/worktree-name.js +124 -0
  108. package/dist/server/server/ws-handler.js +784 -478
  109. package/dist/server/shared/backend-api.js +1 -0
  110. package/dist/server/shared/backend-protocol.js +164 -0
  111. package/dist/server/shared/jsonl-sync.js +41 -0
  112. package/dist/server/shared/node-trust-crypto.js +61 -0
  113. package/dist/server/shared/rendezvous-protocol.js +243 -0
  114. package/dist/server/shared/rpc-timeouts.js +4 -0
  115. package/dist/server/shared/thinking-levels.js +94 -0
  116. package/dist/server/shared/tool-runtime.js +1 -0
  117. package/dist/server/shared/trust-protocol.js +97 -0
  118. package/dist/server/shared/updates.js +4 -0
  119. package/dist/server/shared/ws-protocol.js +473 -0
  120. package/docs/protocol.md +127 -0
  121. package/extensions/canvas.ts +1 -1
  122. package/package.json +42 -17
  123. package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +0 -1
  124. package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +0 -1
  125. package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +0 -1
  126. package/dist/client/assets/_node-stub_node_os-TTKJha15.js +0 -1
  127. package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +0 -1
  128. package/dist/client/assets/_node-stub_stream-DDexIdn_.js +0 -1
  129. package/dist/client/assets/index-BtDAXAo1.css +0 -1
  130. package/dist/client/assets/index-CAQqlK61.js +0 -131
  131. package/dist/client/assets/index-CJEeBmBj.js +0 -3
  132. package/dist/client/assets/index-DUEhKmPQ.js +0 -1
  133. package/dist/client/assets/index-XEVKscP-.js +0 -4443
  134. package/dist/client/assets/index-woI4agCy.js +0 -45
  135. package/dist/server/server/attached-session.js +0 -209
  136. package/dist/server/server/load-trace-store.js +0 -48
  137. package/dist/server/server/session-lifecycle.js +0 -155
  138. package/patches/@mariozechner+pi-web-ui+0.55.3.patch +0 -3437
@@ -1,24 +1,44 @@
1
- /**
2
- * Check the npm registry for the latest version of a package.
3
- * Returns null if the check fails (network error, timeout, etc.)
4
- */
5
- export async function fetchLatestVersion(packageName, timeoutMs = 3000) {
1
+ async function fetchJson(url, timeoutMs, headers) {
2
+ const controller = new AbortController();
3
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6
4
  try {
7
- const controller = new AbortController();
8
- const timer = setTimeout(() => controller.abort(), timeoutMs);
9
- const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
5
+ const response = await fetch(url, {
10
6
  signal: controller.signal,
11
- headers: { Accept: "application/json" },
7
+ headers,
12
8
  });
13
- clearTimeout(timer);
14
- if (!res.ok)
9
+ if (!response.ok)
15
10
  return null;
16
- const data = (await res.json());
17
- return data.version ?? null;
11
+ return await response.json();
18
12
  }
19
13
  catch {
20
14
  return null;
21
15
  }
16
+ finally {
17
+ clearTimeout(timer);
18
+ }
19
+ }
20
+ /** Check the npm registry for the latest version of a package. */
21
+ export async function fetchLatestVersion(packageName, timeoutMs = 3000) {
22
+ const data = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, timeoutMs, { Accept: "application/json" });
23
+ return typeof data?.version === "string" && data.version.trim() ? data.version.trim() : null;
24
+ }
25
+ /** Check Pi's release endpoint, including package migration metadata. */
26
+ export async function fetchLatestPiRelease(currentVersion, timeoutMs = 3000) {
27
+ const data = await fetchJson("https://pi.dev/api/latest-version", timeoutMs, {
28
+ Accept: "application/json",
29
+ "User-Agent": `pipane/${currentVersion}`,
30
+ });
31
+ if (typeof data?.version !== "string" || !data.version.trim())
32
+ return null;
33
+ return {
34
+ version: data.version.trim(),
35
+ ...(typeof data.packageName === "string" && data.packageName.trim()
36
+ ? { packageName: data.packageName.trim() }
37
+ : {}),
38
+ ...(typeof data.note === "string" && data.note.trim()
39
+ ? { note: data.note.trim() }
40
+ : {}),
41
+ };
22
42
  }
23
43
  /**
24
44
  * Compare two semver strings. Returns:
@@ -0,0 +1,231 @@
1
+ import { spawn } from "node:child_process";
2
+ import { DefaultPackageManager, SettingsManager, getAgentDir, } from "@earendil-works/pi-coding-agent";
3
+ import { compareSemver, fetchLatestPiRelease, fetchLatestVersion, } from "./update-check.js";
4
+ const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60_000;
5
+ const MAX_COMMAND_OUTPUT = 16 * 1024;
6
+ const NO_UPDATE_MESSAGES = {
7
+ pipane: "pipane is already up to date.",
8
+ pi: "Pi is already up to date.",
9
+ extensions: "Pi packages are already up to date.",
10
+ };
11
+ function appendBounded(current, chunk) {
12
+ const next = current + chunk.toString();
13
+ return next.length > MAX_COMMAND_OUTPUT ? next.slice(-MAX_COMMAND_OUTPUT) : next;
14
+ }
15
+ export function runUpdateCommand(command, args, cwd, timeoutMs = UPDATE_COMMAND_TIMEOUT_MS) {
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn(command, args, {
18
+ cwd,
19
+ env: process.env,
20
+ stdio: ["ignore", "pipe", "pipe"],
21
+ });
22
+ let stdout = "";
23
+ let stderr = "";
24
+ let timedOut = false;
25
+ let settled = false;
26
+ const timer = setTimeout(() => {
27
+ timedOut = true;
28
+ child.kill("SIGTERM");
29
+ }, timeoutMs);
30
+ child.stdout?.on("data", (chunk) => { stdout = appendBounded(stdout, chunk); });
31
+ child.stderr?.on("data", (chunk) => { stderr = appendBounded(stderr, chunk); });
32
+ child.on("error", (error) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ clearTimeout(timer);
37
+ reject(new Error(`Failed to start ${command}: ${error.message}`));
38
+ });
39
+ child.on("close", (code, signal) => {
40
+ if (settled)
41
+ return;
42
+ settled = true;
43
+ clearTimeout(timer);
44
+ if (timedOut) {
45
+ reject(new Error(`Update command timed out after ${Math.round(timeoutMs / 1000)} seconds.`));
46
+ return;
47
+ }
48
+ if (code !== 0) {
49
+ const detail = (stderr || stdout).trim();
50
+ const suffix = detail ? `: ${detail}` : signal ? ` (signal ${signal})` : "";
51
+ reject(new Error(`Update command failed with exit code ${code ?? "unknown"}${suffix}`));
52
+ return;
53
+ }
54
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
55
+ });
56
+ });
57
+ }
58
+ async function getInstalledPiVersion(launch, cwd) {
59
+ try {
60
+ const result = await runUpdateCommand(launch.command, [...launch.baseArgs, "--version"], cwd, 10_000);
61
+ const match = `${result.stdout}\n${result.stderr}`.match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/);
62
+ return match?.[1] ?? null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ async function checkManagedExtensionUpdates(cwd) {
69
+ if (process.env.PI_OFFLINE)
70
+ return [];
71
+ const agentDir = getAgentDir();
72
+ // RPC mode does not prompt for project trust. Check the user-level package set,
73
+ // which is shared by every Pi worker, without loading untrusted project config.
74
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
75
+ const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
76
+ const updates = await packageManager.checkForAvailableUpdates();
77
+ return updates.map((update) => update.displayName);
78
+ }
79
+ function cloneSnapshot(snapshot) {
80
+ return {
81
+ checkedAt: snapshot.checkedAt,
82
+ notices: snapshot.notices.map((notice) => ({
83
+ ...notice,
84
+ ...(notice.packages ? { packages: [...notice.packages] } : {}),
85
+ })),
86
+ };
87
+ }
88
+ export class UpdateManager {
89
+ options;
90
+ dependencies;
91
+ snapshot = { checkedAt: new Date(0).toISOString(), notices: [] };
92
+ checkPromise;
93
+ activeUpdate;
94
+ constructor(options) {
95
+ this.options = options;
96
+ this.dependencies = {
97
+ fetchLatestVersion,
98
+ fetchLatestPiRelease,
99
+ getPiVersion: getInstalledPiVersion,
100
+ checkExtensionUpdates: checkManagedExtensionUpdates,
101
+ runCommand: runUpdateCommand,
102
+ ...options.dependencies,
103
+ };
104
+ }
105
+ get currentSnapshot() {
106
+ return cloneSnapshot(this.snapshot);
107
+ }
108
+ check() {
109
+ if (!this.checkPromise) {
110
+ this.checkPromise = this.performCheck();
111
+ }
112
+ return this.checkPromise.then(cloneSnapshot);
113
+ }
114
+ async performCheck() {
115
+ if (this.options.skipChecks || process.env.PIPANE_SKIP_UPDATE_CHECK || process.env.PI_OFFLINE) {
116
+ this.snapshot = { checkedAt: new Date().toISOString(), notices: [] };
117
+ return this.snapshot;
118
+ }
119
+ const pipaneCheck = this.checkPipaneUpdate();
120
+ const piCheck = process.env.PI_SKIP_VERSION_CHECK
121
+ ? Promise.resolve(null)
122
+ : this.checkPiUpdate();
123
+ const extensionCheck = this.checkExtensionUpdate();
124
+ const notices = (await Promise.all([pipaneCheck, piCheck, extensionCheck]))
125
+ .filter((notice) => notice !== null);
126
+ this.snapshot = { checkedAt: new Date().toISOString(), notices };
127
+ return this.snapshot;
128
+ }
129
+ async checkPipaneUpdate() {
130
+ try {
131
+ const latestVersion = await this.dependencies.fetchLatestVersion(this.options.pipanePackageName);
132
+ if (!latestVersion || compareSemver(this.options.pipaneVersion, latestVersion) >= 0)
133
+ return null;
134
+ return {
135
+ target: "pipane",
136
+ currentVersion: this.options.pipaneVersion,
137
+ latestVersion,
138
+ };
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ async checkPiUpdate() {
145
+ try {
146
+ const currentVersion = await this.dependencies.getPiVersion(this.options.piLaunch, this.options.cwd);
147
+ if (!currentVersion)
148
+ return null;
149
+ const release = await this.dependencies.fetchLatestPiRelease(currentVersion);
150
+ if (!release || compareSemver(currentVersion, release.version) >= 0)
151
+ return null;
152
+ return {
153
+ target: "pi",
154
+ currentVersion,
155
+ latestVersion: release.version,
156
+ };
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ async checkExtensionUpdate() {
163
+ try {
164
+ const packages = [...new Set(await this.dependencies.checkExtensionUpdates(this.options.cwd))]
165
+ .sort((left, right) => left.localeCompare(right));
166
+ return packages.length > 0 ? { target: "extensions", packages } : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ async run(target) {
173
+ await this.check();
174
+ if (this.activeUpdate) {
175
+ throw new Error(`An update is already running for ${this.activeUpdate}.`);
176
+ }
177
+ const notice = this.snapshot.notices.find((candidate) => candidate.target === target);
178
+ if (!notice) {
179
+ return {
180
+ target,
181
+ message: NO_UPDATE_MESSAGES[target],
182
+ restartRequired: false,
183
+ };
184
+ }
185
+ this.activeUpdate = target;
186
+ try {
187
+ const result = await this.performUpdate(notice);
188
+ this.snapshot = {
189
+ checkedAt: new Date().toISOString(),
190
+ notices: this.snapshot.notices.filter((candidate) => candidate.target !== target),
191
+ };
192
+ return result;
193
+ }
194
+ finally {
195
+ this.activeUpdate = undefined;
196
+ }
197
+ }
198
+ async performUpdate(notice) {
199
+ switch (notice.target) {
200
+ case "pipane": {
201
+ if (!notice.latestVersion)
202
+ throw new Error("The pipane update has no target version.");
203
+ await this.dependencies.runCommand("npm", ["install", "-g", "--ignore-scripts", `${this.options.pipanePackageName}@${notice.latestVersion}`], this.options.cwd);
204
+ return {
205
+ target: "pipane",
206
+ message: `pipane v${notice.latestVersion} installed. Restart pipane to use the new version.`,
207
+ restartRequired: true,
208
+ };
209
+ }
210
+ case "pi": {
211
+ await this.dependencies.runCommand(this.options.piLaunch.command, [...this.options.piLaunch.baseArgs, "update", "--self"], this.options.cwd);
212
+ await this.options.onPiRuntimeChanged?.();
213
+ return {
214
+ target: "pi",
215
+ message: `Pi${notice.latestVersion ? ` v${notice.latestVersion}` : ""} installed. Pi workers are restarting.`,
216
+ restartRequired: false,
217
+ };
218
+ }
219
+ case "extensions": {
220
+ await this.dependencies.runCommand(this.options.piLaunch.command, [...this.options.piLaunch.baseArgs, "update", "--extensions"], this.options.cwd);
221
+ await this.options.onPiRuntimeChanged?.();
222
+ const count = notice.packages?.length ?? 0;
223
+ return {
224
+ target: "extensions",
225
+ message: `${count} Pi package${count === 1 ? "" : "s"} updated. Pi workers are restarting.`,
226
+ restartRequired: false,
227
+ };
228
+ }
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Filesystem-only Git checkout lookup. One instance is shared across all
5
+ * sessions in a single listing, then discarded so worktree changes are visible
6
+ * on the next request.
7
+ */
8
+ class GitCheckoutLookup {
9
+ cache = new Map();
10
+ resolve(location) {
11
+ if (!location)
12
+ return null;
13
+ let dir = path.resolve(location);
14
+ const visited = [];
15
+ while (true) {
16
+ if (this.cache.has(dir)) {
17
+ return this.remember(visited, this.cache.get(dir) ?? null);
18
+ }
19
+ visited.push(dir);
20
+ const gitPath = path.join(dir, ".git");
21
+ if (existsSync(gitPath)) {
22
+ return this.remember(visited, this.readCheckout(dir, gitPath));
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir)
26
+ return this.remember(visited, null);
27
+ dir = parent;
28
+ }
29
+ }
30
+ readCheckout(root, gitPath) {
31
+ try {
32
+ const stat = statSync(gitPath);
33
+ if (stat.isDirectory()) {
34
+ return {
35
+ root,
36
+ commonGitDir: canonicalPath(gitPath),
37
+ linked: false,
38
+ };
39
+ }
40
+ if (!stat.isFile())
41
+ return null;
42
+ const marker = readFileSync(gitPath, "utf8").trim();
43
+ if (!marker.startsWith("gitdir: "))
44
+ return null;
45
+ const gitDir = path.resolve(root, marker.slice("gitdir: ".length).trim());
46
+ if (!existsSync(gitDir))
47
+ return null;
48
+ const commonDirMarker = path.join(gitDir, "commondir");
49
+ if (!existsSync(commonDirMarker)) {
50
+ // A separate Git directory or submodule checkout uses a .git file too,
51
+ // but unlike a linked worktree it has no commondir pointer.
52
+ return {
53
+ root,
54
+ commonGitDir: canonicalPath(gitDir),
55
+ linked: false,
56
+ };
57
+ }
58
+ const commonDir = path.resolve(gitDir, readFileSync(commonDirMarker, "utf8").trim());
59
+ if (!existsSync(commonDir))
60
+ return null;
61
+ return {
62
+ root,
63
+ commonGitDir: canonicalPath(commonDir),
64
+ linked: true,
65
+ };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ remember(visited, checkout) {
72
+ for (const location of visited)
73
+ this.cache.set(location, checkout);
74
+ return checkout;
75
+ }
76
+ }
77
+ function canonicalPath(value) {
78
+ try {
79
+ return realpathSync(value);
80
+ }
81
+ catch {
82
+ return path.resolve(value);
83
+ }
84
+ }
85
+ function checkoutName(checkout) {
86
+ return checkout?.linked ? path.basename(checkout.root) || "root" : "root";
87
+ }
88
+ /**
89
+ * Create a resolver with a request-scoped filesystem cache. This avoids Git
90
+ * subprocesses and ensures shared cwd/path ancestors are only inspected once
91
+ * while listing many sessions.
92
+ */
93
+ export function createWorktreeNameResolver() {
94
+ const lookup = new GitCheckoutLookup();
95
+ return (cwd, recentToolPaths = []) => {
96
+ const cwdCheckout = lookup.resolve(cwd);
97
+ // Tool paths are stored oldest-to-newest. The newest successful project
98
+ // file operation is stronger evidence than Pi's immutable session cwd.
99
+ for (let i = recentToolPaths.length - 1; i >= 0; i--) {
100
+ const activityCheckout = lookup.resolve(recentToolPaths[i]);
101
+ if (!activityCheckout)
102
+ continue;
103
+ if (!cwdCheckout || activityCheckout.commonGitDir === cwdCheckout.commonGitDir) {
104
+ return checkoutName(activityCheckout);
105
+ }
106
+ }
107
+ // The session cwd says where Pi was launched, not which checkout the
108
+ // conversation is actively using. Until successful same-repository file
109
+ // activity provides evidence, treat the session as root.
110
+ return "root";
111
+ };
112
+ }
113
+ /**
114
+ * Return a short label for the Git checkout a session is actively using.
115
+ *
116
+ * Pi cannot change its process cwd persistently, so the recorded cwd only
117
+ * identifies the repository. Recent successful read/write/edit paths identify
118
+ * its active checkout; without one, the label defaults to root. Linked
119
+ * worktrees are recognized by their commondir pointer. Removed worktrees
120
+ * naturally stop matching because their .git metadata is no longer present.
121
+ */
122
+ export function resolveWorktreeName(cwd, recentToolPaths = []) {
123
+ return createWorktreeNameResolver()(cwd, recentToolPaths);
124
+ }