@narumitw/pi-sync 0.54.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +40 -15
  2. package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
  3. package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
  4. package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
  5. package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
  6. package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
  7. package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
  8. package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
  9. package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
  10. package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
  11. package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
  12. package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
  13. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
  14. package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
  15. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
  16. package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
  17. package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
  18. package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
  19. package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
  20. package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
  21. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
  22. package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
  23. package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
  24. package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
  25. package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
  26. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
  27. package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
  28. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
  29. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
  30. package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
  31. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
  32. package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
  33. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
  34. package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
  35. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
  36. package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
  37. package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
  38. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
  39. package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
  40. package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
  41. package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
  42. package/dist/chunks/chunk-QGK445K7.ts +21 -0
  43. package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
  44. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
  45. package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
  46. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
  47. package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
  48. package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
  49. package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
  50. package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
  51. package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
  52. package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
  53. package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
  54. package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
  55. package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
  56. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
  57. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  58. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  59. package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
  60. package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
  61. package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
  62. package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
  63. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
  64. package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
  65. package/dist/chunks/config-KGIYTTXQ.ts +37 -0
  66. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
  67. package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
  68. package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
  69. package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
  70. package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
  71. package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
  72. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
  73. package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
  74. package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
  75. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
  76. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
  77. package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
  78. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
  79. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
  80. package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
  81. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
  82. package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
  83. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
  84. package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
  85. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
  86. package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
  87. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
  88. package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
  89. package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
  90. package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
  91. package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
  92. package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
  93. package/dist/index.ts +246 -131
  94. package/dist/index.ts.map +3 -3
  95. package/docs/agent-mode-verification.md +99 -0
  96. package/docs/continuous-sync-verification.md +87 -0
  97. package/docs/settings.md +131 -11
  98. package/docs/tui-contract.md +61 -0
  99. package/package.json +3 -2
  100. package/src/backends/git/git-backend.ts +1 -4
  101. package/src/backends/git/git-runner.ts +1 -5
  102. package/src/backends/s3/s3-backend.ts +3 -25
  103. package/src/backends/s3/s3-client.ts +14 -2
  104. package/src/backends/snapshot-pointer.ts +27 -0
  105. package/src/backends/webdav/webdav-backend.ts +3 -29
  106. package/src/backends/webdav/webdav-client.ts +11 -7
  107. package/src/commands/command-execution.ts +7 -3
  108. package/src/commands/command-handler.ts +15 -3
  109. package/src/commands/command-types.ts +10 -0
  110. package/src/commands/command.ts +4 -1
  111. package/src/settings/config.ts +20 -10
  112. package/src/settings/settings-store.ts +1 -34
  113. package/src/settings/settings-types.ts +10 -0
  114. package/src/settings/settings-validation.ts +4 -1
  115. package/src/snapshot/snapshot-codec.ts +1 -5
  116. package/src/state/lock.ts +13 -10
  117. package/src/state/merge-baseline-store.ts +19 -1
  118. package/src/state/state-directory.ts +8 -1
  119. package/src/sync/agent-candidate.ts +251 -0
  120. package/src/sync/agent-plan.ts +87 -0
  121. package/src/sync/agent-policy.ts +13 -0
  122. package/src/sync/agent-request.ts +17 -0
  123. package/src/sync/agent-runner.ts +188 -0
  124. package/src/sync/agent-store.ts +151 -0
  125. package/src/sync/agent-sync.ts +91 -0
  126. package/src/sync/apply-review.ts +63 -0
  127. package/src/sync/automatic-sync.ts +46 -33
  128. package/src/sync/continuous-errors.ts +85 -0
  129. package/src/sync/continuous-lease.ts +221 -0
  130. package/src/sync/continuous-record.ts +179 -0
  131. package/src/sync/continuous-storage.ts +55 -0
  132. package/src/sync/continuous-sync.ts +516 -0
  133. package/src/sync/continuous-watch.ts +59 -0
  134. package/src/sync/merged-sync.ts +268 -112
  135. package/src/sync/remote-snapshot.ts +2 -3
  136. package/src/sync/settings-merge.ts +19 -3
  137. package/src/sync/setup-switch.ts +1 -5
  138. package/src/sync/startup-check.ts +51 -29
  139. package/src/sync/startup-timings.ts +90 -0
  140. package/src/sync/sync-inspection.ts +16 -5
  141. package/src/sync/sync-mutations.ts +55 -28
  142. package/src/sync/sync-queries.ts +19 -3
  143. package/src/sync/text-merge.ts +3 -3
  144. package/src/sync-extension.ts +75 -19
  145. package/src/ui/agent-mode-setting.ts +91 -0
  146. package/src/ui/manager-state.ts +12 -6
  147. package/src/ui/manager-ui.ts +57 -7
  148. package/src/ui/merge-review.ts +3 -3
  149. package/src/ui/merged-sync-summary.ts +2 -18
  150. package/src/ui/remote-selection-ui.ts +5 -5
  151. package/src/ui/settings-frame.ts +55 -0
  152. package/src/ui/settings-ui.ts +223 -43
  153. package/src/ui/setup/s3-credentials-ui.ts +1 -5
  154. package/src/ui/setup/setup-name-ui.ts +2 -5
  155. package/src/ui/setup/setup-prompts.ts +4 -3
  156. package/src/ui/setup/setup-switcher.ts +2 -0
  157. package/src/ui/sync-behavior.ts +15 -0
  158. package/src/ui/sync-format.ts +69 -38
  159. package/src/ui/sync-setups-ui.ts +3 -5
  160. package/src/ui/transfer-changes.ts +33 -0
  161. package/src/ui/transfer-review.ts +27 -0
  162. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  163. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  164. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  165. package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
  166. package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
  167. package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
  168. package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
  169. package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
  170. package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
  171. package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
  172. package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
  173. package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
  174. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  175. package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
  176. package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
  177. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  178. package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
  179. package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
  180. package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
  181. package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
  182. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
  183. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
  184. /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
  185. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
  186. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
  187. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
  188. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
  189. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
  190. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
  191. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
  192. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
  193. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
  194. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
