@narumitw/pi-sync 0.53.1 → 0.54.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 (29) hide show
  1. package/README.md +6 -4
  2. package/dist/chunks/{chunk-TRCSEIAQ.ts → chunk-5ZWQ33XL.ts} +38 -18
  3. package/dist/chunks/{chunk-TRCSEIAQ.ts.map → chunk-5ZWQ33XL.ts.map} +2 -2
  4. package/dist/chunks/{chunk-FJGNDYGC.ts → chunk-VHXBCEVF.ts} +2 -2
  5. package/dist/chunks/{manager-result-dispatcher-AAEAYX57.ts → manager-result-dispatcher-6MQKFCWB.ts} +3 -3
  6. package/dist/chunks/{manager-ui-MHAHAKG3.ts → manager-ui-IURS4WXY.ts} +674 -609
  7. package/dist/chunks/manager-ui-IURS4WXY.ts.map +7 -0
  8. package/dist/chunks/{remote-selection-ui-Q2HUIBTL.ts → remote-selection-ui-6LJJ4IMQ.ts} +2 -2
  9. package/dist/chunks/{sync-operations-Y27F5T7W.ts → sync-operations-OWTE3ZL2.ts} +14 -8
  10. package/dist/chunks/{sync-operations-Y27F5T7W.ts.map → sync-operations-OWTE3ZL2.ts.map} +2 -2
  11. package/dist/index.ts +31 -6
  12. package/dist/index.ts.map +3 -3
  13. package/docs/settings.md +11 -3
  14. package/package.json +4 -4
  15. package/src/commands/command-execution.ts +3 -0
  16. package/src/commands/command-handler.ts +8 -1
  17. package/src/sync/sync-mutations.ts +9 -7
  18. package/src/sync/sync-queries.ts +1 -0
  19. package/src/ui/cancellable-operation.ts +47 -17
  20. package/src/ui/manager-attention.ts +3 -5
  21. package/src/ui/manager-state.ts +32 -15
  22. package/src/ui/manager-ui.ts +59 -87
  23. package/src/ui/operation-dialogs.ts +29 -0
  24. package/src/ui/settings-ui.ts +140 -46
  25. package/src/ui/setup/setup-actions.ts +2 -2
  26. package/dist/chunks/manager-ui-MHAHAKG3.ts.map +0 -7
  27. /package/dist/chunks/{chunk-FJGNDYGC.ts.map → chunk-VHXBCEVF.ts.map} +0 -0
  28. /package/dist/chunks/{manager-result-dispatcher-AAEAYX57.ts.map → manager-result-dispatcher-6MQKFCWB.ts.map} +0 -0
  29. /package/dist/chunks/{remote-selection-ui-Q2HUIBTL.ts.map → remote-selection-ui-6LJJ4IMQ.ts.map} +0 -0
@@ -25,11 +25,12 @@ import { countValidSyncSetups } from "./sync-setups-ui.js";
25
25
  import { safeTerminalText } from "./terminal-text.js";
26
26
 
27
27
  export const MAIN_MENU_ACTIONS = [
28
- "Sync now (recommended)",
29
- "Switch sync setup",
30
- "Status & changes",
28
+ "Sync now",
29
+ "Pull from remote…",
30
+ "Push to remote…",
31
31
  "Settings",
32
- "More…",
32
+ "History",
33
+ "Diagnostics",
33
34
  ] as const;
34
35
 
