@narumitw/pi-sync 0.50.3 → 0.51.1

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 (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -3,209 +3,193 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
  import { syncCheckConfigFingerprint, syncConfigReviewFingerprint } from "../settings/config.js";
4
4
  import type { AnySyncConfig } from "../settings/settings-types.js";
5
5
  import type { StartupObservation } from "../sync/sync-inspection.js";
6
- import {
7
- compareSyncInclude,
8
- type RemoteSelectionDecision,
9
- sameSyncInclude,
10
- } from "../sync/sync-policy.js";
6
+ import { compareSyncInclude, type RemoteSelectionDecision, sameSyncInclude } from "../sync/sync-policy.js";
7
+ import { setSyncStatus } from "./sync-status.js";
11
8
  import { safeTerminalText } from "./terminal-text.js";
12
9
 
13
- const STATUS_KEY = "sync";
14
10
  const WIDGET_KEY = "sync:attention";
15
11
 
16
12
  export type SyncAttentionOrigin = "sync" | "pull" | "push";
17
13
 
18
14
  export interface SyncAttentionState {
19
- decision: RemoteSelectionDecision;
20
- origin: SyncAttentionOrigin;
21
- offered: boolean;
15
+ decision: RemoteSelectionDecision;
16
+ origin: SyncAttentionOrigin;
17
+ offered: boolean;
22
18
  }
23
19
 
24
20
  export function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {
25
- return (
26
- attention.decision.setupName === config.setupName &&
27
- attention.decision.configIdentity === syncConfigReviewFingerprint(config) &&
28
- sameSyncInclude(attention.decision.localInclude, config.include)
29
- );
21
+ return (
22
+ attention.decision.setupName === config.setupName &&
23
+ attention.decision.configIdentity === syncConfigReviewFingerprint(config) &&
24
+ sameSyncInclude(attention.decision.localInclude, config.include)
25
+ );
30
26
  }
31
27
 
32
28
  export interface SyncAttentionController {
33
- observe(observation: StartupObservation): void;
34
- observation(): StartupObservation | undefined;
35
- clearObservation(): void;
36
- notifyObservation(ctx: ExtensionContext): void;
37
- set(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;
38
- current(): SyncAttentionState | undefined;
39
- markOffered(): boolean;
40
- clear(ctx: ExtensionContext): void;
41
- reset(ctx: ExtensionContext): void;
42
- publish(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;
29
+ observe(observation: StartupObservation): void;
30
+ observation(): StartupObservation | undefined;
31
+ clearObservation(): void;
32
+ notifyObservation(ctx: ExtensionContext): void;
33
+ set(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;
34
+ current(): SyncAttentionState | undefined;
35
+ markOffered(): boolean;
36
+ clear(ctx: ExtensionContext): void;
37
+ reset(ctx: ExtensionContext): void;
38
+ publish(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;
43
39
  }
44
40
 
45
41
  export function createSyncAttentionController(): SyncAttentionController {
46
- let state: SyncAttentionState | undefined;
47
- let observation: StartupObservation | undefined;
48
- let generation = 0;
42
+ let state: SyncAttentionState | undefined;
43
+ let observation: StartupObservation | undefined;
44
+ let generation = 0;
49
45
 
50
- return {
51
- observe(value) {
52
- generation++;
53
- observation = value;
54
- },
55
- observation() {
56
- return observation;
57
- },
58
- clearObservation() {
59
- generation++;
60
- observation = undefined;
61
- },
62
- notifyObservation(ctx) {
63
- if (ctx.hasUI && observation && observationNeedsAttention(observation)) {
64
- ctx.ui.notify(observationLines(observation).join("\n"), "warning");
65
- }
66
- },
67
- set(decision, origin) {
68
- generation++;
69
- state = { decision, origin, offered: false };
70
- },
71
- current() {
72
- return state;
73
- },
74
- markOffered() {
75
- if (!state || state.offered) return false;
76
- state = { ...state, offered: true };
77
- return true;
78
- },
79
- clear(ctx) {
80
- generation++;
81
- state = undefined;
82
- clearAttentionPresentation(ctx);
83
- },
84
- reset(ctx) {
85
- generation++;
86
- state = undefined;
87
- observation = undefined;
88
- clearAttentionPresentation(ctx);
89
- },
90
- async publish(ctx, signal) {
91
- const currentGeneration = ++generation;
92
- if (signal?.aborted || !ctx.hasUI) return;
93
- const classification = observation ? classifyObservation(observation) : "none";
94
- if (!state && (classification === "none" || classification === "guidance")) {
95
- clearAttentionPresentation(ctx);
96
- return;
97
- }
98
- if (!state && classification === "status") {
99
- ctx.ui.setWidget(WIDGET_KEY, undefined);
100
- ctx.ui.setStatus(
101
- STATUS_KEY,
102
- observation?.inspection.remoteChanged
103
- ? "remote changes pending"
104
- : "local changes pending",
105
- );
106
- return;
107
- }
108
- const presentation = state
109
- ? attentionPresentation(state.decision)
110
- : {
111
- status: "changes to review",
112
- lines: observationLines(observation as StartupObservation),
113
- };
114
- if (ctx.mode !== "tui") {
115
- ctx.ui.setWidget(WIDGET_KEY, undefined);
116
- ctx.ui.setStatus(STATUS_KEY, presentation.status);
117
- return;
118
- }
119
- // Keep Kit outside the eager startup graph; module loading owns no cancellable resources.
120
- const { EditorStatusWidget } = await import("./attention-widget.js").catch((error) => {
121
- if (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);
122
- throw error;
123
- });
124
- if (generation !== currentGeneration || signal?.aborted) return;
125
- ctx.ui.setStatus(STATUS_KEY, presentation.status);
126
- ctx.ui.setWidget(
127
- WIDGET_KEY,
128
- (_tui, theme) =>
129
- new EditorStatusWidget({
130
- theme,
131
- renderBody: (width) =>
132
- presentation.lines.map((line, index) =>
133
- width === 0
134
- ? ""
135
- : truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), width, "…"),
136
- ),
137
- }),
138
- );
139
- },
140
- };
46
+ return {
47
+ observe(value) {
48
+ generation++;
49
+ observation = value;
50
+ },
51
+ observation() {
52
+ return observation;
53
+ },
54
+ clearObservation() {
55
+ generation++;
56
+ observation = undefined;
57
+ },
58
+ notifyObservation(ctx) {
59
+ if (ctx.hasUI && observation && observationNeedsAttention(observation)) {
60
+ ctx.ui.notify(observationLines(observation).join("\n"), "warning");
61
+ }
62
+ },
63
+ set(decision, origin) {
64
+ generation++;
65
+ state = { decision, origin, offered: false };
66
+ },
67
+ current() {
68
+ return state;
69
+ },
70
+ markOffered() {
71
+ if (!state || state.offered) return false;
72
+ state = { ...state, offered: true };
73
+ return true;
74
+ },
75
+ clear(ctx) {
76
+ generation++;
77
+ state = undefined;
78
+ clearAttentionPresentation(ctx);
79
+ },
80
+ reset(ctx) {
81
+ generation++;
82
+ state = undefined;
83
+ observation = undefined;
84
+ clearAttentionPresentation(ctx);
85
+ },
86
+ async publish(ctx, signal) {
87
+ const currentGeneration = ++generation;
88
+ if (signal?.aborted || !ctx.hasUI) return;
89
+ const classification = observation ? classifyObservation(observation) : "none";
90
+ if (!state && (classification === "none" || classification === "guidance")) {
91
+ clearAttentionPresentation(ctx);
92
+ return;
93
+ }
94
+ if (!state && classification === "status") {
95
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
96
+ setSyncStatus(ctx, observation?.inspection.remoteChanged ? "sync ⇣" : "sync ⇡");
97
+ return;
98
+ }
99
+ const presentation = state
100
+ ? attentionPresentation(state.decision)
101
+ : {
102
+ status: "sync ⇕",
103
+ lines: observationLines(observation as StartupObservation),
104
+ };
105
+ if (ctx.mode !== "tui") {
106
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
107
+ setSyncStatus(ctx, presentation.status);
108
+ return;
109
+ }
110
+ // Keep Kit outside the eager startup graph; module loading owns no cancellable resources.
111
+ const { EditorStatusWidget } = await import("./attention-widget.js").catch((error) => {
112
+ if (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);
113
+ throw error;
114
+ });
115
+ if (generation !== currentGeneration || signal?.aborted) return;
116
+ setSyncStatus(ctx, presentation.status);
117
+ ctx.ui.setWidget(
118
+ WIDGET_KEY,
119
+ (_tui, theme) =>
120
+ new EditorStatusWidget({
121
+ theme,
122
+ renderBody: (width) =>
123
+ presentation.lines.map((line, index) =>
124
+ width === 0 ? "" : truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), width, "…"),
125
+ ),
126
+ }),
127
+ );
128
+ },
129
+ };
141
130
  }
142
131
 
143
132
  function attentionPresentation(decision: RemoteSelectionDecision) {
144
- const setupName = safeTerminalText(decision.setupName);
145
- const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
146
- const difference =
147
- comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
148
- ? "Only list order differs"
149
- : `Remote ${comparison.remoteOnly.length} · Device ${comparison.localOnly.length}`;
150
- return {
151
- status: "review needed",
152
- lines: [`Pi Sync needs review · ${setupName}`, difference, "No changes · Run /sync to review"],
153
- };
133
+ const setupName = safeTerminalText(decision.setupName);
134
+ const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
135
+ const difference =
136
+ comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
137
+ ? "Only list order differs"
138
+ : `Remote ${comparison.remoteOnly.length} · Device ${comparison.localOnly.length}`;
139
+ return {
140
+ status: "sync ⇕",
141
+ lines: [`Pi Sync needs review · ${setupName}`, difference, "No changes · Run /sync to review"],
142
+ };
154
143
  }
155
144
 
156
145
  export function observationMatchesConfig(observation: StartupObservation, config: AnySyncConfig) {
157
- return observation.configIdentity === syncCheckConfigFingerprint(config);
146
+ return observation.configIdentity === syncCheckConfigFingerprint(config);
158
147
  }
159
148
 
160
149
  export function observationSummary(observation: StartupObservation) {
161
- const result = observation.inspection;
162
- if (result.emptyInclude) return "No included content selected";
163
- if (result.selectionState?.kind === "different")
164
- return "Synced-content list differs; review needed";
165
- if (result.firstSync)
166
- return result.head
167
- ? "No sync baseline; review remote and local content"
168
- : "Remote empty; no sync baseline";
169
- if (!result.head) return "Remote snapshot missing; review needed";
170
- if (result.localChanged && result.remoteChanged)
171
- return "Local and remote changed since last sync; review needed";
172
- if (result.remoteChanged) return "Remote changed since last sync";
173
- if (result.localChanged) return "Local content changed since last sync";
174
- if (result.selectionState?.kind === "legacy")
175
- return "Legacy remote has no authoritative content list";
176
- return "No changes detected since last sync";
150
+ const result = observation.inspection;
151
+ if (result.emptyInclude) return "No included content selected";
152
+ if (result.selectionState?.kind === "different") return "Synced-content list differs; review needed";
153
+ if (result.firstSync)
154
+ return result.head ? "No sync baseline; review remote and local content" : "Remote empty; no sync baseline";
155
+ if (!result.head) return "Remote snapshot missing; review needed";
156
+ if (result.localChanged && result.remoteChanged) return "Local and remote changed since last sync; review needed";
157
+ if (result.remoteChanged) return "Remote changed since last sync";
158
+ if (result.localChanged) return "Local content changed since last sync";
159
+ if (result.selectionState?.kind === "legacy") return "Legacy remote has no authoritative content list";
160
+ return "No changes detected since last sync";
177
161
  }
178
162
 
179
163
  export type ObservationClassification = "none" | "guidance" | "status" | "review";
180
164
 
181
165
  /** Presentation only: never discard an observation or authorize a transfer here. */
182
166
  export function classifyObservation(observation: StartupObservation): ObservationClassification {
183
- const result = observation.inspection;
184
- // Sync now exits before reading remote content when nothing is selected.
185
- if (result.emptyInclude) return "guidance";
186
- // Ordering is part of the existing transfer policy, not merely display order.
187
- if (result.selectionState?.kind === "different") return "review";
188
- if (result.firstSync) return result.head ? "review" : "guidance";
189
- if (!result.head || (result.localChanged && result.remoteChanged)) return "review";
190
- if (result.localChanged || result.remoteChanged) return "status";
191
- // Legacy metadata alone requires no scope confirmation in transfer operations.
192
- return "none";
167
+ const result = observation.inspection;
168
+ // Sync now exits before reading remote content when nothing is selected.
169
+ if (result.emptyInclude) return "guidance";
170
+ // Ordering is part of the existing transfer policy, not merely display order.
171
+ if (result.selectionState?.kind === "different") return "review";
172
+ if (result.firstSync) return result.head ? "review" : "guidance";
173
+ if (!result.head || (result.localChanged && result.remoteChanged)) return "review";
174
+ if (result.localChanged || result.remoteChanged) return "status";
175
+ // Legacy metadata alone requires no scope confirmation in transfer operations.
176
+ return "none";
193
177
  }
194
178
 
195
179
  export function observationNeedsAttention(observation: StartupObservation) {
196
- return classifyObservation(observation) === "review";
180
+ return classifyObservation(observation) === "review";
197
181
  }
198
182
 
199
183
  function observationLines(observation: StartupObservation) {
200
- return [
201
- `Pi Sync check · ${safeTerminalText(observation.setupName)}`,
202
- observationSummary(observation),
203
- "No startup transfer · Run /sync to review",
204
- ];
184
+ return [
185
+ `Pi Sync check · ${safeTerminalText(observation.setupName)}`,
186
+ observationSummary(observation),
187
+ "No startup transfer · Run /sync to review",
188
+ ];
205
189
  }
206
190
 
207
191
  function clearAttentionPresentation(ctx: ExtensionContext) {
208
- if (!ctx.hasUI) return;
209
- ctx.ui.setStatus(STATUS_KEY, undefined);
210
- ctx.ui.setWidget(WIDGET_KEY, undefined);
192
+ if (!ctx.hasUI) return;
193
+ setSyncStatus(ctx, undefined);
194
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
211
195
  }
@@ -7,159 +7,148 @@ import { fileHashMap } from "../sync/sync-state.js";
7
7
  import { safeTerminalText } from "./terminal-text.js";
8
8
 
9
9
  export function formatDiff(local: Snapshot, remote: Snapshot) {
10
- const localMap = fileHashMap(local);
11
- const remoteMap = fileHashMap(remote);
12
- const allPaths = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
13
- const lines = [
14
- `local: ${local.files.length} files`,
15
- `remote: ${remote.id} (${remote.files.length} files)`,
16
- "",
17
- ];
18
- let changed = 0;
19
- for (const filePath of allPaths) {
20
- if (!localMap[filePath]) {
21
- lines.push(`Remote only: ${filePath}`);
22
- changed += 1;
23
- } else if (!remoteMap[filePath]) {
24
- lines.push(`Local only: ${filePath}`);
25
- changed += 1;
26
- } else if (localMap[filePath] !== remoteMap[filePath]) {
27
- lines.push(`Different: ${filePath}`);
28
- changed += 1;
29
- }
30
- }
31
- if (changed === 0) lines.push("No file differences.");
32
- return lines.join("\n");
10
+ const localMap = fileHashMap(local);
11
+ const remoteMap = fileHashMap(remote);
12
+ const allPaths = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
13
+ const lines = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
14
+ let changed = 0;
15
+ for (const filePath of allPaths) {
16
+ if (!localMap[filePath]) {
17
+ lines.push(`Remote only: ${filePath}`);
18
+ changed += 1;
19
+ } else if (!remoteMap[filePath]) {
20
+ lines.push(`Local only: ${filePath}`);
21
+ changed += 1;
22
+ } else if (localMap[filePath] !== remoteMap[filePath]) {
23
+ lines.push(`Different: ${filePath}`);
24
+ changed += 1;
25
+ }
26
+ }
27
+ if (changed === 0) lines.push("No file differences.");
28
+ return lines.join("\n");
33
29
  }
34
30
 
35
31
  export function formatSnapshotOnlyDiff(title: string, snapshot: Snapshot) {
36
- return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${file.path}`)].join(
37
- "\n",
38
- );
32
+ return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${file.path}`)].join("\n");
39
33
  }
