@narumitw/pi-sync 0.54.1 → 0.54.3

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 (102) hide show
  1. package/README.md +19 -11
  2. package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
  3. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
  4. package/dist/chunks/{chunk-HUTYAPFP.ts → chunk-35GM6XKM.ts} +275 -151
  5. package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
  6. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
  7. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
  8. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
  9. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
  10. package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
  11. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
  12. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
  13. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
  14. package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
  15. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
  16. package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
  17. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
  18. package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
  19. package/dist/chunks/{chunk-ANWNQIR7.ts → chunk-KGE47GCZ.ts} +4 -4
  20. package/dist/chunks/chunk-KGE47GCZ.ts.map +7 -0
  21. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
  22. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  23. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  24. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
  25. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
  26. package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
  27. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
  28. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
  29. package/dist/chunks/{conflict-review-JRFJIBEN.ts → conflict-review-ZOTOHL2E.ts} +11 -10
  30. package/dist/chunks/{conflict-review-JRFJIBEN.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
  31. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
  32. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
  33. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
  34. package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
  35. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
  36. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
  37. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
  38. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
  39. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
  40. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
  41. package/dist/chunks/{sync-operations-QQ2GXKVC.ts → sync-operations-2CWST333.ts} +140 -24
  42. package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
  43. package/dist/index.ts +103 -68
  44. package/dist/index.ts.map +2 -2
  45. package/docs/settings.md +47 -10
  46. package/docs/tui-contract.md +61 -0
  47. package/package.json +1 -1
  48. package/src/commands/command-execution.ts +2 -1
  49. package/src/commands/command-types.ts +4 -0
  50. package/src/commands/command.ts +3 -1
  51. package/src/settings/config.ts +13 -10
  52. package/src/settings/settings-store.ts +1 -0
  53. package/src/settings/settings-types.ts +2 -0
  54. package/src/settings/settings-validation.ts +3 -0
  55. package/src/snapshot/snapshot-transaction.ts +4 -2
  56. package/src/state/merge-baseline-store.ts +18 -0
  57. package/src/sync/apply-review.ts +63 -0
  58. package/src/sync/automatic-sync.ts +45 -33
  59. package/src/sync/merged-sync.ts +202 -104
  60. package/src/sync/startup-check.ts +39 -21
  61. package/src/sync/startup-timings.ts +90 -0
  62. package/src/sync/sync-inspection.ts +8 -4
  63. package/src/sync/sync-mutations.ts +49 -11
  64. package/src/sync/sync-queries.ts +14 -3
  65. package/src/ui/manager-state.ts +10 -6
  66. package/src/ui/manager-ui.ts +57 -7
  67. package/src/ui/merge-review.ts +3 -3
  68. package/src/ui/merged-sync-summary.ts +2 -18
  69. package/src/ui/settings-frame.ts +55 -0
  70. package/src/ui/settings-ui.ts +141 -43
  71. package/src/ui/setup/setup-prompts.ts +4 -3
  72. package/src/ui/sync-behavior.ts +15 -0
  73. package/src/ui/sync-format.ts +69 -38
  74. package/src/ui/transfer-changes.ts +33 -0
  75. package/src/ui/transfer-review.ts +27 -0
  76. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  77. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  78. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  79. package/dist/chunks/chunk-ANWNQIR7.ts.map +0 -7
  80. package/dist/chunks/chunk-HUTYAPFP.ts.map +0 -7
  81. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  82. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  83. package/dist/chunks/sync-operations-QQ2GXKVC.ts.map +0 -7
  84. /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
  85. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
  86. /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
  87. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
  88. /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
  89. /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
  90. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
  91. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
  92. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
  93. /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
  94. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
  95. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
  96. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
  97. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
  98. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
  99. /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
  100. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
  101. /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
  102. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → sync-inspection-3EMS2J5B.ts.map} +0 -0
@@ -1,7 +1,6 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
- AUTOMATIC_SYNC_DESCRIPTION,
5
4
  applyS3CredentialUpdate,
6
5
  chooseS3CredentialUpdate,
7
6
  chooseS3Credentials,
