@narumitw/pi-sync 0.57.9 → 0.57.11

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 (169) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -1
  3. package/dist/chunks/{agent-candidate-GNEYR225.ts → agent-candidate-OCDPRI5G.ts} +12 -9
  4. package/dist/chunks/{agent-plan-3L7CRFDM.ts → agent-plan-P5B5RGZ7.ts} +22 -18
  5. package/dist/chunks/{agent-plan-3L7CRFDM.ts.map → agent-plan-P5B5RGZ7.ts.map} +1 -1
  6. package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts → agent-refusal-retry-476YGNU5.ts} +11 -8
  7. package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts.map → agent-refusal-retry-476YGNU5.ts.map} +1 -1
  8. package/dist/chunks/{agent-store-55CZOCPX.ts → agent-store-RJSMNOYP.ts} +12 -11
  9. package/dist/chunks/{agent-store-55CZOCPX.ts.map → agent-store-RJSMNOYP.ts.map} +1 -1
  10. package/dist/chunks/agent-sync-EUK2CPSS.ts +22 -0
  11. package/dist/chunks/{chunk-U3XRI2HF.ts → chunk-3IYAPMST.ts} +11 -11
  12. package/dist/chunks/{chunk-B4QZBDBT.ts → chunk-45COPAK7.ts} +10 -3
  13. package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
  14. package/dist/chunks/chunk-5SFMBY5P.ts +26 -0
  15. package/dist/chunks/chunk-5SFMBY5P.ts.map +7 -0
  16. package/dist/chunks/{chunk-ED3TYIVW.ts → chunk-5TSCBBPP.ts} +5 -5
  17. package/dist/chunks/{chunk-5QAE3G3K.ts → chunk-67INJPKR.ts} +6 -6
  18. package/dist/chunks/{chunk-IWZH5KYS.ts → chunk-6LD5KX55.ts} +10 -10
  19. package/dist/chunks/{chunk-AAEEHR7H.ts → chunk-6O3EDBRL.ts} +2 -2
  20. package/dist/chunks/{chunk-CRKTAPSL.ts → chunk-7A35ZAVO.ts} +333 -237
  21. package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
  22. package/dist/chunks/{chunk-6FVVRTL5.ts → chunk-7OLLPPLU.ts} +77 -63
  23. package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
  24. package/dist/chunks/{chunk-PQW6GS4J.ts → chunk-7ZIYU5QS.ts} +12 -10
  25. package/dist/chunks/{chunk-PQW6GS4J.ts.map → chunk-7ZIYU5QS.ts.map} +1 -1
  26. package/dist/chunks/{chunk-S7QS4AWK.ts → chunk-AKMV47YC.ts} +5 -5
  27. package/dist/chunks/chunk-AR2DGDUI.ts +55 -0
  28. package/dist/chunks/chunk-AR2DGDUI.ts.map +7 -0
  29. package/dist/chunks/{chunk-GNH73BJ5.ts → chunk-BEST45H2.ts} +7 -7
  30. package/dist/chunks/chunk-CBLTWXAU.ts +114 -0
  31. package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
  32. package/dist/chunks/{chunk-GP5DU62N.ts → chunk-CPT2BN6N.ts} +2 -2
  33. package/dist/chunks/{chunk-UPNVIUBN.ts → chunk-DN4IA7MR.ts} +6 -6
  34. package/dist/chunks/{chunk-PG7NMFP2.ts → chunk-DRTC4PKM.ts} +8 -8
  35. package/dist/chunks/chunk-ESKOPQVB.ts +348 -0
  36. package/dist/chunks/chunk-ESKOPQVB.ts.map +7 -0
  37. package/dist/chunks/{chunk-MKKF4IMY.ts → chunk-F7E3VFXU.ts} +5 -5
  38. package/dist/chunks/{chunk-HUZ5CR4W.ts → chunk-FHE2DCA2.ts} +3 -52
  39. package/dist/chunks/chunk-FHE2DCA2.ts.map +7 -0
  40. package/dist/chunks/{chunk-UBV24CG4.ts → chunk-FLHCOJCD.ts} +3 -3
  41. package/dist/chunks/chunk-GNYGNHFF.ts +27 -0
  42. package/dist/chunks/chunk-GNYGNHFF.ts.map +7 -0
  43. package/dist/chunks/chunk-KYTT7KV4.ts +319 -0
  44. package/dist/chunks/chunk-KYTT7KV4.ts.map +7 -0
  45. package/dist/chunks/{chunk-A6HU4ERM.ts → chunk-MKSMJPVA.ts} +6 -23
  46. package/dist/chunks/chunk-MKSMJPVA.ts.map +7 -0
  47. package/dist/chunks/{chunk-ORGBI36T.ts → chunk-OJARL2TG.ts} +4 -4
  48. package/dist/chunks/{chunk-JCH3VXIO.ts → chunk-OKPPZA4D.ts} +150 -91
  49. package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
  50. package/dist/chunks/{chunk-QO333NM4.ts → chunk-OVBAW7KV.ts} +180 -133
  51. package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
  52. package/dist/chunks/{chunk-GSNPASKB.ts → chunk-Q23JJW7E.ts} +6 -6
  53. package/dist/chunks/{chunk-YK52VDXG.ts → chunk-TIMWN2RH.ts} +10 -8
  54. package/dist/chunks/{chunk-YK52VDXG.ts.map → chunk-TIMWN2RH.ts.map} +1 -1
  55. package/dist/chunks/{chunk-UOHR7LRO.ts → chunk-TZC7GDJE.ts} +10 -22
  56. package/dist/chunks/chunk-TZC7GDJE.ts.map +7 -0
  57. package/dist/chunks/chunk-X7R6DCVF.ts +28 -0
  58. package/dist/chunks/chunk-X7R6DCVF.ts.map +7 -0
  59. package/dist/chunks/{chunk-76NB4R7U.ts → chunk-XJHQPB3E.ts} +2 -2
  60. package/dist/chunks/{chunk-BDB4IDM3.ts → chunk-Y4UVXOLQ.ts} +8 -6
  61. package/dist/chunks/{chunk-BDB4IDM3.ts.map → chunk-Y4UVXOLQ.ts.map} +1 -1
  62. package/dist/chunks/{chunk-P7DD5PWZ.ts → chunk-ZDDBZH72.ts} +3 -3
  63. package/dist/chunks/command-handler-F2E7A5T5.ts +548 -0
  64. package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
  65. package/dist/chunks/{config-KGIYTTXQ.ts → config-HYG7G5EM.ts} +7 -6
  66. package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts → conflict-artifacts-J6WSDCWP.ts} +13 -9
  67. package/dist/chunks/{conflict-review-3X37QEIT.ts → conflict-review-2QMO2K6Q.ts} +31 -25
  68. package/dist/chunks/{conflict-review-3X37QEIT.ts.map → conflict-review-2QMO2K6Q.ts.map} +1 -1
  69. package/dist/chunks/{continuous-record-SQJ47CXN.ts → continuous-record-AH7JGYKM.ts} +8 -7
  70. package/dist/chunks/{continuous-sync-Q6QUD6TY.ts → continuous-sync-VRMXCA5X.ts} +33 -28
  71. package/dist/chunks/{continuous-sync-Q6QUD6TY.ts.map → continuous-sync-VRMXCA5X.ts.map} +1 -1
  72. package/dist/chunks/{file-selection-3JJYOVNR.ts → file-selection-IELTXSGZ.ts} +11 -10
  73. package/dist/chunks/{file-selection-3JJYOVNR.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
  74. package/dist/chunks/{git-backend-NYKSG5X6.ts → git-backend-KHD36W27.ts} +15 -12
  75. package/dist/chunks/{git-backend-NYKSG5X6.ts.map → git-backend-KHD36W27.ts.map} +1 -1
  76. package/dist/chunks/{local-fields-6DGYOOIK.ts → local-fields-JEKAPZKD.ts} +9 -5
  77. package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
  78. package/dist/chunks/{manager-ui-ZH6DEC2G.ts → manager-ui-LM7DSJ2Y.ts} +124 -67
  79. package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
  80. package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts → merge-baseline-store-65GKUMXI.ts} +6 -5
  81. package/dist/chunks/remote-selection-ui-TD4C4W3J.ts +31 -0
  82. package/dist/chunks/{s3-backend-F7PR5FB3.ts → s3-backend-L24UZ6IK.ts} +24 -18
  83. package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
  84. package/dist/chunks/{setup-switch-E7PHKI6J.ts → setup-switch-PIPXYPCW.ts} +6 -7
  85. package/dist/chunks/{setup-wizard-OVHZYXNW.ts → setup-wizard-RR5U3SUB.ts} +8 -9
  86. package/dist/chunks/{setup-wizard-OVHZYXNW.ts.map → setup-wizard-RR5U3SUB.ts.map} +1 -1
  87. package/dist/chunks/{snapshot-THS5EY2L.ts → snapshot-RGNDYT6A.ts} +10 -6
  88. package/dist/chunks/snapshot-transaction-26BQO36S.ts +24 -0
  89. package/dist/chunks/sync-inspection-MOMLGUTD.ts +25 -0
  90. package/dist/chunks/sync-inspection-MOMLGUTD.ts.map +7 -0
  91. package/dist/chunks/{sync-operations-Y72GNIQ2.ts → sync-operations-CV3TULLD.ts} +66 -57
  92. package/dist/chunks/{sync-operations-Y72GNIQ2.ts.map → sync-operations-CV3TULLD.ts.map} +1 -1
  93. package/dist/chunks/{sync-state-CFL4RRRB.ts → sync-state-7L4NT7W2.ts} +8 -5
  94. package/dist/chunks/sync-state-7L4NT7W2.ts.map +7 -0
  95. package/dist/chunks/{webdav-backend-TGFUH3SF.ts → webdav-backend-3ZCACILP.ts} +15 -12
  96. package/dist/chunks/{webdav-backend-TGFUH3SF.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
  97. package/dist/index.ts +54 -537
  98. package/dist/index.ts.map +3 -3
  99. package/package.json +2 -2
  100. package/skills/configure-pi-sync/references/configuration.md +8 -1
  101. package/src/backends/s3/s3-client.ts +8 -1
  102. package/src/commands/command-execution.ts +8 -3
  103. package/src/paths.ts +6 -1
  104. package/src/settings/config-file.ts +205 -118
  105. package/src/settings/config.ts +2 -2
  106. package/src/settings/settings-management.ts +15 -1
  107. package/src/settings/settings-store.ts +0 -14
  108. package/src/settings/settings-validation.ts +1 -1
  109. package/src/snapshot/session-paths.ts +15 -2
  110. package/src/snapshot/snapshot-mutation-queues.ts +142 -0
  111. package/src/snapshot/snapshot-transaction-directory.ts +16 -0
  112. package/src/snapshot/snapshot-transaction.ts +123 -137
  113. package/src/sync/automatic-sync.ts +12 -6
  114. package/src/sync/local-field-policy.ts +29 -0
  115. package/src/sync/local-fields.ts +3 -29
  116. package/src/sync/merge-apply.ts +6 -0
  117. package/src/sync/mutation-owner.ts +22 -0
  118. package/src/sync/startup-check.ts +3 -2
  119. package/src/sync/sync-loaders.ts +4 -0
  120. package/src/sync/sync-local.ts +2 -19
  121. package/src/sync-extension.ts +7 -2
  122. package/src/ui/cancellable-operation.ts +36 -13
  123. package/src/ui/manager-state.ts +3 -1
  124. package/src/ui/manager-ui.ts +76 -24
  125. package/src/ui/remote-selection-ui.ts +131 -82
  126. package/src/ui/settings-ui.ts +2 -1
  127. package/dist/chunks/chunk-6FVVRTL5.ts.map +0 -7
  128. package/dist/chunks/chunk-A6HU4ERM.ts.map +0 -7
  129. package/dist/chunks/chunk-B4QZBDBT.ts.map +0 -7
  130. package/dist/chunks/chunk-CRKTAPSL.ts.map +0 -7
  131. package/dist/chunks/chunk-HUZ5CR4W.ts.map +0 -7
  132. package/dist/chunks/chunk-JCH3VXIO.ts.map +0 -7
  133. package/dist/chunks/chunk-QO333NM4.ts.map +0 -7
  134. package/dist/chunks/chunk-QOFMXPM5.ts +0 -735
  135. package/dist/chunks/chunk-QOFMXPM5.ts.map +0 -7
  136. package/dist/chunks/chunk-UOHR7LRO.ts.map +0 -7
  137. package/dist/chunks/manager-result-dispatcher-HOCM2GDA.ts +0 -24
  138. package/dist/chunks/manager-ui-ZH6DEC2G.ts.map +0 -7
  139. package/dist/chunks/remote-selection-ui-YFPGYGNE.ts +0 -23
  140. package/dist/chunks/s3-backend-F7PR5FB3.ts.map +0 -7
  141. package/dist/chunks/sync-inspection-J25EEKZK.ts +0 -21
  142. /package/dist/chunks/{agent-candidate-GNEYR225.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
  143. /package/dist/chunks/{config-KGIYTTXQ.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
  144. /package/dist/chunks/{chunk-U3XRI2HF.ts.map → chunk-3IYAPMST.ts.map} +0 -0
  145. /package/dist/chunks/{chunk-ED3TYIVW.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
  146. /package/dist/chunks/{chunk-5QAE3G3K.ts.map → chunk-67INJPKR.ts.map} +0 -0
  147. /package/dist/chunks/{chunk-IWZH5KYS.ts.map → chunk-6LD5KX55.ts.map} +0 -0
  148. /package/dist/chunks/{chunk-AAEEHR7H.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
  149. /package/dist/chunks/{chunk-S7QS4AWK.ts.map → chunk-AKMV47YC.ts.map} +0 -0
  150. /package/dist/chunks/{chunk-GNH73BJ5.ts.map → chunk-BEST45H2.ts.map} +0 -0
  151. /package/dist/chunks/{chunk-GP5DU62N.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
  152. /package/dist/chunks/{chunk-UPNVIUBN.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
  153. /package/dist/chunks/{chunk-PG7NMFP2.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
  154. /package/dist/chunks/{chunk-MKKF4IMY.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
  155. /package/dist/chunks/{chunk-UBV24CG4.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
  156. /package/dist/chunks/{chunk-ORGBI36T.ts.map → chunk-OJARL2TG.ts.map} +0 -0
  157. /package/dist/chunks/{chunk-GSNPASKB.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
  158. /package/dist/chunks/{chunk-76NB4R7U.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
  159. /package/dist/chunks/{chunk-P7DD5PWZ.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
  160. /package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts.map → config-HYG7G5EM.ts.map} +0 -0
  161. /package/dist/chunks/{continuous-record-SQJ47CXN.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
  162. /package/dist/chunks/{local-fields-6DGYOOIK.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
  163. /package/dist/chunks/{manager-result-dispatcher-HOCM2GDA.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
  164. /package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
  165. /package/dist/chunks/{remote-selection-ui-YFPGYGNE.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
  166. /package/dist/chunks/{setup-switch-E7PHKI6J.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
  167. /package/dist/chunks/{snapshot-THS5EY2L.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
  168. /package/dist/chunks/{sync-inspection-J25EEKZK.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
  169. /package/dist/chunks/{sync-state-CFL4RRRB.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
@@ -0,0 +1,22 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { agentDir } from "../snapshot/session-paths.js";
3
+ import { throwIfAborted } from "./signals.js";
4
+
5
+ export function captureMutationOwner(ctx: ExtensionContext | ExtensionCommandContext, signal?: AbortSignal) {
6
+ const manager = ctx.sessionManager;
7
+ const file = manager.getSessionFile?.();
8
+ const id = manager.getSessionId?.();
9
+ const cwd = manager.getCwd?.();
10
+ const root = agentDir();
11
+ return () => {
12
+ throwIfAborted(signal);
13
+ if (
14
+ ctx.sessionManager !== manager ||
15
+ manager.getSessionFile?.() !== file ||
16
+ manager.getSessionId?.() !== id ||
17
+ manager.getCwd?.() !== cwd ||
18
+ agentDir() !== root
19
+ )
20
+ throw new Error("Session changed during file mutation; evidence retained for review.");
21
+ };
22
+ }
@@ -10,7 +10,6 @@ import type { SyncAttentionController } from "../ui/sync-attention.js";
10
10
  import { setSyncStatus } from "../ui/sync-status.js";
11
11
  import { safeTerminalText } from "../ui/terminal-text.js";
12
12
  import { startupTransferEnabled } from "./agent-policy.js";
13
- import { withAgentSyncLock } from "./agent-sync.js";
14
13
  import { combineSignals, throwIfAborted } from "./signals.js";
15
14
  import { createStartupTimings, type StartupTimingPublisher } from "./startup-timings.js";
16
15
  import { errorMessage } from "./sync-errors.js";
@@ -122,7 +121,9 @@ export function createStartupCheck(
122
121
  inspection.head &&
123
122
  inspection.selectionState?.kind === "same"
124
123
  ) {
125
- const operations = await timings.measure("operations import", () => loaders.operations());
124
+ const [operations, { withAgentSyncLock }] = await timings.measure("operations import", () =>
125
+ Promise.all([loaders.operations(), import("./agent-sync.js")]),
126
+ );
126
127
  throwIfAborted(signal);
127
128
  if (!isCurrent()) return;
128
129
  if (!ctx.isIdle()) {
@@ -2,10 +2,12 @@ type SetupSwitchModule = Pick<typeof import("./setup-switch.js"), "useSyncSetup"
2
2
  type SnapshotModule = Pick<typeof import("../snapshot/snapshot.js"), "createSnapshot">;
3
3
  type SyncStateModule = Pick<typeof import("./sync-state.js"), "hasLocalChanges">;
4
4
  type SyncOperations = typeof import("./sync-operations.js");
5
+ type CommandHandlerModule = Pick<typeof import("../commands/command-handler.js"), "handleCommand">;
5
6
 
6
7
  type SyncInspectionModule = Pick<typeof import("./sync-inspection.js"), "inspectSync">;
7
8
 
8
9
  export interface SyncDependencies {
10
+ loadCommandHandler(): Promise<CommandHandlerModule>;
9
11
  loadSyncInspection(): Promise<SyncInspectionModule>;
10
12
  loadSetupSwitch(): Promise<SetupSwitchModule>;
11
13
  loadSnapshot(): Promise<SnapshotModule>;
@@ -14,6 +16,7 @@ export interface SyncDependencies {
14
16
  }
15
17
 
16
18
  export interface SyncLoaders {
19
+ commands(): Promise<CommandHandlerModule>;
17
20
  inspection(): Promise<SyncInspectionModule>;
18
21
  setupSwitch(): Promise<SetupSwitchModule>;
19
22
  snapshot(): Promise<SnapshotModule>;
@@ -23,6 +26,7 @@ export interface SyncLoaders {
23
26
 
24
27
  export function createSyncLoaders(dependencies: Partial<SyncDependencies>): SyncLoaders {
25
28
  const loaders: SyncLoaders = {
29
+ commands: cachedModuleLoader(dependencies.loadCommandHandler ?? (() => import("../commands/command-handler.js"))),
26
30
  inspection: cachedModuleLoader(dependencies.loadSyncInspection ?? (() => import("./sync-inspection.js"))),
27
31
  setupSwitch: cachedModuleLoader(dependencies.loadSetupSwitch ?? (() => import("./setup-switch.js"))),
28
32
  snapshot: cachedModuleLoader(dependencies.loadSnapshot ?? (() => import("../snapshot/snapshot.js"))),
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { agentDir, sessionDirFromContext } from "../snapshot/session-paths.js";
4
+ import { sessionDirFromContext } from "../snapshot/session-paths.js";
5
5
  import { createSnapshot, sessionSnapshotPathFromAbsolute } from "../snapshot/snapshot.js";
6
6
  import { encodeSnapshot } from "../snapshot/snapshot-codec.js";
7
7
  import type { SnapshotOptions } from "../snapshot/snapshot-types.js";
@@ -20,24 +20,7 @@ export function protectedSessionPaths(
20
20
  return snapshotPath ? new Set([snapshotPath]) : new Set<string>();
21
21
  }
22
22
 
23
- export function captureMutationOwner(ctx: ExtensionContext | ExtensionCommandContext, signal?: AbortSignal) {
24
- const manager = ctx.sessionManager;
25
- const file = manager.getSessionFile?.();
26
- const id = manager.getSessionId?.();
27
- const cwd = manager.getCwd?.();
28
- const root = agentDir();
29
- return () => {
30
- throwIfAborted(signal);
31
- if (
32
- ctx.sessionManager !== manager ||
33
- manager.getSessionFile?.() !== file ||
34
- manager.getSessionId?.() !== id ||
35
- manager.getCwd?.() !== cwd ||
36
- agentDir() !== root
37
- )
38
- throw new Error("Session changed during file mutation; evidence retained for review.");
39
- };
40
- }
23
+ export { captureMutationOwner } from "./mutation-owner.js";
41
24
 
42
25
  export async function backupLocal(profile: string, options: SnapshotOptions = {}, signal?: AbortSignal) {
43
26
  throwIfAborted(signal);
@@ -1,6 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { completeSyncArguments, splitArgs } from "./commands/command.js";
3
- import { handleCommand } from "./commands/command-handler.js";
4
3
  import type { AnySyncConfig } from "./settings/settings-types.js";
5
4
  import { withStateDirectoryAccess } from "./state/state-directory.js";
6
5
  import { startupTransferEnabled } from "./sync/agent-policy.js";
@@ -82,8 +81,14 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
82
81
  );
83
82
  return;
84
83
  }
84
+ const commands = await loaders.commands().catch((error) => {
85
+ if (!signal.aborted) {
86
+ ctx.ui.notify(`pi-sync command unavailable: ${safeTerminalText(errorMessage(error))}`, "error");
87
+ }
88
+ });
89
+ if (signal.aborted || !commands) return;
85
90
  const run = () =>
86
- handleCommand(args, ctx, signal, loaders, attention, (setup) => {
91
+ commands.handleCommand(args, ctx, signal, loaders, attention, (setup) => {
87
92
  retry = { setup };
88
93
  });
89
94
  // Drain background children before foreground guards. Help remains reachable during repair.
@@ -28,6 +28,7 @@ interface CancellableOperationOptions {
28
28
  cancelledMessage?: string | null;
29
29
  target?: string;
30
30
  signal?: AbortSignal;
31
+ isCurrent?: () => boolean;
31
32
  }
32
33
 
33
34
  export async function runCancellableOperation(
@@ -37,21 +38,45 @@ export async function runCancellableOperation(
37
38
  runRoute: RunRoute,
38
39
  options: CancellableOperationOptions = {},
39
40
  ): Promise<CancellableOperationResult> {
41
+ const result = await runCancellableTask(
42
+ ctx,
43
+ message,
44
+ (signal, onCommit, onDialog) => runRoute(route, signal, onCommit, options.target, onDialog),
45
+ options,
46
+ );
47
+ return result.kind === "completed" ? (result.value ?? { kind: "failed" }) : result;
48
+ }
49
+
50
+ export type CancellableTaskResult<Value> =
51
+ | { kind: "completed"; value: Value }
52
+ | { kind: "closed" }
53
+ | { kind: "cancelled" };
54
+
55
+ /** Keep non-route work under the same cancellation and drain owner as transfers. */
56
+ export async function runCancellableTask<Value>(
57
+ ctx: ExtensionContext,
58
+ message: string,
59
+ task: (
60
+ signal: AbortSignal | undefined,
61
+ onCommit?: () => void,
62
+ onDialog?: (active: boolean) => void,
63
+ ) => Promise<Value>,
64
+ options: Omit<CancellableOperationOptions, "target"> = {},
65
+ ): Promise<CancellableTaskResult<Value>> {
40
66
  const {
41
67
  commitAware = false,
42
68
  cancelAcrossDialogs = false,
43
69
  cancelledMessage = "Check cancelled; no settings or files were changed.",
44
- target,
45
70
  signal,
71
+ isCurrent = () => true,
46
72
  } = options;
47
- if (ctx.mode !== "tui") {
48
- return (await runRoute(route, signal, undefined, target)) ?? { kind: "failed" };
49
- }
73
+ if (signal?.aborted || !isCurrent()) return { kind: "closed" };
74
+ if (ctx.mode !== "tui") return { kind: "completed", value: await task(signal) };
50
75
  let commitStarted = false;
51
- let routeResult: RunRouteResult | undefined;
76
+ let taskResult!: Value;
52
77
  const interaction = await runCustomInteraction<{ cancelled?: boolean; error?: unknown }, ExtensionContext>(ctx, {
53
78
  signal,
54
- isCurrent: () => !signal?.aborted,
79
+ isCurrent: () => !signal?.aborted && isCurrent(),
55
80
  create: ({ tui, theme, keybindings, signal: interactionSignal, complete }) => {
56
81
  const loader = new BorderedLoader(tui, theme, message, { cancellable: false });
57
82
  const cancelHint = formatInteractionHints(keybindings, [
@@ -80,13 +105,11 @@ export async function runCancellableOperation(
80
105
  return cancel(data) ? { consume: true } : undefined;
81
106
  })
82
107
  : undefined;
83
- let pendingRoute: ReturnType<RunRoute>;
108
+ let pendingTask: Promise<Value>;
84
109
  try {
85
- pendingRoute = runRoute(
86
- route,
110
+ pendingTask = task(
87
111
  interactionSignal,
88
112
  commitAware ? () => (commitStarted = true) : undefined,
89
- target,
90
113
  cancelAcrossDialogs
91
114
  ? (active) => {
92
115
  activeDialogs += active ? 1 : -1;
@@ -98,9 +121,9 @@ export async function runCancellableOperation(
98
121
  loader.dispose();
99
122
  throw error;
100
123
  }
101
- const operation = pendingRoute.then(
124
+ const operation = pendingTask.then(
102
125
  (result) => {
103
- routeResult = result;
126
+ taskResult = result;
104
127
  complete({});
105
128
  },
106
129
  (error: unknown) => complete({ error }),
@@ -134,7 +157,7 @@ export async function runCancellableOperation(
134
157
  return { kind: "cancelled" };
135
158
  }
136
159
  if (interaction.value.error) throw interaction.value.error;
137
- return routeResult ?? { kind: "failed" };
160
+ return { kind: "completed", value: taskResult };
138
161
  }
139
162
 
140
163
  function hasControlCharacter(value: string) {
@@ -1,5 +1,5 @@
1
1
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
- import { loadConfig } from "../settings/config.js";
2
+ import { loadConfig, syncCheckConfigFingerprint } from "../settings/config.js";
3
3
  import { activeLocalConfigPath } from "../settings/config-file.js";
4
4
  import { readLocalConfigObject } from "../settings/settings-store.js";
5
5
  import type { AnySyncConfig } from "../settings/settings-types.js";
@@ -39,6 +39,7 @@ export const MAIN_MENU_ACTIONS = [
39
39
  export interface ManagerDescription {
40
40
  title: string;
41
41
  actions: string[];
42
+ configReview?: { setupName: string; fingerprint: string };
42
43
  operation?: OperationAvailability;
43
44
  attention?: SyncAttentionState;
44
45
  observation?: StartupObservation;
@@ -176,6 +177,7 @@ export async function describeManagerState(
176
177
  return {
177
178
  title: (changesBlocked ? ["Manage sync", ...operationStatusLines(operation)] : ordinaryTitle).join("\n"),
178
179
  actions: operationActions(operation, noSyncedContent, canSwitch, mainActions),
180
+ configReview: { setupName: config.setupName, fingerprint: syncCheckConfigFingerprint(config) },
179
181
  operation,
180
182
  ...(currentObservation ? { observation: currentObservation } : {}),
181
183
  ...(currentAttention
@@ -31,6 +31,15 @@ export async function showSyncManager(
31
31
  await runRoute("help");
32
32
  return;
33
33
  }
34
+ const validateOwner = captureMutationOwner(ctx, sessionSignal);
35
+ const isCurrent = () => {
36
+ try {
37
+ validateOwner();
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ };
34
43
  type Screen = "main";
35
44
  type Action =
36
45
  | "review-attention"
@@ -77,10 +86,11 @@ export async function showSyncManager(
77
86
  },
78
87
  },
79
88
  actions: {
80
- "review-attention": async () => {
81
- const attention = options.getAttention?.();
89
+ "review-attention": async ({ state }) => {
90
+ const attention = state.manager.attention;
91
+ if (attention && options.getAttention?.() !== attention) return { kind: "stay" };
82
92
  if (!attention) {
83
- const observation = options.getObservation?.();
93
+ const observation = state.manager.observation;
84
94
  if (!observation) return { kind: "stay" };
85
95
  // A head-only observation is not an authoritative decision. Load a fresh
86
96
  // snapshot only after the user activates the existing review flow.
@@ -113,24 +123,21 @@ export async function showSyncManager(
113
123
  );
114
124
  return { kind: disposition };
115
125
  },
116
- sync: async () => {
126
+ sync: async ({ state }) => {
117
127
  const pendingAttention = options.getAttention?.();
118
- if (pendingAttention) {
119
- const activeConfig = await loadConfig();
120
- if (sessionSignal?.aborted) return { kind: "close" };
121
- if (pendingAttention.decision.setupName === activeConfig.setupName) {
122
- ctx.ui.notify("Review synced content before starting Sync now.", "warning");
123
- return { kind: "stay" };
124
- }
128
+ if (pendingAttention?.decision.setupName === state.manager.configReview?.setupName) {
129
+ ctx.ui.notify("Review synced content before starting Sync now.", "warning");
130
+ return { kind: "stay" };
125
131
  }
126
132
  const result = await runCancellableOperation(ctx, "Checking current sync setup…", "sync", runRoute, {
127
133
  commitAware: true,
128
134
  signal: sessionSignal,
135
+ target: state.manager.configReview?.setupName,
129
136
  });
130
137
  const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
131
138
  return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
132
139
  },
133
- "retry-refusal": async ({ signal: actionSignal }) => {
140
+ "retry-refusal": async ({ state, signal: actionSignal }) => {
134
141
  const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
135
142
  const validate = captureMutationOwner(ctx, signal);
136
143
  validate();
@@ -140,6 +147,7 @@ export async function showSyncManager(
140
147
  commitAware: true,
141
148
  cancelAcrossDialogs: true,
142
149
  signal,
150
+ target: state.manager.configReview?.setupName,
143
151
  cancelledMessage: "Model retry cancelled. Submitted requests and committed writes cannot be undone.",
144
152
  });
145
153
  if (signal.aborted) return { kind: "close" };
@@ -155,23 +163,25 @@ export async function showSyncManager(
155
163
  const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
156
164
  return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
157
165
  },
158
- settings: async () => {
159
- const result = await showSyncSettings(ctx, runRoute, sessionSignal);
166
+ settings: async ({ state }) => {
167
+ const result = await showSyncSettings(ctx, runRoute, sessionSignal, state.manager.configReview?.setupName);
160
168
  return { kind: result === "exit" ? "close" : "stay" };
161
169
  },
162
- pull: async () => {
170
+ pull: async ({ state }) => {
163
171
  const result = await runCancellableOperation(ctx, "Checking remote changes…", "pull", runRoute, {
164
172
  commitAware: true,
165
173
  cancelledMessage: "Pull check cancelled; no local files were changed.",
174
+ target: state.manager.configReview?.setupName,
166
175
  signal: sessionSignal,
167
176
  });
168
177
  const disposition = await dispatchManagerResult(ctx, result, "pull", runRoute, sessionSignal);
169
178
  return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
170
179
  },
171
- push: async () => {
180
+ push: async ({ state }) => {
172
181
  const result = await runCancellableOperation(ctx, "Preparing push preview…", "push", runRoute, {
173
182
  commitAware: true,
174
183
  cancelledMessage: "Push preparation cancelled; no remote files were changed.",
184
+ target: state.manager.configReview?.setupName,
175
185
  signal: sessionSignal,
176
186
  });
177
187
  const disposition = await dispatchManagerResult(ctx, result, "push", runRoute, sessionSignal);
@@ -185,31 +195,34 @@ export async function showSyncManager(
185
195
  await showStorageConnections(ctx, sessionSignal);
186
196
  return { kind: "stay" };
187
197
  },
188
- history: async () => {
198
+ history: async ({ state }) => {
189
199
  const result = await runCancellableOperation(ctx, "Loading sync history…", "history", runRoute, {
190
200
  signal: sessionSignal,
191
201
  commitAware: true,
192
202
  cancelAcrossDialogs: true,
203
+ target: state.manager.configReview?.setupName,
193
204
  cancelledMessage: "History review cancelled; no files were changed.",
194
205
  });
195
206
  return { kind: result.kind === "closed" ? "close" : "stay" };
196
207
  },
197
- diff: async () => {
208
+ diff: async ({ state }) => {
198
209
  const result = await runCancellableOperation(ctx, "Comparing local and remote…", "diff", runRoute, {
210
+ target: state.manager.configReview?.setupName,
199
211
  signal: sessionSignal,
200
212
  });
201
213
  return { kind: result.kind === "closed" ? "close" : "stay" };
202
214
  },
203
- status: async () => {
215
+ status: async ({ state }) => {
204
216
  const result = await runCancellableOperation(ctx, "Checking remote status…", "status", runRoute, {
217
+ target: state.manager.configReview?.setupName,
205
218
  signal: sessionSignal,
206
219
  });
207
220
  return { kind: result.kind === "closed" ? "close" : "stay" };
208
221
  },
209
- doctor: async ({ signal: actionSignal }) => {
222
+ doctor: async ({ state, signal: actionSignal }) => {
210
223
  const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
211
224
  const validate = captureMutationOwner(ctx, signal);
212
- const config = await loadConfig();
225
+ const config = await loadConfig(state.manager.configReview?.setupName);
213
226
  validate();
214
227
  const confirmed = await ctx.ui.confirm(
215
228
  "Run diagnostics?",
@@ -241,11 +254,12 @@ export async function showSyncManager(
241
254
  return { kind: "close" };
242
255
  },
243
256
  init: async () => {
244
- await runRoute("init");
257
+ await runRoute("init", sessionSignal);
245
258
  return { kind: "stay" };
246
259
  },
247
- conflicts: async () => {
260
+ conflicts: async ({ state }) => {
248
261
  const result = await runCancellableOperation(ctx, "Reviewing private conflicts…", "conflicts", runRoute, {
262
+ target: state.manager.configReview?.setupName,
249
263
  commitAware: true,
250
264
  signal: sessionSignal,
251
265
  });
@@ -254,6 +268,44 @@ export async function showSyncManager(
254
268
  },
255
269
  },
256
270
  });
271
+ // Setup-specific actions must use the configuration shown by this screen, not
272
+ // an active setup selected by another process while the menu was open.
273
+ for (const action of [
274
+ "review-attention",
275
+ "sync",
276
+ "retry-refusal",
277
+ "settings",
278
+ "pull",
279
+ "push",
280
+ "history",
281
+ "diff",
282
+ "status",
283
+ "doctor",
284
+ "conflicts",
285
+ ] as const) {
286
+ const activate = menu.actions[action];
287
+ menu.actions[action] = async (invocation) => {
288
+ validateOwner();
289
+ const review = invocation.state.manager.configReview;
290
+ let current = false;
291
+ try {
292
+ const config = await loadConfig();
293
+ validateOwner();
294
+ invocation.signal.throwIfAborted();
295
+ current = review?.fingerprint === syncCheckConfigFingerprint(config);
296
+ } catch {
297
+ if (!isCurrent() || invocation.signal.aborted) return { kind: "close" };
298
+ }
299
+ if (!current) {
300
+ ctx.ui.notify(
301
+ "Sync setup changed while the manager was open. Review the refreshed setup before continuing.",
302
+ "warning",
303
+ );
304
+ return { kind: "stay" };
305
+ }
306
+ return activate(invocation);
307
+ };
308
+ }
257
309
  const menuContext: ExtensionCommandContext = {
258
310
  ...ctx,
259
311
  ui: {
@@ -293,7 +345,7 @@ export async function showSyncManager(
293
345
  return { manager };
294
346
  },
295
347
  signal: sessionSignal,
296
- isCurrent: () => !sessionSignal?.aborted,
348
+ isCurrent,
297
349
  });
298
350
  }
299
351