40
34
 
41
35
  export function formatPushSummary(
42
- config: CommonSyncConfig,
43
- destination: string,
44
- upload: Snapshot,
45
- head: RemoteHead | undefined,
46
- preservedRemoteFileCount = 0,
47
- remote?: Snapshot,
36
+ config: CommonSyncConfig,
37
+ destination: string,
38
+ upload: Snapshot,
39
+ head: RemoteHead | undefined,
40
+ preservedRemoteFileCount = 0,
41
+ remote?: Snapshot,
48
42
  ) {
49
- return [
50
- `Sync setup: ${safeTerminalText(config.setupName)}`,
51
- `Storage location: ${safeTerminalText(destination)}`,
52
- `Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
53
- `Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
54
- head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
55
- "Publication effect: the backend's active head will reference the new immutable snapshot.",
56
- ...(remote && inspectRemoteSelection(config.include, remote).kind === "different"
57
- ? [
58
- "Included-content policy effect: replace the differing remote selection with this setup's local selection.",
59
- ]
60
- : []),
61
- formatPublicationPreview(remote, upload),
62
- preservedRemoteFileCount > 0
63
- ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
64
- : "Possible secrets were scanned before this prompt.",
65
- ].join("\n");
43
+ return [
44
+ `Sync setup: ${safeTerminalText(config.setupName)}`,
45
+ `Storage location: ${safeTerminalText(destination)}`,
46
+ `Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
47
+ `Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
48
+ head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
49
+ "Publication effect: the backend's active head will reference the new immutable snapshot.",
50
+ ...(remote && inspectRemoteSelection(config.include, remote).kind === "different"
51
+ ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."]
52
+ : []),
53
+ formatPublicationPreview(remote, upload),
54
+ preservedRemoteFileCount > 0
55
+ ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
56
+ : "Possible secrets were scanned before this prompt.",
57
+ ].join("\n");
66
58
  }