@@ -20,33 +19,33 @@ import {
20
19
  showEditS3Target,
21
20
  showEditWebDavStorageProfile,
22
21
  showEditWebDavTarget
23
- } from "./chunk-4SBTAVFL.ts";
22
+ } from "./chunk-I4JYVFUS.ts";
24
23
  import {
25
24
  dispatchManagerResult
26
- } from "./chunk-VHXBCEVF.ts";
25
+ } from "./chunk-HMXUKU43.ts";
27
26
  import {
28
27
  runCancellableOperation
29
- } from "./chunk-5ZWQ33XL.ts";
30
- import "./chunk-V6VZ5TLG.ts";
31
- import "./chunk-SDZCTCYQ.ts";
28
+ } from "./chunk-CAGJOKB7.ts";
29
+ import "./chunk-A3TFXY4O.ts";
30
+ import "./chunk-2FETCGGT.ts";
32
31
  import {
33
32
  SETUP_SWITCH_ACTION_OPTIONS,
34
33
  saveOnSwitch,
35
34
  setupSwitchActionFromLabel,
36
35
  setupSwitchActionLabel,
37
36
  useSyncSetup
38
- } from "./chunk-UQ3ALBIB.ts";
37
+ } from "./chunk-YTHQEW35.ts";
39
38
  import {
40
39
  observationMatchesConfig,
41
40
  observationSummary,
42
41
  syncAttentionMatchesConfig
43
- } from "./chunk-5RZPINUQ.ts";
42
+ } from "./chunk-AAZDBIBJ.ts";
44
43
  import {
45
44
  captureMutationOwner,
46
45
  inspectLock,
47
46
  isLockGuardHeld,
48
47
  isStaleLock
49
- } from "./chunk-IJ6VV6FN.ts";
48
+ } from "./chunk-GC3QOL6J.ts";
50
49
  import "./chunk-PWMWOJP4.ts";
51
50
  import {
52
51
  automaticSyncSummary
@@ -54,14 +53,18 @@ import {
54
53
  import {
55
54
  configureSyncStatus
56
55
  } from "./chunk-K3RIEL6J.ts";
57
- import "./chunk-2DI6PDZV.ts";
56
+ import {
57
+ AUTOMATIC_SYNC_DESCRIPTION,
58
+ STARTUP_TRANSFER_DESCRIPTION,
59
+ startupTransferSummary
60
+ } from "./chunk-ITFR6NNQ.ts";
58
61
  import {
59
62
  safeTerminalText
60
63
  } from "./chunk-AMJRUXFT.ts";
61
64
  import {
62
65
  readStateForConfig,
63
66
  syncStateFingerprint
64
- } from "./chunk-4QVPTIZB.ts";
67
+ } from "./chunk-VCL7TZYW.ts";
65
68
  import "./chunk-BVVP33U5.ts";
66
69
  import {
67
70
  addStorageConnection,
@@ -69,7 +72,7 @@ import {
69
72
  removeSyncSetup,
70
73
  updateStorageConnection,
71
74
  updateSyncSetup
72
- } from "./chunk-CHSIAC4N.ts";
75
+ } from "./chunk-R35N4CL3.ts";
73
76
  import {
74
77
  activeLocalConfigPath,
75
78
  isCloudflareR2Endpoint,
@@ -80,9 +83,10 @@ import {
80
83
  normalizeS3Endpoint,
81
84
  ownRecord,
82
85
  readLocalConfigObject,
86
+ syncCheckConfigFingerprint,
83
87
  syncConfigReviewIdentity,
84
88
  updateLocalConfig
85
- } from "./chunk-3B4Z2SKV.ts";
89
+ } from "./chunk-KATKRR3D.ts";
86
90
  import "./chunk-2JZFHNLM.ts";
87
91
  import "./chunk-T2SJD3N7.ts";
88
92
  import "./chunk-ZX2Q6THB.ts";
@@ -172,7 +176,7 @@ function blockedSyncMenuItem(label, manager) {
172
176
  };
173
177
  }