2
+ import { defineMenu, type MenuSettingItem, runMenu } from "@narumitw/pi-tui-kit";
3
3
  import { loadConfig } from "../settings/config.js";
4
4
  import { localConfigPath } from "../settings/config-file.js";
5
5
  import { updateSyncSetup } from "../settings/settings-management.js";
@@ -12,11 +12,18 @@ import {
12
12
  setupSwitchActionLabel,
13
13
  } from "../sync/setup-switch.js";
14
14
  import { captureMutationOwner } from "../sync/sync-local.js";
15
+ import {
16
+ AGENT_MODE_DESCRIPTION,
17
+ CONTINUOUS_DESCRIPTION,
18
+ saveAgentMode,
19
+ saveContinuousMode,
20
+ } from "./agent-mode-setting.js";
15
21
  import type { RunRoute } from "./cancellable-operation.js";
16
22
  import { dispatchManagerResult } from "./manager-result-dispatcher.js";
23
+ import { boundedSettingsUI } from "./settings-frame.js";
17
24
  import { showEditSyncSetupStorage, showSyncSetupManager } from "./setup/setup-actions.js";
18
- import { AUTOMATIC_SYNC_DESCRIPTION } from "./setup/setup-prompts.js";
19
25
  import { showStorageConnections } from "./storage-connections-ui.js";
26
+ import { AUTOMATIC_SYNC_DESCRIPTION, STARTUP_TRANSFER_DESCRIPTION, startupTransferSummary } from "./sync-behavior.js";
20
27
  import { configureSyncStatus } from "./sync-status.js";
21
28
  import { safeTerminalText } from "./terminal-text.js";
22
29
 