67
59
 
68
60
  export function formatApplyPreview(local: Snapshot, remote: Snapshot) {
69
- return formatDirectionalChanges(local, remote, {
70
- add: "Add locally",
71
- update: "Update locally",
72
- remove: "Remove locally",
73
- });
61
+ return formatDirectionalChanges(local, remote, {
62
+ add: "Add locally",
63
+ update: "Update locally",
64
+ remove: "Remove locally",
65
+ });
74
66
  }
75
67
 
76
68
  export function formatPullSummary(
77
- config: CommonSyncConfig,
78
- destination: string,
79
- local: Snapshot,
80
- remote: Snapshot,
81
- protectedSessionCount: number,
69
+ config: CommonSyncConfig,
70
+ destination: string,
71
+ local: Snapshot,
72
+ remote: Snapshot,
73
+ protectedSessionCount: number,
82
74
  ) {
83
- return [
84
- `Sync setup: ${safeTerminalText(config.setupName)}`,
85
- `Storage location: ${safeTerminalText(destination)}`,
86
- `Snapshot: ${safeTerminalText(remote.id)}`,
87
- `Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
88
- `Protected live sessions: ${protectedSessionCount || "none"}`,
89
- formatApplyPreview(local, remote),
90
- "A local backup is created before these writes/deletes. The remote active snapshot is unchanged.",
91
- ].join("\n");
75
+ return [
76
+ `Sync setup: ${safeTerminalText(config.setupName)}`,
77
+ `Storage location: ${safeTerminalText(destination)}`,
78
+ `Snapshot: ${safeTerminalText(remote.id)}`,
79
+ `Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
80
+ `Protected live sessions: ${protectedSessionCount || "none"}`,
81
+ formatApplyPreview(local, remote),
82
+ "A local backup is created before these writes/deletes. The remote active snapshot is unchanged.",
83
+ ].join("\n");
92
84
  }