174
178
  async function showManagerAttention(ctx, attention, runRoute, signal, onSelectionResolved) {
175
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
179
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-H6AEVA3Q.ts");
176
180
  if (signal?.aborted) return "close";
177
181
  const review = await showRemoteSelectionReview(ctx, attention.decision.setupName, signal, void 0, {
178
182
  decision: attention.decision,
@@ -453,6 +457,8 @@ var MAIN_MENU_ACTIONS = [
453
457
  "Sync now",
454
458
  "Pull from remote\u2026",
455
459
  "Push to remote\u2026",
460
+ "View differences",
461
+ "Check remote status",
456
462
  "Settings",
457
463
  "History",
458
464
  "Diagnostics"
@@ -514,7 +520,7 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
514
520
  const noSyncedContent = config.include.length === 0;
515
521
  const syncState = changesBlocked ? void 0 : await readStateForConfig(config).catch(() => void 0);
516
522
  const currentObservation = observation && observationMatchesConfig(observation, config) && syncState && syncStateFingerprint(syncState) === observation.inspection.stateIdentity ? observation : void 0;
517
- const lastAppliedSnapshot = changesBlocked ? "Unavailable while operations are locked" : syncState?.lastAppliedSnapshot ? safeTerminalText(syncState.lastAppliedSnapshot) : syncState ? "Never synced" : "Unavailable";
523
+ const lastAppliedSnapshot = changesBlocked ? "Unavailable while operations are locked" : syncState?.lastAppliedSnapshot ? `${truncateToWidth(safeTerminalText(syncState.lastAppliedSnapshot), 20, "\u2026")} (application time unavailable; full ID in History)` : syncState ? "Never synced" : "Unavailable";
518
524
  const canSwitch = await countValidSyncSetups(configuredTargets, signal) > 1;
519
525
  const mainActions = [...MAIN_MENU_ACTIONS];
520
526
  if (canSwitch) mainActions.push("Switch sync setup");
@@ -524,10 +530,7 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
524
530
  "",
525
531
  `Current sync setup: ${safeTerminalText(config.setupName)}`,
526
532
  `Storage: ${backendStorageDescription(config)}`,
527
- `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} \xB7 ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} \xB7 Sessions ${selection.sessions ? "on" : "off"}`,
528
- `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
529
- `Automatic transfer at startup: ${config.automaticTransfer ? "On \u2014 upload/replace/delete at idle; no reload" : "Off"}`,
530
- `Last applied: ${lastAppliedSnapshot}`,
533
+ "CURRENT STATE",
531
534
  ...syncState?.unresolved?.length ? [
532
535
  `Unresolved: ${syncState.unresolved.length} dependency groups \xB7 choose Review unresolved conflicts`,
533
536
  `Last observed: ${safeTerminalText(syncState.lastObservedSnapshot ?? "unknown")}`
@@ -540,9 +543,13 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
540
543
  `Startup check (${safeTerminalText(currentObservation.checkedAt)}): ${observationSummary(currentObservation)}`,
541
544
  "Check-time observation only; synchronization rechecks current content."
542
545
  ] : ["Remote status: Not checked"],
546
+ `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} \xB7 ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} \xB7 Sessions ${selection.sessions ? "on" : "off"}`,
547
+ `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
548
+ `Startup transfer: ${startupTransferSummary(config)}`,
549
+ `Last applied: ${lastAppliedSnapshot}`,
543
550
  ...noSyncedContent ? ["", "No included content is selected. Choose included content in Settings before syncing."] : [],
544
551
  "",
545
- "What do you want to do?"
552
+ "ACTIONS \xB7 transfer, inspect, or manage"
546
553
  ];
547
554
  return {
548
555
  title: (changesBlocked ? ["Manage sync", ...operationStatusLines(operation)] : ordinaryTitle).join("\n"),
@@ -630,6 +637,50 @@ function backendStorageDescription(config) {
630
637
  // src/ui/settings-ui.ts
631
638
  import { defineMenu as defineMenu4, runMenu as runMenu4 } from "@narumitw/pi-tui-kit";
632
639
 
640
+ // src/ui/settings-frame.ts
641
+ import { stripVTControlCharacters } from "node:util";
642
+ function boundedSettingsUI(ctx) {
643
+ return {
644
+ ...ctx.ui,
645
+ custom: (factory, options) => ctx.ui.custom((tui, ...args) => {
646
+ const terminal = new Proxy(tui.terminal, {
647
+ get: (target, key) => key === "rows" ? Math.max(8, target.rows) : Reflect.get(target, key)
648
+ });
649
+ const screenTui = new Proxy(tui, {
650
+ get(target, key) {
651
+ if (key === "terminal") return terminal;
652
+ const value = Reflect.get(target, key);
653
+ return typeof value === "function" ? value.bind(target) : value;
654
+ }
655
+ });
656
+ const guard = (component2) => {
657
+ const render = component2.render.bind(component2);
658
+ const handleMouse = component2.handleMouse?.bind(component2);
659
+ let sourceRow = 0;
660
+ const compact = () => tui.terminal.rows < 8;
661
+ component2.render = (width) => {
662
+ if (width <= 0) return [];
663
+ const lines = render(width);
664
+ if (!compact()) return lines;
665
+ const active = lines.findIndex((line) => stripVTControlCharacters(line).startsWith("\u2192"));
666
+ const search = lines.findIndex((line) => stripVTControlCharacters(line).startsWith(">"));
667
+ sourceRow = active < 0 ? Math.max(0, search) : active;
668
+ return lines.slice(sourceRow, sourceRow + 1);
669
+ };
670
+ if (handleMouse)
671
+ component2.handleMouse = (event) => {
672
+ if (event.width <= 0 || compact() && event.y !== 0) return void 0;
673
+ component2.render(event.width);
674
+ return handleMouse(compact() ? { ...event, y: event.y + sourceRow } : event);
675
+ };
676
+ return component2;
677
+ };
678
+ const component = factory(screenTui, ...args);
679
+ return component instanceof Promise ? component.then(guard) : guard(component);
680
+ }, options)
681
+ };
682
+ }
683
+
633
684
  // src/ui/storage-connections-ui.ts
634
685
  import { isDeepStrictEqual } from "node:util";
635
686
  import { defineMenu as defineMenu2, runMenu as runMenu2 } from "@narumitw/pi-tui-kit";
@@ -1227,118 +1278,153 @@ async function showSyncSettings(ctx, runRoute, signal) {
1227
1278
  const initial = await loadConfig();
1228
1279
  if (signal?.aborted) return "back";
1229
1280
  const setupName = initial.setupName;
1281
+ const groups = {
1282
+ content: { label: "Sync content", ids: ["include", "remoteInclude", "localFields"] },
1283
+ behavior: { label: "Sync behavior", ids: ["automatic", "automaticTransfer", "onSwitch"] },
1284
+ storage: { label: "Storage & setups", ids: ["storageLocation", "setups", "connections"] },
1285
+ advanced: { label: "Advanced", ids: ["mergeSettings", "contentPolicy", "skipSecretScan", "showStatus", "debug"] },
1286
+ all: { label: "All settings (search)", ids: [] }
1287
+ };
1288
+ let saveStatus = "Simple choices save immediately. Back does not undo saved changes.";
1289
+ const settingsScreen = (state, group) => ({
1290
+ kind: "settings",
1291
+ title: `Pi Sync Settings \xB7 ${groups[group].label}`,
1292
+ lines: [
1293
+ `Setup: ${safeTerminalText(state.setupName)} \xB7 Connection: ${safeTerminalText(state.connectionName)}`,
1294
+ saveStatus
1295
+ ],
1296
+ items: [
1297
+ {
1298
+ id: "include",
1299
+ label: "Included paths",
1300
+ description: "This setup: choose paths in a draft, then review and Save or Discard. No transfer is started.",
1301
+ currentValue: `${state.include.length} paths \xB7 Edit`,
1302
+ action: "include"
1303
+ },
1304
+ {
1305
+ id: "remoteInclude",
1306
+ label: "Compare included paths",
1307
+ description: "This setup: compare included-path policies, not file contents. Review before saving either list; no files transfer unless you explicitly Continue.",
1308
+ currentValue: "Review",
1309
+ action: "remote-include"
1310
+ },
1311
+ {
1312
+ id: "localFields",
1313
+ label: "Machine-local fields",
1314
+ description: "This setup: settings.json root fields excluded from future portable snapshots, not project overrides. Policy changes require reviewed directional migration; old history remains.",
1315
+ currentValue: `${state.localFields?.length ?? 0} fields \xB7 Edit`,
1316
+ action: "local-fields"
1317
+ },
1318
+ {
1319
+ id: "storageLocation",
1320
+ label: "Storage location",
1321
+ description: "This setup: review and save bucket, branch, or path changes. Remote history is not moved.",
1322
+ 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),
1323
+ action: "storage-location"
1324
+ },
1325
+ {
1326
+ id: "setups",
1327
+ label: "Manage sync setups",
1328
+ description: "This device: add, switch, edit, or remove sync setups. Each edit identifies its owner before saving.",
1329
+ currentValue: "Open",
1330
+ action: "setups"
1331
+ },
1332
+ {
1333
+ id: "connections",
1334
+ label: "Manage storage connections",
1335
+ description: "Shared connections on this device: review affected setups before saving server or credential changes.",
1336
+ currentValue: "Open",
1337
+ action: "connections"
1338
+ },
1339
+ {
1340
+ id: "automatic",
1341
+ label: "Automatic sync",
1342
+ description: AUTOMATIC_SYNC_DESCRIPTION,
1343
+ currentValue: state.automatic ? "On" : "Off",
1344
+ values: ["On", "Off"],
1345
+ action: "automatic"
1346
+ },
1347
+ {
1348
+ id: "automaticTransfer",
1349
+ label: "Startup transfer",
1350
+ description: `${STARTUP_TRANSFER_DESCRIPTION} Saved preference and requirements: ${startupTransferSummary(state)}.`,
1351
+ currentValue: state.automaticTransfer ? "On" : "Off",
1352
+ values: ["On", "Off"],
1353
+ action: "automatic-transfer"
1354
+ },
1355
+ {
1356
+ id: "onSwitch",
1357
+ label: "On setup switch",
1358
+ description: "All setups on this device: ask before a pull review, open it automatically, or switch without pulling. Applies to subsequent switches.",
1359
+ currentValue: setupSwitchActionLabel(state.onSwitch),
1360
+ values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
1361
+ action: "on-switch"
1362
+ },
1363
+ {
1364
+ id: "mergeSettings",
1365
+ label: "Merge settings fields",
1366
+ description: "This setup, experimental opt-in: merge independent global settings.json root fields with a verified private ancestor. Arrays and nested objects remain atomic. Applies to subsequent sync; no reload.",
1367
+ currentValue: state.mergeSettings ? "On" : "Off",
1368
+ values: ["On", "Off"],
1369
+ action: "merge-settings"
1370
+ },
1371
+ {
1372
+ id: "contentPolicy",
1373
+ label: "Content merge / partial progress",
1374
+ description: "This setup, experimental opt-in: review the version-5 upgrade before saving. Merge bounded text and prefix-only sessions; partial progress keeps withheld versions and baselines. Applies to subsequent sync; disabling does not clear recovery evidence.",
1375
+ currentValue: state.mergeContent ? state.partialSync ? "Merge + partial" : "Merge only" : state.partialSync ? "Partial only" : "Off",
1376
+ values: ["Off", "Merge only", "Merge + partial", "Partial only"],
1377
+ action: "content-policy"
1378
+ },
1379
+ {
1380
+ id: "skipSecretScan",
1381
+ label: "Skip secret scan",
1382
+ description: "All setups on this device: skip possible-secret checks on subsequent pushes, including automatic pushes. Other safety checks remain. Diagnostics still scans.",
1383
+ currentValue: state.skipSecretScan ? "On" : "Off",
1384
+ values: ["On", "Off"],
1385
+ action: "skip-secret-scan"
1386
+ },
1387
+ {
1388
+ id: "showStatus",
1389
+ label: "Show sync status",
1390
+ description: "All setups on this device: saved and applied immediately to Pi status. Review widgets and notifications remain available.",
1391
+ currentValue: state.showStatus ? "On" : "Off",
1392
+ values: ["On", "Off"],
1393
+ action: "show-status"
1394
+ },
1395
+ {
1396
+ id: "debug",
1397
+ label: "Debug startup timings",
1398
+ description: "All setups on this device: show startup durations in TUI/RPC notifications. Applies next session start; no credentials or file contents.",
1399
+ currentValue: state.debug ? "On" : "Off",
1400
+ values: ["On", "Off"],
1401
+ action: "debug"
1402
+ }
1403
+ ].filter((item) => group === "all" || groups[group].ids.includes(item.id))
1404
+ });
1230
1405
  const menu = defineMenu4({
1231
- start: "settings",
1406
+ start: "categories",
1232
1407
  screens: {
1233
- settings: ({ state }) => ({
1234
- kind: "settings",
1408
+ categories: ({ state }) => ({
1409
+ kind: "actions",
1235
1410
  title: "Pi Sync Settings",
1236
1411
  lines: [
1237
- `Sync setup: ${safeTerminalText(state.setupName)} \xB7 Storage connection: ${safeTerminalText(state.connectionName)}`
1412
+ `Setup: ${safeTerminalText(state.setupName)} \xB7 Connection: ${safeTerminalText(state.connectionName)}`,
1413
+ "Choose a task, or search all settings. Scope is shown in each setting's description.",
1414
+ saveStatus
1238
1415
  ],
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
- })
1416
+ items: Object.entries(groups).map(([id, group]) => ({
1417
+ id,
1418
+ label: group.label,
1419
+ to: id
1420
+ })),
1421
+ hint: "back"
1422
+ }),
1423
+ content: ({ state }) => settingsScreen(state, "content"),
1424
+ behavior: ({ state }) => settingsScreen(state, "behavior"),
1425
+ storage: ({ state }) => settingsScreen(state, "storage"),
1426
+ advanced: ({ state }) => settingsScreen(state, "advanced"),
1427
+ all: ({ state }) => settingsScreen(state, "all")
1342
1428
  },
1343
1429
  actions: {
1344
1430
  "storage-location": async ({ signal: actionSignal }) => {
@@ -1362,8 +1448,8 @@ async function showSyncSettings(ctx, runRoute, signal) {
1362
1448
  try {
1363
1449
  const previous = await loadConfig(setupName);
1364
1450
  validate();
1365
- const mergeContent = value === "Content only" || value === "Content & partial";
1366
- const partialSync = value === "Partial only" || value === "Content & partial";
1451
+ const mergeContent = value === "Merge only" || value === "Merge + partial";
1452
+ const partialSync = value === "Partial only" || value === "Merge + partial";
1367
1453
  if (!await ctx.ui.confirm(
1368
1454
  "Save experimental content policy?",
1369
1455
  "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.",
@@ -1371,6 +1457,7 @@ async function showSyncSettings(ctx, runRoute, signal) {
1371
1457
  ))
1372
1458
  return { kind: "rejected" };
1373
1459
  validate();
1460
+ ctx.ui.notify("Saving reviewed Pi Sync policy\u2026", "info");
1374
1461
  await updateLocalConfig((current) => {
1375
1462
  validate();
1376
1463
  const setup = current.syncSetups[setupName];
@@ -1386,6 +1473,8 @@ async function showSyncSettings(ctx, runRoute, signal) {
1386
1473
  };
1387
1474
  }, mutationSignal);
1388
1475
  validate();
1476
+ saveStatus = "Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
1477
+ ctx.ui.notify(saveStatus, "info");
1389
1478
  return { kind: "stay" };
1390
1479
  } catch (error) {
1391
1480
  if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
@@ -1436,6 +1525,7 @@ async function showSyncSettings(ctx, runRoute, signal) {
1436
1525
  );
1437
1526
  validate();
1438
1527
  if (!confirmed) return { kind: "rejected" };
1528
+ ctx.ui.notify("Saving reviewed Pi Sync policy\u2026", "info");
1439
1529
  await updateLocalConfig((current) => {
1440
1530
  validate();
1441
1531
  const setup = current.syncSetups[setupName];
@@ -1451,6 +1541,8 @@ async function showSyncSettings(ctx, runRoute, signal) {
1451
1541
  };
1452
1542
  }, mutationSignal);
1453
1543
  validate();
1544
+ saveStatus = "Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
1545
+ ctx.ui.notify(saveStatus, "info");
1454
1546
  return { kind: "stay" };
1455
1547
  } catch (error) {
1456
1548
  if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
@@ -1529,6 +1621,18 @@ async function showSyncSettings(ctx, runRoute, signal) {
1529
1621
  return { kind: "rejected" };
1530
1622
  }
1531
1623
  },
1624
+ debug: async ({ value, signal: actionSignal }) => {
1625
+ const debug = value === "On";
1626
+ const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1627
+ try {
1628
+ await updateLocalConfig((settings) => ({ ...settings, debug }), mutationSignal);
1629
+ if (mutationSignal.aborted) return { kind: "rejected" };
1630
+ return { kind: "stay" };
1631
+ } catch (error) {
1632
+ if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
1633
+ return { kind: "rejected" };
1634
+ }
1635
+ },
1532
1636
  "on-switch": async ({ value, signal: actionSignal }) => {
1533
1637
  const action = value ? setupSwitchActionFromLabel(value) : void 0;
1534
1638
  if (!action) return { kind: "rejected" };
@@ -1553,7 +1657,7 @@ async function showSyncSettings(ctx, runRoute, signal) {
1553
1657
  },
1554
1658
  "remote-include": async ({ signal: actionSignal }) => {
1555
1659
  const reviewSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
1556
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
1660
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-H6AEVA3Q.ts");
1557
1661
  if (reviewSignal.aborted) return { kind: "rejected" };
1558
1662
  const review = await showRemoteSelectionReview(ctx, setupName, reviewSignal, void 0, {
1559
1663
  origin: "settings",
@@ -1568,6 +1672,28 @@ async function showSyncSettings(ctx, runRoute, signal) {
1568
1672
  }
1569
1673
  }
1570
1674
  });
1675
+ for (const id of [
1676
+ "automatic",
1677
+ "automatic-transfer",
1678
+ "merge-settings",
1679
+ "skip-secret-scan",
1680
+ "show-status",
1681
+ "debug",
1682
+ "on-switch"
1683
+ ]) {
1684
+ const save = menu.actions[id];
1685
+ menu.actions[id] = async (action) => {
1686
+ const validate = captureMutationOwner(ctx, action.signal);
1687
+ validate();
1688
+ ctx.ui.notify("Saving Pi Sync setting\u2026", "info");
1689
+ const result = await save(action);
1690
+ if (action.signal.aborted || signal?.aborted) return { kind: "rejected" };
1691
+ validate();
1692
+ saveStatus = result?.kind === "rejected" ? "Save failed or rejected. Previous value retained; retry is available." : id === "automatic-transfer" || id === "automatic" || id === "debug" ? "Saved. Enabling startup work applies next session start; completed transfers are not undone." : "Saved. Applies to subsequent operations; status visibility applies immediately.";
1693
+ ctx.ui.notify(saveStatus, result?.kind === "rejected" ? "warning" : "info");
1694
+ return result;
1695
+ };
1696
+ }
1571
1697
  async function resumeSettings(editorSignal) {
1572
1698
  if (editorSignal.aborted) return { kind: "close" };
1573
1699
  try {
@@ -1578,7 +1704,7 @@ async function showSyncSettings(ctx, runRoute, signal) {
1578
1704
  return { kind: "close" };
1579
1705
  }
1580
1706
  }
1581
- await runMenu4(ctx, menu, {
1707
+ await runMenu4({ ...ctx, ui: boundedSettingsUI(ctx) }, menu, {
1582
1708
  getState: () => loadConfig(setupName),
1583
1709
  signal,
1584
1710
  isCurrent: () => !signal?.aborted
@@ -1626,7 +1752,7 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1626
1752
  if (!attention) {
1627
1753
  const observation = options.getObservation?.();
1628
1754
  if (!observation) return { kind: "stay" };
1629
- const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
1755
+ const { showRemoteSelectionReview } = await import("./remote-selection-ui-H6AEVA3Q.ts");
1630
1756
  if (sessionSignal?.aborted) return { kind: "close" };
1631
1757
  const config = await loadConfig();
1632
1758
  if (sessionSignal?.aborted) return { kind: "close" };
@@ -1717,12 +1843,45 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
1717
1843
  });
1718
1844
  return { kind: result.kind === "closed" ? "close" : "stay" };
1719
1845
  },
1720
- doctor: async () => {
1721
- const result = await runCancellableOperation(ctx, "Checking setup access\u2026", "doctor", runRoute, {
1846
+ diff: async () => {
1847
+ const result = await runCancellableOperation(ctx, "Comparing local and remote\u2026", "diff", runRoute, {
1722
1848
  signal: sessionSignal
1723
1849
  });
1724
1850
  return { kind: result.kind === "closed" ? "close" : "stay" };
1725
1851
  },
1852
+ status: async () => {
1853
+ const result = await runCancellableOperation(ctx, "Checking remote status\u2026", "status", runRoute, {
1854
+ signal: sessionSignal
1855
+ });
1856
+ return { kind: result.kind === "closed" ? "close" : "stay" };
1857
+ },
1858
+ doctor: async ({ signal: actionSignal }) => {
1859
+ const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
1860
+ const validate = captureMutationOwner(ctx, signal);
1861
+ const config = await loadConfig();
1862
+ validate();
1863
+ const confirmed = await ctx.ui.confirm(
1864
+ "Run diagnostics?",
1865
+ `Setup: ${safeTerminalText(config.setupName)}
1866
+ Storage: ${backendStorageDescription(config)}
1867
+ S3/R2 reads the latest pointer without writing. Git reads remote snapshots and checks its cache. WebDAV probes conditional writes and cleanup, and can repair a missing history entry. Possible-secret checks still run. This is not the read-only Check remote status action.`,
1868
+ { signal }
1869
+ );
1870
+ if (signal.aborted) return { kind: "close" };
1871
+ validate();
1872
+ if (!confirmed) return { kind: "stay" };
1873
+ const latest = await loadConfig();
1874
+ validate();
1875
+ if (syncCheckConfigFingerprint(config) !== syncCheckConfigFingerprint(latest)) {
1876
+ ctx.ui.notify("Setup changed during diagnostics review; reopen Diagnostics.", "warning");
1877
+ return { kind: "stay" };
1878
+ }
1879
+ const result = await runCancellableOperation(ctx, "Checking setup access\u2026", "doctor", runRoute, {
1880
+ signal,
1881
+ target: config.setupName
1882
+ });
1883
+ return { kind: result.kind === "closed" ? "close" : "stay" };
1884
+ },
1726
1885
  recover: async ({ state, signal: actionSignal }) => {
1727
1886
  const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
1728
1887
  return { kind: result === "close" ? "close" : "stay" };
@@ -1769,6 +1928,8 @@ function syncMainMenuItem(label) {
1769
1928
  "Sync now": "sync",
1770
1929
  "Pull from remote\u2026": "pull",
1771
1930
  "Push to remote\u2026": "push",
1931
+ "View differences": "diff",
1932
+ "Check remote status": "status",
1772
1933
  "Switch sync setup": "switch",
1773
1934
  Settings: "settings",
1774
1935
  History: "history",
@@ -1788,10 +1949,10 @@ function syncMainMenuItem(label) {
1788
1949
  id: action,
1789
1950
  label,
1790
1951
  action,
1791
- ...action === "doctor" ? { description: "Check setup access; WebDAV also probes writes and repairs history." } : {}
1952
+ description: action === "doctor" ? "Manage: review backend checks before running." : action === "settings" || action === "history" || action === "switch" ? "Manage" : action === "sync" ? "Transfer: choose a safe direction or review conflicts." : action === "pull" ? "Transfer: remote \u2192 local; review writes and removals." : action === "push" ? "Transfer: local \u2192 remote active snapshot; review publication." : action === "status" || action === "diff" ? "Inspect: read current local and remote state without transferring files." : void 0
1792
1953
  };
1793
1954
  }
1794
1955
  export {
1795
1956
  showSyncManager
1796
1957
  };
1797
- //# sourceMappingURL=manager-ui-IURS4WXY.ts.map
1958
+ //# sourceMappingURL=manager-ui-ZIN4BUSF.ts.map