35
36
  export interface ManagerDescription {
@@ -125,7 +126,9 @@ export async function describeManagerState(
125
126
  ? "Never synced"
126
127
  : "Unavailable";
127
128
  const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
128
- const mainActions = MAIN_MENU_ACTIONS.filter((action) => action !== "Switch sync setup" || canSwitch);
129
+ const mainActions: string[] = [...MAIN_MENU_ACTIONS];
130
+ if (canSwitch) mainActions.push("Switch sync setup");
131
+ if (syncState?.unresolved?.length) mainActions.unshift("Review unresolved conflicts");
129
132
  const ordinaryTitle = [
130
133
  "Manage sync",
131
134
  "",
@@ -137,7 +140,7 @@ export async function describeManagerState(
137
140
  `Last applied: ${lastAppliedSnapshot}`,
138
141
  ...(syncState?.unresolved?.length
139
142
  ? [
140
- `Unresolved: ${syncState.unresolved.length} dependency groups · review History & recovery`,
143
+ `Unresolved: ${syncState.unresolved.length} dependency groups · choose Review unresolved conflicts`,
141
144
  `Last observed: ${safeTerminalText(syncState.lastObservedSnapshot ?? "unknown")}`,
142
145
  ]
143
146
  : []),
@@ -189,7 +192,7 @@ export async function describeManagerState(
189
192
  "",
190
193
  "What do you want to do?",
191
194
  ].join("\n"),
192
- actions: ["Sync setups…", "Storage connections…", "History & recovery…", "Help"],
195
+ actions: ["Sync setups…", "Storage connections…", "History", "Help"],
193
196
  };
194
197
  }
195
198
  }
@@ -201,13 +204,19 @@ function operationActions(
201
204
  mainActions: string[],
202
205
  ) {
203
206
  if (operationCanRecover(operation)) {
204
- return ["Restore sync access… (recommended)", "Status & changes", "History & recovery…", "Help"];
207
+ return ["Restore sync access… (recommended)", "History", "Help"];
205
208
  }
206
209
  if (operation.kind !== "free") {
207
- return ["Refresh operation status", "Status & changes", "History & recovery…", "Help"];
210
+ return ["Refresh operation status", "History", "Help"];
208
211
  }
209
212
  return noSyncedContent
210
- ? ["Settings", ...(canSwitch ? ["Switch sync setup"] : []), "Status & changes", "More…"]
213
+ ? [
214
+ ...(mainActions.includes("Review unresolved conflicts") ? ["Review unresolved conflicts"] : []),
215
+ "Settings",
216
+ ...(canSwitch ? ["Switch sync setup"] : []),
217
+ "History",
218
+ "Diagnostics",
219
+ ]
211
220
  : mainActions;
212
221
  }
213
222
 
@@ -217,16 +226,24 @@ function operationStatusLines(operation: OperationAvailability): string[] {
217
226
  return [];
218
227
  case "live": {
219
228
  const command = truncateToWidth(safeTerminalText(operation.lock.command), 16, "…");
220
- return [`Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Settings and More return.`];
229
+ return [
230
+ `Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Transfer actions and Settings return.`,
231
+ ];
221
232
  }
222
233
  case "busy":
223
- return ["Pi-sync may be starting or finishing. Wait, then refresh; Settings and More remain unavailable."];
234
+ return [
235
+ "Pi-sync may be starting or finishing. Wait, then refresh; Transfer actions and Settings remain unavailable.",
236
+ ];
224
237
  case "recoverable-stale":
225
- return ["Sync paused: old lock remains. Close other Pi sessions then restore; Settings and More return."];
238
+ return [
239
+ "Sync paused: old lock remains. Close other Pi sessions then restore; Transfer actions and Settings return.",
240
+ ];
226
241
  case "recoverable-unreadable":
227
- return ["Sync paused: owner unknown. Close other Pi sessions then restore; Settings and More return."];
242
+ return [
243
+ "Sync paused: owner unknown. Close other Pi sessions then restore; Transfer actions and Settings return.",
244
+ ];
228
245
  case "inspection-error":
229
- return ["Lock check failed. Fix path access, then refresh; Settings and More remain unavailable."];
246
+ return ["Lock check failed. Fix path access, then refresh; Transfer actions and Settings remain unavailable."];
230
247
  }
231
248
  }
232
249
 
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
3
  import { loadConfig } from "../settings/config.js";
4
- import { operationBlocksChanges, operationCanRecover } from "../state/operation-availability.js";
4
+ import { operationBlocksChanges } from "../state/operation-availability.js";
5
5
  import { type RunRoute, runCancellableOperation } from "./cancellable-operation.js";
