@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
@@ -23,10 +23,10 @@ import {
23
23
  } from "./chunk-4SBTAVFL.ts";
24
24
  import {
25
25
  dispatchManagerResult
26
- } from "./chunk-FJGNDYGC.ts";
26
+ } from "./chunk-VHXBCEVF.ts";
27
27
  import {
28
28
  runCancellableOperation
29
- } from "./chunk-TRCSEIAQ.ts";
29
+ } from "./chunk-5ZWQ33XL.ts";
30
30
  import "./chunk-V6VZ5TLG.ts";
31
31
  import "./chunk-SDZCTCYQ.ts";
32
32
  import {
@@ -161,7 +161,7 @@ function attentionMainMenuItems(manager) {
161
161
  ];
162
162
  }
163
163
  function blockedSyncMenuItem(label, manager) {
164
- if (label !== "Sync now (recommended)" || !manager.attentionBlocksSync) return void 0;
164
+ if (label !== "Sync now" || !manager.attentionBlocksSync) return void 0;
165
165
  return {
166
166
  id: "sync",
167
167
  label,
@@ -172,7 +172,7 @@ function blockedSyncMenuItem(label, manager) {
172
172
  };
173
173
  }
174
174
  async function showManagerAttention(ctx, attention, runRoute, signal, onSelectionResolved) {
175
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-Q2HUIBTL.ts");
175
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
176
176
  if (signal?.aborted) return "close";
177
177
  const review = await showRemoteSelectionReview(ctx, attention.decision.setupName, signal, void 0, {
178
178
  decision: attention.decision,
@@ -450,11 +450,12 @@ function storageLocation(config) {
450
450
 
451
451
  // src/ui/manager-state.ts
452
452
  var MAIN_MENU_ACTIONS = [
453
- "Sync now (recommended)",
454
- "Switch sync setup",
455
- "Status & changes",
453
+ "Sync now",
454
+ "Pull from remote\u2026",
455
+ "Push to remote\u2026",
456
456
  "Settings",
457
- "More\u2026"
457
+ "History",
458
+ "Diagnostics"
458
459
  ];
459
460
  async function describeManagerState(signal, attention, inspectOperation = inspectOperationAvailability, observation) {
460
461
  let raw;
@@ -515,7 +516,9 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
515
516
  const currentObservation = observation && observationMatchesConfig(observation, config) && syncState && syncStateFingerprint(syncState) === observation.inspection.stateIdentity ? observation : void 0;
516
517
  const lastAppliedSnapshot = changesBlocked ? "Unavailable while operations are locked" : syncState?.lastAppliedSnapshot ? safeTerminalText(syncState.lastAppliedSnapshot) : syncState ? "Never synced" : "Unavailable";
517
518
  const canSwitch = await countValidSyncSetups(configuredTargets, signal) > 1;
518
- const mainActions = MAIN_MENU_ACTIONS.filter((action) => action !== "Switch sync setup" || canSwitch);
519
+ const mainActions = [...MAIN_MENU_ACTIONS];
520
+ if (canSwitch) mainActions.push("Switch sync setup");
521
+ if (syncState?.unresolved?.length) mainActions.unshift("Review unresolved conflicts");
519
522
  const ordinaryTitle = [
520
523
  "Manage sync",
521
524
  "",
@@ -526,7 +529,7 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
526
529
  `Automatic transfer at startup: ${config.automaticTransfer ? "On \u2014 upload/replace/delete at idle; no reload" : "Off"}`,
527
530
  `Last applied: ${lastAppliedSnapshot}`,
528
531
  ...syncState?.unresolved?.length ? [
529
- `Unresolved: ${syncState.unresolved.length} dependency groups \xB7 review History & recovery`,
532
+ `Unresolved: ${syncState.unresolved.length} dependency groups \xB7 choose Review unresolved conflicts`,
530
533
  `Last observed: ${safeTerminalText(syncState.lastObservedSnapshot ?? "unknown")}`
531
534
  ] : [],
532
535
  ...currentAttention ? [
@@ -565,18 +568,24 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
565
568
  "",
566
569
  "What do you want to do?"
567
570
  ].join("\n"),
568
- actions: ["Sync setups\u2026", "Storage connections\u2026", "History & recovery\u2026", "Help"]
571
+ actions: ["Sync setups\u2026", "Storage connections\u2026", "History", "Help"]
569
572
  };
570
573
  }
571
574
  }
572
575
  function operationActions(operation, noSyncedContent, canSwitch, mainActions) {
573
576
  if (operationCanRecover(operation)) {
574
- return ["Restore sync access\u2026 (recommended)", "Status & changes", "History & recovery\u2026", "Help"];
577
+ return ["Restore sync access\u2026 (recommended)", "History", "Help"];
575
578
  }
576
579
  if (operation.kind !== "free") {
577
- return ["Refresh operation status", "Status & changes", "History & recovery\u2026", "Help"];
580
+ return ["Refresh operation status", "History", "Help"];
578
581
  }
579
- return noSyncedContent ? ["Settings", ...canSwitch ? ["Switch sync setup"] : [], "Status & changes", "More\u2026"] : mainActions;
582
+ return noSyncedContent ? [
583
+ ...mainActions.includes("Review unresolved conflicts") ? ["Review unresolved conflicts"] : [],
584
+ "Settings",
585
+ ...canSwitch ? ["Switch sync setup"] : [],
586
+ "History",
587
+ "Diagnostics"
588
+ ] : mainActions;
580
589
  }
581
590
  function operationStatusLines(operation) {
582
591
  switch (operation.kind) {
@@ -584,16 +593,24 @@ function operationStatusLines(operation) {
584
593
  return [];
585
594
  case "live": {
586
595
  const command = truncateToWidth(safeTerminalText(operation.lock.command), 16, "\u2026");
587
- return [`Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Settings and More return.`];
596
+ return [
597
+ `Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Transfer actions and Settings return.`
598
+ ];
588
599
  }
589
600
  case "busy":
590
- return ["Pi-sync may be starting or finishing. Wait, then refresh; Settings and More remain unavailable."];
601
+ return [
602
+ "Pi-sync may be starting or finishing. Wait, then refresh; Transfer actions and Settings remain unavailable."
603
+ ];
591
604
  case "recoverable-stale":
592
- return ["Sync paused: old lock remains. Close other Pi sessions then restore; Settings and More return."];
605
+ return [
606
+ "Sync paused: old lock remains. Close other Pi sessions then restore; Transfer actions and Settings return."
607
+ ];
593
608
  case "recoverable-unreadable":
594
- return ["Sync paused: owner unknown. Close other Pi sessions then restore; Settings and More return."];
609
+ return [
610
+ "Sync paused: owner unknown. Close other Pi sessions then restore; Transfer actions and Settings return."
611
+ ];
595
612
  case "inspection-error":
596
- return ["Lock check failed. Fix path access, then refresh; Settings and More remain unavailable."];
613
+ return ["Lock check failed. Fix path access, then refresh; Transfer actions and Settings remain unavailable."];
597
614
  }
598
615
  }
599
616
  function backendStorageDescription(config) {
@@ -611,419 +628,96 @@ function backendStorageDescription(config) {
611
628
  }
612
629
 
613
630
  // src/ui/settings-ui.ts
631
+ import { defineMenu as defineMenu4, runMenu as runMenu4 } from "@narumitw/pi-tui-kit";
632
+
633
+ // src/ui/storage-connections-ui.ts
634
+ import { isDeepStrictEqual } from "node:util";
614
635
  import { defineMenu as defineMenu2, runMenu as runMenu2 } from "@narumitw/pi-tui-kit";
615
- async function showSyncSettings(ctx, runRoute, signal) {
616
- if (ctx.mode !== "tui") {
617
- ctx.ui.notify(`Edit pi-sync settings manually: ${safeTerminalText(localConfigPath())}`, "info");
618
- return;
619
- }
620
- const initial = await loadConfig();
621
- if (signal?.aborted) return;
622
- const setupName = initial.setupName;
636
+ async function showStorageConnections(ctx, signal) {
637
+ let selectedName;
638
+ const nameById = /* @__PURE__ */ new Map();
623
639
  const menu = defineMenu2({
624
- start: "settings",
640
+ start: "list",
625
641
  screens: {
626
- settings: ({ state }) => ({
627
- kind: "settings",
628
- title: "Pi Sync Settings",
629
- lines: [
630
- `Sync setup: ${safeTerminalText(state.setupName)} \xB7 Storage connection: ${safeTerminalText(state.connectionName)}`
631
- ],
632
- items: [
633
- {
634
- id: "automatic",
635
- label: "Automatic sync",
636
- description: AUTOMATIC_SYNC_DESCRIPTION,
637
- currentValue: state.automatic ? "On" : "Off",
638
- values: ["On", "Off"],
639
- action: "automatic"
640
- },
641
- {
642
- id: "skipSecretScan",
643
- label: "Skip secret scan (all setups)",
644
- description: "All setups: allow pushes without checking managed local files for possible secrets.",
645
- currentValue: state.skipSecretScan ? "On" : "Off",
646
- values: ["On", "Off"],
647
- action: "skip-secret-scan"
648
- },
649
- {
650
- id: "showStatus",
651
- label: "Show status (all setups)",
652
- description: "All setups: show sync progress and attention in Pi status.",
653
- currentValue: state.showStatus ? "On" : "Off",
654
- values: ["On", "Off"],
655
- action: "show-status"
656
- },
657
- {
658
- id: "onSwitch",
659
- label: "After switching setup (all setups)",
660
- description: "All setups: ask before a pull review, open it automatically, or switch without pulling.",
661
- currentValue: setupSwitchActionLabel(state.onSwitch),
662
- values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
663
- action: "on-switch"
664
- },
665
- {
666
- id: "include",
667
- label: "Included content",
668
- description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
669
- currentValue: "Open editor",
670
- action: "include"
671
- },
672
- {
673
- id: "remoteInclude",
674
- label: "Compare synced content",
675
- description: "Review this device and remote content lists before choosing either one.",
676
- currentValue: "Review",
677
- action: "remote-include"
678
- },
679
- {
680
- id: "automaticTransfer",
681
- label: "Automatic transfer at startup",
682
- description: "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.",
683
- currentValue: state.automaticTransfer ? "On" : "Off",
684
- values: ["On", "Off"],
685
- action: "automatic-transfer"
686
- },
687
- {
688
- id: "mergeSettings",
689
- label: "Settings field merge (experimental)",
690
- description: "Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
691
- currentValue: state.mergeSettings ? "On" : "Off",
692
- values: ["On", "Off"],
693
- action: "merge-settings"
694
- },
695
- {
696
- id: "localFields",
697
- label: "Machine-local settings fields",
698
- description: "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
699
- currentValue: `${state.localFields?.length ?? 0} fields \xB7 Edit`,
700
- action: "local-fields"
701
- },
702
- {
703
- id: "contentPolicy",
704
- label: "Content / partial sync (experimental)",
705
- description: "Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
706
- currentValue: state.mergeContent ? state.partialSync ? "Content & partial" : "Content only" : state.partialSync ? "Partial only" : "Off",
707
- values: ["Off", "Content only", "Content & partial", "Partial only"],
708
- action: "content-policy"
709
- }
710
- ]
642
+ list: ({ state }) => {
643
+ nameById.clear();
644
+ const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
645
+ return {
646
+ kind: "actions",
647
+ title: "Storage connections",
648
+ lines: state.supportedVersion ? ["Server addresses and sign-in details, reusable across sync setups."] : ["Create supported pi-sync settings before managing storage connections."],
649
+ items: state.supportedVersion ? [
650
+ { id: "add", label: "Add storage connection", action: "add" },
651
+ ...names.map((name, index) => {
652
+ const id = `connection:${index}`;
653
+ nameById.set(id, name);
654
+ return {
655
+ id,
656
+ label: safeTerminalText(name),
657
+ description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
658
+ action: "select"
659
+ };
660
+ })
661
+ ] : [],
662
+ hint: "back"
663
+ };
664
+ },
665
+ detail: ({ state }) => ({
666
+ kind: "actions",
667
+ title: state.selected ? `Storage connection \u201C${safeTerminalText(state.selected.name)}\u201D` : "Storage connection",
668
+ lines: state.selected?.lines ?? ["This storage connection no longer exists."],
669
+ items: state.selected ? [
670
+ { id: "edit", label: "Edit storage connection\u2026", action: "edit" },
671
+ ...state.selected.usedBy.length === 0 ? [
672
+ {
673
+ id: "remove",
674
+ label: "Remove storage connection\u2026",
675
+ action: "remove"
676
+ }
677
+ ] : [],
678
+ { id: "back", label: "Back", action: "back" }
679
+ ] : [{ id: "back", label: "Back", action: "back" }],
680
+ hint: "back"
711
681
  })
712
682
  },
713
683
  actions: {
714
- "content-policy": async ({ value, signal: actionSignal }) => {
715
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
716
- const validate = captureMutationOwner(ctx, mutationSignal);
684
+ add: async () => {
717
685
  try {
718
- const previous = await loadConfig(setupName);
719
- validate();
720
- const mergeContent = value === "Content only" || value === "Content & partial";
721
- const partialSync = value === "Partial only" || value === "Content & partial";
722
- if (!await ctx.ui.confirm(
723
- "Save experimental content policy?",
724
- "This explicitly upgrades settings to version 5; partial snapshots use version 3 and older clients must refuse them. Text/session merge is conservative. Partial sync preserves withheld versions and old baselines. Disabling does not clear unresolved groups or recovery evidence. Review a directional migration first if portable policy changes.",
725
- { signal: mutationSignal }
726
- ))
727
- return { kind: "rejected" };
728
- validate();
729
- await updateLocalConfig((current) => {
730
- validate();
731
- const setup = current.syncSetups[setupName];
732
- if (!setup || Boolean(setup.sync.mergeContent) !== Boolean(previous.mergeContent) || Boolean(setup.sync.partialSync) !== Boolean(previous.partialSync))
733
- throw new Error("Content policy changed during review.");
734
- return {
735
- ...current,
736
- version: 5,
737
- syncSetups: {
738
- ...current.syncSetups,
739
- [setupName]: { ...setup, sync: { ...setup.sync, mergeContent, partialSync } }
740
- }
741
- };
742
- }, mutationSignal);
743
- validate();
744
- return { kind: "stay" };
686
+ await showAddStorageConnection(ctx, signal);
745
687
  } catch (error) {
746
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
747
- return { kind: "rejected" };
688
+ if (!signal?.aborted) {
689
+ ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
690
+ }
748
691
  }
692
+ return { kind: "stay" };
749
693
  },
750
- "merge-settings": async ({ value, signal: actionSignal }) => {
751
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
752
- const validate = captureMutationOwner(ctx, mutationSignal);
694
+ select: async ({ itemId }) => {
695
+ selectedName = nameById.get(itemId);
696
+ return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
697
+ },
698
+ edit: async ({ state }) => {
699
+ if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
753
700
  try {
754
- validate();
755
- await updateSyncSetup(
756
- setupName,
757
- (setup) => ({ ...setup, sync: { ...setup.sync, mergeSettings: value === "On" } }),
758
- { signal: mutationSignal }
759
- );
760
- validate();
761
- return { kind: "stay" };
701
+ await editStorageConnection(ctx, state.selected.name, state.selected.profile, state.selected.usedBy, signal);
762
702
  } catch (error) {
763
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
764
- return { kind: "rejected" };
703
+ notifyConnectionError(ctx, state.selected.name, error, signal);
765
704
  }
705
+ return { kind: "stay" };
766
706
  },
767
- "local-fields": async ({ signal: actionSignal }) => {
768
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
769
- const validate = captureMutationOwner(ctx, mutationSignal);
707
+ remove: async ({ state }) => {
708
+ if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
709
+ const name = state.selected.name;
710
+ const confirmed = await ctx.ui.confirm(
711
+ "Remove storage connection?",
712
+ `Remove local storage connection \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
713
+ { signal }
714
+ );
715
+ if (!confirmed || signal?.aborted) return { kind: "rejected" };
770
716
  try {
771
- const previous = await loadConfig(setupName);
772
- validate();
773
- const input = await ctx.ui.input(
774
- "Machine-local settings.json root fields (JSON string array)",
775
- JSON.stringify(previous.localFields ?? []),
776
- { signal: mutationSignal }
777
- );
778
- validate();
779
- if (input === void 0) return { kind: "rejected" };
780
- let fields;
781
- try {
782
- fields = normalizeLocalFields(JSON.parse(input));
783
- } catch {
784
- throw new Error("Invalid localFields JSON array; no policy was changed.");
785
- }
786
- if (sameLocalFields(previous.localFields, fields)) return { kind: "stay" };
787
- const confirmed = await ctx.ui.confirm(
788
- "Save portable field policy?",
789
- "This opts into settings/snapshot version 4/2; older clients must refuse them. Future snapshots omit these fields, but old remote history is NOT erased. Review an explicit force push/pull migration before further sync. Removed rules can expose or replace local-only values.",
790
- { signal: mutationSignal }
791
- );
792
- validate();
793
- if (!confirmed) return { kind: "rejected" };
794
- await updateLocalConfig((current) => {
795
- validate();
796
- const setup = current.syncSetups[setupName];
797
- if (!setup || !sameLocalFields(setup.sync.localFields, previous.localFields))
798
- throw new Error("Field policy changed while under review; reopen settings.");
799
- return {
800
- ...current,
801
- version: current.version === 5 ? 5 : 4,
802
- syncSetups: {
803
- ...current.syncSetups,
804
- [setupName]: { ...setup, sync: { ...setup.sync, localFields: fields } }
805
- }
806
- };
807
- }, mutationSignal);
808
- validate();
809
- return { kind: "stay" };
810
- } catch (error) {
811
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
812
- return { kind: "rejected" };
813
- }
814
- },
815
- automatic: async ({ value, signal: actionSignal }) => {
816
- const automatic = value === "On";
817
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
818
- try {
819
- const latest = await loadConfig(setupName);
820
- if (mutationSignal.aborted) return { kind: "rejected" };
821
- if (latest.automatic === automatic) return { kind: "stay" };
822
- await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automatic } }), {
823
- signal: mutationSignal
824
- });
825
- if (mutationSignal.aborted) return { kind: "rejected" };
826
- ctx.ui.notify(
827
- `Automatic sync ${automatic ? "enabled" : "disabled"} for \u201C${safeTerminalText(setupName)}\u201D.`,
828
- "info"
829
- );
830
- return { kind: "stay" };
831
- } catch (error) {
832
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
833
- return { kind: "rejected" };
834
- }
835
- },
836
- "automatic-transfer": async ({ value, signal: actionSignal }) => {
837
- const automaticTransfer = value === "On";
838
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
839
- try {
840
- await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automaticTransfer } }), {
841
- signal: mutationSignal
842
- });
843
- if (mutationSignal.aborted) return { kind: "rejected" };
844
- ctx.ui.notify(
845
- `Automatic startup transfer ${automaticTransfer ? "enabled for the next session start" : "disabled"}. Completed transfers are not undone.`,
846
- "info"
847
- );
848
- return { kind: "stay" };
849
- } catch (error) {
850
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
851
- return { kind: "rejected" };
852
- }
853
- },
854
- "skip-secret-scan": async ({ value, signal: actionSignal }) => {
855
- const skipSecretScan = value === "On";
856
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
857
- try {
858
- const latest = await loadConfig(setupName);
859
- if (mutationSignal.aborted) return { kind: "rejected" };
860
- if (latest.skipSecretScan === skipSecretScan) return { kind: "stay" };
861
- await updateLocalConfig((settings) => ({ ...settings, skipSecretScan }), mutationSignal);
862
- if (mutationSignal.aborted) return { kind: "rejected" };
863
- ctx.ui.notify(`Secret scan ${skipSecretScan ? "disabled" : "enabled"} for pushes in all setups.`, "info");
864
- return { kind: "stay" };
865
- } catch (error) {
866
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
867
- return { kind: "rejected" };
868
- }
869
- },
870
- "show-status": async ({ value, signal: actionSignal }) => {
871
- const showStatus = value === "On";
872
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
873
- try {
874
- const latest = await loadConfig(setupName);
875
- if (mutationSignal.aborted) return { kind: "rejected" };
876
- if (latest.showStatus === showStatus) return { kind: "stay" };
877
- await updateLocalConfig((settings) => ({ ...settings, showStatus }), mutationSignal);
878
- if (mutationSignal.aborted) return { kind: "rejected" };
879
- configureSyncStatus(ctx, showStatus);
880
- ctx.ui.notify(`Pi Sync status ${showStatus ? "enabled" : "disabled"} for all setups.`, "info");
881
- return { kind: "stay" };
882
- } catch (error) {
883
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
884
- return { kind: "rejected" };
885
- }
886
- },
887
- "on-switch": async ({ value, signal: actionSignal }) => {
888
- const action = value ? setupSwitchActionFromLabel(value) : void 0;
889
- if (!action) return { kind: "rejected" };
890
- const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
891
- try {
892
- const latest = await loadConfig(setupName);
893
- if (mutationSignal.aborted) return { kind: "rejected" };
894
- if (latest.onSwitch === action) return { kind: "stay" };
895
- await saveOnSwitch(action, mutationSignal);
896
- if (mutationSignal.aborted) return { kind: "rejected" };
897
- ctx.ui.notify(`After switching setup: ${value}.`, "info");
898
- return { kind: "stay" };
899
- } catch (error) {
900
- if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
901
- return { kind: "rejected" };
902
- }
903
- },
904
- include: async ({ signal: actionSignal }) => {
905
- const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
906
- await runRoute("files", editorSignal, void 0, setupName);
907
- return editorSignal.aborted ? { kind: "rejected" } : { kind: "stay" };
908
- },
909
- "remote-include": async ({ signal: actionSignal }) => {
910
- const reviewSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
911
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-Q2HUIBTL.ts");
912
- if (reviewSignal.aborted) return { kind: "rejected" };
913
- const review = await showRemoteSelectionReview(ctx, setupName, reviewSignal, void 0, {
914
- origin: "settings",
915
- runRoute
916
- });
917
- if (reviewSignal.aborted) return { kind: "rejected" };
918
- if (review.kind === "route-result") {
919
- const disposition = await dispatchManagerResult(ctx, review.result, review.route, runRoute, reviewSignal);
920
- return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
921
- }
922
- return review.kind === "closed" || review.kind === "stale" ? { kind: "close" } : { kind: "stay" };
923
- }
924
- }
925
- });
926
- await runMenu2(ctx, menu, {
927
- getState: () => loadConfig(setupName),
928
- signal,
929
- isCurrent: () => !signal?.aborted
930
- });
931
- }
932
- function notifySaveFailure(ctx, error) {
933
- ctx.ui.notify(
934
- `Pi Sync settings save failed: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
935
- "error"
936
- );
937
- }
938
-
939
- // src/ui/storage-connections-ui.ts
940
- import { isDeepStrictEqual } from "node:util";
941
- import { defineMenu as defineMenu3, runMenu as runMenu3 } from "@narumitw/pi-tui-kit";
942
- async function showStorageConnections(ctx, signal) {
943
- let selectedName;
944
- const nameById = /* @__PURE__ */ new Map();
945
- const menu = defineMenu3({
946
- start: "list",
947
- screens: {
948
- list: ({ state }) => {
949
- nameById.clear();
950
- const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
951
- return {
952
- kind: "actions",
953
- title: "Storage connections",
954
- lines: state.supportedVersion ? ["Server addresses and sign-in details, reusable across sync setups."] : ["Create supported pi-sync settings before managing storage connections."],
955
- items: state.supportedVersion ? [
956
- { id: "add", label: "Add storage connection", action: "add" },
957
- ...names.map((name, index) => {
958
- const id = `connection:${index}`;
959
- nameById.set(id, name);
960
- return {
961
- id,
962
- label: safeTerminalText(name),
963
- description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
964
- action: "select"
965
- };
966
- })
967
- ] : [],
968
- hint: "back"
969
- };
970
- },
971
- detail: ({ state }) => ({
972
- kind: "actions",
973
- title: state.selected ? `Storage connection \u201C${safeTerminalText(state.selected.name)}\u201D` : "Storage connection",
974
- lines: state.selected?.lines ?? ["This storage connection no longer exists."],
975
- items: state.selected ? [
976
- { id: "edit", label: "Edit storage connection\u2026", action: "edit" },
977
- ...state.selected.usedBy.length === 0 ? [
978
- {
979
- id: "remove",
980
- label: "Remove storage connection\u2026",
981
- action: "remove"
982
- }
983
- ] : [],
984
- { id: "back", label: "Back", action: "back" }
985
- ] : [{ id: "back", label: "Back", action: "back" }],
986
- hint: "back"
987
- })
988
- },
989
- actions: {
990
- add: async () => {
991
- try {
992
- await showAddStorageConnection(ctx, signal);
993
- } catch (error) {
994
- if (!signal?.aborted) {
995
- ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
996
- }
997
- }
998
- return { kind: "stay" };
999
- },
1000
- select: async ({ itemId }) => {
1001
- selectedName = nameById.get(itemId);
1002
- return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
1003
- },
1004
- edit: async ({ state }) => {
1005
- if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
1006
- try {
1007
- await editStorageConnection(ctx, state.selected.name, state.selected.profile, state.selected.usedBy, signal);
1008
- } catch (error) {
1009
- notifyConnectionError(ctx, state.selected.name, error, signal);
1010
- }
1011
- return { kind: "stay" };
1012
- },
1013
- remove: async ({ state }) => {
1014
- if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
1015
- const name = state.selected.name;
1016
- const confirmed = await ctx.ui.confirm(
1017
- "Remove storage connection?",
1018
- `Remove local storage connection \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
1019
- { signal }
1020
- );
1021
- if (!confirmed || signal?.aborted) return { kind: "rejected" };
1022
- try {
1023
- await removeStorageConnection(name, signal);
1024
- ctx.ui.notify(`Removed storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
1025
- selectedName = void 0;
1026
- return { kind: "back" };
717
+ await removeStorageConnection(name, signal);
718
+ ctx.ui.notify(`Removed storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
719
+ selectedName = void 0;
720
+ return { kind: "back" };
1027
721
  } catch (error) {
1028
722
  notifyConnectionError(ctx, name, error, signal);
1029
723
  return { kind: "stay" };
@@ -1035,7 +729,7 @@ async function showStorageConnections(ctx, signal) {
1035
729
  }
1036
730
  }
1037
731
  });
1038
- await runMenu3(ctx, menu, {
732
+ await runMenu2(ctx, menu, {
1039
733
  getState: () => loadStorageMenuState(selectedName, signal),
1040
734
  signal,
1041
735
  isCurrent: () => !signal?.aborted
@@ -1251,12 +945,12 @@ function credentialSource(profile) {
1251
945
  }
1252
946
 
1253
947
  // src/ui/setup/setup-switcher.ts
1254
- import { defineMenu as defineMenu4, runMenu as runMenu4 } from "@narumitw/pi-tui-kit";
948
+ import { defineMenu as defineMenu3, runMenu as runMenu3 } from "@narumitw/pi-tui-kit";
1255
949
  async function selectSetupForSwitch(ctx, raw, targets, active, signal) {
1256
950
  let selectedName;
1257
951
  const nameById = /* @__PURE__ */ new Map();
1258
952
  const profiles = ownRecord(raw.storageConnections);
1259
- const menu = defineMenu4({
953
+ const menu = defineMenu3({
1260
954
  start: "setups",
1261
955
  screens: {
1262
956
  setups: () => ({
@@ -1288,7 +982,7 @@ async function selectSetupForSwitch(ctx, raw, targets, active, signal) {
1288
982
  }
1289
983
  }
1290
984
  });
1291
- await runMenu4(ctx, menu, {
985
+ await runMenu3(ctx, menu, {
1292
986
  getState: () => void 0,
1293
987
  signal,
1294
988
  isCurrent: () => !signal?.aborted
@@ -1372,124 +1066,530 @@ async function showSetupSwitcher(ctx, runRoute, selectedName, signal) {
1372
1066
  if (pullResult.kind === "closed" || pullResult.kind.endsWith("required")) {
1373
1067
  pullClosed = disposition.kind === "close";
1374
1068
  }
1375
- if (disposition.appliedRoute === "pull") return "applied";
1376
- if (pullResult.kind === "completed") return pullResult.outcome;
1377
- return pullResult.kind === "cancelled" ? "cancelled" : void 0;
1069
+ if (disposition.appliedRoute === "pull") return "applied";
1070
+ if (pullResult.kind === "completed") return pullResult.outcome;
1071
+ return pullResult.kind === "cancelled" ? "cancelled" : void 0;
1072
+ },
1073
+ onSwitch,
1074
+ signal,
1075
+ syncConfigReviewIdentity(config)
1076
+ );
1077
+ if (pullClosed) return "closed";
1078
+ return result.pullApplied ? "pull-attempted" : "switched";
1079
+ } catch (error) {
1080
+ if (signal?.aborted) return false;
1081
+ ctx.ui.notify(
1082
+ `Sync setup \u201C${safeTerminalText(name)}\u201D was not switched: ${safeTerminalText(errorMessage(error))}`,
1083
+ "error"
1084
+ );
1085
+ return false;
1086
+ }
1087
+ }
1088
+
1089
+ // src/ui/setup/setup-actions.ts
1090
+ async function showSyncSetupManager(ctx, runRoute, signal) {
1091
+ return showSyncSetups(
1092
+ ctx,
1093
+ {
1094
+ add: async (setupSignal) => {
1095
+ await showAddTarget(ctx, setupSignal);
1096
+ },
1097
+ edit: async (name, setupSignal) => {
1098
+ await showEditSyncSetupStorage(ctx, name, setupSignal);
1099
+ },
1100
+ makeCurrent: async (name, setupSignal) => {
1101
+ const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
1102
+ return result === "pull-attempted" || result === "closed" ? "exit" : void 0;
1103
+ },
1104
+ remove: async (name, setupSignal) => {
1105
+ await showRemoveTarget(ctx, name, setupSignal);
1106
+ }
1107
+ },
1108
+ signal
1109
+ );
1110
+ }
1111
+ async function showAddTarget(ctx, signal) {
1112
+ let raw = await readLocalConfigObject();
1113
+ if (signal?.aborted) return;
1114
+ if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
1115
+ if (raw.version !== 3 && raw.version !== 4 && raw.version !== 5) {
1116
+ ctx.ui.notify("Version 1 and version 2 settings are unsupported and are never migrated.", "error");
1117
+ return;
1118
+ }
1119
+ let profiles = ownRecord(raw.storageConnections) ?? {};
1120
+ const name = await promptResourceName(ctx, "sync setup", "work", signal);
1121
+ if (!name) return;
1122
+ const createConnection = "Add a new storage connection\u2026";
1123
+ let profile = await ctx.ui.select(
1124
+ "Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
1125
+ [...Object.keys(profiles).sort(), createConnection, "Cancel"],
1126
+ { signal }
1127
+ );
1128
+ if (signal?.aborted || !profile || profile === "Cancel") return;
1129
+ if (profile === createConnection) {
1130
+ const previousNames = new Set(Object.keys(profiles));
1131
+ if (!await showAddStorageConnection(ctx, signal)) return;
1132
+ if (signal?.aborted) return;
1133
+ raw = await readLocalConfigObject() ?? raw;
1134
+ if (signal?.aborted) return;
1135
+ profiles = ownRecord(raw.storageConnections) ?? {};
1136
+ profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
1137
+ if (!profile) return;
1138
+ ctx.ui.notify(
1139
+ `Connection \u201C${safeTerminalText(profile)}\u201D saved. Cancelling setup will keep this connection.`,
1140
+ "info"
1141
+ );
1142
+ }
1143
+ const storageKind = ownRecord(profiles[profile])?.type;
1144
+ if (storageKind === "webdav") {
1145
+ const saved = await showAddWebDavTarget(ctx, name, profile, signal);
1146
+ if (signal?.aborted) return;
1147
+ if (saved) await refreshTargetCompletions();
1148
+ return;
1149
+ }
1150
+ if (storageKind === "git") {
1151
+ const saved = await showAddGitTarget(ctx, name, profile, signal);
1152
+ if (signal?.aborted) return;
1153
+ if (saved) await refreshTargetCompletions();
1154
+ return;
1155
+ }
1156
+ await showAddS3Target(ctx, raw, profile, name, signal);
1157
+ }
1158
+ async function showEditSyncSetupStorage(ctx, name, signal) {
1159
+ const partial = await loadPartialConfig(name);
1160
+ if (signal?.aborted) return;
1161
+ if (!partial.setupName) {
1162
+ ctx.ui.notify("Create supported pi-sync settings before editing a named sync setup.", "info");
1163
+ return;
1164
+ }
1165
+ if (partial.storageKind === "webdav") {
1166
+ await showEditWebDavTarget(ctx, partial, signal);
1167
+ return;
1168
+ }
1169
+ if (partial.storageKind === "git") {
1170
+ await showEditGitTarget(ctx, partial, signal);
1171
+ return;
1172
+ }
1173
+ await showEditS3Target(ctx, partial, signal);
1174
+ }
1175
+ async function showRemoveTarget(ctx, name, signal) {
1176
+ const confirmed = await ctx.ui.confirm(
1177
+ "Remove sync setup?",
1178
+ `Remove local sync setup \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
1179
+ { signal }
1180
+ );
1181
+ if (signal?.aborted || !confirmed) return;
1182
+ await removeSyncSetup(name, signal);
1183
+ if (signal?.aborted) return;
1184
+ await refreshTargetCompletions();
1185
+ if (signal?.aborted) return;
1186
+ ctx.ui.notify(`Removed sync setup \u201C${safeTerminalText(name)}\u201D; remote data was not deleted.`, "info");
1187
+ }
1188
+
1189
+ // src/ui/settings-ui.ts
1190
+ async function showSyncSettings(ctx, runRoute, signal) {
1191
+ let disposition = "back";
1192
+ const exitSettings = () => {
1193
+ disposition = "exit";
1194
+ return { kind: "close" };
1195
+ };
1196
+ if (ctx.mode !== "tui") {
1197
+ ctx.ui.notify(`Edit pi-sync settings manually: ${safeTerminalText(localConfigPath())}`, "info");
1198
+ if (ctx.mode === "rpc" && ctx.hasUI) {
1199
+ const management = defineMenu4({
1200
+ start: "management",
1201
+ screens: {
1202
+ management: () => ({
1203
+ kind: "actions",
1204
+ title: "Sync settings management",
1205
+ items: [
1206
+ { id: "setups", label: "Manage sync setups", action: "setups" },
1207
+ { id: "connections", label: "Manage storage connections", action: "connections" }
1208
+ ],
1209
+ hint: "back"
1210
+ })
1211
+ },
1212
+ actions: {
1213
+ setups: async ({ signal: actionSignal }) => {
1214
+ const result = await showSyncSetupManager(ctx, runRoute, actionSignal);
1215
+ return result === "exit" ? exitSettings() : { kind: "stay" };
1216
+ },
1217
+ connections: async ({ signal: actionSignal }) => {
1218
+ await showStorageConnections(ctx, actionSignal);
1219
+ return { kind: "stay" };
1220
+ }
1221
+ }
1222
+ });
1223
+ await runMenu4(ctx, management, { getState: () => void 0, signal, isCurrent: () => !signal?.aborted });
1224
+ }
1225
+ return disposition;
1226
+ }
1227
+ const initial = await loadConfig();
1228
+ if (signal?.aborted) return "back";
1229
+ const setupName = initial.setupName;
1230
+ const menu = defineMenu4({
1231
+ start: "settings",
1232
+ screens: {
1233
+ settings: ({ state }) => ({
1234
+ kind: "settings",
1235
+ title: "Pi Sync Settings",
1236
+ lines: [
1237
+ `Sync setup: ${safeTerminalText(state.setupName)} \xB7 Storage connection: ${safeTerminalText(state.connectionName)}`
1238
+ ],
1239
+ // Flat, searchable task ordering: Content, Destination, Automation, Merge, Other.
1240
+ // Kit has no heading rows; avoid focusable fake headings or a new component.
1241
+ items: [
1242
+ {
1243
+ id: "include",
1244
+ label: "Included content",
1245
+ description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
1246
+ currentValue: "Open editor",
1247
+ action: "include"
1248
+ },
1249
+ {
1250
+ id: "remoteInclude",
1251
+ label: "Compare synced content",
1252
+ description: "Review this device and remote content lists before choosing either one.",
1253
+ currentValue: "Review",
1254
+ action: "remote-include"
1255
+ },
1256
+ {
1257
+ id: "localFields",
1258
+ label: "Machine-local settings fields",
1259
+ description: "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
1260
+ currentValue: `${state.localFields?.length ?? 0} fields \xB7 Edit`,
1261
+ action: "local-fields"
1262
+ },
1263
+ {
1264
+ id: "storageLocation",
1265
+ label: "Storage location",
1266
+ description: "Destination: edit this setup's bucket, branch, or path; remote history is not moved.",
1267
+ currentValue: state.backend.type === "s3" ? `${safeTerminalText(state.backend.destination.bucket)} \xB7 ${safeTerminalText(state.backend.destination.prefix)}` : state.backend.type === "git" ? `${safeTerminalText(state.backend.destination.branch)} \xB7 ${safeTerminalText(state.backend.destination.directory)}` : safeTerminalText(state.backend.destination.path),
1268
+ action: "storage-location"
1269
+ },
1270
+ {
1271
+ id: "setups",
1272
+ label: "Manage sync setups",
1273
+ description: "Destination: add, switch, edit, or remove sync setups.",
1274
+ currentValue: "Open",
1275
+ action: "setups"
1276
+ },
1277
+ {
1278
+ id: "connections",
1279
+ label: "Manage storage connections",
1280
+ description: "Destination: manage shared server addresses and credentials; edits may affect multiple setups.",
1281
+ currentValue: "Open",
1282
+ action: "connections"
1283
+ },
1284
+ {
1285
+ id: "automatic",
1286
+ label: "Automatic sync",
1287
+ description: AUTOMATIC_SYNC_DESCRIPTION,
1288
+ currentValue: state.automatic ? "On" : "Off",
1289
+ values: ["On", "Off"],
1290
+ action: "automatic"
1291
+ },
1292
+ {
1293
+ id: "automaticTransfer",
1294
+ label: "Automatic transfer at startup",
1295
+ description: "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.",
1296
+ currentValue: state.automaticTransfer ? "On" : "Off",
1297
+ values: ["On", "Off"],
1298
+ action: "automatic-transfer"
1299
+ },
1300
+ {
1301
+ id: "onSwitch",
1302
+ label: "After switching setup (all setups)",
1303
+ description: "All setups: ask before a pull review, open it automatically, or switch without pulling.",
1304
+ currentValue: setupSwitchActionLabel(state.onSwitch),
1305
+ values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
1306
+ action: "on-switch"
1307
+ },
1308
+ {
1309
+ id: "mergeSettings",
1310
+ label: "Settings field merge (experimental)",
1311
+ description: "Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
1312
+ currentValue: state.mergeSettings ? "On" : "Off",
1313
+ values: ["On", "Off"],
1314
+ action: "merge-settings"
1315
+ },
1316
+ {
1317
+ id: "contentPolicy",
1318
+ label: "Content / partial sync (experimental)",
1319
+ description: "Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
1320
+ currentValue: state.mergeContent ? state.partialSync ? "Content & partial" : "Content only" : state.partialSync ? "Partial only" : "Off",
1321
+ values: ["Off", "Content only", "Content & partial", "Partial only"],
1322
+ action: "content-policy"
1323
+ },
1324
+ {
1325
+ id: "skipSecretScan",
1326
+ label: "Skip secret scan (all setups)",
1327
+ description: "All setups: allow pushes without checking managed local files for possible secrets.",
1328
+ currentValue: state.skipSecretScan ? "On" : "Off",
1329
+ values: ["On", "Off"],
1330
+ action: "skip-secret-scan"
1331
+ },
1332
+ {
1333
+ id: "showStatus",
1334
+ label: "Show status (all setups)",
1335
+ description: "All setups: show sync progress and attention in Pi status.",
1336
+ currentValue: state.showStatus ? "On" : "Off",
1337
+ values: ["On", "Off"],
1338
+ action: "show-status"
1339
+ }
1340
+ ]
1341
+ })
1342
+ },
1343
+ actions: {
1344
+ "storage-location": async ({ signal: actionSignal }) => {
1345
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1346
+ await showEditSyncSetupStorage(ctx, setupName, editorSignal);
1347
+ return resumeSettings(editorSignal);
1348
+ },
1349
+ setups: async ({ signal: actionSignal }) => {
1350
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1351
+ const result = await showSyncSetupManager(ctx, runRoute, editorSignal);
1352
+ return result === "exit" ? exitSettings() : resumeSettings(editorSignal);
1353
+ },
1354
+ connections: async ({ signal: actionSignal }) => {
1355
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1356
+ await showStorageConnections(ctx, editorSignal);
1357
+ return resumeSettings(editorSignal);
1358
+ },
1359
+ "content-policy": async ({ value, signal: actionSignal }) => {
1360
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1361
+ const validate = captureMutationOwner(ctx, mutationSignal);
1362
+ try {
1363
+ const previous = await loadConfig(setupName);
1364
+ validate();
1365
+ const mergeContent = value === "Content only" || value === "Content & partial";
1366
+ const partialSync = value === "Partial only" || value === "Content & partial";
1367
+ if (!await ctx.ui.confirm(
1368
+ "Save experimental content policy?",
1369
+ "This explicitly upgrades settings to version 5; partial snapshots use version 3 and older clients must refuse them. Text/session merge is conservative. Partial sync preserves withheld versions and old baselines. Disabling does not clear unresolved groups or recovery evidence. Review a directional migration first if portable policy changes.",
1370
+ { signal: mutationSignal }
1371
+ ))
1372
+ return { kind: "rejected" };
1373
+ validate();
1374
+ await updateLocalConfig((current) => {
1375
+ validate();
1376
+ const setup = current.syncSetups[setupName];
1377
+ if (!setup || Boolean(setup.sync.mergeContent) !== Boolean(previous.mergeContent) || Boolean(setup.sync.partialSync) !== Boolean(previous.partialSync))
1378
+ throw new Error("Content policy changed during review.");
1379
+ return {
1380
+ ...current,
1381
+ version: 5,
1382
+ syncSetups: {
1383
+ ...current.syncSetups,
1384
+ [setupName]: { ...setup, sync: { ...setup.sync, mergeContent, partialSync } }
1385
+ }
1386
+ };
1387
+ }, mutationSignal);
1388
+ validate();
1389
+ return { kind: "stay" };
1390
+ } catch (error) {
1391
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1392
+ return { kind: "rejected" };
1393
+ }
1394
+ },
1395
+ "merge-settings": async ({ value, signal: actionSignal }) => {
1396
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1397
+ const validate = captureMutationOwner(ctx, mutationSignal);
1398
+ try {
1399
+ validate();
1400
+ await updateSyncSetup(
1401
+ setupName,
1402
+ (setup) => ({ ...setup, sync: { ...setup.sync, mergeSettings: value === "On" } }),
1403
+ { signal: mutationSignal }
1404
+ );
1405
+ validate();
1406
+ return { kind: "stay" };
1407
+ } catch (error) {
1408
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1409
+ return { kind: "rejected" };
1410
+ }
1411
+ },
1412
+ "local-fields": async ({ signal: actionSignal }) => {
1413
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1414
+ const validate = captureMutationOwner(ctx, mutationSignal);
1415
+ try {
1416
+ const previous = await loadConfig(setupName);
1417
+ validate();
1418
+ const input = await ctx.ui.input(
1419
+ "Machine-local settings.json root fields (JSON string array)",
1420
+ JSON.stringify(previous.localFields ?? []),
1421
+ { signal: mutationSignal }
1422
+ );
1423
+ validate();
1424
+ if (input === void 0) return { kind: "rejected" };
1425
+ let fields;
1426
+ try {
1427
+ fields = normalizeLocalFields(JSON.parse(input));
1428
+ } catch {
1429
+ throw new Error("Invalid localFields JSON array; no policy was changed.");
1430
+ }
1431
+ if (sameLocalFields(previous.localFields, fields)) return { kind: "stay" };
1432
+ const confirmed = await ctx.ui.confirm(
1433
+ "Save portable field policy?",
1434
+ "This opts into settings/snapshot version 4/2; older clients must refuse them. Future snapshots omit these fields, but old remote history is NOT erased. Review an explicit force push/pull migration before further sync. Removed rules can expose or replace local-only values.",
1435
+ { signal: mutationSignal }
1436
+ );
1437
+ validate();
1438
+ if (!confirmed) return { kind: "rejected" };
1439
+ await updateLocalConfig((current) => {
1440
+ validate();
1441
+ const setup = current.syncSetups[setupName];
1442
+ if (!setup || !sameLocalFields(setup.sync.localFields, previous.localFields))
1443
+ throw new Error("Field policy changed while under review; reopen settings.");
1444
+ return {
1445
+ ...current,
1446
+ version: current.version === 5 ? 5 : 4,
1447
+ syncSetups: {
1448
+ ...current.syncSetups,
1449
+ [setupName]: { ...setup, sync: { ...setup.sync, localFields: fields } }
1450
+ }
1451
+ };
1452
+ }, mutationSignal);
1453
+ validate();
1454
+ return { kind: "stay" };
1455
+ } catch (error) {
1456
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1457
+ return { kind: "rejected" };
1458
+ }
1459
+ },
1460
+ automatic: async ({ value, signal: actionSignal }) => {
1461
+ const automatic = value === "On";
1462
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1463
+ try {
1464
+ const latest = await loadConfig(setupName);
1465
+ if (mutationSignal.aborted) return { kind: "rejected" };
1466
+ if (latest.automatic === automatic) return { kind: "stay" };
1467
+ await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automatic } }), {
1468
+ signal: mutationSignal
1469
+ });
1470
+ if (mutationSignal.aborted) return { kind: "rejected" };
1471
+ ctx.ui.notify(
1472
+ `Automatic sync ${automatic ? "enabled" : "disabled"} for \u201C${safeTerminalText(setupName)}\u201D.`,
1473
+ "info"
1474
+ );
1475
+ return { kind: "stay" };
1476
+ } catch (error) {
1477
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1478
+ return { kind: "rejected" };
1479
+ }
1378
1480
  },
1379
- onSwitch,
1380
- signal,
1381
- syncConfigReviewIdentity(config)
1382
- );
1383
- if (pullClosed) return "closed";
1384
- return result.pullApplied ? "pull-attempted" : "switched";
1385
- } catch (error) {
1386
- if (signal?.aborted) return false;
1387
- ctx.ui.notify(
1388
- `Sync setup \u201C${safeTerminalText(name)}\u201D was not switched: ${safeTerminalText(errorMessage(error))}`,
1389
- "error"
1390
- );
1391
- return false;
1392
- }
1393
- }
1394
-
1395
- // src/ui/setup/setup-actions.ts
1396
- async function showSyncSetupManager(ctx, runRoute, signal) {
1397
- return showSyncSetups(
1398
- ctx,
1399
- {
1400
- add: async (setupSignal) => {
1401
- await showAddTarget(ctx, setupSignal);
1481
+ "automatic-transfer": async ({ value, signal: actionSignal }) => {
1482
+ const automaticTransfer = value === "On";
1483
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1484
+ try {
1485
+ await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automaticTransfer } }), {
1486
+ signal: mutationSignal
1487
+ });
1488
+ if (mutationSignal.aborted) return { kind: "rejected" };
1489
+ ctx.ui.notify(
1490
+ `Automatic startup transfer ${automaticTransfer ? "enabled for the next session start" : "disabled"}. Completed transfers are not undone.`,
1491
+ "info"
1492
+ );
1493
+ return { kind: "stay" };
1494
+ } catch (error) {
1495
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1496
+ return { kind: "rejected" };
1497
+ }
1402
1498
  },
1403
- edit: async (name, setupSignal) => {
1404
- await showEditTarget(ctx, name, setupSignal);
1499
+ "skip-secret-scan": async ({ value, signal: actionSignal }) => {
1500
+ const skipSecretScan = value === "On";
1501
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1502
+ try {
1503
+ const latest = await loadConfig(setupName);
1504
+ if (mutationSignal.aborted) return { kind: "rejected" };
1505
+ if (latest.skipSecretScan === skipSecretScan) return { kind: "stay" };
1506
+ await updateLocalConfig((settings) => ({ ...settings, skipSecretScan }), mutationSignal);
1507
+ if (mutationSignal.aborted) return { kind: "rejected" };
1508
+ ctx.ui.notify(`Secret scan ${skipSecretScan ? "disabled" : "enabled"} for pushes in all setups.`, "info");
1509
+ return { kind: "stay" };
1510
+ } catch (error) {
1511
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1512
+ return { kind: "rejected" };
1513
+ }
1405
1514
  },
1406
- makeCurrent: async (name, setupSignal) => {
1407
- const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
1408
- return result === "pull-attempted" || result === "closed" ? "exit" : void 0;
1515
+ "show-status": async ({ value, signal: actionSignal }) => {
1516
+ const showStatus = value === "On";
1517
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1518
+ try {
1519
+ const latest = await loadConfig(setupName);
1520
+ if (mutationSignal.aborted) return { kind: "rejected" };
1521
+ if (latest.showStatus === showStatus) return { kind: "stay" };
1522
+ await updateLocalConfig((settings) => ({ ...settings, showStatus }), mutationSignal);
1523
+ if (mutationSignal.aborted) return { kind: "rejected" };
1524
+ configureSyncStatus(ctx, showStatus);
1525
+ ctx.ui.notify(`Pi Sync status ${showStatus ? "enabled" : "disabled"} for all setups.`, "info");
1526
+ return { kind: "stay" };
1527
+ } catch (error) {
1528
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1529
+ return { kind: "rejected" };
1530
+ }
1409
1531
  },
1410
- remove: async (name, setupSignal) => {
1411
- await showRemoveTarget(ctx, name, setupSignal);
1532
+ "on-switch": async ({ value, signal: actionSignal }) => {
1533
+ const action = value ? setupSwitchActionFromLabel(value) : void 0;
1534
+ if (!action) return { kind: "rejected" };
1535
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1536
+ try {
1537
+ const latest = await loadConfig(setupName);
1538
+ if (mutationSignal.aborted) return { kind: "rejected" };
1539
+ if (latest.onSwitch === action) return { kind: "stay" };
1540
+ await saveOnSwitch(action, mutationSignal);
1541
+ if (mutationSignal.aborted) return { kind: "rejected" };
1542
+ ctx.ui.notify(`After switching setup: ${value}.`, "info");
1543
+ return { kind: "stay" };
1544
+ } catch (error) {
1545
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1546
+ return { kind: "rejected" };
1547
+ }
1548
+ },
1549
+ include: async ({ signal: actionSignal }) => {
1550
+ const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1551
+ await runRoute("files", editorSignal, void 0, setupName);
1552
+ return editorSignal.aborted ? { kind: "rejected" } : { kind: "stay" };
1553
+ },
1554
+ "remote-include": async ({ signal: actionSignal }) => {
1555
+ const reviewSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1556
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
1557
+ if (reviewSignal.aborted) return { kind: "rejected" };
1558
+ const review = await showRemoteSelectionReview(ctx, setupName, reviewSignal, void 0, {
1559
+ origin: "settings",
1560
+ runRoute
1561
+ });
1562
+ if (reviewSignal.aborted) return { kind: "rejected" };
1563
+ if (review.kind === "route-result") {
1564
+ const disposition2 = await dispatchManagerResult(ctx, review.result, review.route, runRoute, reviewSignal);
1565
+ return disposition2.kind === "close" ? exitSettings() : { kind: "stay" };
1566
+ }
1567
+ return review.kind === "closed" || review.kind === "stale" ? exitSettings() : { kind: "stay" };
1412
1568
  }
1413
- },
1414
- signal
1415
- );
1416
- }
1417
- async function showAddTarget(ctx, signal) {
1418
- let raw = await readLocalConfigObject();
1419
- if (signal?.aborted) return;
1420
- if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
1421
- if (raw.version !== 3 && raw.version !== 4 && raw.version !== 5) {
1422
- ctx.ui.notify("Version 1 and version 2 settings are unsupported and are never migrated.", "error");
1423
- return;
1424
- }
1425
- let profiles = ownRecord(raw.storageConnections) ?? {};
1426
- const name = await promptResourceName(ctx, "sync setup", "work", signal);
1427
- if (!name) return;
1428
- const createConnection = "Add a new storage connection\u2026";
1429
- let profile = await ctx.ui.select(
1430
- "Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
1431
- [...Object.keys(profiles).sort(), createConnection, "Cancel"],
1432
- { signal }
1433
- );
1434
- if (signal?.aborted || !profile || profile === "Cancel") return;
1435
- if (profile === createConnection) {
1436
- const previousNames = new Set(Object.keys(profiles));
1437
- if (!await showAddStorageConnection(ctx, signal)) return;
1438
- if (signal?.aborted) return;
1439
- raw = await readLocalConfigObject() ?? raw;
1440
- if (signal?.aborted) return;
1441
- profiles = ownRecord(raw.storageConnections) ?? {};
1442
- profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
1443
- if (!profile) return;
1444
- ctx.ui.notify(
1445
- `Connection \u201C${safeTerminalText(profile)}\u201D saved. Cancelling setup will keep this connection.`,
1446
- "info"
1447
- );
1448
- }
1449
- const storageKind = ownRecord(profiles[profile])?.type;
1450
- if (storageKind === "webdav") {
1451
- const saved = await showAddWebDavTarget(ctx, name, profile, signal);
1452
- if (signal?.aborted) return;
1453
- if (saved) await refreshTargetCompletions();
1454
- return;
1455
- }
1456
- if (storageKind === "git") {
1457
- const saved = await showAddGitTarget(ctx, name, profile, signal);
1458
- if (signal?.aborted) return;
1459
- if (saved) await refreshTargetCompletions();
1460
- return;
1461
- }
1462
- await showAddS3Target(ctx, raw, profile, name, signal);
1463
- }
1464
- async function showEditTarget(ctx, name, signal) {
1465
- const partial = await loadPartialConfig(name);
1466
- if (signal?.aborted) return;
1467
- if (!partial.setupName) {
1468
- ctx.ui.notify("Create supported pi-sync settings before editing a named sync setup.", "info");
1469
- return;
1470
- }
1471
- if (partial.storageKind === "webdav") {
1472
- await showEditWebDavTarget(ctx, partial, signal);
1473
- return;
1474
- }
1475
- if (partial.storageKind === "git") {
1476
- await showEditGitTarget(ctx, partial, signal);
1477
- return;
1569
+ }
1570
+ });
1571
+ async function resumeSettings(editorSignal) {
1572
+ if (editorSignal.aborted) return { kind: "close" };
1573
+ try {
1574
+ const active = await loadConfig();
1575
+ if (editorSignal.aborted || active.setupName !== setupName) return { kind: "close" };
1576
+ return { kind: "stay" };
1577
+ } catch {
1578
+ return { kind: "close" };
1579
+ }
1478
1580
  }
1479
- await showEditS3Target(ctx, partial, signal);
1581
+ await runMenu4(ctx, menu, {
1582
+ getState: () => loadConfig(setupName),
1583
+ signal,
1584
+ isCurrent: () => !signal?.aborted
1585
+ });
1586
+ return disposition;
1480
1587
  }
1481
- async function showRemoveTarget(ctx, name, signal) {
1482
- const confirmed = await ctx.ui.confirm(
1483
- "Remove sync setup?",
1484
- `Remove local sync setup \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
1485
- { signal }
1588
+ function notifySaveFailure(ctx, error) {
1589
+ ctx.ui.notify(
1590
+ `Pi Sync settings save failed: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
1591
+ "error"
1486
1592
  );
1487
- if (signal?.aborted || !confirmed) return;
1488
- await removeSyncSetup(name, signal);
1489
- if (signal?.aborted) return;
1490
- await refreshTargetCompletions();
1491
- if (signal?.aborted) return;
1492
- ctx.ui.notify(`Removed sync setup \u201C${safeTerminalText(name)}\u201D; remote data was not deleted.`, "info");
1493
1593
  }
1494
1594
 
1495
1595
  // src/ui/manager-ui.ts
@@ -1510,52 +1610,15 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1510
1610
  return {
1511
1611
  kind: "actions",
1512
1612
  title: "Manage sync",
1513
- lines: state.manager.title.split("\n").slice(1),
1613
+ // Frequent transfers stay visible; status is advisory and Help remains a direct route.
1614
+ lines: [
1615
+ ...state.manager.title.split("\n").slice(1),
1616
+ "Help: /sync help \xB7 Fresh status: /sync status \xB7 Differences: /sync diff"
1617
+ ],
1514
1618
  items: operationFirst ? [...managerItems, ...attentionItems] : [...attentionItems, ...managerItems],
1515
1619
  hint: "close"
1516
1620
  };
1517
- },
1518
- more: () => ({
1519
- kind: "actions",
1520
- title: "More options",
1521
- items: [
1522
- { id: "pull", label: "Pull from remote\u2026", action: "pull" },
1523
- { id: "push", label: "Push to remote\u2026", action: "push" },
1524
- { id: "setups", label: "Sync setups\u2026", action: "setups" },
1525
- {
1526
- id: "connections",
1527
- label: "Storage connections\u2026",
1528
- action: "connections"
1529
- },
1530
- {
1531
- id: "doctor",
1532
- label: "Check setup",
1533
- description: "Check current setup access. WebDAV also probes writes and repairs history.",
1534
- action: "doctor"
1535
- },
1536
- { id: "recovery", label: "History & recovery\u2026", to: "recovery" },
1537
- { id: "help", label: "Help", action: "help" },
1538
- { id: "back", label: "Back", action: "back" }
1539
- ],
1540
- hint: "back"
1541
- }),
1542
- recovery: ({ state }) => ({
1543
- kind: "actions",
1544
- title: "History & recovery",
1545
- items: [
1546
- { id: "history", label: "Browse history", action: "history" },
1547
- { id: "conflicts", label: "Review unresolved conflicts", action: "conflicts" },
1548
- {
1549
- id: "doctor",
1550
- label: "Check setup",
1551
- description: "WebDAV also probes writes and repairs history.",
1552
- action: "doctor"
1553
- },
1554
- ...state.manager.operation && operationCanRecover(state.manager.operation) ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" }] : [],
1555
- { id: "back", label: "Back", action: "back" }
1556
- ],
1557
- hint: "back"
1558
- })
1621
+ }
1559
1622
  },
1560
1623
  actions: {
1561
1624
  "review-attention": async () => {
@@ -1563,7 +1626,7 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1563
1626
  if (!attention) {
1564
1627
  const observation = options.getObservation?.();
1565
1628
  if (!observation) return { kind: "stay" };
1566
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-Q2HUIBTL.ts");
1629
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
1567
1630
  if (sessionSignal?.aborted) return { kind: "close" };
1568
1631
  const config = await loadConfig();
1569
1632
  if (sessionSignal?.aborted) return { kind: "close" };
@@ -1615,15 +1678,9 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1615
1678
  const result = await showSetupSwitcher(ctx, runRoute, void 0, sessionSignal);
1616
1679
  return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
1617
1680
  },
1618
- diff: async () => {
1619
- const result = await runCancellableOperation(ctx, "Checking current sync setup\u2026", "diff", runRoute, {
1620
- signal: sessionSignal
1621
- });
1622
- return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
1623
- },
1624
1681
  settings: async () => {
1625
- await showSyncSettings(ctx, runRoute, sessionSignal);
1626
- return { kind: "stay" };
1682
+ const result = await showSyncSettings(ctx, runRoute, sessionSignal);
1683
+ return { kind: result === "exit" ? "close" : "stay" };
1627
1684
  },
1628
1685
  pull: async () => {
1629
1686
  const result = await runCancellableOperation(ctx, "Checking remote changes\u2026", "pull", runRoute, {
@@ -1652,8 +1709,13 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1652
1709
  return { kind: "stay" };
1653
1710
  },
1654
1711
  history: async () => {
1655
- await runRoute("history");
1656
- return { kind: "stay" };
1712
+ const result = await runCancellableOperation(ctx, "Loading sync history\u2026", "history", runRoute, {
1713
+ signal: sessionSignal,
1714
+ commitAware: true,
1715
+ cancelAcrossDialogs: true,
1716
+ cancelledMessage: "History review cancelled; no files were changed."
1717
+ });
1718
+ return { kind: result.kind === "closed" ? "close" : "stay" };
1657
1719
  },
1658
1720
  doctor: async () => {
1659
1721
  const result = await runCancellableOperation(ctx, "Checking setup access\u2026", "doctor", runRoute, {
@@ -1661,11 +1723,6 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1661
1723
  });
1662
1724
  return { kind: result.kind === "closed" ? "close" : "stay" };
1663
1725
  },
1664
- unlock: async ({ state, signal: actionSignal }) => {
1665
- const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
1666
- if (result === "close") return { kind: "close" };
1667
- return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
1668
- },
1669
1726
  recover: async ({ state, signal: actionSignal }) => {
1670
1727
  const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
1671
1728
  return { kind: result === "close" ? "close" : "stay" };
@@ -1686,8 +1743,7 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1686
1743
  });
1687
1744
  const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
1688
1745
  return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
1689
- },
1690
- back: async () => ({ kind: "back" })
1746
+ }
1691
1747
  }
1692
1748
  });
1693
1749
  await runMenu5(ctx, menu, {
@@ -1709,24 +1765,33 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1709
1765
  });
1710
1766
  }
1711
1767
  function syncMainMenuItem(label) {
1712
- if (label === "More\u2026") return { id: "more", label, to: "more" };
1713
- if (label === "History & recovery\u2026") return { id: "recovery", label, to: "recovery" };
1714
- const actions = /* @__PURE__ */ new Map([
1715
- ["Sync now (recommended)", "sync"],
1716
- ["Switch sync setup", "switch"],
1717
- ["Status & changes", "diff"],
1718
- ["Settings", "settings"],
1719
- ["Restore sync access\u2026 (recommended)", "recover"],
1720
- ["Refresh operation status", "refresh"],
1721
- ["Sync setups\u2026", "setups"],
1722
- ["Storage connections\u2026", "connections"],
1723
- ["Help", "help"],
1724
- ["Set up sync", "init"],
1725
- ["Use existing settings", "init"]
1726
- ]);
1727
- return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
1768
+ const actions = {
1769
+ "Sync now": "sync",
1770
+ "Pull from remote\u2026": "pull",
1771
+ "Push to remote\u2026": "push",
1772
+ "Switch sync setup": "switch",
1773
+ Settings: "settings",
1774
+ History: "history",
1775
+ Diagnostics: "doctor",
1776
+ "Review unresolved conflicts": "conflicts",
1777
+ "Restore sync access\u2026 (recommended)": "recover",
1778
+ "Refresh operation status": "refresh",
1779
+ "Sync setups\u2026": "setups",
1780
+ "Storage connections\u2026": "connections",
1781
+ Help: "help",
1782
+ "Set up sync": "init",
1783
+ "Use existing settings": "init"
1784
+ };
1785
+ const action = actions[label];
1786
+ if (!action) throw new Error(`Unknown sync manager action: ${label}`);
1787
+ return {
1788
+ id: action,
1789
+ label,
1790
+ action,
1791
+ ...action === "doctor" ? { description: "Check setup access; WebDAV also probes writes and repairs history." } : {}
1792
+ };
1728
1793
  }
1729
1794
  export {
1730
1795
  showSyncManager
1731
1796
  };
1732
- //# sourceMappingURL=manager-ui-MHAHAKG3.ts.map
1797
+ //# sourceMappingURL=manager-ui-IURS4WXY.ts.map