@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
@@ -4,10 +4,10 @@ import { loadConfig } from "../settings/config.js";
4
4
  import { operationBlocksChanges, operationCanRecover } from "../state/operation-availability.js";
5
5
  import { type RunRoute, runCancellableOperation } from "./cancellable-operation.js";
6
6
  import {
7
- attentionMainMenuItems,
8
- blockedSyncMenuItem,
9
- type SyncManagerAttentionOptions,
10
- showManagerAttention,
7
+ attentionMainMenuItems,
8
+ blockedSyncMenuItem,
9
+ type SyncManagerAttentionOptions,
10
+ showManagerAttention,
11
11
  } from "./manager-attention.js";
12
12
  import { recoverSyncAccess } from "./manager-recovery.js";
13
13
  import { dispatchManagerResult } from "./manager-result-dispatcher.js";
@@ -19,368 +19,270 @@ import { showStorageConnections } from "./storage-connections-ui.js";
19
19
  import { observationMatchesConfig } from "./sync-attention.js";
20
20
 
21
21
  export async function showSyncManager(
22
- ctx: ExtensionCommandContext,
23
- runRoute: RunRoute,
24
- sessionSignal?: AbortSignal,
25
- options: SyncManagerAttentionOptions = {},
22
+ ctx: ExtensionCommandContext,
23
+ runRoute: RunRoute,
24
+ sessionSignal?: AbortSignal,
25
+ options: SyncManagerAttentionOptions = {},
26
26
  ): Promise<void> {
27
- if (!ctx.hasUI) {
28
- await runRoute("help");
29
- return;
30
- }
31
- type Screen = "main" | "more" | "recovery";
32
- type Action =
33
- | "review-attention"
34
- | "sync"
35
- | "switch"
36
- | "diff"
37
- | "settings"
38
- | "pull"
39
- | "push"
40
- | "setups"
41
- | "connections"
42
- | "history"
43
- | "doctor"
44
- | "unlock"
45
- | "recover"
46
- | "refresh"
47
- | "help"
48
- | "init"
49
- | "back";
50
- interface State {
51
- manager: Awaited<ReturnType<typeof describeManagerState>>;
52
- }
53
- const menu = defineMenu<State, Screen, Action, ExtensionCommandContext>({
54
- start: "main",
55
- screens: {
56
- main: ({ state }) => {
57
- const attentionItems = attentionMainMenuItems(state.manager);
58
- const managerItems = state.manager.actions.map(
59
- (label) => blockedSyncMenuItem(label, state.manager) ?? syncMainMenuItem(label),
60
- );
61
- const operationFirst =
62
- state.manager.operation !== undefined && state.manager.operation.kind !== "free";
63
- return {
64
- kind: "actions",
65
- title: "Manage sync",
66
- lines: state.manager.title.split("\n").slice(1),
67
- items: operationFirst
68
- ? [...managerItems, ...attentionItems]
69
- : [...attentionItems, ...managerItems],
70
- hint: "close",
71
- };
72
- },
73
- more: () => ({
74
- kind: "actions",
75
- title: "More options",
76
- items: [
77
- { id: "pull", label: "Pull from remote…", action: "pull" },
78
- { id: "push", label: "Push to remote…", action: "push" },
79
- { id: "setups", label: "Sync setups…", action: "setups" },
80
- {
81
- id: "connections",
82
- label: "Storage connections…",
83
- action: "connections",
84
- },
85
- {
86
- id: "doctor",
87
- label: "Check setup",
88
- description:
89
- "Check current setup access. WebDAV also probes writes and repairs history.",
90
- action: "doctor",
91
- },
92
- { id: "recovery", label: "History & recovery…", to: "recovery" },
93
- { id: "help", label: "Help", action: "help" },
94
- { id: "back", label: "Back", action: "back" },
95
- ],
96
- hint: "back",
97
- }),
98
- recovery: ({ state }) => ({
99
- kind: "actions",
100
- title: "History & recovery",
101
- items: [
102
- { id: "history", label: "Browse history", action: "history" },
103
- {
104
- id: "doctor",
105
- label: "Check setup",
106
- description: "WebDAV also probes writes and repairs history.",
107
- action: "doctor",
108
- },
109
- ...(state.manager.operation && operationCanRecover(state.manager.operation)
110
- ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" as const }]
111
- : []),
112
- { id: "back", label: "Back", action: "back" },
113
- ],
114
- hint: "back",
115
- }),
116
- },
117
- actions: {
118
- "review-attention": async () => {
119
- const attention = options.getAttention?.();
120
- if (!attention) {
121
- const observation = options.getObservation?.();
122
- if (!observation) return { kind: "stay" };
123
- // A head-only observation is not an authoritative decision. Load a fresh
124
- // snapshot only after the user activates the existing review flow.
125
- const { showRemoteSelectionReview } = await import("./remote-selection-ui.js");
126
- if (sessionSignal?.aborted) return { kind: "close" };
127
- const config = await loadConfig();
128
- if (sessionSignal?.aborted) return { kind: "close" };
129
- if (
130
- options.getObservation?.() !== observation ||
131
- !observationMatchesConfig(observation, config)
132
- ) {
133
- options.onObservationInvalidated?.();
134
- return { kind: "stay" };
135
- }
136
- // Keep the check-time hint on cancellation or failure. Route commits and
137
- // the manager's config/baseline reconciliation own invalidation.
138
- const result = await showRemoteSelectionReview(
139
- ctx,
140
- observation.setupName,
141
- sessionSignal,
142
- undefined,
143
- {
144
- origin: "sync",
145
- runRoute,
146
- onSelectionResolved: () => {
147
- if (!sessionSignal?.aborted && options.getObservation?.() === observation)
148
- options.onObservationInvalidated?.();
149
- },
150
- },
151
- );
152
- if (sessionSignal?.aborted) return { kind: "close" };
153
- if (result.kind === "route-result") {
154
- return dispatchManagerResult(ctx, result.result, result.route, runRoute, sessionSignal);
155
- }
156
- return { kind: result.kind === "closed" || result.kind === "stale" ? "close" : "stay" };
157
- }
158
- const disposition = await showManagerAttention(
159
- ctx,
160
- attention,
161
- runRoute,
162
- sessionSignal,
163
- () => options.onSelectionResolved?.(attention),
164
- );
165
- return { kind: disposition };
166
- },
167
- sync: async () => {
168
- const pendingAttention = options.getAttention?.();
169
- if (pendingAttention) {
170
- const activeConfig = await loadConfig();
171
- if (sessionSignal?.aborted) return { kind: "close" };
172
- if (pendingAttention.decision.setupName === activeConfig.setupName) {
173
- ctx.ui.notify("Review synced content before starting Sync now.", "warning");
174
- return { kind: "stay" };
175
- }
176
- }
177
- const result = await runCancellableOperation(
178
- ctx,
179
- "Checking current sync setup…",
180
- "sync",
181
- runRoute,
182
- {
183
- commitAware: true,
184
- signal: sessionSignal,
185
- },
186
- );
187
- const disposition = await dispatchManagerResult(
188
- ctx,
189
- result,
190
- "sync",
191
- runRoute,
192
- sessionSignal,
193
- );
194
- return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
195
- },
196
- switch: async () => {
197
- const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
198
- return result === "pull-attempted" || result === "closed"
199
- ? { kind: "close" }
200
- : { kind: "stay" };
201
- },
202
- diff: async () => {
203
- const result = await runCancellableOperation(
204
- ctx,
205
- "Checking current sync setup…",
206
- "diff",
207
- runRoute,
208
- { signal: sessionSignal },
209
- );
210
- return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
211
- },
212
- settings: async () => {
213
- await showSyncSettings(ctx, runRoute, sessionSignal);
214
- return { kind: "stay" };
215
- },
216
- pull: async () => {
217
- const result = await runCancellableOperation(
218
- ctx,
219
- "Checking remote changes…",
220
- "pull",
221
- runRoute,
222
- {
223
- commitAware: true,
224
- cancelledMessage: "Pull check cancelled; no local files were changed.",
225
- signal: sessionSignal,
226
- },
227
- );
228
- const disposition = await dispatchManagerResult(
229
- ctx,
230
- result,
231
- "pull",
232
- runRoute,
233
- sessionSignal,
234
- );
235
- return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
236
- },
237
- push: async () => {
238
- const result = await runCancellableOperation(
239
- ctx,
240
- "Preparing push preview…",
241
- "push",
242
- runRoute,
243
- {
244
- commitAware: true,
245
- cancelledMessage: "Push preparation cancelled; no remote files were changed.",
246
- signal: sessionSignal,
247
- },
248
- );
249
- const disposition = await dispatchManagerResult(
250
- ctx,
251
- result,
252
- "push",
253
- runRoute,
254
- sessionSignal,
255
- );
256
- return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
257
- },
258
- setups: async () => {
259
- const result = await showSyncSetupManager(ctx, runRoute, sessionSignal);
260
- return result === "exit" ? { kind: "close" } : { kind: "stay" };
261
- },
262
- connections: async () => {
263
- await showStorageConnections(ctx, sessionSignal);
264
- return { kind: "stay" };
265
- },
266
- history: async () => {
267
- await runRoute("history");
268
- return { kind: "stay" };
269
- },
270
- doctor: async () => {
271
- const result = await runCancellableOperation(
272
- ctx,
273
- "Checking setup access…",
274
- "doctor",
275
- runRoute,
276
- { signal: sessionSignal },
277
- );
278
- return { kind: result.kind === "closed" ? "close" : "stay" };
279
- },
280
- unlock: async ({ state, signal: actionSignal }) => {
281
- const result = await recoverSyncAccess(
282
- ctx,
283
- state.manager,
284
- runRoute,
285
- sessionSignal,
286
- actionSignal,
287
- );
288
- if (result === "close") return { kind: "close" };
289
- return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
290
- },
291
- recover: async ({ state, signal: actionSignal }) => {
292
- const result = await recoverSyncAccess(
293
- ctx,
294
- state.manager,
295
- runRoute,
296
- sessionSignal,
297
- actionSignal,
298
- );
299
- return { kind: result === "close" ? "close" : "stay" };
300
- },
301
- refresh: async () => ({ kind: "stay" }),
302
- help: async () => {
303
- await runRoute("help");
304
- return { kind: "close" };
305
- },
306
- init: async () => {
307
- await runRoute("init");
308
- return { kind: "stay" };
309
- },
310
- back: async () => ({ kind: "back" }),
311
- },
312
- });
313
- await runMenu(ctx, menu, {
314
- getState: async () => {
315
- const pendingAttention = options.getAttention?.();
316
- const observation = options.getObservation?.();
317
- const manager = await describeManagerState(
318
- sessionSignal,
319
- pendingAttention,
320
- undefined,
321
- observation,
322
- );
323
- if (sessionSignal?.aborted) return { manager };
324
- // A busy operation suppresses baseline reads, not the stored check-time hint.
325
- if (
326
- observation &&
327
- options.getObservation?.() === observation &&
328
- !manager.observation &&
329
- !(manager.operation && operationBlocksChanges(manager.operation))
330
- ) {
331
- options.onObservationInvalidated?.();
332
- }
333
- if (pendingAttention && options.getAttention?.() === pendingAttention && !manager.attention) {
334
- options.onSelectionResolved?.(pendingAttention);
335
- }
336
- return { manager };
337
- },
338
- signal: sessionSignal,
339
- isCurrent: () => !sessionSignal?.aborted,
340
- });
27
+ if (!ctx.hasUI) {
28
+ await runRoute("help");
29
+ return;
30
+ }
31
+ type Screen = "main" | "more" | "recovery";
32
+ type Action =
33
+ | "review-attention"
34
+ | "sync"
35
+ | "switch"
36
+ | "diff"
37
+ | "settings"
38
+ | "pull"
39
+ | "push"
40
+ | "setups"
41
+ | "connections"
42
+ | "history"
43
+ | "doctor"
44
+ | "unlock"
45
+ | "recover"
46
+ | "refresh"
47
+ | "help"
48
+ | "init"
49
+ | "back";
50
+ interface State {
51
+ manager: Awaited<ReturnType<typeof describeManagerState>>;
52
+ }
53
+ const menu = defineMenu<State, Screen, Action, ExtensionCommandContext>({
54
+ start: "main",
55
+ screens: {
56
+ main: ({ state }) => {
57
+ const attentionItems = attentionMainMenuItems(state.manager);
58
+ const managerItems = state.manager.actions.map(
59
+ (label) => blockedSyncMenuItem(label, state.manager) ?? syncMainMenuItem(label),
60
+ );
61
+ const operationFirst = state.manager.operation !== undefined && state.manager.operation.kind !== "free";
62
+ return {
63
+ kind: "actions",
64
+ title: "Manage sync",
65
+ lines: state.manager.title.split("\n").slice(1),
66
+ items: operationFirst ? [...managerItems, ...attentionItems] : [...attentionItems, ...managerItems],
67
+ hint: "close",
68
+ };
69
+ },
70
+ more: () => ({
71
+ kind: "actions",
72
+ title: "More options",
73
+ items: [
74
+ { id: "pull", label: "Pull from remote…", action: "pull" },
75
+ { id: "push", label: "Push to remote…", action: "push" },
76
+ { id: "setups", label: "Sync setups…", action: "setups" },
77
+ {
78
+ id: "connections",
79
+ label: "Storage connections…",
80
+ action: "connections",
81
+ },
82
+ {
83
+ id: "doctor",
84
+ label: "Check setup",
85
+ description: "Check current setup access. WebDAV also probes writes and repairs history.",
86
+ action: "doctor",
87
+ },
88
+ { id: "recovery", label: "History & recovery…", to: "recovery" },
89
+ { id: "help", label: "Help", action: "help" },
90
+ { id: "back", label: "Back", action: "back" },
91
+ ],
92
+ hint: "back",
93
+ }),
94
+ recovery: ({ state }) => ({
95
+ kind: "actions",
96
+ title: "History & recovery",
97
+ items: [
98
+ { id: "history", label: "Browse history", action: "history" },
99
+ {
100
+ id: "doctor",
101
+ label: "Check setup",
102
+ description: "WebDAV also probes writes and repairs history.",
103
+ action: "doctor",
104
+ },
105
+ ...(state.manager.operation && operationCanRecover(state.manager.operation)
106
+ ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" as const }]
107
+ : []),
108
+ { id: "back", label: "Back", action: "back" },
109
+ ],
110
+ hint: "back",
111
+ }),
112
+ },
113
+ actions: {
114
+ "review-attention": async () => {
115
+ const attention = options.getAttention?.();
116
+ if (!attention) {
117
+ const observation = options.getObservation?.();
118
+ if (!observation) return { kind: "stay" };
119
+ // A head-only observation is not an authoritative decision. Load a fresh
120
+ // snapshot only after the user activates the existing review flow.
121
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui.js");
122
+ if (sessionSignal?.aborted) return { kind: "close" };
123
+ const config = await loadConfig();
124
+ if (sessionSignal?.aborted) return { kind: "close" };
125
+ if (options.getObservation?.() !== observation || !observationMatchesConfig(observation, config)) {
126
+ options.onObservationInvalidated?.();
127
+ return { kind: "stay" };
128
+ }
129
+ // Keep the check-time hint on cancellation or failure. Route commits and
130
+ // the manager's config/baseline reconciliation own invalidation.
131
+ const result = await showRemoteSelectionReview(ctx, observation.setupName, sessionSignal, undefined, {
132
+ origin: "sync",
133
+ runRoute,
134
+ onSelectionResolved: () => {
135
+ if (!sessionSignal?.aborted && options.getObservation?.() === observation)
136
+ options.onObservationInvalidated?.();
137
+ },
138
+ });
139
+ if (sessionSignal?.aborted) return { kind: "close" };
140
+ if (result.kind === "route-result") {
141
+ return dispatchManagerResult(ctx, result.result, result.route, runRoute, sessionSignal);
142
+ }
143
+ return { kind: result.kind === "closed" || result.kind === "stale" ? "close" : "stay" };
144
+ }
145
+ const disposition = await showManagerAttention(ctx, attention, runRoute, sessionSignal, () =>
146
+ options.onSelectionResolved?.(attention),
147
+ );
148
+ return { kind: disposition };
149
+ },
150
+ sync: async () => {
151
+ const pendingAttention = options.getAttention?.();
152
+ if (pendingAttention) {
153
+ const activeConfig = await loadConfig();
154
+ if (sessionSignal?.aborted) return { kind: "close" };
155
+ if (pendingAttention.decision.setupName === activeConfig.setupName) {
156
+ ctx.ui.notify("Review synced content before starting Sync now.", "warning");
157
+ return { kind: "stay" };
158
+ }
159
+ }
160
+ const result = await runCancellableOperation(ctx, "Checking current sync setup…", "sync", runRoute, {
161
+ commitAware: true,
162
+ signal: sessionSignal,
163
+ });
164
+ const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
165
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
166
+ },
167
+ switch: async () => {
168
+ const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
169
+ return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
170
+ },
171
+ diff: async () => {
172
+ const result = await runCancellableOperation(ctx, "Checking current sync setup…", "diff", runRoute, {
173
+ signal: sessionSignal,
174
+ });
175
+ return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
176
+ },
177
+ settings: async () => {
178
+ await showSyncSettings(ctx, runRoute, sessionSignal);
179
+ return { kind: "stay" };
180
+ },
181
+ pull: async () => {
182
+ const result = await runCancellableOperation(ctx, "Checking remote changes…", "pull", runRoute, {
183
+ commitAware: true,
184
+ cancelledMessage: "Pull check cancelled; no local files were changed.",
185
+ signal: sessionSignal,
186
+ });
187
+ const disposition = await dispatchManagerResult(ctx, result, "pull", runRoute, sessionSignal);
188
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
189
+ },
190
+ push: async () => {
191
+ const result = await runCancellableOperation(ctx, "Preparing push preview…", "push", runRoute, {
192
+ commitAware: true,
193
+ cancelledMessage: "Push preparation cancelled; no remote files were changed.",
194
+ signal: sessionSignal,
195
+ });
196
+ const disposition = await dispatchManagerResult(ctx, result, "push", runRoute, sessionSignal);
197
+ return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
198
+ },
199
+ setups: async () => {
200
+ const result = await showSyncSetupManager(ctx, runRoute, sessionSignal);
201
+ return result === "exit" ? { kind: "close" } : { kind: "stay" };
202
+ },
203
+ connections: async () => {
204
+ await showStorageConnections(ctx, sessionSignal);
205
+ return { kind: "stay" };
206
+ },
207
+ history: async () => {
208
+ await runRoute("history");
209
+ return { kind: "stay" };
210
+ },
211
+ doctor: async () => {
212
+ const result = await runCancellableOperation(ctx, "Checking setup access…", "doctor", runRoute, {
213
+ signal: sessionSignal,
214
+ });
215
+ return { kind: result.kind === "closed" ? "close" : "stay" };
216
+ },
217
+ unlock: async ({ state, signal: actionSignal }) => {
218
+ const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
219
+ if (result === "close") return { kind: "close" };
220
+ return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
221
+ },
222
+ recover: async ({ state, signal: actionSignal }) => {
223
+ const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
224
+ return { kind: result === "close" ? "close" : "stay" };
225
+ },
226
+ refresh: async () => ({ kind: "stay" }),
227
+ help: async () => {
228
+ await runRoute("help");
229
+ return { kind: "close" };
230
+ },
231
+ init: async () => {
232
+ await runRoute("init");
233
+ return { kind: "stay" };
234
+ },
235
+ back: async () => ({ kind: "back" }),
236
+ },
237
+ });
238
+ await runMenu(ctx, menu, {
239
+ getState: async () => {
240
+ const pendingAttention = options.getAttention?.();
241
+ const observation = options.getObservation?.();
242
+ const manager = await describeManagerState(sessionSignal, pendingAttention, undefined, observation);
243
+ if (sessionSignal?.aborted) return { manager };
244
+ // A busy operation suppresses baseline reads, not the stored check-time hint.
245
+ if (
246
+ observation &&
247
+ options.getObservation?.() === observation &&
248
+ !manager.observation &&
249
+ !(manager.operation && operationBlocksChanges(manager.operation))
250
+ ) {
251
+ options.onObservationInvalidated?.();
252
+ }
253
+ if (pendingAttention && options.getAttention?.() === pendingAttention && !manager.attention) {
254
+ options.onSelectionResolved?.(pendingAttention);
255
+ }
256
+ return { manager };
257
+ },
258
+ signal: sessionSignal,
259
+ isCurrent: () => !sessionSignal?.aborted,
260
+ });
341
261
  }
