@narumitw/pi-sync 0.54.2 → 0.56.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.
Files changed (194) hide show
  1. package/README.md +40 -15
  2. package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
  3. package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
  4. package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
  5. package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
  6. package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
  7. package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
  8. package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
  9. package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
  10. package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
  11. package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
  12. package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
  13. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
  14. package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
  15. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
  16. package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
  17. package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
  18. package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
  19. package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
  20. package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
  21. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
  22. package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
  23. package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
  24. package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
  25. package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
  26. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
  27. package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
  28. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
  29. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
  30. package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
  31. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
  32. package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
  33. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
  34. package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
  35. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
  36. package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
  37. package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
  38. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
  39. package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
  40. package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
  41. package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
  42. package/dist/chunks/chunk-QGK445K7.ts +21 -0
  43. package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
  44. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
  45. package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
  46. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
  47. package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
  48. package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
  49. package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
  50. package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
  51. package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
  52. package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
  53. package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
  54. package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
  55. package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
  56. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
  57. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  58. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  59. package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
  60. package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
  61. package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
  62. package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
  63. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
  64. package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
  65. package/dist/chunks/config-KGIYTTXQ.ts +37 -0
  66. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
  67. package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
  68. package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
  69. package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
  70. package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
  71. package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
  72. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
  73. package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
  74. package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
  75. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
  76. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
  77. package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
  78. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
  79. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
  80. package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
  81. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
  82. package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
  83. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
  84. package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
  85. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
  86. package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
  87. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
  88. package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
  89. package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
  90. package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
  91. package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
  92. package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
  93. package/dist/index.ts +246 -131
  94. package/dist/index.ts.map +3 -3
  95. package/docs/agent-mode-verification.md +99 -0
  96. package/docs/continuous-sync-verification.md +87 -0
  97. package/docs/settings.md +131 -11
  98. package/docs/tui-contract.md +61 -0
  99. package/package.json +3 -2
  100. package/src/backends/git/git-backend.ts +1 -4
  101. package/src/backends/git/git-runner.ts +1 -5
  102. package/src/backends/s3/s3-backend.ts +3 -25
  103. package/src/backends/s3/s3-client.ts +14 -2
  104. package/src/backends/snapshot-pointer.ts +27 -0
  105. package/src/backends/webdav/webdav-backend.ts +3 -29
  106. package/src/backends/webdav/webdav-client.ts +11 -7
  107. package/src/commands/command-execution.ts +7 -3
  108. package/src/commands/command-handler.ts +15 -3
  109. package/src/commands/command-types.ts +10 -0
  110. package/src/commands/command.ts +4 -1
  111. package/src/settings/config.ts +20 -10
  112. package/src/settings/settings-store.ts +1 -34
  113. package/src/settings/settings-types.ts +10 -0
  114. package/src/settings/settings-validation.ts +4 -1
  115. package/src/snapshot/snapshot-codec.ts +1 -5
  116. package/src/state/lock.ts +13 -10
  117. package/src/state/merge-baseline-store.ts +19 -1
  118. package/src/state/state-directory.ts +8 -1
  119. package/src/sync/agent-candidate.ts +251 -0
  120. package/src/sync/agent-plan.ts +87 -0
  121. package/src/sync/agent-policy.ts +13 -0
  122. package/src/sync/agent-request.ts +17 -0
  123. package/src/sync/agent-runner.ts +188 -0
  124. package/src/sync/agent-store.ts +151 -0
  125. package/src/sync/agent-sync.ts +91 -0
  126. package/src/sync/apply-review.ts +63 -0
  127. package/src/sync/automatic-sync.ts +46 -33
  128. package/src/sync/continuous-errors.ts +85 -0
  129. package/src/sync/continuous-lease.ts +221 -0
  130. package/src/sync/continuous-record.ts +179 -0
  131. package/src/sync/continuous-storage.ts +55 -0
  132. package/src/sync/continuous-sync.ts +516 -0
  133. package/src/sync/continuous-watch.ts +59 -0
  134. package/src/sync/merged-sync.ts +268 -112
  135. package/src/sync/remote-snapshot.ts +2 -3
  136. package/src/sync/settings-merge.ts +19 -3
  137. package/src/sync/setup-switch.ts +1 -5
  138. package/src/sync/startup-check.ts +51 -29
  139. package/src/sync/startup-timings.ts +90 -0
  140. package/src/sync/sync-inspection.ts +16 -5
  141. package/src/sync/sync-mutations.ts +55 -28
  142. package/src/sync/sync-queries.ts +19 -3
  143. package/src/sync/text-merge.ts +3 -3
  144. package/src/sync-extension.ts +75 -19
  145. package/src/ui/agent-mode-setting.ts +91 -0
  146. package/src/ui/manager-state.ts +12 -6
  147. package/src/ui/manager-ui.ts +57 -7
  148. package/src/ui/merge-review.ts +3 -3
  149. package/src/ui/merged-sync-summary.ts +2 -18
  150. package/src/ui/remote-selection-ui.ts +5 -5
  151. package/src/ui/settings-frame.ts +55 -0
  152. package/src/ui/settings-ui.ts +223 -43
  153. package/src/ui/setup/s3-credentials-ui.ts +1 -5
  154. package/src/ui/setup/setup-name-ui.ts +2 -5
  155. package/src/ui/setup/setup-prompts.ts +4 -3
  156. package/src/ui/setup/setup-switcher.ts +2 -0
  157. package/src/ui/sync-behavior.ts +15 -0
  158. package/src/ui/sync-format.ts +69 -38
  159. package/src/ui/sync-setups-ui.ts +3 -5
  160. package/src/ui/transfer-changes.ts +33 -0
  161. package/src/ui/transfer-review.ts +27 -0
  162. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  163. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  164. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  165. package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
  166. package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
  167. package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
  168. package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
  169. package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
  170. package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
  171. package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
  172. package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
  173. package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
  174. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  175. package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
  176. package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
  177. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  178. package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
  179. package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
  180. package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
  181. package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
  182. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
  183. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
  184. /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
  185. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
  186. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
  187. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
  188. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
  189. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
  190. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
  191. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
  192. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
  193. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
  194. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { completeSyncArguments, splitArgs } from "./commands/command.js";