@@ -70,55 +77,74 @@ export async function showSyncSettings(
70
77
  type Action =
71
78
  | "automatic"
72
79
  | "automatic-transfer"
80
+ | "agent-mode"
81
+ | "continuous"
73
82
  | "merge-settings"
74
83
  | "local-fields"
75
84
  | "content-policy"
76
85
  | "skip-secret-scan"
77
86
  | "show-status"
87
+ | "debug"
78
88
  | "on-switch"
79
89
  | "include"
80
90
  | "remote-include"
81
91
  | "storage-location"
82
92
  | "setups"
83
93
  | "connections";
84
- const menu = defineMenu<Awaited<ReturnType<typeof loadConfig>>, "settings", Action, ExtensionCommandContext>({
85
- start: "settings",
86
- screens: {
87
- settings: ({ state }) => ({
88
- kind: "settings",
89
- title: "Pi Sync Settings",
90
- lines: [
91
- `Sync setup: ${safeTerminalText(state.setupName)} · Storage connection: ${safeTerminalText(state.connectionName)}`,
92
- ],
93
- // Flat, searchable task ordering: Content, Destination, Automation, Merge, Other.
94
- // Kit has no heading rows; avoid focusable fake headings or a new component.
95
- items: [
94
+ type Group = "content" | "behavior" | "storage" | "advanced" | "all";
95
+ type Screen = "categories" | Group;
96
+ const groups: Record<Group, { label: string; ids: readonly string[] }> = {
97
+ content: { label: "Sync content", ids: ["include", "remoteInclude", "localFields"] },
98
+ behavior: {
99
+ label: "Sync behavior",
100
+ ids: ["automatic", "automaticTransfer", "agentMode", "continuous", "onSwitch"],
101
+ },
102
+ storage: { label: "Storage & setups", ids: ["storageLocation", "setups", "connections"] },
103
+ advanced: { label: "Advanced", ids: ["mergeSettings", "contentPolicy", "skipSecretScan", "showStatus", "debug"] },
104
+ all: { label: "All settings (search)", ids: [] },
105
+ };
106
+ let saveStatus = "Simple choices save immediately. Back does not undo saved changes.";
107
+ // Keep Kit's existing settings adapter: native SettingsList has no async save/rollback
108
+ // contract or injected keybindings. Kit preserves these safety and accessibility behaviors.
109
+ // Categories use public navigation; All settings retains cross-category search.
110
+ const settingsScreen = (state: Awaited<ReturnType<typeof loadConfig>>, group: Group) =>
111
+ ({
112
+ kind: "settings",
113
+ title: `Pi Sync Settings · ${groups[group].label}`,
114
+ lines: [
115
+ `Setup: ${safeTerminalText(state.setupName)} · Connection: ${safeTerminalText(state.connectionName)}`,
116
+ saveStatus,
117
+ ],
118
+ items: (
119
+ [
96
120
  {
97
121
  id: "include",
98
- label: "Included content",
99
- description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
100
- currentValue: "Open editor",
122
+ label: "Included paths",
123
+ description:
124
+ "This setup: choose paths in a draft, then review and Save or Discard. No transfer is started.",
125
+ currentValue: `${state.include.length} paths · Edit`,
101
126
  action: "include",
102
127
  },
103
128
  {
104
129
  id: "remoteInclude",
105
- label: "Compare synced content",
106
- description: "Review this device and remote content lists before choosing either one.",
130
+ label: "Compare included paths",
131
+ description:
132
+ "This setup: compare included-path policies, not file contents. Review before saving either list; no files transfer unless you explicitly Continue.",
107
133
  currentValue: "Review",
108
134
  action: "remote-include",
109
135
  },
110
136
  {
111
137
  id: "localFields",
112
- label: "Machine-local settings fields",
138
+ label: "Machine-local fields",
113
139
  description:
114
- "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
140
+ "This setup: settings.json root fields excluded from future portable snapshots, not project overrides. Policy changes require reviewed directional migration; old history remains.",
115
141
  currentValue: `${state.localFields?.length ?? 0} fields · Edit`,
116
142
  action: "local-fields",
117
143
  },
118
144
  {
119
145
  id: "storageLocation",
120
146
  label: "Storage location",
121
- description: "Destination: edit this setup's bucket, branch, or path; remote history is not moved.",
147
+ description: "This setup: review and save bucket, branch, or path changes. Remote history is not moved.",
122
148
  currentValue:
123
149
  state.backend.type === "s3"
124
150
  ? `${safeTerminalText(state.backend.destination.bucket)} · ${safeTerminalText(state.backend.destination.prefix)}`
@@ -130,7 +156,8 @@ export async function showSyncSettings(
130
156
  {
131
157
  id: "setups",
132
158
  label: "Manage sync setups",
133
- description: "Destination: add, switch, edit, or remove sync setups.",
159
+ description:
160
+ "This device: add, switch, edit, or remove sync setups. Each edit identifies its owner before saving.",
134
161
  currentValue: "Open",
135
162
  action: "setups",
136
163
  },
@@ -138,7 +165,7 @@ export async function showSyncSettings(
138
165
  id: "connections",
139
166
  label: "Manage storage connections",
140
167
  description:
141
- "Destination: manage shared server addresses and credentials; edits may affect multiple setups.",
168
+ "Shared connections on this device: review affected setups before saving server or credential changes.",
142
169
  currentValue: "Open",
143
170
  action: "connections",
144
171
  },
@@ -152,63 +179,114 @@ export async function showSyncSettings(
152
179
  },
153
180
  {
154
181
  id: "automaticTransfer",
155
- label: "Automatic transfer at startup",
156
- description:
157
- "May upload, replace, or delete selected files once at idle startup in TUI/RPC. Requires a baseline and conditional/lease publication; never reloads resources. Turning off cancels pending work.",
182
+ label: "Startup transfer",
183
+ description: `${STARTUP_TRANSFER_DESCRIPTION} Saved preference and requirements: ${startupTransferSummary(state)}.`,
158
184
  currentValue: state.automaticTransfer ? "On" : "Off",
159
185
  values: ["On", "Off"],
160
186
  action: "automatic-transfer",
161
187
  },
188
+ {
189
+ id: "agentMode",
190
+ label: "Agent mode (experimental)",
191
+ description: AGENT_MODE_DESCRIPTION,
192
+ currentValue: state.agentMode ? "On" : "Off",
193
+ values: ["On", "Off"],
194
+ action: "agent-mode",
195
+ },
196
+ {
197
+ id: "continuous",
198
+ label: "Continuous sync (experimental)",
199
+ description: `${CONTINUOUS_DESCRIPTION}${state.continuous && !state.agentMode ? " Saved On, but paused until Agent mode is enabled." : ""}`,
200
+ currentValue: state.continuous ? "On" : "Off",
201
+ values: ["On", "Off"],
202
+ action: "continuous",
203
+ },
162
204
  {
163
205
  id: "onSwitch",
164
- label: "After switching setup (all setups)",
165
- description: "All setups: ask before a pull review, open it automatically, or switch without pulling.",
206
+ label: "On setup switch",
207
+ description:
208
+ "All setups on this device: ask before a pull review, open it automatically, or switch without pulling. Applies to subsequent switches.",
166
209
  currentValue: setupSwitchActionLabel(state.onSwitch),
167
210
  values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
168
211
  action: "on-switch",
169
212
  },
170
213
  {
171
214
  id: "mergeSettings",
172
- label: "Settings field merge (experimental)",
215
+ label: "Merge settings fields",
173
216
  description:
174
- "Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
217
+ "This setup, experimental opt-in: merge independent global settings.json root fields with a verified private ancestor. Arrays and nested objects remain atomic. Applies to subsequent sync; no reload.",
175
218
  currentValue: state.mergeSettings ? "On" : "Off",
176
219
  values: ["On", "Off"],
177
220
  action: "merge-settings",
178
221
  },
179
222
  {
180
223
  id: "contentPolicy",
181
- label: "Content / partial sync (experimental)",
224
+ label: "Content merge / partial progress",
182
225
  description:
183
- "Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
226
+ "This setup, experimental opt-in: review the version-5 upgrade before saving. Merge bounded text and prefix-only sessions; partial progress keeps withheld versions and baselines. Applies to subsequent sync; disabling does not clear recovery evidence.",
184
227
  currentValue: state.mergeContent
185
228
  ? state.partialSync
186
- ? "Content & partial"
187
- : "Content only"
229
+ ? "Merge + partial"
230
+ : "Merge only"
188
231
  : state.partialSync
189
232
  ? "Partial only"
190
233
  : "Off",
191
- values: ["Off", "Content only", "Content & partial", "Partial only"],
234
+ values: ["Off", "Merge only", "Merge + partial", "Partial only"],
192
235
  action: "content-policy",
193
236
  },
194
237
  {
195
238
  id: "skipSecretScan",
196
- label: "Skip secret scan (all setups)",
197
- description: "All setups: allow pushes without checking managed local files for possible secrets.",
239
+ label: "Skip secret scan",
240
+ description:
241
+ "All setups on this device: skip possible-secret checks on subsequent pushes, including automatic pushes. Other safety checks remain. Diagnostics still scans.",
198
242
  currentValue: state.skipSecretScan ? "On" : "Off",
199
243
  values: ["On", "Off"],
200
244
  action: "skip-secret-scan",
201
245
  },
202
246
  {
203
247
  id: "showStatus",
204
- label: "Show status (all setups)",
205
- description: "All setups: show sync progress and attention in Pi status.",
248
+ label: "Show sync status",
249
+ description:
250
+ "All setups on this device: saved and applied immediately to Pi status. Review widgets and notifications remain available.",
206
251
  currentValue: state.showStatus ? "On" : "Off",
207
252
  values: ["On", "Off"],
208
253
  action: "show-status",
209
254
  },
255
+ {
256
+ id: "debug",
257
+ label: "Debug startup timings",
258
+ description:
259
+ "All setups on this device: show startup durations in TUI/RPC notifications. Applies next session start; no credentials or file contents.",
260
+ currentValue: state.debug ? "On" : "Off",
261
+ values: ["On", "Off"],
262
+ action: "debug",
263
+ },
264
+ ] satisfies MenuSettingItem<Action>[]
265
+ ).filter((item) => group === "all" || groups[group].ids.includes(item.id)),
266
+ }) as const;
267
+ const menu = defineMenu<Awaited<ReturnType<typeof loadConfig>>, Screen, Action, ExtensionCommandContext>({
268
+ start: "categories",
269
+ screens: {
270
+ categories: ({ state }) => ({
271
+ kind: "actions",
272
+ title: "Pi Sync Settings",
273
+ lines: [
274
+ `Setup: ${safeTerminalText(state.setupName)} · Connection: ${safeTerminalText(state.connectionName)}`,
275
+ "Choose a task, or search all settings. Scope is shown in each setting's description.",
276
+ saveStatus,
210
277
  ],
278
+ items: (Object.entries(groups) as [Group, (typeof groups)[Group]][]).map(([id, group]) => ({
279
+ id,
280
+ label: group.label,
281
+ to: id,
282
+ })),
283
+ hint: "back",
211
284
  }),
285
+ content: ({ state }) => settingsScreen(state, "content"),
286
+ behavior: ({ state }) => settingsScreen(state, "behavior"),
287
+ storage: ({ state }) => settingsScreen(state, "storage"),
288
+ advanced: ({ state }) => settingsScreen(state, "advanced"),
289
+ all: ({ state }) => settingsScreen(state, "all"),
212
290
  },
213
291
  actions: {
214
292
  "storage-location": async ({ signal: actionSignal }) => {
@@ -232,8 +310,8 @@ export async function showSyncSettings(
232
310
  try {
233
311
  const previous = await loadConfig(setupName);
234
312
  validate();
235
- const mergeContent = value === "Content only" || value === "Content & partial";
236
- const partialSync = value === "Partial only" || value === "Content & partial";
313
+ const mergeContent = value === "Merge only" || value === "Merge + partial";
314
+ const partialSync = value === "Partial only" || value === "Merge + partial";
237
315
  if (
238
316
  !(await ctx.ui.confirm(
239
317
  "Save experimental content policy?",
@@ -243,6 +321,7 @@ export async function showSyncSettings(
243
321
  )
244
322
  return { kind: "rejected" };
245
323
  validate();
324
+ ctx.ui.notify("Saving reviewed Pi Sync policy…", "info");
246
325
  await updateLocalConfig((current) => {
247
326
  validate();
248
327
  const setup = current.syncSetups[setupName];
@@ -262,6 +341,9 @@ export async function showSyncSettings(
262
341
  };
263
342
  }, mutationSignal);
264
343
  validate();
344
+ saveStatus =
345
+ "Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
346
+ ctx.ui.notify(saveStatus, "info");
265
347
  return { kind: "stay" };
266
348
  } catch (error) {
267
349
  if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
@@ -312,6 +394,7 @@ export async function showSyncSettings(
312
394
  );
313
395
  validate();
314
396
  if (!confirmed) return { kind: "rejected" };
397
+ ctx.ui.notify("Saving reviewed Pi Sync policy…", "info");
315
398
  await updateLocalConfig((current) => {
316
399
  validate();
317
400
  const setup = current.syncSetups[setupName];
@@ -327,6 +410,9 @@ export async function showSyncSettings(
327
410
  };
328
411
  }, mutationSignal);
329
412
  validate();
413
+ saveStatus =
414
+ "Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
415
+ ctx.ui.notify(saveStatus, "info");
330
416
  return { kind: "stay" };
331
417
  } catch (error) {
332
418
  if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
@@ -354,6 +440,61 @@ export async function showSyncSettings(
354
440
  return { kind: "rejected" };
355
441
  }
356
442
  },
443
+ "agent-mode": async ({ value, signal: actionSignal }) => {
444
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
445
+ const validate = captureMutationOwner(ctx, mutationSignal);
446
+ try {
447
+ const saved = await saveAgentMode(ctx, setupName, value === "On", mutationSignal);
448
+ if (!saved) return { kind: "rejected" };
449
+ try {
450
+ validate();
451
+ ctx.ui.notify(
452
+ "Agent mode saved. Applies to subsequent sync and the next session start; no transfer started.",
453
+ "info",
454
+ );
455
+ } catch {
456
+ // The save committed. A closed or replaced UI must not report a failed save.
457
+ // The next Settings read shows the persisted value.
458
+ }
459
+ return { kind: "stay" };
460
+ } catch (error) {
461
+ try {
462
+ validate();
463
+ } catch {
464
+ return { kind: "rejected" };
465
+ }
466
+ notifySaveFailure(ctx, error);
467
+ return { kind: "rejected" };
468
+ }
469
+ },
470
+ continuous: async ({ value, signal: actionSignal }) => {
471
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
472
+ const validate = captureMutationOwner(ctx, mutationSignal);
473
+ try {
474
+ const saved = await saveContinuousMode(ctx, setupName, value === "On", mutationSignal);
475
+ if (!saved) return { kind: "rejected" };
476
+ try {
477
+ validate();
478
+ ctx.ui.notify(
479
+ value === "On"
480
+ ? "Continuous sync enabled. Monitoring starts after the /sync command or manager closes; completed work is not undone."
481
+ : "Continuous sync disabled. Completed work is not undone.",
482
+ "info",
483
+ );
484
+ } catch {
485
+ /* committed save; do not use stale UI */
486
+ }
487
+ return { kind: "stay" };
488
+ } catch (error) {
489
+ try {
490
+ validate();
491
+ } catch {
492
+ return { kind: "rejected" };
493
+ }
494
+ notifySaveFailure(ctx, error);
495
+ return { kind: "rejected" };
496
+ }
497
+ },
357
498
  "automatic-transfer": async ({ value, signal: actionSignal }) => {
358
499
  const automaticTransfer = value === "On";
359
500
  const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
@@ -405,6 +546,18 @@ export async function showSyncSettings(
405
546
  return { kind: "rejected" };
406
547
  }
407
548
  },
549
+ debug: async ({ value, signal: actionSignal }) => {
550
+ const debug = value === "On";
551
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
552
+ try {
553
+ await updateLocalConfig((settings) => ({ ...settings, debug }), mutationSignal);
554
+ if (mutationSignal.aborted) return { kind: "rejected" };
555
+ return { kind: "stay" };
556
+ } catch (error) {
557
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
558
+ return { kind: "rejected" };
559
+ }
560
+ },
408
561
  "on-switch": async ({ value, signal: actionSignal }) => {
409
562
  const action = value ? setupSwitchActionFromLabel(value) : undefined;
410
563
  if (!action) return { kind: "rejected" };
@@ -444,6 +597,33 @@ export async function showSyncSettings(
444
597
  },
445
598
  },
446
599
  });
600
+ for (const id of [
601
+ "automatic",
602
+ "automatic-transfer",
603
+ "merge-settings",
604
+ "skip-secret-scan",
605
+ "show-status",
606
+ "debug",
607
+ "on-switch",
608
+ ] as const) {
609
+ const save = menu.actions[id];
610
+ menu.actions[id] = async (action) => {
611
+ const validate = captureMutationOwner(ctx, action.signal);
612
+ validate();
613
+ ctx.ui.notify("Saving Pi Sync setting…", "info");
614
+ const result = await save(action);
615
+ if (action.signal.aborted || signal?.aborted) return { kind: "rejected" };
616
+ validate();
617
+ saveStatus =
618
+ result?.kind === "rejected"
619
+ ? "Save failed or rejected. Previous value retained; retry is available."
620
+ : id === "automatic-transfer" || id === "automatic" || id === "debug"
621
+ ? "Saved. Enabling startup work applies next session start; completed transfers are not undone."
622
+ : "Saved. Applies to subsequent operations; status visibility applies immediately.";
623
+ ctx.ui.notify(saveStatus, result?.kind === "rejected" ? "warning" : "info");
624
+ return result;
625
+ };
626
+ }
447
627
  async function resumeSettings(editorSignal: AbortSignal) {
448
628
  if (editorSignal.aborted) return { kind: "close" as const };
449
629
  try {
@@ -455,7 +635,7 @@ export async function showSyncSettings(
455
635
  return { kind: "close" as const };
456
636
  }
457
637
  }
458
- await runMenu(ctx, menu, {
638
+ await runMenu({ ...ctx, ui: boundedSettingsUI(ctx) }, menu, {
459
639
  getState: () => loadConfig(setupName),
460
640
  signal,
461
641
  isCurrent: () => !signal?.aborted,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { throwIfAborted } from "../../sync/signals.js";
2
3
  import { promptSecret } from "../secret-input.js";
3
4
  import { requiredValueInput } from "./text-input.js";
4
5
 
@@ -70,8 +71,3 @@ export async function chooseS3Credentials(
70
71
  ready: true,
71
72
  };
72
73
  }
73
-
74
- function throwIfAborted(signal?: AbortSignal) {
75
- if (!signal?.aborted) return;
76
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
77
- }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { validateConfigName } from "../../settings/settings-validation.js";
3
+ import { throwIfAborted } from "../../sync/signals.js";
3
4
  import { errorMessage } from "../../sync/sync-errors.js";
4
5
  import { safeTerminalText } from "../terminal-text.js";
5
6
 
@@ -10,11 +11,7 @@ export async function promptInitialSetupName(ctx: ExtensionCommandContext, signa
10
11
  const guidance = ctx.mode === "tui" ? ctx.ui.theme.fg("muted", hint) : hint;
11
12
  // This compact prompt owns its default hint; the general helper would repeat it.
12
13
  const value = await ctx.ui.input(`Sync setup name\n${guidance}`, undefined, { signal });
13
- if (signal?.aborted) {
14
- throw signal.reason instanceof Error
15
- ? signal.reason
16
- : new DOMException("The operation was aborted", "AbortError");
17
- }
14
+ throwIfAborted(signal);
18
15
  if (value === undefined) return undefined;
19
16
  const name = value.trim() || "default";
20
17
  try {
@@ -2,7 +2,11 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { readLocalConfigObject } from "../../settings/settings-store.js";
3
3
  import { validateConfigName } from "../../settings/settings-validation.js";
4
4
  import { DEFAULT_SYNC_INCLUDE } from "../../sync/sync-policy.js";
5
+ import { AUTOMATIC_SYNC_DESCRIPTION } from "../sync-behavior.js";
5
6
  import { safeTerminalText } from "../terminal-text.js";
7
+
8
+ export { AUTOMATIC_SYNC_DESCRIPTION } from "../sync-behavior.js";
9
+
6
10
  import { promptTextInput } from "./text-input.js";
7
11
 
8
12
  export async function readSetupConnection(name: string, signal?: AbortSignal) {
@@ -12,9 +16,6 @@ export async function readSetupConnection(name: string, signal?: AbortSignal) {
12
16
  return connection;
13
17
  }
14
18
 
15
- export const AUTOMATIC_SYNC_DESCRIPTION =
16
- "Check the current setup in the background at startup; review changes in /sync before transferring. At shutdown, automatically push selected content only when sessions are included. No startup dialogs.";
17
-
18
19
  export async function chooseSetupContent(ctx: ExtensionCommandContext, signal?: AbortSignal) {
19
20
  const choice = await ctx.ui.select(
20
21
  "Choose included content\n\nRecommended: Pi settings, instructions, skills, prompts, themes, and extensions.\nMinimal: settings.json and AGENTS.md.\nReview the exact paths before saving.",
@@ -129,6 +129,8 @@ export async function showSetupSwitcher(
129
129
  `Included content: ${config.include.length} paths`,
130
130
  `Automatic sync: ${automaticSyncSummary(config.automatic)} · Sessions: ${config.include.includes("sessions") ? "On" : "Off"}`,
131
131
  `Automatic transfer at startup: ${config.automaticTransfer ? "On — may upload, replace, or delete selected files; no reload" : "Off"}`,
132
+ `Agent mode: ${config.agentMode ? "On (experimental; autonomous merged sync)" : "Off"}`,
133
+ `Continuous sync: ${config.continuous ? (config.agentMode ? "On (experimental; recurring requests and transfers)" : "Saved On; paused until Agent mode is enabled") : "Off"}`,
132
134
  "",
133
135
  switchEffect,
134
136
  ].join("\n"),
@@ -0,0 +1,15 @@
1
+ export const AUTOMATIC_SYNC_DESCRIPTION =
2
+ "This setup: check remote state once at startup in TUI/RPC. At shutdown, push all selected content only when sessions are included; reload skips that push. Saves apply to subsequent operations; a new check waits for the next session start. This does not enable startup transfer.";
3
+
4
+ export const STARTUP_TRANSFER_DESCRIPTION =
5
+ "This setup: separate opt-in for one idle startup transfer in TUI/RPC. Requires an accepted baseline, matching remote included paths, and conditional or lease-protected publication (Git, verified WebDAV, or R2/S3 with verified conditional writes). May upload, replace, or remove selected files. Never reloads resources. Enabling applies next session start; opening Settings cancels pending work. Completed transfers are not undone.";
6
+
7
+ export function startupTransferSummary(config: {
8
+ automaticTransfer?: boolean;
9
+ backend: { type: "s3" | "git" | "webdav" };
10
+ }) {
11
+ if (!config.automaticTransfer) return "Off";
12
+ return config.backend.type === "s3"
13
+ ? "On (next idle startup; R2/S3 conditional-write verification required)"
14
+ : "On (next idle startup; baseline and remote policy checks required)";
15
+ }