93
85
 
94
86
  export function formatRollbackSummary(
95
- config: CommonSyncConfig,
96
- destination: string,
97
- local: Snapshot,
98
- remote: Snapshot,
99
- requestedSnapshot: string,
100
- protectedSessionCount: number,
87
+ config: CommonSyncConfig,
88
+ destination: string,
89
+ local: Snapshot,
90
+ remote: Snapshot,
91
+ requestedSnapshot: string,
92
+ protectedSessionCount: number,
101
93
  ) {
102
- return [
103
- `Sync setup: ${safeTerminalText(config.setupName)}`,
104
- `Storage location: ${safeTerminalText(destination)}`,
105
- `Snapshot: ${safeTerminalText(requestedSnapshot)}`,
106
- `Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
107
- `Protected live sessions: ${protectedSessionCount || "none"}`,
108
- formatApplyPreview(local, remote),
109
- "A local backup is created before applying; the backend's active head will change.",
110
- ].join("\n");
94
+ return [
95
+ `Sync setup: ${safeTerminalText(config.setupName)}`,
96
+ `Storage location: ${safeTerminalText(destination)}`,
97
+ `Snapshot: ${safeTerminalText(requestedSnapshot)}`,
98
+ `Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
99
+ `Protected live sessions: ${protectedSessionCount || "none"}`,
100
+ formatApplyPreview(local, remote),
101
+ "A local backup is created before applying; the backend's active head will change.",
102
+ ].join("\n");
111
103
  }
