@narumitw/pi-sync 0.51.1 → 0.52.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.
- package/README.md +30 -5
- package/dist/chunks/{chunk-7KHKWL3Q.ts → chunk-2CBF6SMT.ts} +8 -7
- package/dist/chunks/chunk-2CBF6SMT.ts.map +7 -0
- package/dist/chunks/{chunk-YFADF5ZQ.ts → chunk-2DI6PDZV.ts} +5 -1
- package/dist/chunks/chunk-2DI6PDZV.ts.map +7 -0
- package/dist/chunks/chunk-2GUXI2XT.ts +1702 -0
- package/dist/chunks/chunk-2GUXI2XT.ts.map +7 -0
- package/dist/chunks/{chunk-4MPIH46N.ts → chunk-3B4Z2SKV.ts} +40 -6
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +7 -0
- package/dist/chunks/{chunk-YPXGU5OR.ts → chunk-4QVPTIZB.ts} +16 -6
- package/dist/chunks/{chunk-YPXGU5OR.ts.map → chunk-4QVPTIZB.ts.map} +2 -2
- package/dist/chunks/{chunk-UA7RXOIT.ts → chunk-4SBTAVFL.ts} +6 -6
- package/dist/chunks/{chunk-6546TS54.ts → chunk-5RZPINUQ.ts} +7 -4
- package/dist/chunks/{chunk-6546TS54.ts.map → chunk-5RZPINUQ.ts.map} +2 -2
- package/dist/chunks/chunk-76NB4R7U.ts +107 -0
- package/dist/chunks/chunk-76NB4R7U.ts.map +7 -0
- package/dist/chunks/{chunk-DM4X4FIA.ts → chunk-7D6RDCVC.ts} +8 -10
- package/dist/chunks/{chunk-DM4X4FIA.ts.map → chunk-7D6RDCVC.ts.map} +2 -2
- package/dist/chunks/chunk-A6HU4ERM.ts +124 -0
- package/dist/chunks/chunk-A6HU4ERM.ts.map +7 -0
- package/dist/chunks/chunk-AMJRUXFT.ts +17 -0
- package/dist/chunks/chunk-AMJRUXFT.ts.map +7 -0
- package/dist/chunks/chunk-ANWNQIR7.ts +1013 -0
- package/dist/chunks/chunk-ANWNQIR7.ts.map +7 -0
- package/dist/chunks/{chunk-T56RNRZJ.ts → chunk-BDB4IDM3.ts} +19 -10
- package/dist/chunks/chunk-BDB4IDM3.ts.map +7 -0
- package/dist/chunks/{chunk-2RZSQWBH.ts → chunk-CHSIAC4N.ts} +5 -5
- package/dist/chunks/{chunk-2RZSQWBH.ts.map → chunk-CHSIAC4N.ts.map} +2 -2
- package/dist/chunks/chunk-HUZ5CR4W.ts +110 -0
- package/dist/chunks/chunk-HUZ5CR4W.ts.map +7 -0
- package/dist/chunks/{chunk-QLSN7AGX.ts → chunk-IJ6VV6FN.ts} +81 -19
- package/dist/chunks/chunk-IJ6VV6FN.ts.map +7 -0
- package/dist/chunks/chunk-ORQRHEEO.ts +362 -0
- package/dist/chunks/chunk-ORQRHEEO.ts.map +7 -0
- package/dist/chunks/chunk-QE7NZZWT.ts +380 -0
- package/dist/chunks/chunk-QE7NZZWT.ts.map +7 -0
- package/dist/chunks/{chunk-HGJH56KR.ts → chunk-QHSQCK7L.ts} +4 -4
- package/dist/chunks/chunk-QOFMXPM5.ts +735 -0
- package/dist/chunks/chunk-QOFMXPM5.ts.map +7 -0
- package/dist/chunks/{chunk-DZUSTGQV.ts → chunk-UQ3ALBIB.ts} +4 -4
- package/dist/chunks/{chunk-Q32CQWPM.ts → chunk-V6VZ5TLG.ts} +13 -6
- package/dist/chunks/chunk-V6VZ5TLG.ts.map +7 -0
- package/dist/chunks/{chunk-E42GXD6D.ts → chunk-WKPX2SIC.ts} +4 -4
- package/dist/chunks/chunk-ZW5ZLGBI.ts +154 -0
- package/dist/chunks/chunk-ZW5ZLGBI.ts.map +7 -0
- package/dist/chunks/{chunk-6NT3YFGY.ts → chunk-ZX2Q6THB.ts} +8 -3
- package/dist/chunks/{chunk-6NT3YFGY.ts.map → chunk-ZX2Q6THB.ts.map} +2 -2
- package/dist/chunks/conflict-artifacts-BB2FZMIE.ts +27 -0
- package/dist/chunks/conflict-review-T3PB7ZSD.ts +175 -0
- package/dist/chunks/conflict-review-T3PB7ZSD.ts.map +7 -0
- package/dist/chunks/{file-selection-RAVHRYFG.ts → file-selection-WZX7HAY2.ts} +8 -7
- package/dist/chunks/{file-selection-RAVHRYFG.ts.map → file-selection-WZX7HAY2.ts.map} +1 -1
- package/dist/chunks/{git-backend-IAANDL3K.ts → git-backend-HLGKUQJT.ts} +19 -7
- package/dist/chunks/git-backend-HLGKUQJT.ts.map +7 -0
- package/dist/chunks/local-fields-6DGYOOIK.ts +22 -0
- package/dist/chunks/manager-result-dispatcher-VIDT3QBU.ts +23 -0
- package/dist/chunks/{manager-ui-763NEKJY.ts → manager-ui-HSMHS3OK.ts} +236 -57
- package/dist/chunks/manager-ui-HSMHS3OK.ts.map +7 -0
- package/dist/chunks/merge-baseline-store-EVQAAML6.ts +22 -0
- package/dist/chunks/remote-selection-ui-SMF5CLFO.ts +22 -0
- package/dist/chunks/{s3-backend-XGXUOPUN.ts → s3-backend-DJ2NT3SZ.ts} +16 -9
- package/dist/chunks/s3-backend-DJ2NT3SZ.ts.map +7 -0
- package/dist/chunks/{setup-switch-5G2VNJHL.ts → setup-switch-KTUWL6DE.ts} +9 -6
- package/dist/chunks/{setup-wizard-YIEWGX32.ts → setup-wizard-GSDPDFC4.ts} +10 -7
- package/dist/chunks/{setup-wizard-YIEWGX32.ts.map → setup-wizard-GSDPDFC4.ts.map} +1 -1
- package/dist/chunks/{snapshot-JL5DFSXI.ts → snapshot-THS5EY2L.ts} +6 -9
- package/dist/chunks/snapshot-THS5EY2L.ts.map +7 -0
- package/dist/chunks/{sync-inspection-P3ENJLNF.ts → sync-inspection-VBJF2FWC.ts} +10 -10
- package/dist/chunks/sync-inspection-VBJF2FWC.ts.map +7 -0
- package/dist/chunks/{sync-operations-AR76EB6C.ts → sync-operations-27VQUDH2.ts} +309 -508
- package/dist/chunks/sync-operations-27VQUDH2.ts.map +7 -0
- package/dist/chunks/{sync-state-VHJUZ4YG.ts → sync-state-CFL4RRRB.ts} +6 -6
- package/dist/chunks/sync-state-CFL4RRRB.ts.map +7 -0
- package/dist/chunks/{webdav-backend-3EGUONAT.ts → webdav-backend-ETQGSI6I.ts} +13 -7
- package/dist/chunks/webdav-backend-ETQGSI6I.ts.map +7 -0
- package/dist/index.ts +142 -43
- package/dist/index.ts.map +2 -2
- package/docs/content-merge-audit.md +156 -0
- package/docs/merge-implementation-audit.md +312 -0
- package/docs/settings-merge-audit.md +153 -0
- package/docs/settings.md +36 -0
- package/package.json +5 -5
- package/src/backends/git/git-backend.ts +2 -0
- package/src/backends/git/git-storage.ts +8 -3
- package/src/backends/s3/s3-backend.ts +2 -1
- package/src/backends/webdav/webdav-backend.ts +2 -1
- package/src/commands/command-execution.ts +10 -0
- package/src/commands/command.ts +4 -0
- package/src/settings/config.ts +12 -0
- package/src/settings/settings-management.ts +2 -2
- package/src/settings/settings-types.ts +14 -1
- package/src/settings/settings-validation.ts +24 -3
- package/src/snapshot/session-paths.ts +34 -7
- package/src/snapshot/snapshot-apply.ts +81 -32
- package/src/snapshot/snapshot-case-replacement.ts +80 -0
- package/src/snapshot/snapshot-codec.ts +6 -1
- package/src/snapshot/snapshot-root-transition.ts +154 -0
- package/src/snapshot/snapshot-transaction-plan.ts +72 -0
- package/src/snapshot/snapshot-transaction.ts +844 -102
- package/src/snapshot/snapshot-types.ts +5 -0
- package/src/state/json-file.ts +32 -5
- package/src/state/merge-baseline-store.ts +182 -0
- package/src/state/mutation-directory-sync.ts +34 -0
- package/src/state/state-types.ts +4 -0
- package/src/state/sync-state-store.ts +14 -1
- package/src/sync/automatic-sync.ts +11 -3
- package/src/sync/conflict-artifact-ownership.ts +63 -0
- package/src/sync/conflict-artifacts.ts +195 -0
- package/src/sync/conflict-groups.ts +68 -0
- package/src/sync/conflict-preview.ts +56 -0
- package/src/sync/conflict-resolution.ts +93 -0
- package/src/sync/conflict-retention.ts +72 -0
- package/src/sync/conflict-review.ts +95 -0
- package/src/sync/content-conflicts.ts +73 -0
- package/src/sync/field-migration.ts +39 -0
- package/src/sync/file-merge-planner.ts +143 -0
- package/src/sync/json-document.ts +184 -0
- package/src/sync/local-fields.ts +134 -0
- package/src/sync/merge-apply.ts +380 -0
- package/src/sync/merge-journal.ts +219 -0
- package/src/sync/merged-sync.ts +723 -0
- package/src/sync/partial-progress.ts +54 -0
- package/src/sync/remote-snapshot.ts +6 -0
- package/src/sync/session-merge.ts +194 -0
- package/src/sync/settings-conflicts.ts +52 -0
- package/src/sync/settings-merge.ts +57 -0
- package/src/sync/startup-check.ts +90 -5
- package/src/sync/sync-inspection.ts +1 -0
- package/src/sync/sync-local.ts +61 -0
- package/src/sync/sync-mutations.ts +256 -114
- package/src/sync/sync-queries.ts +6 -2
- package/src/sync/sync-state.ts +14 -5
- package/src/sync/text-merge.ts +133 -0
- package/src/sync-extension.ts +8 -0
- package/src/ui/manager-state.ts +12 -1
- package/src/ui/manager-ui.ts +10 -0
- package/src/ui/merge-review.ts +28 -0
- package/src/ui/remote-selection-ui.ts +1 -0
- package/src/ui/settings-ui.ts +183 -2
- package/src/ui/setup/setup-actions.ts +2 -2
- package/src/ui/setup/setup-switcher.ts +2 -1
- package/src/ui/storage-connections-ui.ts +11 -7
- package/src/ui/sync-attention.ts +3 -0
- package/src/ui/sync-format.ts +16 -12
- package/src/ui/sync-setups-ui.ts +1 -0
- package/src/ui/terminal-text.ts +9 -2
- package/dist/chunks/chunk-4AIHXRKE.ts +0 -197
- package/dist/chunks/chunk-4AIHXRKE.ts.map +0 -7
- package/dist/chunks/chunk-4MPIH46N.ts.map +0 -7
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +0 -7
- package/dist/chunks/chunk-EEJPYTWE.ts +0 -348
- package/dist/chunks/chunk-EEJPYTWE.ts.map +0 -7
- package/dist/chunks/chunk-IVXEDPAD.ts +0 -370
- package/dist/chunks/chunk-IVXEDPAD.ts.map +0 -7
- package/dist/chunks/chunk-MSSW6F3R.ts +0 -12
- package/dist/chunks/chunk-MSSW6F3R.ts.map +0 -7
- package/dist/chunks/chunk-OVDTF5CH.ts +0 -90
- package/dist/chunks/chunk-OVDTF5CH.ts.map +0 -7
- package/dist/chunks/chunk-Q32CQWPM.ts.map +0 -7
- package/dist/chunks/chunk-QLSN7AGX.ts.map +0 -7
- package/dist/chunks/chunk-SWUXA3MH.ts +0 -18
- package/dist/chunks/chunk-SWUXA3MH.ts.map +0 -7
- package/dist/chunks/chunk-T56RNRZJ.ts.map +0 -7
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +0 -7
- package/dist/chunks/git-backend-IAANDL3K.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +0 -23
- package/dist/chunks/manager-ui-763NEKJY.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +0 -22
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +0 -7
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +0 -7
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +0 -7
- /package/dist/chunks/{chunk-UA7RXOIT.ts.map → chunk-4SBTAVFL.ts.map} +0 -0
- /package/dist/chunks/{chunk-HGJH56KR.ts.map → chunk-QHSQCK7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-DZUSTGQV.ts.map → chunk-UQ3ALBIB.ts.map} +0 -0
- /package/dist/chunks/{chunk-E42GXD6D.ts.map → chunk-WKPX2SIC.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-OTUMQM26.ts.map → conflict-artifacts-BB2FZMIE.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-TZTJF32Y.ts.map → local-fields-6DGYOOIK.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-5G2VNJHL.ts.map → manager-result-dispatcher-VIDT3QBU.ts.map} +0 -0
- /package/dist/chunks/{snapshot-JL5DFSXI.ts.map → merge-baseline-store-EVQAAML6.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-P3ENJLNF.ts.map → remote-selection-ui-SMF5CLFO.ts.map} +0 -0
- /package/dist/chunks/{sync-state-VHJUZ4YG.ts.map → setup-switch-KTUWL6DE.ts.map} +0 -0
package/src/sync/sync-state.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../snapshot/snapshot.js";
|
|
9
9
|
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
10
10
|
import type { SyncState } from "../state/state-types.js";
|
|
11
|
+
import { portableSnapshot, sameLocalFields } from "./local-fields.js";
|
|
11
12
|
import {
|
|
12
13
|
customIncludePathsByLower,
|
|
13
14
|
includeFromSelectionConfig,
|
|
@@ -15,10 +16,13 @@ import {
|
|
|
15
16
|
type SyncSelectionConfig,
|
|
16
17
|
} from "./sync-policy.js";
|
|
17
18
|
|
|
18
|
-
type SyncPolicyConfig = SyncSelectionConfig;
|
|
19
|
+
type SyncPolicyConfig = SyncSelectionConfig & { localFields?: string[] };
|
|
19
20
|
|
|
20
21
|
export function hasLocalChanges(local: Snapshot, state: SyncState, config: SyncPolicyConfig) {
|
|
21
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
(Boolean(state.lastAppliedSnapshot) && !sameLocalFields(state.localFields, config.localFields)) ||
|
|
24
|
+
!sameHashes(fileHashMap(portableSnapshot(local, config.localFields)), stateHashMapForConfig(state, config))
|
|
25
|
+
);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function remoteChangedSinceState(
|
|
@@ -30,7 +34,7 @@ export function remoteChangedSinceState(
|
|
|
30
34
|
if (!head) return Boolean(state.lastAppliedSnapshot);
|
|
31
35
|
if (head.snapshotId !== state.lastAppliedSnapshot) return true;
|
|
32
36
|
if (state.lastRemoteRevision && !sameRevision(head.revision, state.lastRemoteRevision)) return true;
|
|
33
|
-
if (
|
|
37
|
+
if (syncPolicyChanged(state, config)) return true;
|
|
34
38
|
return (
|
|
35
39
|
includeFromSelectionConfig(config).includes("sessions") && !state.include?.includes("sessions") && head.syncSessions
|
|
36
40
|
);
|
|
@@ -43,7 +47,12 @@ export function hasRemoteChanges(
|
|
|
43
47
|
ignoredPaths = new Set<string>(),
|
|
44
48
|
) {
|
|
45
49
|
if (remote.id === state.lastAppliedSnapshot && !syncPolicyChanged(state, config)) return false;
|
|
46
|
-
return !snapshotHashesMatchState(
|
|
50
|
+
return !snapshotHashesMatchState(
|
|
51
|
+
portableSnapshot(filterSnapshotForConfigPolicy(remote, config), remote.localFields),
|
|
52
|
+
state,
|
|
53
|
+
config,
|
|
54
|
+
ignoredPaths,
|
|
55
|
+
);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export function sameHashes(left: Record<string, string>, right: Record<string, string>) {
|
|
@@ -87,7 +96,7 @@ function withoutHashPaths(hashes: Record<string, string>, ignoredPaths: Set<stri
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
export function syncPolicyChanged(state: SyncState, config: SyncPolicyConfig) {
|
|
90
|
-
return syncIncludeChanged(state, config);
|
|
99
|
+
return syncIncludeChanged(state, config) || !sameLocalFields(state.localFields, config.localFields);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
export function shouldRefreshSyncedState(
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export const TEXT_LIMIT = 1024 * 1024;
|
|
3
|
+
export const TEXT_MERGE_WORK_LIMIT = 8_000_000;
|
|
4
|
+
export interface TextMergeBudget {
|
|
5
|
+
remaining: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TextMergeOptions {
|
|
8
|
+
budget?: TextMergeBudget;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
function charge(budget: TextMergeBudget, work: number) {
|
|
12
|
+
if (work > budget.remaining) throw new Error("Text merge operation complexity bound exceeded.");
|
|
13
|
+
budget.remaining -= work;
|
|
14
|
+
}
|
|
15
|
+
export function isMergeTextPath(filePath: string) {
|
|
16
|
+
return (
|
|
17
|
+
filePath === "AGENTS.md" ||
|
|
18
|
+
(/\.(?:md|txt)$/u.test(filePath) &&
|
|
19
|
+
["prompts", "skills"].includes(filePath.split("/")[0] ?? "") &&
|
|
20
|
+
path.posix.basename(filePath) !== "keybindings.json")
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
export function text(bytes: Buffer) {
|
|
24
|
+
if (bytes.length > TEXT_LIMIT) throw new Error("Text merge bound exceeded.");
|
|
25
|
+
const decoded = bytes.toString("utf8");
|
|
26
|
+
if (
|
|
27
|
+
!Buffer.from(decoded).equals(bytes) ||
|
|
28
|
+
bytes.some((byte) => (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) || byte === 127)
|
|
29
|
+
)
|
|
30
|
+
throw new Error("Unsupported text encoding.");
|
|
31
|
+
return decoded;
|
|
32
|
+
}
|
|
33
|
+
type Hunk = { start: number; end: number; lines: string[] };
|
|
34
|
+
function lines(value: string) {
|
|
35
|
+
return value.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
|
|
36
|
+
}
|
|
37
|
+
function edits(base: string[], next: string[], signal?: AbortSignal): Hunk[] {
|
|
38
|
+
const width = next.length + 1;
|
|
39
|
+
if ((base.length + 1) * width > 4_000_000) throw new Error("Text merge complexity bound exceeded.");
|
|
40
|
+
// Intern once so each charged DP cell compares fixed-size IDs, not long line strings.
|
|
41
|
+
const ids = new Map<string, number>();
|
|
42
|
+
const intern = (line: string) => {
|
|
43
|
+
let id = ids.get(line);
|
|
44
|
+
if (id === undefined) {
|
|
45
|
+
id = ids.size;
|
|
46
|
+
ids.set(line, id);
|
|
47
|
+
}
|
|
48
|
+
return id;
|
|
49
|
+
};
|
|
50
|
+
const baseIds = base.map(intern);
|
|
51
|
+
const nextIds = next.map(intern);
|
|
52
|
+
const grid = new Uint32Array((base.length + 1) * width);
|
|
53
|
+
for (let i = base.length - 1; i >= 0; i--) {
|
|
54
|
+
signal?.throwIfAborted();
|
|
55
|
+
for (let j = next.length - 1; j >= 0; j--)
|
|
56
|
+
grid[i * width + j] =
|
|
57
|
+
baseIds[i] === nextIds[j]
|
|
58
|
+
? 1 + (grid[(i + 1) * width + j + 1] ?? 0)
|
|
59
|
+
: Math.max(grid[(i + 1) * width + j] ?? 0, grid[i * width + j + 1] ?? 0);
|
|
60
|
+
}
|
|
61
|
+
const result: Hunk[] = [];
|
|
62
|
+
let i = 0;
|
|
63
|
+
let j = 0;
|
|
64
|
+
let active: Hunk | undefined;
|
|
65
|
+
while (i < base.length || j < next.length) {
|
|
66
|
+
if (i < base.length && j < next.length && baseIds[i] === nextIds[j]) {
|
|
67
|
+
if (active) {
|
|
68
|
+
result.push(active);
|
|
69
|
+
active = undefined;
|
|
70
|
+
}
|
|
71
|
+
i++;
|
|
72
|
+
j++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
active ??= { start: i, end: i, lines: [] };
|
|
76
|
+
if (j < next.length && (i === base.length || (grid[i * width + j + 1] ?? 0) >= (grid[(i + 1) * width + j] ?? 0)))
|
|
77
|
+
active.lines.push(next[j++] ?? "");
|
|
78
|
+
else active.end = ++i;
|
|
79
|
+
}
|
|
80
|
+
if (active) result.push(active);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
export function mergeText(
|
|
84
|
+
baseBytes: Buffer,
|
|
85
|
+
localBytes: Buffer,
|
|
86
|
+
remoteBytes: Buffer,
|
|
87
|
+
options: TextMergeOptions = {},
|
|
88
|
+
): Buffer | undefined {
|
|
89
|
+
const budget = options.budget ?? { remaining: TEXT_MERGE_WORK_LIMIT };
|
|
90
|
+
try {
|
|
91
|
+
options.signal?.throwIfAborted();
|
|
92
|
+
const base = lines(text(baseBytes));
|
|
93
|
+
const ours = lines(text(localBytes));
|
|
94
|
+
const theirs = lines(text(remoteBytes));
|
|
95
|
+
const cells = [ours, theirs].map((next) => (base.length + 1) * (next.length + 1));
|
|
96
|
+
if (cells.some((count) => count > 4_000_000)) return;
|
|
97
|
+
charge(budget, (cells[0] ?? 0) + (cells[1] ?? 0));
|
|
98
|
+
const local = edits(base, ours, options.signal);
|
|
99
|
+
const remote = edits(base, theirs, options.signal);
|
|
100
|
+
const combined = [...local];
|
|
101
|
+
for (const right of remote) {
|
|
102
|
+
let same = false;
|
|
103
|
+
for (const left of local) {
|
|
104
|
+
options.signal?.throwIfAborted();
|
|
105
|
+
charge(budget, 1);
|
|
106
|
+
if (left.start === right.start && left.end === right.end && left.lines.join("") === right.lines.join("")) {
|
|
107
|
+
same = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const overlap =
|
|
111
|
+
left.start === left.end || right.start === right.end
|
|
112
|
+
? left.start <= right.end && right.start <= left.end
|
|
113
|
+
: left.start < right.end && right.start < left.end;
|
|
114
|
+
if (overlap) return;
|
|
115
|
+
}
|
|
116
|
+
if (!same) combined.push(right);
|
|
117
|
+
}
|
|
118
|
+
combined.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
119
|
+
let cursor = 0;
|
|
120
|
+
const output: string[] = [];
|
|
121
|
+
for (const edit of combined) {
|
|
122
|
+
output.push(...base.slice(cursor, edit.start), ...edit.lines);
|
|
123
|
+
cursor = edit.end;
|
|
124
|
+
}
|
|
125
|
+
output.push(...base.slice(cursor));
|
|
126
|
+
const result = Buffer.from(output.join(""));
|
|
127
|
+
text(result);
|
|
128
|
+
return result;
|
|
129
|
+
} catch {
|
|
130
|
+
options.signal?.throwIfAborted();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/sync-extension.ts
CHANGED
|
@@ -75,6 +75,14 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
75
75
|
if (ready && !signal.aborted) check.start(ctx, signal, ready);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
79
|
+
await check.interrupt(ctx);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
83
|
+
if (!sessionAbort.signal.aborted) check.settled(ctx);
|
|
84
|
+
});
|
|
85
|
+
|
|
78
86
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
79
87
|
sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
|
|
80
88
|
attention.reset(ctx);
|
package/src/ui/manager-state.ts
CHANGED
|
@@ -72,7 +72,11 @@ export async function describeManagerState(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
const configuredTargets = ownRecord(raw.syncSetups);
|
|
75
|
-
if (
|
|
75
|
+
if (
|
|
76
|
+
(raw.version === 3 || raw.version === 4 || raw.version === 5) &&
|
|
77
|
+
configuredTargets &&
|
|
78
|
+
Object.keys(configuredTargets).length === 0
|
|
79
|
+
) {
|
|
76
80
|
return {
|
|
77
81
|
title: [
|
|
78
82
|
"Manage sync",
|
|
@@ -129,7 +133,14 @@ export async function describeManagerState(
|
|
|
129
133
|
`Storage: ${backendStorageDescription(config)}`,
|
|
130
134
|
`Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
|
|
131
135
|
`Automatic sync: ${automaticSyncSummary(config.automatic)}`,
|
|
136
|
+
`Automatic transfer at startup: ${config.automaticTransfer ? "On — upload/replace/delete at idle; no reload" : "Off"}`,
|
|
132
137
|
`Last applied: ${lastAppliedSnapshot}`,
|
|
138
|
+
...(syncState?.unresolved?.length
|
|
139
|
+
? [
|
|
140
|
+
`Unresolved: ${syncState.unresolved.length} dependency groups · review History & recovery`,
|
|
141
|
+
`Last observed: ${safeTerminalText(syncState.lastObservedSnapshot ?? "unknown")}`,
|
|
142
|
+
]
|
|
143
|
+
: []),
|
|
133
144
|
...(currentAttention
|
|
134
145
|
? [
|
|
135
146
|
currentAttention.decision.setupName === config.setupName
|
package/src/ui/manager-ui.ts
CHANGED
|
@@ -40,6 +40,7 @@ export async function showSyncManager(
|
|
|
40
40
|
| "setups"
|
|
41
41
|
| "connections"
|
|
42
42
|
| "history"
|
|
43
|
+
| "conflicts"
|
|
43
44
|
| "doctor"
|
|
44
45
|
| "unlock"
|
|
45
46
|
| "recover"
|
|
@@ -96,6 +97,7 @@ export async function showSyncManager(
|
|
|
96
97
|
title: "History & recovery",
|
|
97
98
|
items: [
|
|
98
99
|
{ id: "history", label: "Browse history", action: "history" },
|
|
100
|
+
{ id: "conflicts", label: "Review unresolved conflicts", action: "conflicts" },
|
|
99
101
|
{
|
|
100
102
|
id: "doctor",
|
|
101
103
|
label: "Check setup",
|
|
@@ -232,6 +234,14 @@ export async function showSyncManager(
|
|
|
232
234
|
await runRoute("init");
|
|
233
235
|
return { kind: "stay" };
|
|
234
236
|
},
|
|
237
|
+
conflicts: async () => {
|
|
238
|
+
const result = await runCancellableOperation(ctx, "Reviewing private conflicts…", "conflicts", runRoute, {
|
|
239
|
+
commitAware: true,
|
|
240
|
+
signal: sessionSignal,
|
|
241
|
+
});
|
|
242
|
+
const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
|
|
243
|
+
return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
|
|
244
|
+
},
|
|
235
245
|
back: async () => ({ kind: "back" }),
|
|
236
246
|
},
|
|
237
247
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runDocumentReview } from "@narumitw/pi-tui-kit";
|
|
3
|
+
|
|
4
|
+
/** Cell-exact scrolling in TUI and paginated observable confirmation in RPC. */
|
|
5
|
+
export async function confirmMergeReview(
|
|
6
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
7
|
+
title: string,
|
|
8
|
+
content: string,
|
|
9
|
+
signal: AbortSignal | undefined,
|
|
10
|
+
isCurrent: () => boolean,
|
|
11
|
+
confirmationLabel = "Apply merged transfer",
|
|
12
|
+
) {
|
|
13
|
+
const result = await runDocumentReview(ctx, {
|
|
14
|
+
title,
|
|
15
|
+
content,
|
|
16
|
+
format: { kind: "text" },
|
|
17
|
+
viewportSize: "adaptive",
|
|
18
|
+
confirmation: { label: confirmationLabel },
|
|
19
|
+
hint: "close",
|
|
20
|
+
signal,
|
|
21
|
+
isCurrent,
|
|
22
|
+
onError: () => {},
|
|
23
|
+
});
|
|
24
|
+
if (result.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
|
|
25
|
+
if (result.kind === "unsupported")
|
|
26
|
+
throw new Error("Merge review requires observable TUI or RPC; review the plan before using --yes.");
|
|
27
|
+
return result.kind === "confirmed";
|
|
28
|
+
}
|
|
@@ -239,6 +239,7 @@ async function showSelectionDifference(
|
|
|
239
239
|
setupName: state.decision.setupName,
|
|
240
240
|
include: [...state.decision.remoteInclude],
|
|
241
241
|
automatic: review.config.automatic,
|
|
242
|
+
automaticTransfer: review.config.automaticTransfer,
|
|
242
243
|
onSwitch: review.config.onSwitch,
|
|
243
244
|
showStatus: review.config.showStatus,
|
|
244
245
|
};
|
package/src/ui/settings-ui.ts
CHANGED
|
@@ -4,12 +4,14 @@ import { loadConfig } from "../settings/config.js";
|
|
|
4
4
|
import { localConfigPath } from "../settings/config-file.js";
|
|
5
5
|
import { updateSyncSetup } from "../settings/settings-management.js";
|
|
6
6
|
import { updateLocalConfig } from "../settings/settings-store.js";
|
|
7
|
+
import { normalizeLocalFields, sameLocalFields } from "../sync/local-fields.js";
|
|
7
8
|
import {
|
|
8
9
|
SETUP_SWITCH_ACTION_OPTIONS,
|
|
9
10
|
saveOnSwitch,
|
|
10
11
|
setupSwitchActionFromLabel,
|
|
11
12
|
setupSwitchActionLabel,
|
|
12
13
|
} from "../sync/setup-switch.js";
|
|
14
|
+
import { captureMutationOwner } from "../sync/sync-local.js";
|
|
13
15
|
import type { RunRoute } from "./cancellable-operation.js";
|
|
14
16
|
import { dispatchManagerResult } from "./manager-result-dispatcher.js";
|
|
15
17
|
import { AUTOMATIC_SYNC_DESCRIPTION } from "./setup/setup-prompts.js";
|
|
@@ -30,7 +32,17 @@ export async function showSyncSettings(
|
|
|
30
32
|
const initial = await loadConfig();
|
|
31
33
|
if (signal?.aborted) return;
|
|
32
34
|
const setupName = initial.setupName;
|
|
33
|
-
type Action =
|
|
35
|
+
type Action =
|
|
36
|
+
| "automatic"
|
|
37
|
+
| "automatic-transfer"
|
|
38
|
+
| "merge-settings"
|
|
39
|
+
| "local-fields"
|
|
40
|
+
| "content-policy"
|
|
41
|
+
| "skip-secret-scan"
|
|
42
|
+
| "show-status"
|
|
43
|
+
| "on-switch"
|
|
44
|
+
| "include"
|
|
45
|
+
| "remote-include";
|
|
34
46
|
const menu = defineMenu<Awaited<ReturnType<typeof loadConfig>>, "settings", Action, ExtensionCommandContext>({
|
|
35
47
|
start: "settings",
|
|
36
48
|
screens: {
|
|
@@ -87,10 +99,158 @@ export async function showSyncSettings(
|
|
|
87
99
|
currentValue: "Review",
|
|
88
100
|
action: "remote-include",
|
|
89
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: "automaticTransfer",
|
|
104
|
+
label: "Automatic transfer at startup",
|
|
105
|
+
description:
|
|
106
|
+
"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.",
|
|
107
|
+
currentValue: state.automaticTransfer ? "On" : "Off",
|
|
108
|
+
values: ["On", "Off"],
|
|
109
|
+
action: "automatic-transfer",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "mergeSettings",
|
|
113
|
+
label: "Settings field merge (experimental)",
|
|
114
|
+
description:
|
|
115
|
+
"Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
|
|
116
|
+
currentValue: state.mergeSettings ? "On" : "Off",
|
|
117
|
+
values: ["On", "Off"],
|
|
118
|
+
action: "merge-settings",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "localFields",
|
|
122
|
+
label: "Machine-local settings fields",
|
|
123
|
+
description:
|
|
124
|
+
"Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
|
|
125
|
+
currentValue: `${state.localFields?.length ?? 0} fields · Edit`,
|
|
126
|
+
action: "local-fields",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "contentPolicy",
|
|
130
|
+
label: "Content / partial sync (experimental)",
|
|
131
|
+
description:
|
|
132
|
+
"Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
|
|
133
|
+
currentValue: state.mergeContent
|
|
134
|
+
? state.partialSync
|
|
135
|
+
? "Content & partial"
|
|
136
|
+
: "Content only"
|
|
137
|
+
: state.partialSync
|
|
138
|
+
? "Partial only"
|
|
139
|
+
: "Off",
|
|
140
|
+
values: ["Off", "Content only", "Content & partial", "Partial only"],
|
|
141
|
+
action: "content-policy",
|
|
142
|
+
},
|
|
90
143
|
],
|
|
91
144
|
}),
|
|
92
145
|
},
|
|
93
146
|
actions: {
|
|
147
|
+
"content-policy": async ({ value, signal: actionSignal }) => {
|
|
148
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
149
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
150
|
+
try {
|
|
151
|
+
const previous = await loadConfig(setupName);
|
|
152
|
+
validate();
|
|
153
|
+
const mergeContent = value === "Content only" || value === "Content & partial";
|
|
154
|
+
const partialSync = value === "Partial only" || value === "Content & partial";
|
|
155
|
+
if (
|
|
156
|
+
!(await ctx.ui.confirm(
|
|
157
|
+
"Save experimental content policy?",
|
|
158
|
+
"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.",
|
|
159
|
+
{ signal: mutationSignal },
|
|
160
|
+
))
|
|
161
|
+
)
|
|
162
|
+
return { kind: "rejected" };
|
|
163
|
+
validate();
|
|
164
|
+
await updateLocalConfig((current) => {
|
|
165
|
+
validate();
|
|
166
|
+
const setup = current.syncSetups[setupName];
|
|
167
|
+
if (
|
|
168
|
+
!setup ||
|
|
169
|
+
Boolean(setup.sync.mergeContent) !== Boolean(previous.mergeContent) ||
|
|
170
|
+
Boolean(setup.sync.partialSync) !== Boolean(previous.partialSync)
|
|
171
|
+
)
|
|
172
|
+
throw new Error("Content policy changed during review.");
|
|
173
|
+
return {
|
|
174
|
+
...current,
|
|
175
|
+
version: 5,
|
|
176
|
+
syncSetups: {
|
|
177
|
+
...current.syncSetups,
|
|
178
|
+
[setupName]: { ...setup, sync: { ...setup.sync, mergeContent, partialSync } },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}, mutationSignal);
|
|
182
|
+
validate();
|
|
183
|
+
return { kind: "stay" };
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
186
|
+
return { kind: "rejected" };
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"merge-settings": async ({ value, signal: actionSignal }) => {
|
|
190
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
191
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
192
|
+
try {
|
|
193
|
+
validate();
|
|
194
|
+
await updateSyncSetup(
|
|
195
|
+
setupName,
|
|
196
|
+
(setup) => ({ ...setup, sync: { ...setup.sync, mergeSettings: value === "On" } }),
|
|
197
|
+
{ signal: mutationSignal },
|
|
198
|
+
);
|
|
199
|
+
validate();
|
|
200
|
+
return { kind: "stay" };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
203
|
+
return { kind: "rejected" };
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"local-fields": async ({ signal: actionSignal }) => {
|
|
207
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
208
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
209
|
+
try {
|
|
210
|
+
const previous = await loadConfig(setupName);
|
|
211
|
+
validate();
|
|
212
|
+
const input = await ctx.ui.input(
|
|
213
|
+
"Machine-local settings.json root fields (JSON string array)",
|
|
214
|
+
JSON.stringify(previous.localFields ?? []),
|
|
215
|
+
{ signal: mutationSignal },
|
|
216
|
+
);
|
|
217
|
+
validate();
|
|
218
|
+
if (input === undefined) return { kind: "rejected" };
|
|
219
|
+
let fields: string[];
|
|
220
|
+
try {
|
|
221
|
+
fields = normalizeLocalFields(JSON.parse(input));
|
|
222
|
+
} catch {
|
|
223
|
+
throw new Error("Invalid localFields JSON array; no policy was changed.");
|
|
224
|
+
}
|
|
225
|
+
if (sameLocalFields(previous.localFields, fields)) return { kind: "stay" };
|
|
226
|
+
const confirmed = await ctx.ui.confirm(
|
|
227
|
+
"Save portable field policy?",
|
|
228
|
+
"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.",
|
|
229
|
+
{ signal: mutationSignal },
|
|
230
|
+
);
|
|
231
|
+
validate();
|
|
232
|
+
if (!confirmed) return { kind: "rejected" };
|
|
233
|
+
await updateLocalConfig((current) => {
|
|
234
|
+
validate();
|
|
235
|
+
const setup = current.syncSetups[setupName];
|
|
236
|
+
if (!setup || !sameLocalFields(setup.sync.localFields, previous.localFields))
|
|
237
|
+
throw new Error("Field policy changed while under review; reopen settings.");
|
|
238
|
+
return {
|
|
239
|
+
...current,
|
|
240
|
+
version: current.version === 5 ? 5 : 4,
|
|
241
|
+
syncSetups: {
|
|
242
|
+
...current.syncSetups,
|
|
243
|
+
[setupName]: { ...setup, sync: { ...setup.sync, localFields: fields } },
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}, mutationSignal);
|
|
247
|
+
validate();
|
|
248
|
+
return { kind: "stay" };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
251
|
+
return { kind: "rejected" };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
94
254
|
automatic: async ({ value, signal: actionSignal }) => {
|
|
95
255
|
const automatic = value === "On";
|
|
96
256
|
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
@@ -112,6 +272,24 @@ export async function showSyncSettings(
|
|
|
112
272
|
return { kind: "rejected" };
|
|
113
273
|
}
|
|
114
274
|
},
|
|
275
|
+
"automatic-transfer": async ({ value, signal: actionSignal }) => {
|
|
276
|
+
const automaticTransfer = value === "On";
|
|
277
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
278
|
+
try {
|
|
279
|
+
await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automaticTransfer } }), {
|
|
280
|
+
signal: mutationSignal,
|
|
281
|
+
});
|
|
282
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
283
|
+
ctx.ui.notify(
|
|
284
|
+
`Automatic startup transfer ${automaticTransfer ? "enabled for the next session start" : "disabled"}. Completed transfers are not undone.`,
|
|
285
|
+
"info",
|
|
286
|
+
);
|
|
287
|
+
return { kind: "stay" };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
290
|
+
return { kind: "rejected" };
|
|
291
|
+
}
|
|
292
|
+
},
|
|
115
293
|
"skip-secret-scan": async ({ value, signal: actionSignal }) => {
|
|
116
294
|
const skipSecretScan = value === "On";
|
|
117
295
|
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
@@ -192,5 +370,8 @@ export async function showSyncSettings(
|
|
|
192
370
|
}
|
|
193
371
|
|
|
194
372
|
function notifySaveFailure(ctx: ExtensionCommandContext, error: unknown) {
|
|
195
|
-
ctx.ui.notify(
|
|
373
|
+
ctx.ui.notify(
|
|
374
|
+
`Pi Sync settings save failed: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
|
|
375
|
+
"error",
|
|
376
|
+
);
|
|
196
377
|
}
|
|
@@ -40,7 +40,7 @@ async function showAddTarget(ctx: ExtensionCommandContext, signal?: AbortSignal)
|
|
|
40
40
|
let raw = await readLocalConfigObject();
|
|
41
41
|
if (signal?.aborted) return;
|
|
42
42
|
if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
|
|
43
|
-
if (raw.version !== 3) {
|
|
43
|
+
if (raw.version !== 3 && raw.version !== 4 && raw.version !== 5) {
|
|
44
44
|
ctx.ui.notify("Version 1 and version 2 settings are unsupported and are never migrated.", "error");
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
@@ -88,7 +88,7 @@ async function showEditTarget(ctx: ExtensionCommandContext, name: string, signal
|
|
|
88
88
|
const partial = await loadPartialConfig(name);
|
|
89
89
|
if (signal?.aborted) return;
|
|
90
90
|
if (!partial.setupName) {
|
|
91
|
-
ctx.ui.notify("Create
|
|
91
|
+
ctx.ui.notify("Create supported pi-sync settings before editing a named sync setup.", "info");
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
if (partial.storageKind === "webdav") {
|
|
@@ -78,7 +78,7 @@ export async function showSetupSwitcher(
|
|
|
78
78
|
) {
|
|
79
79
|
const raw = await readLocalConfigObject();
|
|
80
80
|
if (signal?.aborted) return false;
|
|
81
|
-
if (raw?.version !== 3) {
|
|
81
|
+
if (raw?.version !== 3 && raw?.version !== 4 && raw?.version !== 5) {
|
|
82
82
|
ctx.ui.notify("Add a second sync setup before switching setups.", "info");
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
@@ -128,6 +128,7 @@ export async function showSetupSwitcher(
|
|
|
128
128
|
`Storage: ${backendStorageDescription(config)}`,
|
|
129
129
|
`Included content: ${config.include.length} paths`,
|
|
130
130
|
`Automatic sync: ${automaticSyncSummary(config.automatic)} · Sessions: ${config.include.includes("sessions") ? "On" : "Off"}`,
|
|
131
|
+
`Automatic transfer at startup: ${config.automaticTransfer ? "On — may upload, replace, or delete selected files; no reload" : "Off"}`,
|
|
131
132
|
"",
|
|
132
133
|
switchEffect,
|
|
133
134
|
].join("\n"),
|
|
@@ -31,10 +31,10 @@ export async function showStorageConnections(ctx: ExtensionCommandContext, signa
|
|
|
31
31
|
return {
|
|
32
32
|
kind: "actions",
|
|
33
33
|
title: "Storage connections",
|
|
34
|
-
lines: state.
|
|
34
|
+
lines: state.supportedVersion
|
|
35
35
|
? ["Server addresses and sign-in details, reusable across sync setups."]
|
|
36
|
-
: ["Create
|
|
37
|
-
items: state.
|
|
36
|
+
: ["Create supported pi-sync settings before managing storage connections."],
|
|
37
|
+
items: state.supportedVersion
|
|
38
38
|
? [
|
|
39
39
|
{ id: "add", label: "Add storage connection", action: "add" as const },
|
|
40
40
|
...names.map((name, index) => {
|
|
@@ -136,11 +136,15 @@ async function loadStorageMenuState(selectedName: string | undefined, signal?: A
|
|
|
136
136
|
const profiles = ownRecord(raw?.storageConnections) ?? {};
|
|
137
137
|
const profile = selectedName ? ownRecord(profiles[selectedName]) : undefined;
|
|
138
138
|
if (!selectedName || !profile) {
|
|
139
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
supportedVersion: raw?.version === 3 || raw?.version === 4 || raw?.version === 5,
|
|
141
|
+
profiles,
|
|
142
|
+
selected: undefined,
|
|
143
|
+
};
|
|
140
144
|
}
|
|
141
145
|
const usedBy = referencingSetups(raw, selectedName);
|
|
142
146
|
return {
|
|
143
|
-
|
|
147
|
+
supportedVersion: raw?.version === 3 || raw?.version === 4 || raw?.version === 5,
|
|
144
148
|
profiles,
|
|
145
149
|
selected: {
|
|
146
150
|
name: selectedName,
|
|
@@ -175,7 +179,7 @@ async function editStorageConnection(
|
|
|
175
179
|
) {
|
|
176
180
|
if (ctx.mode !== "tui") {
|
|
177
181
|
ctx.ui.notify(
|
|
178
|
-
"Editing storage connections requires TUI mode for safe credential handling. Edit the private
|
|
182
|
+
"Editing storage connections requires TUI mode for safe credential handling. Edit the private pi-sync settings file instead.",
|
|
179
183
|
"warning",
|
|
180
184
|
);
|
|
181
185
|
return;
|
|
@@ -250,7 +254,7 @@ async function editStorageConnection(
|
|
|
250
254
|
export async function showAddStorageConnection(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
251
255
|
if (ctx.mode !== "tui") {
|
|
252
256
|
ctx.ui.notify(
|
|
253
|
-
"Adding storage connections requires TUI mode for safe credential handling. Edit the private
|
|
257
|
+
"Adding storage connections requires TUI mode for safe credential handling. Edit the private pi-sync settings file instead.",
|
|
254
258
|
"warning",
|
|
255
259
|
);
|
|
256
260
|
return false;
|
package/src/ui/sync-attention.ts
CHANGED
|
@@ -153,6 +153,8 @@ export function observationSummary(observation: StartupObservation) {
|
|
|
153
153
|
if (result.firstSync)
|
|
154
154
|
return result.head ? "No sync baseline; review remote and local content" : "Remote empty; no sync baseline";
|
|
155
155
|
if (!result.head) return "Remote snapshot missing; review needed";
|
|
156
|
+
if (observation.automaticTransfer && result.selectionState?.kind === "legacy")
|
|
157
|
+
return "Legacy remote has no authoritative content list; review /sync and choose an explicit direction to adopt policy";
|
|
156
158
|
if (result.localChanged && result.remoteChanged) return "Local and remote changed since last sync; review needed";
|
|
157
159
|
if (result.remoteChanged) return "Remote changed since last sync";
|
|
158
160
|
if (result.localChanged) return "Local content changed since last sync";
|
|
@@ -171,6 +173,7 @@ export function classifyObservation(observation: StartupObservation): Observatio
|
|
|
171
173
|
if (result.selectionState?.kind === "different") return "review";
|
|
172
174
|
if (result.firstSync) return result.head ? "review" : "guidance";
|
|
173
175
|
if (!result.head || (result.localChanged && result.remoteChanged)) return "review";
|
|
176
|
+
if (observation.automaticTransfer && result.selectionState?.kind === "legacy") return "review";
|
|
174
177
|
if (result.localChanged || result.remoteChanged) return "status";
|
|
175
178
|
// Legacy metadata alone requires no scope confirmation in transfer operations.
|
|
176
179
|
return "none";
|