342
262
 
343
263
  function syncMainMenuItem(
344
- label: string,
264
+ label: string,
345
265
  ): ActionMenuItem<
346
- "main" | "more" | "recovery",
347
- | "sync"
348
- | "switch"
349
- | "diff"
350
- | "settings"
351
- | "setups"
352
- | "connections"
353
- | "recover"
354
- | "refresh"
355
- | "help"
356
- | "init"
266
+ "main" | "more" | "recovery",
267
+ "sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "recover" | "refresh" | "help" | "init"
357
268
  > {
358
- if (label === "More…") return { id: "more", label, to: "more" };
359
- if (label === "History & recovery…") return { id: "recovery", label, to: "recovery" };
360
- const actions = new Map<
361
- string,
362
- | "sync"
363
- | "switch"
364
- | "diff"
365
- | "settings"
366
- | "setups"
367
- | "connections"
368
- | "recover"
369
- | "refresh"
370
- | "help"
371
- | "init"
372
- >([
373
- ["Sync now (recommended)", "sync"],
374
- ["Switch sync setup", "switch"],
375
- ["Status & changes", "diff"],
376
- ["Settings", "settings"],
377
- ["Restore sync access… (recommended)", "recover"],
378
- ["Refresh operation status", "refresh"],
379
- ["Sync setups…", "setups"],
380
- ["Storage connections…", "connections"],
381
- ["Help", "help"],
382
- ["Set up sync", "init"],
383
- ["Use existing settings", "init"],
384
- ]);
385
- return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
269
+ if (label === "More…") return { id: "more", label, to: "more" };
270
+ if (label === "History & recovery…") return { id: "recovery", label, to: "recovery" };
271
+ const actions = new Map<
272
+ string,
273
+ "sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "recover" | "refresh" | "help" | "init"
274
+ >([
275
+ ["Sync now (recommended)", "sync"],
276
+ ["Switch sync setup", "switch"],
277
+ ["Status & changes", "diff"],
278
+ ["Settings", "settings"],
279
+ ["Restore sync access… (recommended)", "recover"],
280
+ ["Refresh operation status", "refresh"],
281
+ ["Sync setups…", "setups"],
282
+ ["Storage connections…", "connections"],
283
+ ["Help", "help"],
284
+ ["Set up sync", "init"],
285
+ ["Use existing settings", "init"],
286
+ ]);
287
+ return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
386
288
  }