112
104
 
113
105
  function formatPublicationPreview(remote: Snapshot | undefined, upload: Snapshot) {
114
- if (!remote) {
115
- return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${file.path}`)].join(
116
- "\n",
117
- );
118
- }
119
- return formatDirectionalChanges(remote, upload, {
120
- add: "Add remotely",
121
- update: "Update remotely",
122
- remove: "Remove remotely",
123
- });
106
+ if (!remote) {
107
+ return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${file.path}`)].join("\n");
108
+ }
109
+ return formatDirectionalChanges(remote, upload, {
110
+ add: "Add remotely",
111
+ update: "Update remotely",
112
+ remove: "Remove remotely",
113
+ });
124
114
  }
125
115
 
126
116
  function formatDirectionalChanges(
127
- before: Snapshot,
128
- after: Snapshot,
129
- labels: { add: string; update: string; remove: string },
117
+ before: Snapshot,
118
+ after: Snapshot,
119
+ labels: { add: string; update: string; remove: string },
130
120
  ) {
131
- const beforeMap = fileHashMap(before);
132
- const afterMap = fileHashMap(after);
133
- const paths = [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
134
- const lines: string[] = [];
135
- for (const filePath of paths) {
136
- if (!beforeMap[filePath]) lines.push(`${labels.add}: ${filePath}`);
137
- else if (!afterMap[filePath]) lines.push(`${labels.remove}: ${filePath}`);
138
- else if (beforeMap[filePath] !== afterMap[filePath])
139
- lines.push(`${labels.update}: ${filePath}`);
140
- }
141
- if (lines.length === 0) lines.push("No file changes.");
142
- return lines.join("\n");
121
+ const beforeMap = fileHashMap(before);
122
+ const afterMap = fileHashMap(after);
123
+ const paths = [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
124
+ const lines: string[] = [];
125
+ for (const filePath of paths) {
126
+ if (!beforeMap[filePath]) lines.push(`${labels.add}: ${filePath}`);
127
+ else if (!afterMap[filePath]) lines.push(`${labels.remove}: ${filePath}`);
128
+ else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${filePath}`);
129
+ }
130
+ if (lines.length === 0) lines.push("No file changes.");
131
+ return lines.join("\n");
143
132
  }
144
133
 
145
134
  export function countPreservedRemoteFiles(local: Snapshot, upload: Snapshot) {
146
- const localPaths = new Set(local.files.map((file) => file.path));
147
- return upload.files.filter((file) => !localPaths.has(file.path)).length;
135
+ const localPaths = new Set(local.files.map((file) => file.path));
136
+ return upload.files.filter((file) => !localPaths.has(file.path)).length;
148
137
  }
149
138
 
150
139
  export function publicationCapabilityDescription(capability: PublicationCapability) {
151
- switch (capability) {
152
- case "lease-protected":
153
- return "lease-protected (exact expected-ref update)";
154
- case "atomic-conditional":
155
- return "atomic-conditional (verified atomic precondition)";
156
- case "conditional-required":
157
- return "conditional-required (read-only until atomic preconditions are verified)";
158
- case "read-check-write-verify":
159
- return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
160
- }
140
+ switch (capability) {
141
+ case "lease-protected":
142
+ return "lease-protected (exact expected-ref update)";
143
+ case "atomic-conditional":
144
+ return "atomic-conditional (verified atomic precondition)";
145
+ case "conditional-required":
146
+ return "conditional-required (read-only until atomic preconditions are verified)";
147
+ case "read-check-write-verify":
148
+ return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
149
+ }
161
150
  }
162
151
 
163
152
  export function redact(value: string) {
164
- return value.length <= 8 ? "configured" : `${value.slice(0, 4)}…${value.slice(-4)}`;
153
+ return value.length <= 8 ? "configured" : `${value.slice(0, 4)}…${value.slice(-4)}`;
165
154
  }