3
3
  import { handleCommand } from "./commands/command-handler.js";
4
4
  import type { AnySyncConfig } from "./settings/settings-types.js";
@@ -19,6 +19,31 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
19
19
  let sessionAbort = new AbortController();
20
20
  let shutdownAbort: AbortController | undefined;
21
21
  let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
22
+ let continuous: ReturnType<typeof import("./sync/continuous-sync.js")["createContinuousSync"]> | undefined;
23
+ let owner: ExtensionContext["sessionManager"] | undefined;
24
+ let continuousPossible = false;
25
+ const foreground = new Map<ExtensionContext["sessionManager"], number>();
26
+ async function resumeContinuous(ctx: ExtensionContext, signal: AbortSignal, retry?: { setup?: string }) {
27
+ const isCurrent = () => !signal.aborted && !foreground.has(ctx.sessionManager) && owner === ctx.sessionManager;
28
+ if (!isCurrent() || !ctx.hasUI) return;
29
+ try {
30
+ const { loadConfigForCheck } = await import("./settings/config.js");
31
+ if (!isCurrent()) return;
32
+ const config = await loadConfigForCheck();
33
+ if (!isCurrent()) return;
34
+ continuousPossible = Boolean(config.continuous && config.agentMode);
35
+ if (!continuousPossible) {
36
+ await continuous?.stop(ctx.sessionManager);
37
+ return;
38
+ }
39
+ const { createContinuousSync } = await import("./sync/continuous-sync.js");
40
+ if (!isCurrent()) return;
41
+ continuous ??= createContinuousSync(loaders);
42
+ continuous.start(ctx, signal, Boolean(retry && (retry.setup === undefined || retry.setup === config.setupName)));
43
+ } catch {
44
+ /* Existing initialization/command paths report settings errors. */
45
+ }
46
+ }
22
47
 