6
6
  import {
7
7
  attentionMainMenuItems,
@@ -28,12 +28,11 @@ export async function showSyncManager(
28
28
  await runRoute("help");
29
29
  return;
30
30
  }
31
- type Screen = "main" | "more" | "recovery";
31
+ type Screen = "main";
32
32
  type Action =
33
33
  | "review-attention"
34
34
  | "sync"
35
35
  | "switch"
36
- | "diff"
37
36
  | "settings"
38
37
  | "pull"
39
38
  | "push"
@@ -42,12 +41,10 @@ export async function showSyncManager(
42
41
  | "history"
43
42
  | "conflicts"
44
43
  | "doctor"
45
- | "unlock"
46
44
  | "recover"
47
45
  | "refresh"
48
46
  | "help"
49
- | "init"
50
- | "back";
47
+ | "init";
51
48
  interface State {
52
49
  manager: Awaited<ReturnType<typeof describeManagerState>>;
53
50
  }
@@ -63,54 +60,15 @@ export async function showSyncManager(
63
60
  return {
64
61
  kind: "actions",
65
62
  title: "Manage sync",
66
- lines: state.manager.title.split("\n").slice(1),
63
+ // Frequent transfers stay visible; status is advisory and Help remains a direct route.
64
+ lines: [
65
+ ...state.manager.title.split("\n").slice(1),
66
+ "Help: /sync help · Fresh status: /sync status · Differences: /sync diff",
67
+ ],
67
68
  items: operationFirst ? [...managerItems, ...attentionItems] : [...attentionItems, ...managerItems],
68
69
  hint: "close",
69
70
  };
70
71
  },
71
- more: () => ({
72
- kind: "actions",
73
- title: "More options",
74
- items: [
75
- { id: "pull", label: "Pull from remote…", action: "pull" },
76
- { id: "push", label: "Push to remote…", action: "push" },
77
- { id: "setups", label: "Sync setups…", action: "setups" },
78
- {
79
- id: "connections",
80
- label: "Storage connections…",
81
- action: "connections",
82
- },
83
- {
84
- id: "doctor",
85
- label: "Check setup",
86
- description: "Check current setup access. WebDAV also probes writes and repairs history.",
87
- action: "doctor",
88
- },
89
- { id: "recovery", label: "History & recovery…", to: "recovery" },
90
- { id: "help", label: "Help", action: "help" },
91
- { id: "back", label: "Back", action: "back" },
92
- ],
93
- hint: "back",
94
- }),
95
- recovery: ({ state }) => ({
96
- kind: "actions",
97
- title: "History & recovery",
98
- items: [
99
- { id: "history", label: "Browse history", action: "history" },
100
- { id: "conflicts", label: "Review unresolved conflicts", action: "conflicts" },
101
- {
102
- id: "doctor",
103
- label: "Check setup",
104
- description: "WebDAV also probes writes and repairs history.",
105
- action: "doctor",
106
- },
107
- ...(state.manager.operation && operationCanRecover(state.manager.operation)
108
- ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" as const }]
109
- : []),
110
- { id: "back", label: "Back", action: "back" },
111
- ],
112
- hint: "back",
113
- }),
114
72
  },
115
73
  actions: {
116
74
  "review-attention": async () => {
@@ -170,15 +128,9 @@ export async function showSyncManager(
170
128
  const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
171
129
  return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
172
130
  },
173
- diff: async () => {
174
- const result = await runCancellableOperation(ctx, "Checking current sync setup…", "diff", runRoute, {
175
- signal: sessionSignal,
176
- });
177
- return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
178
- },
179
131
  settings: async () => {
180
- await showSyncSettings(ctx, runRoute, sessionSignal);
181
- return { kind: "stay" };
132
+ const result = await showSyncSettings(ctx, runRoute, sessionSignal);
133
+ return { kind: result === "exit" ? "close" : "stay" };
182
134
  },
183
135
  pull: async () => {
184
136
  const result = await runCancellableOperation(ctx, "Checking remote changes…", "pull", runRoute, {
@@ -207,8 +159,13 @@ export async function showSyncManager(
207
159
  return { kind: "stay" };
208
160
  },
209
161
  history: async () => {
210
- await runRoute("history");
211
- return { kind: "stay" };
162
+ const result = await runCancellableOperation(ctx, "Loading sync history…", "history", runRoute, {
163
+ signal: sessionSignal,
164
+ commitAware: true,
165
+ cancelAcrossDialogs: true,
166
+ cancelledMessage: "History review cancelled; no files were changed.",
167
+ });
168
+ return { kind: result.kind === "closed" ? "close" : "stay" };
212
169
  },
213
170
  doctor: async () => {
214
171
  const result = await runCancellableOperation(ctx, "Checking setup access…", "doctor", runRoute, {
@@ -216,11 +173,6 @@ export async function showSyncManager(
216
173
  });
217
174
  return { kind: result.kind === "closed" ? "close" : "stay" };
218
175
  },
219
- unlock: async ({ state, signal: actionSignal }) => {
220
- const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
221
- if (result === "close") return { kind: "close" };
222
- return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
223
- },
224
176
  recover: async ({ state, signal: actionSignal }) => {
225
177
  const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
226
178
  return { kind: result === "close" ? "close" : "stay" };
@@ -242,7 +194,6 @@ export async function showSyncManager(
242
194
  const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
243
195
  return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
244
196
  },
245
- back: async () => ({ kind: "back" }),
246
197
  },
247
198
  });
248
199
  await runMenu(ctx, menu, {
@@ -273,26 +224,47 @@ export async function showSyncManager(
273
224
  function syncMainMenuItem(
274
225
  label: string,
275
226
  ): ActionMenuItem<
276
- "main" | "more" | "recovery",
277
- "sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "recover" | "refresh" | "help" | "init"
227
+ "main",
228
+ | "sync"
229
+ | "pull"
230
+ | "push"
231
+ | "switch"
232
+ | "settings"
233
+ | "setups"
234
+ | "connections"
235
+ | "history"
236
+ | "conflicts"
237
+ | "doctor"
238
+ | "recover"
239
+ | "refresh"
240
+ | "help"
241
+ | "init"
278
242
  > {
279
- if (label === "More…") return { id: "more", label, to: "more" };
280
- if (label === "History & recovery…") return { id: "recovery", label, to: "recovery" };
281
- const actions = new Map<
282
- string,
283
- "sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "recover" | "refresh" | "help" | "init"
284
- >([
285
- ["Sync now (recommended)", "sync"],
286
- ["Switch sync setup", "switch"],
287
- ["Status & changes", "diff"],
288
- ["Settings", "settings"],
289
- ["Restore sync access… (recommended)", "recover"],
290
- ["Refresh operation status", "refresh"],
291
- ["Sync setups…", "setups"],
292
- ["Storage connections…", "connections"],
293
- ["Help", "help"],
294
- ["Set up sync", "init"],
295
- ["Use existing settings", "init"],
296
- ]);
297
- return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
243
+ const actions = {
244
+ "Sync now": "sync",
245
+ "Pull from remote…": "pull",
246
+ "Push to remote…": "push",
247
+ "Switch sync setup": "switch",
248
+ Settings: "settings",
249
+ History: "history",
250
+ Diagnostics: "doctor",
251
+ "Review unresolved conflicts": "conflicts",
252
+ "Restore sync access… (recommended)": "recover",
253
+ "Refresh operation status": "refresh",
254
+ "Sync setups…": "setups",
255
+ "Storage connections…": "connections",
256
+ Help: "help",
257
+ "Set up sync": "init",
258
+ "Use existing settings": "init",
259
+ } as const;
260
+ const action = actions[label as keyof typeof actions];
261
+ if (!action) throw new Error(`Unknown sync manager action: ${label}`);
262
+ return {
263
+ id: action,
264
+ label,
265
+ action,
266
+ ...(action === "doctor"
267
+ ? { description: "Check setup access; WebDAV also probes writes and repairs history." }
268
+ : {}),
269
+ };
298
270
  }
@@ -0,0 +1,29 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Track History's Pi-owned selector/confirmation lifetime without inspecting renderer internals. */
4
+ export function trackOperationDialogs<Context extends ExtensionContext>(
5
+ ctx: Context,
6
+ onDialog?: (active: boolean) => void,
7
+ ): Context {
8
+ if (!onDialog) return ctx;
9
+ const runDialog = async <Value>(task: () => Promise<Value>): Promise<Value> => {
10
+ onDialog(true);
11
+ try {
12
+ return await task();
13
+ } finally {
14
+ onDialog(false);
15
+ }
16
+ };
17
+ const ui: ExtensionContext["ui"] = {
18
+ ...ctx.ui,
19
+ select: (...args) => runDialog(() => ctx.ui.select(...args)),
20
+ confirm: (...args) => runDialog(() => ctx.ui.confirm(...args)),
21
+ };
22
+ // Forward live context properties: copying the context would hide session
23
+ // replacement from mutation-owner revalidation after a dialog settles.
24
+ return new Proxy(ctx, {
25
+ get(target, key, receiver) {
26
+ return key === "ui" ? ui : Reflect.get(target, key, receiver);
27
+ },
28
+ });
29
+ }
@@ -14,7 +14,9 @@ import {
14
14
  import { captureMutationOwner } from "../sync/sync-local.js";
15
15
  import type { RunRoute } from "./cancellable-operation.js";
16
16
  import { dispatchManagerResult } from "./manager-result-dispatcher.js";
17
+ import { showEditSyncSetupStorage, showSyncSetupManager } from "./setup/setup-actions.js";
17
18
  import { AUTOMATIC_SYNC_DESCRIPTION } from "./setup/setup-prompts.js";
19
+ import { showStorageConnections } from "./storage-connections-ui.js";
18
20
  import { configureSyncStatus } from "./sync-status.js";
19
21
  import { safeTerminalText } from "./terminal-text.js";
20
22
 
@@ -24,13 +26,46 @@ export async function showSyncSettings(
24
26
  ctx: ExtensionCommandContext,
25
27
  runRoute: SyncSettingsRoute,
26
28
  signal?: AbortSignal,
27
- ) {
29
+ ): Promise<"back" | "exit"> {
30
+ let disposition: "back" | "exit" = "back";
31
+ const exitSettings = () => {
32
+ disposition = "exit";
33
+ return { kind: "close" as const };
34
+ };
28
35
  if (ctx.mode !== "tui") {
29
36
  ctx.ui.notify(`Edit pi-sync settings manually: ${safeTerminalText(localConfigPath())}`, "info");
30
- return;
37
+ if (ctx.mode === "rpc" && ctx.hasUI) {
38
+ // Preserve RPC catalog management; preference/credential editing stays TUI-only.
39
+ const management = defineMenu<undefined, "management", "setups" | "connections", ExtensionCommandContext>({
40
+ start: "management",
41
+ screens: {
42
+ management: () => ({
43
+ kind: "actions",
44
+ title: "Sync settings management",
45
+ items: [
46
+ { id: "setups", label: "Manage sync setups", action: "setups" },
47
+ { id: "connections", label: "Manage storage connections", action: "connections" },
48
+ ],
49
+ hint: "back",
50
+ }),
51
+ },
52
+ actions: {
53
+ setups: async ({ signal: actionSignal }) => {
54
+ const result = await showSyncSetupManager(ctx, runRoute, actionSignal);
55
+ return result === "exit" ? exitSettings() : { kind: "stay" };
56
+ },
57
+ connections: async ({ signal: actionSignal }) => {
58
+ await showStorageConnections(ctx, actionSignal);
59
+ return { kind: "stay" };
60
+ },
61
+ },
62
+ });
63
+ await runMenu(ctx, management, { getState: () => undefined, signal, isCurrent: () => !signal?.aborted });
64
+ }
65
+ return disposition;
31
66
  }
32
67
  const initial = await loadConfig();
33
- if (signal?.aborted) return;
68
+ if (signal?.aborted) return "back";
34
69
  const setupName = initial.setupName;
35
70
  type Action =
36
71
  | "automatic"
@@ -42,7 +77,10 @@ export async function showSyncSettings(
42
77
  | "show-status"
43
78
  | "on-switch"
44
79
  | "include"
45
- | "remote-include";
80
+ | "remote-include"
81
+ | "storage-location"
82
+ | "setups"
83
+ | "connections";
46
84
  const menu = defineMenu<Awaited<ReturnType<typeof loadConfig>>, "settings", Action, ExtensionCommandContext>({
47
85
  start: "settings",
48
86
  screens: {
@@ -52,39 +90,9 @@ export async function showSyncSettings(
52
90
  lines: [
53
91
  `Sync setup: ${safeTerminalText(state.setupName)} · Storage connection: ${safeTerminalText(state.connectionName)}`,
54
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.
55
95
  items: [
56
- {
57
- id: "automatic",
58
- label: "Automatic sync",
59
- description: AUTOMATIC_SYNC_DESCRIPTION,
60
- currentValue: state.automatic ? "On" : "Off",
61
- values: ["On", "Off"],
62
- action: "automatic",
63
- },
64
- {
65
- id: "skipSecretScan",
66
- label: "Skip secret scan (all setups)",
67
- description: "All setups: allow pushes without checking managed local files for possible secrets.",
68
- currentValue: state.skipSecretScan ? "On" : "Off",
69
- values: ["On", "Off"],
70
- action: "skip-secret-scan",
71
- },
72
- {
73
- id: "showStatus",
74
- label: "Show status (all setups)",
75
- description: "All setups: show sync progress and attention in Pi status.",
76
- currentValue: state.showStatus ? "On" : "Off",
77
- values: ["On", "Off"],
78
- action: "show-status",
79
- },
80
- {
81
- id: "onSwitch",
82
- label: "After switching setup (all setups)",
83
- description: "All setups: ask before a pull review, open it automatically, or switch without pulling.",
84
- currentValue: setupSwitchActionLabel(state.onSwitch),
85
- values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
86
- action: "on-switch",
87
- },
88
96
  {
89
97
  id: "include",
90
98
  label: "Included content",
@@ -99,6 +107,49 @@ export async function showSyncSettings(
99
107
  currentValue: "Review",
100
108
  action: "remote-include",
101
109
  },
110
+ {
111
+ id: "localFields",
112
+ label: "Machine-local settings fields",
113
+ description:
114
+ "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
115
+ currentValue: `${state.localFields?.length ?? 0} fields · Edit`,
116
+ action: "local-fields",
117
+ },
118
+ {
119
+ id: "storageLocation",
120
+ label: "Storage location",
121
+ description: "Destination: edit this setup's bucket, branch, or path; remote history is not moved.",
122
+ currentValue:
123
+ state.backend.type === "s3"
124
+ ? `${safeTerminalText(state.backend.destination.bucket)} · ${safeTerminalText(state.backend.destination.prefix)}`
125
+ : state.backend.type === "git"
126
+ ? `${safeTerminalText(state.backend.destination.branch)} · ${safeTerminalText(state.backend.destination.directory)}`
127
+ : safeTerminalText(state.backend.destination.path),
128
+ action: "storage-location",
129
+ },
130
+ {
131
+ id: "setups",
132
+ label: "Manage sync setups",
133
+ description: "Destination: add, switch, edit, or remove sync setups.",
134
+ currentValue: "Open",
135
+ action: "setups",
136
+ },
137
+ {
138
+ id: "connections",
139
+ label: "Manage storage connections",
140
+ description:
141
+ "Destination: manage shared server addresses and credentials; edits may affect multiple setups.",
142
+ currentValue: "Open",
143
+ action: "connections",
144
+ },
145
+ {
146
+ id: "automatic",
147
+ label: "Automatic sync",
148
+ description: AUTOMATIC_SYNC_DESCRIPTION,
149
+ currentValue: state.automatic ? "On" : "Off",
150
+ values: ["On", "Off"],
151
+ action: "automatic",
152
+ },
102
153
  {
103
154
  id: "automaticTransfer",
104
155
  label: "Automatic transfer at startup",
@@ -108,6 +159,14 @@ export async function showSyncSettings(
108
159
  values: ["On", "Off"],
109
160
  action: "automatic-transfer",
110
161
  },
162
+ {
163
+ 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.",
166
+ currentValue: setupSwitchActionLabel(state.onSwitch),
167
+ values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
168
+ action: "on-switch",
169
+ },
111
170
  {
112
171
  id: "mergeSettings",
113
172
  label: "Settings field merge (experimental)",
@@ -117,14 +176,6 @@ export async function showSyncSettings(
117
176
  values: ["On", "Off"],
118
177
  action: "merge-settings",
119
178
  },
120
- {
121
- id: "localFields",
122
- label: "Machine-local settings fields",
123
- description:
124
- "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
125
- currentValue: `${state.localFields?.length ?? 0} fields · Edit`,
126
- action: "local-fields",
127
- },
128
179
  {
129
180
  id: "contentPolicy",
130
181
  label: "Content / partial sync (experimental)",
@@ -140,10 +191,41 @@ export async function showSyncSettings(
140
191
  values: ["Off", "Content only", "Content & partial", "Partial only"],
141
192
  action: "content-policy",
142
193
  },
194
+ {
195
+ id: "skipSecretScan",
196
+ label: "Skip secret scan (all setups)",
197
+ description: "All setups: allow pushes without checking managed local files for possible secrets.",
198
+ currentValue: state.skipSecretScan ? "On" : "Off",
199
+ values: ["On", "Off"],
200
+ action: "skip-secret-scan",
201
+ },
202
+ {
203
+ id: "showStatus",
204
+ label: "Show status (all setups)",
205
+ description: "All setups: show sync progress and attention in Pi status.",
206
+ currentValue: state.showStatus ? "On" : "Off",
207
+ values: ["On", "Off"],
208
+ action: "show-status",
209
+ },
143
210
  ],
144
211
  }),
145
212
  },
146
213
  actions: {
214
+ "storage-location": async ({ signal: actionSignal }) => {
215
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
216
+ await showEditSyncSetupStorage(ctx, setupName, editorSignal);
217
+ return resumeSettings(editorSignal);
218
+ },
219
+ setups: async ({ signal: actionSignal }) => {
220
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
221
+ const result = await showSyncSetupManager(ctx, runRoute, editorSignal);
222
+ return result === "exit" ? exitSettings() : resumeSettings(editorSignal);
223
+ },
224
+ connections: async ({ signal: actionSignal }) => {
225
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
226
+ await showStorageConnections(ctx, editorSignal);
227
+ return resumeSettings(editorSignal);
228
+ },
147
229
  "content-policy": async ({ value, signal: actionSignal }) => {
148
230
  const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
149
231
  const validate = captureMutationOwner(ctx, mutationSignal);
@@ -356,17 +438,29 @@ export async function showSyncSettings(
356
438
  if (reviewSignal.aborted) return { kind: "rejected" };
357
439
  if (review.kind === "route-result") {
358
440
  const disposition = await dispatchManagerResult(ctx, review.result, review.route, runRoute, reviewSignal);
359
- return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
441
+ return disposition.kind === "close" ? exitSettings() : { kind: "stay" };
360
442
  }
361
- return review.kind === "closed" || review.kind === "stale" ? { kind: "close" } : { kind: "stay" };
443
+ return review.kind === "closed" || review.kind === "stale" ? exitSettings() : { kind: "stay" };
362
444
  },
363
445
  },
364
446
  });
447
+ async function resumeSettings(editorSignal: AbortSignal) {
448
+ if (editorSignal.aborted) return { kind: "close" as const };
449
+ try {
450
+ const active = await loadConfig();
451
+ if (editorSignal.aborted || active.setupName !== setupName) return { kind: "close" as const };
452
+ // Stay reloads authoritative rows without pushing a duplicate screen or clearing search.
453
+ return { kind: "stay" as const };
454
+ } catch {
455
+ return { kind: "close" as const };
456
+ }
457
+ }
365
458
  await runMenu(ctx, menu, {
366
459
  getState: () => loadConfig(setupName),
367
460
  signal,
368
461
  isCurrent: () => !signal?.aborted,
369
462
  });
463
+ return disposition;
370
464
  }
371
465
 
372
466
  function notifySaveFailure(ctx: ExtensionCommandContext, error: unknown) {
@@ -22,7 +22,7 @@ export async function showSyncSetupManager(ctx: ExtensionCommandContext, runRout
22
22
  await showAddTarget(ctx, setupSignal);
23
23
  },
24
24
  edit: async (name, setupSignal) => {
25
- await showEditTarget(ctx, name, setupSignal);
25
+ await showEditSyncSetupStorage(ctx, name, setupSignal);
26
26
  },
27
27
  makeCurrent: async (name, setupSignal) => {
28
28
  const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
@@ -84,7 +84,7 @@ async function showAddTarget(ctx: ExtensionCommandContext, signal?: AbortSignal)
84
84
  await showAddS3Target(ctx, raw, profile, name, signal);
85
85
  }
86
86
 
87
- async function showEditTarget(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
87
+ export async function showEditSyncSetupStorage(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
88
88
  const partial = await loadPartialConfig(name);
89
89
  if (signal?.aborted) return;
90
90
  if (!partial.setupName) {