pi-acp 0.0.29 → 0.0.30

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/dist/index.js CHANGED
@@ -529,16 +529,16 @@ function expandSlashCommand(text, fileCommands) {
529
529
  // src/acp/translate/pi-tools.ts
530
530
  function toolResultToText(result) {
531
531
  if (!result) return "";
532
- const content = result.content;
533
- if (Array.isArray(content)) {
534
- const texts = content.map((c) => c?.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean);
535
- if (texts.length) return texts.join("");
536
- }
537
532
  const details = result?.details;
538
533
  const diff = details?.diff;
539
534
  if (typeof diff === "string" && diff.trim()) {
540
535
  return diff;
541
536
  }
537
+ const content = result.content;
538
+ if (Array.isArray(content)) {
539
+ const texts = content.map((c) => c?.type === "text" && typeof c.text === "string" ? c.text : "").filter(Boolean);
540
+ if (texts.length) return texts.join("");
541
+ }
542
542
  const stdout = (typeof details?.stdout === "string" ? details.stdout : void 0) ?? (typeof result?.stdout === "string" ? result.stdout : void 0) ?? (typeof details?.output === "string" ? details.output : void 0) ?? (typeof result?.output === "string" ? result.output : void 0);
543
543
  const stderr = (typeof details?.stderr === "string" ? details.stderr : void 0) ?? (typeof result?.stderr === "string" ? result.stderr : void 0);
544
544
  const exitCode = (typeof details?.exitCode === "number" ? details.exitCode : void 0) ?? (typeof result?.exitCode === "number" ? result.exitCode : void 0) ?? (typeof details?.code === "number" ? details.code : void 0) ?? (typeof result?.code === "number" ? result.code : void 0);
@@ -576,8 +576,58 @@ function findUniqueLineNumber(text, needle) {
576
576
  }
577
577
  return line;
578
578
  }
579
+ function getToolPath(args) {
580
+ const record = args;
581
+ if (typeof record?.path === "string") return record.path;
582
+ if (typeof record?.file_path === "string") return record.file_path;
583
+ return void 0;
584
+ }
585
+ function getParsedEdits(args) {
586
+ const record = args;
587
+ const parsed = [];
588
+ if (typeof record?.oldText === "string" && typeof record?.newText === "string") {
589
+ parsed.push({ oldText: record.oldText, newText: record.newText });
590
+ }
591
+ let edits = record?.edits;
592
+ if (typeof edits === "string") {
593
+ try {
594
+ edits = JSON.parse(edits);
595
+ } catch {
596
+ edits = void 0;
597
+ }
598
+ }
599
+ if (Array.isArray(edits)) {
600
+ for (const edit of edits) {
601
+ const item = edit;
602
+ if (typeof item?.oldText === "string" && typeof item?.newText === "string") {
603
+ parsed.push({ oldText: item.oldText, newText: item.newText });
604
+ }
605
+ }
606
+ }
607
+ return parsed;
608
+ }
609
+ function getEditOldTexts(args) {
610
+ const record = args;
611
+ const oldTexts = getParsedEdits(args).map((edit) => edit.oldText);
612
+ if (typeof record?.oldText === "string" && !oldTexts.includes(record.oldText)) oldTexts.push(record.oldText);
613
+ let edits = record?.edits;
614
+ if (typeof edits === "string") {
615
+ try {
616
+ edits = JSON.parse(edits);
617
+ } catch {
618
+ edits = void 0;
619
+ }
620
+ }
621
+ if (Array.isArray(edits)) {
622
+ for (const edit of edits) {
623
+ const oldText = edit?.oldText;
624
+ if (typeof oldText === "string" && !oldTexts.includes(oldText)) oldTexts.push(oldText);
625
+ }
626
+ }
627
+ return oldTexts;
628
+ }
579
629
  function toToolCallLocations(args, cwd, line) {
580
- const path = typeof args?.path === "string" ? args.path : void 0;
630
+ const path = getToolPath(args);
581
631
  if (!path) return void 0;
582
632
  const resolvedPath = isAbsolute(path) ? path : resolvePath(cwd, path);
583
633
  return [{ path: resolvedPath, ...typeof line === "number" ? { line } : {} }];
@@ -694,10 +744,11 @@ var PiAcpSession = class {
694
744
  // pi can emit multiple `turn_end` events for a single user prompt (e.g. after tool_use).
695
745
  // The overall agent loop completes when `agent_end` is emitted.
696
746
  inAgentLoop = false;
697
- // For ACP diff support: capture file contents before edits, then emit ToolCallContent {type:"diff"}.
698
- // This is due to pi sending diff as a string as opposed to ACP expected diff format.
699
- // Compatible format may need to be implemented in pi in the future.
700
- editSnapshots = /* @__PURE__ */ new Map();
747
+ // For ACP diff support: capture file contents before edit/write mutations,
748
+ // then emit ToolCallContent {type:"diff"}. Compatible structured edit/write
749
+ // events may need to be implemented in pi in the future.
750
+ fileSnapshots = /* @__PURE__ */ new Map();
751
+ fileMutationToolCallIds = /* @__PURE__ */ new Set();
701
752
  // Ensure `session/update` notifications are sent in order and can be awaited
702
753
  // before completing a `session/prompt` request.
703
754
  lastEmit = Promise.resolve();
@@ -878,16 +929,25 @@ var PiAcpSession = class {
878
929
  const toolName = String(ev.toolName ?? "tool");
879
930
  const args = ev.args;
880
931
  let line;
881
- if (toolName === "edit") {
882
- const p = typeof args?.path === "string" ? args.path : void 0;
932
+ const isFileMutation = toolName === "edit" || toolName === "write";
933
+ let snapshotOldText;
934
+ if (isFileMutation) {
935
+ this.fileMutationToolCallIds.add(toolCallId);
936
+ const p = getToolPath(args);
883
937
  if (p) {
884
938
  try {
885
939
  const abs = isAbsolute(p) ? p : resolvePath(this.cwd, p);
886
- const oldText = readFileSync3(abs, "utf8");
887
- this.editSnapshots.set(toolCallId, { path: p, oldText });
888
- const needle = typeof args?.oldText === "string" ? args.oldText : "";
889
- line = findUniqueLineNumber(oldText, needle);
940
+ snapshotOldText = readFileSync3(abs, "utf8");
941
+ this.fileSnapshots.set(toolCallId, { path: p, oldText: snapshotOldText });
942
+ if (toolName === "edit") {
943
+ for (const needle of getEditOldTexts(args)) {
944
+ line = findUniqueLineNumber(snapshotOldText, needle);
945
+ if (typeof line === "number") break;
946
+ }
947
+ }
890
948
  } catch {
949
+ snapshotOldText = null;
950
+ this.fileSnapshots.set(toolCallId, { path: p, oldText: null });
891
951
  }
892
952
  }
893
953
  }
@@ -919,13 +979,13 @@ var PiAcpSession = class {
919
979
  const toolCallId = String(ev.toolCallId ?? "");
920
980
  if (!toolCallId) break;
921
981
  const partial = ev.partialResult;
922
- const text = toolResultToText(partial);
982
+ const text = this.fileMutationToolCallIds.has(toolCallId) ? "" : toolResultToText(partial);
923
983
  this.emit({
924
984
  sessionUpdate: "tool_call_update",
925
985
  toolCallId,
926
986
  status: "in_progress",
927
987
  content: text ? [{ type: "content", content: { type: "text", text } }] : void 0,
928
- rawOutput: partial
988
+ ...this.fileMutationToolCallIds.has(toolCallId) ? {} : { rawOutput: partial }
929
989
  });
930
990
  break;
931
991
  }
@@ -935,27 +995,28 @@ var PiAcpSession = class {
935
995
  const result = ev.result;
936
996
  const isError = Boolean(ev.isError);
937
997
  const text = toolResultToText(result);
938
- const snapshot = this.editSnapshots.get(toolCallId);
998
+ const snapshot = this.fileSnapshots.get(toolCallId);
939
999
  let content;
1000
+ let hasStructuredDiff = false;
940
1001
  if (!isError && snapshot) {
941
1002
  try {
942
1003
  const abs = isAbsolute(snapshot.path) ? snapshot.path : resolvePath(this.cwd, snapshot.path);
943
1004
  const newText = readFileSync3(abs, "utf8");
944
- if (newText !== snapshot.oldText) {
1005
+ if (snapshot.oldText === null || newText !== snapshot.oldText) {
1006
+ hasStructuredDiff = true;
945
1007
  content = [
946
1008
  {
947
1009
  type: "diff",
948
1010
  path: snapshot.path,
949
1011
  oldText: snapshot.oldText,
950
1012
  newText
951
- },
952
- ...text ? [{ type: "content", content: { type: "text", text } }] : []
1013
+ }
953
1014
  ];
954
1015
  }
955
1016
  } catch {
956
1017
  }
957
1018
  }
958
- if (!content && text) {
1019
+ if (!content && !hasStructuredDiff && text) {
959
1020
  content = [{ type: "content", content: { type: "text", text } }];
960
1021
  }
961
1022
  this.emit({
@@ -963,10 +1024,11 @@ var PiAcpSession = class {
963
1024
  toolCallId,
964
1025
  status: isError ? "failed" : "completed",
965
1026
  content,
966
- rawOutput: result
1027
+ ...hasStructuredDiff ? {} : { rawOutput: result }
967
1028
  });
968
1029
  this.currentToolCalls.delete(toolCallId);
969
- this.editSnapshots.delete(toolCallId);
1030
+ this.fileSnapshots.delete(toolCallId);
1031
+ this.fileMutationToolCallIds.delete(toolCallId);
970
1032
  break;
971
1033
  }
972
1034
  case "extension_ui_request": {
@@ -1430,10 +1492,9 @@ function listPiSessions() {
1430
1492
  });
1431
1493
  return items;
1432
1494
  }
1433
- function findPiSessionFile(sessionId) {
1495
+ function findPiSession(sessionId) {
1434
1496
  const all = listPiSessions();
1435
- const found = all.find((s) => s.sessionId === sessionId);
1436
- return found?.sessionFile ?? null;
1497
+ return all.find((s) => s.sessionId === sessionId) ?? null;
1437
1498
  }
1438
1499
 
1439
1500
  // src/acp/translate/pi-messages.ts
@@ -1647,6 +1708,7 @@ var PiAcpAgent = class {
1647
1708
  conn;
1648
1709
  sessions = new SessionManager();
1649
1710
  store = new SessionStore();
1711
+ restoringSessions = /* @__PURE__ */ new Map();
1650
1712
  dispose() {
1651
1713
  this.sessions.disposeAll();
1652
1714
  }
@@ -1667,6 +1729,66 @@ var PiAcpAgent = class {
1667
1729
  }
1668
1730
  this.store.delete(sessionId);
1669
1731
  }
1732
+ findStoredSession(sessionId) {
1733
+ const stored = this.store.get(sessionId);
1734
+ if (stored?.cwd && stored?.sessionFile) {
1735
+ return { cwd: stored.cwd, sessionFile: stored.sessionFile };
1736
+ }
1737
+ const piSession = findPiSession(sessionId);
1738
+ if (!piSession) return null;
1739
+ this.store.upsert({
1740
+ sessionId,
1741
+ cwd: piSession.cwd,
1742
+ sessionFile: piSession.sessionFile
1743
+ });
1744
+ return {
1745
+ cwd: piSession.cwd,
1746
+ sessionFile: piSession.sessionFile
1747
+ };
1748
+ }
1749
+ async restoreSession(sessionId, opts) {
1750
+ const existing = this.sessions.maybeGet(sessionId);
1751
+ if (existing) return existing;
1752
+ const inFlight = this.restoringSessions.get(sessionId);
1753
+ if (inFlight) return inFlight;
1754
+ const restorePromise = (async () => {
1755
+ const stored = this.findStoredSession(sessionId);
1756
+ if (!stored) {
1757
+ throw RequestError3.invalidParams(`Unknown sessionId: ${sessionId}`);
1758
+ }
1759
+ const cwd = opts?.cwd ?? stored.cwd;
1760
+ let proc;
1761
+ try {
1762
+ proc = await PiRpcProcess.spawn({
1763
+ cwd,
1764
+ sessionPath: stored.sessionFile,
1765
+ piCommand: process.env.PI_ACP_PI_COMMAND
1766
+ });
1767
+ } catch (e) {
1768
+ if (e?.name === "PiRpcSpawnError") {
1769
+ throw RequestError3.internalError({ code: e?.code }, String(e?.message ?? e));
1770
+ }
1771
+ throw e;
1772
+ }
1773
+ const fileCommands = loadSlashCommands(cwd);
1774
+ const session = this.sessions.getOrCreate(sessionId, {
1775
+ cwd,
1776
+ mcpServers: opts?.mcpServers ?? [],
1777
+ conn: this.conn,
1778
+ proc,
1779
+ fileCommands
1780
+ });
1781
+ this.lastSessionCwd = cwd;
1782
+ this.store.upsert({ sessionId, cwd, sessionFile: stored.sessionFile });
1783
+ return session;
1784
+ })();
1785
+ this.restoringSessions.set(sessionId, restorePromise);
1786
+ try {
1787
+ return await restorePromise;
1788
+ } finally {
1789
+ this.restoringSessions.delete(sessionId);
1790
+ }
1791
+ }
1670
1792
  async initialize(params) {
1671
1793
  const supportedVersion = 1;
1672
1794
  const requested = params.protocolVersion;
@@ -1813,7 +1935,7 @@ var PiAcpAgent = class {
1813
1935
  return;
1814
1936
  }
1815
1937
  async prompt(params) {
1816
- const session = this.sessions.get(params.sessionId);
1938
+ const session = await this.restoreSession(params.sessionId);
1817
1939
  const { message, images } = promptToPiMessage(params.prompt);
1818
1940
  if (images.length === 0 && message.trimStart().startsWith("/")) {
1819
1941
  const trimmed = message.trim();
@@ -2186,7 +2308,8 @@ ${JSON.stringify(stats, null, 2)}`;
2186
2308
  return { stopReason };
2187
2309
  }
2188
2310
  async cancel(params) {
2189
- const session = this.sessions.get(params.sessionId);
2311
+ const session = this.sessions.maybeGet(params.sessionId);
2312
+ if (!session) return;
2190
2313
  await session.cancel();
2191
2314
  }
2192
2315
  async unstable_listSessions(params) {
@@ -2212,38 +2335,22 @@ ${JSON.stringify(stats, null, 2)}`;
2212
2335
  }
2213
2336
  this.sessions.close(params.sessionId);
2214
2337
  this.lastSessionCwd = params.cwd;
2215
- const stored = this.store.get(params.sessionId);
2216
- const sessionFile = stored?.sessionFile ?? findPiSessionFile(params.sessionId);
2217
- if (!sessionFile) {
2338
+ const stored = this.findStoredSession(params.sessionId);
2339
+ if (!stored) {
2218
2340
  throw RequestError3.invalidParams(`Unknown sessionId: ${params.sessionId}`);
2219
2341
  }
2220
- let proc;
2221
- try {
2222
- proc = await PiRpcProcess.spawn({
2223
- cwd: params.cwd,
2224
- sessionPath: sessionFile,
2225
- piCommand: process.env.PI_ACP_PI_COMMAND
2226
- });
2227
- } catch (e) {
2228
- if (e?.name === "PiRpcSpawnError") {
2229
- throw RequestError3.internalError({ code: e?.code }, String(e?.message ?? e));
2230
- }
2231
- throw e;
2232
- }
2233
- const fileCommands = loadSlashCommands(params.cwd);
2234
2342
  const enableSkillCommands = getEnableSkillCommands(params.cwd);
2235
- const session = this.sessions.getOrCreate(params.sessionId, {
2343
+ const session = await this.restoreSession(params.sessionId, {
2236
2344
  cwd: params.cwd,
2237
- mcpServers: params.mcpServers,
2238
- conn: this.conn,
2239
- proc,
2240
- fileCommands
2345
+ mcpServers: params.mcpServers
2241
2346
  });
2347
+ const proc = session.proc;
2348
+ const fileCommands = loadSlashCommands(params.cwd);
2242
2349
  this.sessions.closeAllExcept?.(session.sessionId);
2243
2350
  this.store.upsert({
2244
2351
  sessionId: params.sessionId,
2245
2352
  cwd: params.cwd,
2246
- sessionFile
2353
+ sessionFile: stored.sessionFile
2247
2354
  });
2248
2355
  const data = await proc.getMessages();
2249
2356
  const messages = Array.isArray(data?.messages) ? data.messages : [];
@@ -2343,12 +2450,12 @@ ${JSON.stringify(stats, null, 2)}`;
2343
2450
  return response;
2344
2451
  }
2345
2452
  async unstable_setSessionModel(params) {
2346
- const session = this.sessions.get(params.sessionId);
2453
+ const session = await this.restoreSession(params.sessionId);
2347
2454
  await setSessionModel(session.proc, params.modelId);
2348
2455
  await emitConfigOptionsUpdate(this.conn, session.sessionId, session.proc);
2349
2456
  }
2350
2457
  async setSessionMode(params) {
2351
- const session = this.sessions.get(params.sessionId);
2458
+ const session = await this.restoreSession(params.sessionId);
2352
2459
  const mode = String(params.modeId);
2353
2460
  if (!isThinkingLevel(mode)) {
2354
2461
  throw RequestError3.invalidParams(`Unknown modeId: ${mode}`);
@@ -2365,7 +2472,7 @@ ${JSON.stringify(stats, null, 2)}`;
2365
2472
  return {};
2366
2473
  }
2367
2474
  async setSessionConfigOption(params) {
2368
- const session = this.sessions.get(params.sessionId);
2475
+ const session = await this.restoreSession(params.sessionId);
2369
2476
  const configId = String(params.configId);
2370
2477
  if (typeof params.value !== "string") {
2371
2478
  throw RequestError3.invalidParams(`Expected string value for config option: ${configId}`);