23
48
  pi.registerCommand("sync", {
24
49
  description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
@@ -28,23 +53,39 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
28
53
  throw new Error("/sync requires TUI or RPC mode so results and safety prompts are observable.");
29
54
  }
30
55
  const signal = sessionAbort.signal;
31
- const ready = await initialization;
32
- if (signal.aborted) return;
33
- await check.stop();
34
- if (signal.aborted) return;
56
+ const manager = ctx.sessionManager;
35
57
  const command = splitArgs(args)[0];
36
- if (ready === false && command !== "help" && command !== "unlock" && command !== "migrate-state") {
37
- ctx.ui.notify(
38
- "pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
39
- "error",
40
- );
41
- return;
58
+ let retry: { setup?: string } | undefined;
59
+ let mayResume = false;
60
+ foreground.set(manager, (foreground.get(manager) ?? 0) + 1);
61
+ try {
62
+ const ready = await initialization;
63
+ mayResume = ready !== false;
64
+ if (signal.aborted) return;
65
+ await continuous?.stop(manager);
66
+ if (signal.aborted) return;
67
+ await check.stop();
68
+ if (signal.aborted) return;
69
+ if (ready === false && command !== "help" && command !== "unlock" && command !== "migrate-state") {
70
+ ctx.ui.notify(
71
+ "pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
72
+ "error",
73
+ );
74
+ return;
75
+ }
76
+ const run = () =>
77
+ handleCommand(args, ctx, signal, loaders, attention, (setup) => {
78
+ retry = { setup };
79
+ });
80
+ // Drain background children before foreground guards. Help remains reachable during repair.
81
+ if (command === "migrate-state" || command === "help") await run();
82
+ else await withStateDirectoryAccess(run);
83
+ } finally {
84
+ const count = (foreground.get(manager) ?? 1) - 1;
85
+ if (count) foreground.set(manager, count);
86
+ else foreground.delete(manager);
87
+ if (mayResume) await resumeContinuous(ctx, signal, retry);
42
88
  }
43
- const run = () => handleCommand(args, ctx, signal, loaders, attention);
44
- // Drain background cache/child cleanup before entering any foreground guard.
45
- // Help must remain reachable when the state roots themselves need repair.
46
- if (command === "migrate-state" || command === "help") await run();
47
- else await withStateDirectoryAccess(run);
48
89
  },
49
90
  });
50
91
 
@@ -54,11 +95,15 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
54
95
  sessionAbort.abort(new DOMException("Session replaced", "AbortError"));
55
96
  sessionAbort = new AbortController();
56
97
  const signal = sessionAbort.signal;
98
+ owner = ctx.sessionManager;
99
+ continuousPossible = false;
57
100
  attention.reset(ctx);
58
101
  const previous = initialization;
59
102
  initialization = (async () => {
60
103
  await previous;
61
104
  if (signal.aborted) return false;
105
+ await continuous?.stop();
106
+ if (signal.aborted) return false;
62
107
  await check.stop();
63
108
  if (signal.aborted) return false;
64
109
  try {
@@ -72,18 +117,27 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
72
117
  }
73
118
  })();
74
119
  const ready = await initialization;
75
- if (ready && !signal.aborted) check.start(ctx, signal, ready);
120
+ if (ready && !signal.aborted) {
121
+ if (ready.continuous && ready.agentMode) await resumeContinuous(ctx, signal);
122
+ else check.start(ctx, signal, ready);
123
+ }
76
124
  });
77
125
 
78
126
  pi.on("agent_start", async (_event, ctx) => {
127
+ await continuous?.stop(ctx.sessionManager);
79
128
  await check.interrupt(ctx);
80
129
  });
81
130
 
82
- pi.on("agent_settled", (_event, ctx) => {
83
- if (!sessionAbort.signal.aborted) check.settled(ctx);
131
+ pi.on("agent_settled", async (_event, ctx) => {
132
+ const signal = sessionAbort.signal;
133
+ if (!signal.aborted) {
134
+ check.settled(ctx);
135
+ if (continuousPossible) await resumeContinuous(ctx, signal);
136
+ }
84
137
  });
85
138
 
86
139
  pi.on("session_shutdown", async (event, ctx) => {
140
+ if (continuous && owner && owner !== ctx.sessionManager) return;
87
141
  sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
88
142
  attention.reset(ctx);
89
143
  shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError"));
@@ -94,6 +148,8 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
94
148
  try {
95
149
  const ready = await initialization;
96
150
  if (signal.aborted) return;
151
+ await continuous?.stop(ctx.sessionManager);
152
+ if (signal.aborted) return;
97
153
  await check.stop();
98
154
  if (signal.aborted) return;
99
155
  if (ready !== false && reason !== "reload") {
@@ -0,0 +1,91 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
+ import { requireSettings, updateLocalConfig } from "../settings/settings-store.js";
4
+ import { captureMutationOwner } from "../sync/sync-local.js";
5
+ import { confirmMergeReview } from "./merge-review.js";
6
+ import { safeTerminalText } from "./terminal-text.js";
7
+
8
+ export const AGENT_MODE_DESCRIPTION =
9
+ "This setup, experimental: one autonomous merged sync next startup and on Sync now. Sends eligible conflict text to the selected Pi model; may incur costs and replace/delete selected local and remote content without transfer prompts. No automatic reload. Unsupported conflicts pause.";
10
+
11
+ export const CONTINUOUS_DESCRIPTION =
12
+ "This setup, experimental: recurring sync while Pi is open. Requires Agent mode. Watches selected files, checks the remote every 60–66 seconds, and retries temporary failures. Local changes and retries can cause extra requests between polls. May disclose conflict text and incur model/storage costs. No automatic reload.";
13
+
14
+ export function saveContinuousMode(
15
+ ctx: ExtensionCommandContext,
16
+ setupName: string,
17
+ enabled: boolean,
18
+ signal?: AbortSignal,
19
+ ) {
20
+ return saveAgentMode(ctx, setupName, enabled, signal, "continuous");
21
+ }
22
+
23
+ export async function saveAgentMode(
24
+ ctx: ExtensionCommandContext,
25
+ setupName: string,
26
+ enabled: boolean,
27
+ signal?: AbortSignal,
28
+ preference: "agentMode" | "continuous" = "agentMode",
29
+ ) {
30
+ const validate = captureMutationOwner(ctx, signal);
31
+ const settings = await requireSettings();
32
+ validate();
33
+ const setup = settings.syncSetups[setupName];
34
+ if (!setup || settings.activeSyncSetup !== setupName) throw new Error("Current sync setup changed; reopen Settings.");
35
+ const continuous = preference === "continuous";
36
+ const label = continuous ? "Continuous sync" : "Agent mode";
37
+ if (enabled && continuous && !setup.sync.agentMode) throw new Error("Enable Agent mode before Continuous sync.");
38
+ if (Boolean(setup.sync[preference]) === enabled) return true;
39
+ if (enabled) {
40
+ const summary = [
41
+ `Setup: ${safeTerminalText(setupName)}`,
42
+ `Storage connection: ${safeTerminalText(setup.storage.connection)}`,
43
+ `Storage path: ${safeTerminalText(setup.storage.path)}`,
44
+ ...(setup.storage.bucket ? [`Bucket: ${safeTerminalText(String(setup.storage.bucket))}`] : []),
45
+ ...(setup.storage.branch ? [`Branch: ${safeTerminalText(String(setup.storage.branch))}`] : []),
46
+ `Included paths: ${setup.sync.include.map(safeTerminalText).join(", ")}`,
47
+ continuous ? CONTINUOUS_DESCRIPTION : AGENT_MODE_DESCRIPTION,
48
+ ...(continuous
49
+ ? [
50
+ "Upgrade all Pi processes sharing this agent directory before enabling Continuous sync. Monitoring starts after the /sync command or manager closes. An elected idle local session uses its selected Pi model; another session can use a different provider/model. Each computer is a separate replica.",
51
+ "Automatic requests are limited to two per candidate and two per replica binding in a rolling 15-minute window, across restarts. Network/storage checks and requests may incur costs. Retries start at five seconds and grow to five minutes. These are request bounds, not a currency cap.",
52
+ ]
53
+ : setup.sync.continuous
54
+ ? ["The saved Continuous sync preference will resume monitoring after the /sync command or manager closes."]
55
+ : []),
56
+ "Only bounded Markdown/plain-text conflicts and atomic global settings choices are eligible. Model errors can change intended behavior. Session histories, code, deletion conflicts and path collisions are not model-resolved.",
57
+ "The existing baseline, selection, secret-scan and publication checks remain. Pi Sync cannot exclude arbitrary concurrent local writers. Disabling does not undo completed transfers or provider charges. No transfer starts from Settings.",
58
+ ].join("\n\n");
59
+ if (
60
+ !(await confirmMergeReview(
61
+ ctx,
62
+ `Enable experimental ${label}?`,
63
+ summary,
64
+ signal,
65
+ () => !signal?.aborted,
66
+ `Enable ${label}`,
67
+ summary.split("\n"),
68
+ ))
69
+ )
70
+ return false;
71
+ validate();
72
+ }
73
+ await updateLocalConfig((current) => {
74
+ validate();
75
+ if (
76
+ current.activeSyncSetup !== setupName ||
77
+ !isDeepStrictEqual(current.syncSetups[setupName], setup) ||
78
+ !isDeepStrictEqual(
79
+ current.storageConnections[setup.storage.connection],
80
+ settings.storageConnections[setup.storage.connection],
81
+ )
82
+ )
83
+ throw new Error("Sync setup or connection changed during Agent mode review; reopen Settings.");
84
+ return {
85
+ ...current,
86
+ syncSetups: { ...current.syncSetups, [setupName]: { ...setup, sync: { ...setup.sync, [preference]: enabled } } },
87
+ };
88
+ }, signal);
89
+ // The atomic writer has committed. Later cancellation cannot undo this save.
90
+ return true;
91
+ }
@@ -21,6 +21,7 @@ import {
21
21
  type SyncAttentionState,
22
22
  syncAttentionMatchesConfig,
23
23
  } from "./sync-attention.js";
24
+ import { startupTransferSummary } from "./sync-behavior.js";
24
25
  import { countValidSyncSetups } from "./sync-setups-ui.js";
25
26
  import { safeTerminalText } from "./terminal-text.js";
26
27
 
@@ -28,6 +29,8 @@ export const MAIN_MENU_ACTIONS = [
28
29
  "Sync now",
29
30
  "Pull from remote…",
30
31
  "Push to remote…",
32
+ "View differences",
33
+ "Check remote status",
31
34
  "Settings",
32
35
  "History",
33
36
  "Diagnostics",
@@ -121,7 +124,7 @@ export async function describeManagerState(
121
124
  const lastAppliedSnapshot = changesBlocked
122
125
  ? "Unavailable while operations are locked"
123
126
  : syncState?.lastAppliedSnapshot
124
- ? safeTerminalText(syncState.lastAppliedSnapshot)
127
+ ? `${truncateToWidth(safeTerminalText(syncState.lastAppliedSnapshot), 20, "…")} (application time unavailable; full ID in History)`
125
128
  : syncState
126
129
  ? "Never synced"
127
130
  : "Unavailable";
@@ -134,10 +137,7 @@ export async function describeManagerState(
134
137
  "",
135
138
  `Current sync setup: ${safeTerminalText(config.setupName)}`,
136
139
  `Storage: ${backendStorageDescription(config)}`,
137
- `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
138
- `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
139
- `Automatic transfer at startup: ${config.automaticTransfer ? "On — upload/replace/delete at idle; no reload" : "Off"}`,
140
- `Last applied: ${lastAppliedSnapshot}`,
140
+ "CURRENT STATE",
141
141
  ...(syncState?.unresolved?.length
142
142
  ? [
143
143
  `Unresolved: ${syncState.unresolved.length} dependency groups · choose Review unresolved conflicts`,
@@ -160,11 +160,17 @@ export async function describeManagerState(
160
160
  "Check-time observation only; synchronization rechecks current content.",
161
161
  ]
162
162
  : ["Remote status: Not checked"]),
163
+ `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
164
+ `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
165
+ `Startup transfer: ${startupTransferSummary(config)}`,
166
+ `Agent mode: ${config.agentMode ? "On (experimental; autonomous merged sync)" : "Off"}`,
167
+ ...(config.continuous ? await (await import("../sync/continuous-record.js")).continuousStatusLines(config) : []),
168
+ `Last applied: ${lastAppliedSnapshot}`,
163
169
  ...(noSyncedContent
164
170
  ? ["", "No included content is selected. Choose included content in Settings before syncing."]
165
171
  : []),
166
172
  "",
167
- "What do you want to do?",
173
+ "ACTIONS · transfer, inspect, or manage",
168
174
  ];
169
175
  return {
170
176
  title: (changesBlocked ? ["Manage sync", ...operationStatusLines(operation)] : ordinaryTitle).join("\n"),
@@ -1,7 +1,8 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
- import { loadConfig } from "../settings/config.js";
3
+ import { loadConfig, syncCheckConfigFingerprint } from "../settings/config.js";
4
4
  import { operationBlocksChanges } from "../state/operation-availability.js";
5
+ import { captureMutationOwner } from "../sync/sync-local.js";
5
6
  import { type RunRoute, runCancellableOperation } from "./cancellable-operation.js";
6
7
  import {
7
8
  attentionMainMenuItems,
@@ -11,12 +12,13 @@ import {
11
12
  } from "./manager-attention.js";
12
13
  import { recoverSyncAccess } from "./manager-recovery.js";
13
14
  import { dispatchManagerResult } from "./manager-result-dispatcher.js";
14
- import { describeManagerState } from "./manager-state.js";
15
+ import { backendStorageDescription, describeManagerState } from "./manager-state.js";
15
16
  import { showSyncSettings } from "./settings-ui.js";
16
17
  import { showSyncSetupManager } from "./setup/setup-actions.js";
17
18
  import { showSetupSwitcher } from "./setup/setup-switcher.js";
18
19
  import { showStorageConnections } from "./storage-connections-ui.js";
19
20
  import { observationMatchesConfig } from "./sync-attention.js";
21
+ import { safeTerminalText } from "./terminal-text.js";
20
22
 
21
23
  export async function showSyncManager(
22
24
  ctx: ExtensionCommandContext,
@@ -36,6 +38,8 @@ export async function showSyncManager(
36
38
  | "settings"
37
39
  | "pull"
38
40
  | "push"
41
+ | "diff"
42
+ | "status"
39
43
  | "setups"
40
44
  | "connections"
41
45
  | "history"
@@ -167,12 +171,43 @@ export async function showSyncManager(
167
171
  });
168
172
  return { kind: result.kind === "closed" ? "close" : "stay" };
169
173
  },
170
- doctor: async () => {
171
- const result = await runCancellableOperation(ctx, "Checking setup access…", "doctor", runRoute, {
174
+ diff: async () => {
175
+ const result = await runCancellableOperation(ctx, "Comparing local and remote…", "diff", runRoute, {
176
+ signal: sessionSignal,
177
+ });
178
+ return { kind: result.kind === "closed" ? "close" : "stay" };
179
+ },
180
+ status: async () => {
181
+ const result = await runCancellableOperation(ctx, "Checking remote status…", "status", runRoute, {
172
182
  signal: sessionSignal,
173
183
  });
174
184
  return { kind: result.kind === "closed" ? "close" : "stay" };
175
185
  },
186
+ doctor: async ({ signal: actionSignal }) => {
187
+ const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
188
+ const validate = captureMutationOwner(ctx, signal);
189
+ const config = await loadConfig();
190
+ validate();
191
+ const confirmed = await ctx.ui.confirm(
192
+ "Run diagnostics?",
193
+ `Setup: ${safeTerminalText(config.setupName)}\nStorage: ${backendStorageDescription(config)}\nS3/R2 reads the latest pointer without writing. Git reads remote snapshots and checks its cache. WebDAV probes conditional writes and cleanup, and can repair a missing history entry. Possible-secret checks still run. This is not the read-only Check remote status action.`,
194
+ { signal },
195
+ );
196
+ if (signal.aborted) return { kind: "close" };
197
+ validate();
198
+ if (!confirmed) return { kind: "stay" };
199
+ const latest = await loadConfig();
200
+ validate();
201
+ if (syncCheckConfigFingerprint(config) !== syncCheckConfigFingerprint(latest)) {
202
+ ctx.ui.notify("Setup changed during diagnostics review; reopen Diagnostics.", "warning");
203
+ return { kind: "stay" };
204
+ }
205
+ const result = await runCancellableOperation(ctx, "Checking setup access…", "doctor", runRoute, {
206
+ signal,
207
+ target: config.setupName,
208
+ });
209
+ return { kind: result.kind === "closed" ? "close" : "stay" };
210
+ },
176
211
  recover: async ({ state, signal: actionSignal }) => {
177
212
  const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
178
213
  return { kind: result === "close" ? "close" : "stay" };
@@ -228,6 +263,8 @@ function syncMainMenuItem(
228
263
  | "sync"
229
264
  | "pull"
230
265
  | "push"
266
+ | "diff"
267
+ | "status"
231
268
  | "switch"
232
269
  | "settings"
233
270
  | "setups"
@@ -244,6 +281,8 @@ function syncMainMenuItem(
244
281
  "Sync now": "sync",
245
282
  "Pull from remote…": "pull",
246
283
  "Push to remote…": "push",
284
+ "View differences": "diff",
285
+ "Check remote status": "status",
247
286
  "Switch sync setup": "switch",
248
287
  Settings: "settings",
249
288
  History: "history",
@@ -263,8 +302,19 @@ function syncMainMenuItem(
263
302
  id: action,
264
303
  label,
265
304
  action,
266
- ...(action === "doctor"
267
- ? { description: "Check setup access; WebDAV also probes writes and repairs history." }
268
- : {}),
305
+ description:
306
+ action === "doctor"
307
+ ? "Manage: review backend checks before running."
308
+ : action === "settings" || action === "history" || action === "switch"
309
+ ? "Manage"
310
+ : action === "sync"
311
+ ? "Transfer: choose a safe direction or review conflicts."
312
+ : action === "pull"
313
+ ? "Transfer: remote → local; review writes and removals."
314
+ : action === "push"
315
+ ? "Transfer: local → remote active snapshot; review publication."
316
+ : action === "status" || action === "diff"
317
+ ? "Inspect: read current local and remote state without transferring files."
318
+ : undefined,
269
319
  };
270
320
  }
@@ -60,9 +60,9 @@ export async function confirmMergeReview(
60
60
  })
61
61
  : await runMenu(ctx, menu, { getState: () => undefined, signal, isCurrent, onError: () => {} });
62
62
  if (!isCurrent() || signal?.aborted) return false;
63
- if (result.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
63
+ if (result.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
64
64
  if (result.kind === "unsupported")
65
- throw new Error("Merge review requires observable TUI or RPC; review the plan before using --yes.");
65
+ throw new Error("Sync review requires observable TUI or RPC; review the plan before using --yes.");
66
66
  if (result.kind === "completed") choice.value = result.value;
67
67
  else if (result.kind !== "closed") return false;
68
68
  // Keep authorization separate from UI termination in both adapters.
@@ -79,7 +79,7 @@ export async function confirmMergeReview(
79
79
  onError: () => {},
80
80
  });
81
81
  if (!isCurrent() || signal?.aborted) return false;
82
- if (detail.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
82
+ if (detail.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
83
83
  if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
84
84
  // A fresh menu discards remembered selection: returning from details always selects No.
85
85
  // Unlike Pi's native Yes-first dialog, approval requires moving off the safe default.
@@ -2,13 +2,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
2
2
  import type { PublicationCapability } from "../backends/sync-backend.js";
3
3
  import type { Snapshot } from "../snapshot/snapshot-types.js";
4
4
  import type { FileMergeDecision } from "../sync/file-merge-planner.js";
5
- import { fileHashMap } from "../sync/sync-state.js";
6
5
  import { safeTerminalText, snapshotPathLabel } from "./terminal-text.js";
7
-
8
- interface FileChange {
9
- path: string;
10
- action: "Add" | "Update" | "Delete";
11
- }
6
+ import { transferChanges as changes, type TransferChange as FileChange } from "./transfer-changes.js";
12
7
 
13
8
  export interface MergedSyncSummaryInput {
14
9
  setupName: string;
@@ -78,6 +73,7 @@ export function mergedSyncSummary(input: MergedSyncSummaryInput) {
78
73
  const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
79
74
  const safety = [
80
75
  `Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
76
+ "Remote removals affect the active snapshot; retained history is not erased.",
81
77
  `Sessions: ${input.sessions ? "included — may contain private conversations" : "not included"}`,
82
78
  input.groups.length
83
79
  ? `Unresolved groups: ${input.groups.length} — these paths stay unchanged on each side`
@@ -114,15 +110,3 @@ export function mergedSyncSummary(input: MergedSyncSummaryInput) {
114
110
  ].join("\n"),
115
111
  };
116
112
  }
117
-
118
- function changes(before: Snapshot, after: Snapshot): FileChange[] {
119
- const beforeMap = fileHashMap(before);
120
- const afterMap = fileHashMap(after);
121
- return [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])]
122
- .sort()
123
- .flatMap((filePath): FileChange[] => {
124
- if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
125
- if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
126
- return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
127
- });
128
- }
@@ -11,6 +11,7 @@ import {
11
11
  import { SyncSetupReviewChangedError, updateSyncSetup } from "../settings/settings-management.js";
12
12
  import type { AnySyncConfig, PartialConfig } from "../settings/settings-types.js";
13
13
  import { readSnapshotForHead } from "../sync/remote-snapshot.js";
14
+ import { throwIfAborted } from "../sync/signals.js";
14
15
  import { errorMessage } from "../sync/sync-errors.js";
15
16
  import {
16
17
  compareSyncInclude,
@@ -240,6 +241,8 @@ async function showSelectionDifference(
240
241
  include: [...state.decision.remoteInclude],
241
242
  automatic: review.config.automatic,
242
243
  automaticTransfer: review.config.automaticTransfer,
244
+ agentMode: review.config.agentMode,
245
+ continuous: review.config.continuous,
243
246
  onSwitch: review.config.onSwitch,
244
247
  showStatus: review.config.showStatus,
245
248
  };
@@ -592,6 +595,8 @@ function sameContinuationReview(left: PartialConfig, right: PartialConfig) {
592
595
  left.storagePath === right.storagePath &&
593
596
  left.bucket === right.bucket &&
594
597
  left.branch === right.branch &&
598
+ Boolean(left.agentMode) === Boolean(right.agentMode) &&
599
+ Boolean(left.continuous) === Boolean(right.continuous) &&
595
600
  sameSyncInclude(left.include, right.include)
596
601
  );
597
602
  }
@@ -639,8 +644,3 @@ function runWithOptionalStateAccess<T>(
639
644
  function combineSignals(sessionSignal: AbortSignal | undefined, actionSignal: AbortSignal) {
640
645
  return sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
641
646
  }
642
-
643
- function throwIfAborted(signal: AbortSignal) {
644
- if (!signal.aborted) return;
645
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
646
- }
@@ -0,0 +1,55 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ /** Adapt only public rendering/input boundaries; Kit still owns search, saves and focus. */
5
+ export function boundedSettingsUI(ctx: ExtensionCommandContext): ExtensionCommandContext["ui"] {
6
+ return {
7
+ ...ctx.ui,
8
+ custom: (factory, options) =>
9
+ ctx.ui.custom((tui, ...args) => {
10
+ // Kit pins search before the selected setting at a one-row budget. Render its
11
+ // smallest complete frame, then retain the active row when the real budget is smaller.
12
+ const terminal = new Proxy(tui.terminal, {
13
+ get: (target, key) => (key === "rows" ? Math.max(8, target.rows) : Reflect.get(target, key)),
14
+ });
15
+ const screenTui = new Proxy(tui, {
16
+ get(target, key) {
17
+ if (key === "terminal") return terminal;
18
+ const value = Reflect.get(target, key);
19
+ return typeof value === "function" ? value.bind(target) : value;
20
+ },
21
+ });
22
+ const guard = <
23
+ T extends {
24
+ render(width: number): string[];
25
+ handleMouse?: import("@earendil-works/pi-tui").Component["handleMouse"];
26
+ },
27
+ >(
28
+ component: T,
29
+ ): T => {
30
+ const render = component.render.bind(component);
31
+ const handleMouse = component.handleMouse?.bind(component);
32
+ let sourceRow = 0;
33
+ const compact = () => tui.terminal.rows < 8;
34
+ component.render = (width) => {
35
+ if (width <= 0) return [];
36
+ const lines = render(width);
37
+ if (!compact()) return lines;
38
+ const active = lines.findIndex((line) => stripVTControlCharacters(line).startsWith("→"));
39
+ const search = lines.findIndex((line) => stripVTControlCharacters(line).startsWith(">"));
40
+ sourceRow = active < 0 ? Math.max(0, search) : active;
41
+ return lines.slice(sourceRow, sourceRow + 1);
42
+ };
43
+ if (handleMouse)
44
+ component.handleMouse = (event) => {
45
+ if (event.width <= 0 || (compact() && event.y !== 0)) return undefined;
46
+ component.render(event.width);
47
+ return handleMouse(compact() ? { ...event, y: event.y + sourceRow } : event);
48
+ };
49
+ return component;
50
+ };
51
+ const component = factory(screenTui, ...args);
52
+ return component instanceof Promise ? component.then(guard) : guard(component);
53
+ }, options),
54
+ };
55
+ }