@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.
- package/README.md +19 -11
- package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
- package/dist/chunks/{chunk-HUTYAPFP.ts → chunk-35GM6XKM.ts} +275 -151
- package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
- package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
- package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
- package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
- package/dist/chunks/{chunk-ANWNQIR7.ts → chunk-KGE47GCZ.ts} +4 -4
- package/dist/chunks/chunk-KGE47GCZ.ts.map +7 -0
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
- package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
- package/dist/chunks/{conflict-review-JRFJIBEN.ts → conflict-review-ZOTOHL2E.ts} +11 -10
- package/dist/chunks/{conflict-review-JRFJIBEN.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
- package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
- package/dist/chunks/{sync-operations-QQ2GXKVC.ts → sync-operations-2CWST333.ts} +140 -24
- package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
- package/dist/index.ts +103 -68
- package/dist/index.ts.map +2 -2
- package/docs/settings.md +47 -10
- package/docs/tui-contract.md +61 -0
- package/package.json +1 -1
- package/src/commands/command-execution.ts +2 -1
- package/src/commands/command-types.ts +4 -0
- package/src/commands/command.ts +3 -1
- package/src/settings/config.ts +13 -10
- package/src/settings/settings-store.ts +1 -0
- package/src/settings/settings-types.ts +2 -0
- package/src/settings/settings-validation.ts +3 -0
- package/src/snapshot/snapshot-transaction.ts +4 -2
- package/src/state/merge-baseline-store.ts +18 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +45 -33
- package/src/sync/merged-sync.ts +202 -104
- package/src/sync/startup-check.ts +39 -21
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +8 -4
- package/src/sync/sync-mutations.ts +49 -11
- package/src/sync/sync-queries.ts +14 -3
- package/src/ui/manager-state.ts +10 -6
- package/src/ui/manager-ui.ts +57 -7
- package/src/ui/merge-review.ts +3 -3
- package/src/ui/merged-sync-summary.ts +2 -18
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +141 -43
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/transfer-changes.ts +33 -0
- package/src/ui/transfer-review.ts +27 -0
- package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
- package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
- package/dist/chunks/chunk-ANWNQIR7.ts.map +0 -7
- package/dist/chunks/chunk-HUTYAPFP.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/sync-operations-QQ2GXKVC.ts.map +0 -7
- /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
- /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
- /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
- /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
- /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
- /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → sync-inspection-3EMS2J5B.ts.map} +0 -0
package/src/ui/settings-ui.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
|
+
import { defineMenu, type MenuSettingItem, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
3
|
import { loadConfig } from "../settings/config.js";
|
|
4
4
|
import { localConfigPath } from "../settings/config-file.js";
|
|
5
5
|
import { updateSyncSetup } from "../settings/settings-management.js";
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
import { captureMutationOwner } from "../sync/sync-local.js";
|
|
15
15
|
import type { RunRoute } from "./cancellable-operation.js";
|
|
16
16
|
import { dispatchManagerResult } from "./manager-result-dispatcher.js";
|
|
17
|
+
import { boundedSettingsUI } from "./settings-frame.js";
|
|
17
18
|
import { showEditSyncSetupStorage, showSyncSetupManager } from "./setup/setup-actions.js";
|
|
18
|
-
import { AUTOMATIC_SYNC_DESCRIPTION } from "./setup/setup-prompts.js";
|
|
19
19
|
import { showStorageConnections } from "./storage-connections-ui.js";
|
|
20
|
+
import { AUTOMATIC_SYNC_DESCRIPTION, STARTUP_TRANSFER_DESCRIPTION, startupTransferSummary } from "./sync-behavior.js";
|
|
20
21
|
import { configureSyncStatus } from "./sync-status.js";
|
|
21
22
|
import { safeTerminalText } from "./terminal-text.js";
|
|
22
23
|
|
|
@@ -75,50 +76,64 @@ export async function showSyncSettings(
|
|
|
75
76
|
| "content-policy"
|
|
76
77
|
| "skip-secret-scan"
|
|
77
78
|
| "show-status"
|
|
79
|
+
| "debug"
|
|
78
80
|
| "on-switch"
|
|
79
81
|
| "include"
|
|
80
82
|
| "remote-include"
|
|
81
83
|
| "storage-location"
|
|
82
84
|
| "setups"
|
|
83
85
|
| "connections";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
type Group = "content" | "behavior" | "storage" | "advanced" | "all";
|
|
87
|
+
type Screen = "categories" | Group;
|
|
88
|
+
const groups: Record<Group, { label: string; ids: readonly string[] }> = {
|
|
89
|
+
content: { label: "Sync content", ids: ["include", "remoteInclude", "localFields"] },
|
|
90
|
+
behavior: { label: "Sync behavior", ids: ["automatic", "automaticTransfer", "onSwitch"] },
|
|
91
|
+
storage: { label: "Storage & setups", ids: ["storageLocation", "setups", "connections"] },
|
|
92
|
+
advanced: { label: "Advanced", ids: ["mergeSettings", "contentPolicy", "skipSecretScan", "showStatus", "debug"] },
|
|
93
|
+
all: { label: "All settings (search)", ids: [] },
|
|
94
|
+
};
|
|
95
|
+
let saveStatus = "Simple choices save immediately. Back does not undo saved changes.";
|
|
96
|
+
// Keep Kit's existing settings adapter: native SettingsList has no async save/rollback
|
|
97
|
+
// contract or injected keybindings. Kit preserves these safety and accessibility behaviors.
|
|
98
|
+
// Categories use public navigation; All settings retains cross-category search.
|
|
99
|
+
const settingsScreen = (state: Awaited<ReturnType<typeof loadConfig>>, group: Group) =>
|
|
100
|
+
({
|
|
101
|
+
kind: "settings",
|
|
102
|
+
title: `Pi Sync Settings · ${groups[group].label}`,
|
|
103
|
+
lines: [
|
|
104
|
+
`Setup: ${safeTerminalText(state.setupName)} · Connection: ${safeTerminalText(state.connectionName)}`,
|
|
105
|
+
saveStatus,
|
|
106
|
+
],
|
|
107
|
+
items: (
|
|
108
|
+
[
|
|
96
109
|
{
|
|
97
110
|
id: "include",
|
|
98
|
-
label: "Included
|
|
99
|
-
description:
|
|
100
|
-
|
|
111
|
+
label: "Included paths",
|
|
112
|
+
description:
|
|
113
|
+
"This setup: choose paths in a draft, then review and Save or Discard. No transfer is started.",
|
|
114
|
+
currentValue: `${state.include.length} paths · Edit`,
|
|
101
115
|
action: "include",
|
|
102
116
|
},
|
|
103
117
|
{
|
|
104
118
|
id: "remoteInclude",
|
|
105
|
-
label: "Compare
|
|
106
|
-
description:
|
|
119
|
+
label: "Compare included paths",
|
|
120
|
+
description:
|
|
121
|
+
"This setup: compare included-path policies, not file contents. Review before saving either list; no files transfer unless you explicitly Continue.",
|
|
107
122
|
currentValue: "Review",
|
|
108
123
|
action: "remote-include",
|
|
109
124
|
},
|
|
110
125
|
{
|
|
111
126
|
id: "localFields",
|
|
112
|
-
label: "Machine-local
|
|
127
|
+
label: "Machine-local fields",
|
|
113
128
|
description:
|
|
114
|
-
"
|
|
129
|
+
"This setup: settings.json root fields excluded from future portable snapshots, not project overrides. Policy changes require reviewed directional migration; old history remains.",
|
|
115
130
|
currentValue: `${state.localFields?.length ?? 0} fields · Edit`,
|
|
116
131
|
action: "local-fields",
|
|
117
132
|
},
|
|
118
133
|
{
|
|
119
134
|
id: "storageLocation",
|
|
120
135
|
label: "Storage location",
|
|
121
|
-
description: "
|
|
136
|
+
description: "This setup: review and save bucket, branch, or path changes. Remote history is not moved.",
|
|
122
137
|
currentValue:
|
|
123
138
|
state.backend.type === "s3"
|
|
124
139
|
? `${safeTerminalText(state.backend.destination.bucket)} · ${safeTerminalText(state.backend.destination.prefix)}`
|
|
@@ -130,7 +145,8 @@ export async function showSyncSettings(
|
|
|
130
145
|
{
|
|
131
146
|
id: "setups",
|
|
132
147
|
label: "Manage sync setups",
|
|
133
|
-
description:
|
|
148
|
+
description:
|
|
149
|
+
"This device: add, switch, edit, or remove sync setups. Each edit identifies its owner before saving.",
|
|
134
150
|
currentValue: "Open",
|
|
135
151
|
action: "setups",
|
|
136
152
|
},
|
|
@@ -138,7 +154,7 @@ export async function showSyncSettings(
|
|
|
138
154
|
id: "connections",
|
|
139
155
|
label: "Manage storage connections",
|
|
140
156
|
description:
|
|
141
|
-
"
|
|
157
|
+
"Shared connections on this device: review affected setups before saving server or credential changes.",
|
|
142
158
|
currentValue: "Open",
|
|
143
159
|
action: "connections",
|
|
144
160
|
},
|
|
@@ -152,63 +168,98 @@ export async function showSyncSettings(
|
|
|
152
168
|
},
|
|
153
169
|
{
|
|
154
170
|
id: "automaticTransfer",
|
|
155
|
-
label: "
|
|
156
|
-
description:
|
|
157
|
-
"May upload, replace, or delete selected files once at idle startup in TUI/RPC. Requires a baseline and conditional/lease publication; never reloads resources. Turning off cancels pending work.",
|
|
171
|
+
label: "Startup transfer",
|
|
172
|
+
description: `${STARTUP_TRANSFER_DESCRIPTION} Saved preference and requirements: ${startupTransferSummary(state)}.`,
|
|
158
173
|
currentValue: state.automaticTransfer ? "On" : "Off",
|
|
159
174
|
values: ["On", "Off"],
|
|
160
175
|
action: "automatic-transfer",
|
|
161
176
|
},
|
|
162
177
|
{
|
|
163
178
|
id: "onSwitch",
|
|
164
|
-
label: "
|
|
165
|
-
description:
|
|
179
|
+
label: "On setup switch",
|
|
180
|
+
description:
|
|
181
|
+
"All setups on this device: ask before a pull review, open it automatically, or switch without pulling. Applies to subsequent switches.",
|
|
166
182
|
currentValue: setupSwitchActionLabel(state.onSwitch),
|
|
167
183
|
values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
|
|
168
184
|
action: "on-switch",
|
|
169
185
|
},
|
|
170
186
|
{
|
|
171
187
|
id: "mergeSettings",
|
|
172
|
-
label: "
|
|
188
|
+
label: "Merge settings fields",
|
|
173
189
|
description:
|
|
174
|
-
"
|
|
190
|
+
"This setup, experimental opt-in: merge independent global settings.json root fields with a verified private ancestor. Arrays and nested objects remain atomic. Applies to subsequent sync; no reload.",
|
|
175
191
|
currentValue: state.mergeSettings ? "On" : "Off",
|
|
176
192
|
values: ["On", "Off"],
|
|
177
193
|
action: "merge-settings",
|
|
178
194
|
},
|
|
179
195
|
{
|
|
180
196
|
id: "contentPolicy",
|
|
181
|
-
label: "Content / partial
|
|
197
|
+
label: "Content merge / partial progress",
|
|
182
198
|
description:
|
|
183
|
-
"
|
|
199
|
+
"This setup, experimental opt-in: review the version-5 upgrade before saving. Merge bounded text and prefix-only sessions; partial progress keeps withheld versions and baselines. Applies to subsequent sync; disabling does not clear recovery evidence.",
|
|
184
200
|
currentValue: state.mergeContent
|
|
185
201
|
? state.partialSync
|
|
186
|
-
? "
|
|
187
|
-
: "
|
|
202
|
+
? "Merge + partial"
|
|
203
|
+
: "Merge only"
|
|
188
204
|
: state.partialSync
|
|
189
205
|
? "Partial only"
|
|
190
206
|
: "Off",
|
|
191
|
-
values: ["Off", "
|
|
207
|
+
values: ["Off", "Merge only", "Merge + partial", "Partial only"],
|
|
192
208
|
action: "content-policy",
|
|
193
209
|
},
|
|
194
210
|
{
|
|
195
211
|
id: "skipSecretScan",
|
|
196
|
-
label: "Skip secret scan
|
|
197
|
-
description:
|
|
212
|
+
label: "Skip secret scan",
|
|
213
|
+
description:
|
|
214
|
+
"All setups on this device: skip possible-secret checks on subsequent pushes, including automatic pushes. Other safety checks remain. Diagnostics still scans.",
|
|
198
215
|
currentValue: state.skipSecretScan ? "On" : "Off",
|
|
199
216
|
values: ["On", "Off"],
|
|
200
217
|
action: "skip-secret-scan",
|
|
201
218
|
},
|
|
202
219
|
{
|
|
203
220
|
id: "showStatus",
|
|
204
|
-
label: "Show status
|
|
205
|
-
description:
|
|
221
|
+
label: "Show sync status",
|
|
222
|
+
description:
|
|
223
|
+
"All setups on this device: saved and applied immediately to Pi status. Review widgets and notifications remain available.",
|
|
206
224
|
currentValue: state.showStatus ? "On" : "Off",
|
|
207
225
|
values: ["On", "Off"],
|
|
208
226
|
action: "show-status",
|
|
209
227
|
},
|
|
228
|
+
{
|
|
229
|
+
id: "debug",
|
|
230
|
+
label: "Debug startup timings",
|
|
231
|
+
description:
|
|
232
|
+
"All setups on this device: show startup durations in TUI/RPC notifications. Applies next session start; no credentials or file contents.",
|
|
233
|
+
currentValue: state.debug ? "On" : "Off",
|
|
234
|
+
values: ["On", "Off"],
|
|
235
|
+
action: "debug",
|
|
236
|
+
},
|
|
237
|
+
] satisfies MenuSettingItem<Action>[]
|
|
238
|
+
).filter((item) => group === "all" || groups[group].ids.includes(item.id)),
|
|
239
|
+
}) as const;
|
|
240
|
+
const menu = defineMenu<Awaited<ReturnType<typeof loadConfig>>, Screen, Action, ExtensionCommandContext>({
|
|
241
|
+
start: "categories",
|
|
242
|
+
screens: {
|
|
243
|
+
categories: ({ state }) => ({
|
|
244
|
+
kind: "actions",
|
|
245
|
+
title: "Pi Sync Settings",
|
|
246
|
+
lines: [
|
|
247
|
+
`Setup: ${safeTerminalText(state.setupName)} · Connection: ${safeTerminalText(state.connectionName)}`,
|
|
248
|
+
"Choose a task, or search all settings. Scope is shown in each setting's description.",
|
|
249
|
+
saveStatus,
|
|
210
250
|
],
|
|
251
|
+
items: (Object.entries(groups) as [Group, (typeof groups)[Group]][]).map(([id, group]) => ({
|
|
252
|
+
id,
|
|
253
|
+
label: group.label,
|
|
254
|
+
to: id,
|
|
255
|
+
})),
|
|
256
|
+
hint: "back",
|
|
211
257
|
}),
|
|
258
|
+
content: ({ state }) => settingsScreen(state, "content"),
|
|
259
|
+
behavior: ({ state }) => settingsScreen(state, "behavior"),
|
|
260
|
+
storage: ({ state }) => settingsScreen(state, "storage"),
|
|
261
|
+
advanced: ({ state }) => settingsScreen(state, "advanced"),
|
|
262
|
+
all: ({ state }) => settingsScreen(state, "all"),
|
|
212
263
|
},
|
|
213
264
|
actions: {
|
|
214
265
|
"storage-location": async ({ signal: actionSignal }) => {
|
|
@@ -232,8 +283,8 @@ export async function showSyncSettings(
|
|
|
232
283
|
try {
|
|
233
284
|
const previous = await loadConfig(setupName);
|
|
234
285
|
validate();
|
|
235
|
-
const mergeContent = value === "
|
|
236
|
-
const partialSync = value === "Partial only" || value === "
|
|
286
|
+
const mergeContent = value === "Merge only" || value === "Merge + partial";
|
|
287
|
+
const partialSync = value === "Partial only" || value === "Merge + partial";
|
|
237
288
|
if (
|
|
238
289
|
!(await ctx.ui.confirm(
|
|
239
290
|
"Save experimental content policy?",
|
|
@@ -243,6 +294,7 @@ export async function showSyncSettings(
|
|
|
243
294
|
)
|
|
244
295
|
return { kind: "rejected" };
|
|
245
296
|
validate();
|
|
297
|
+
ctx.ui.notify("Saving reviewed Pi Sync policy…", "info");
|
|
246
298
|
await updateLocalConfig((current) => {
|
|
247
299
|
validate();
|
|
248
300
|
const setup = current.syncSetups[setupName];
|
|
@@ -262,6 +314,9 @@ export async function showSyncSettings(
|
|
|
262
314
|
};
|
|
263
315
|
}, mutationSignal);
|
|
264
316
|
validate();
|
|
317
|
+
saveStatus =
|
|
318
|
+
"Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
|
|
319
|
+
ctx.ui.notify(saveStatus, "info");
|
|
265
320
|
return { kind: "stay" };
|
|
266
321
|
} catch (error) {
|
|
267
322
|
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
@@ -312,6 +367,7 @@ export async function showSyncSettings(
|
|
|
312
367
|
);
|
|
313
368
|
validate();
|
|
314
369
|
if (!confirmed) return { kind: "rejected" };
|
|
370
|
+
ctx.ui.notify("Saving reviewed Pi Sync policy…", "info");
|
|
315
371
|
await updateLocalConfig((current) => {
|
|
316
372
|
validate();
|
|
317
373
|
const setup = current.syncSetups[setupName];
|
|
@@ -327,6 +383,9 @@ export async function showSyncSettings(
|
|
|
327
383
|
};
|
|
328
384
|
}, mutationSignal);
|
|
329
385
|
validate();
|
|
386
|
+
saveStatus =
|
|
387
|
+
"Saved reviewed policy for this setup. Applies to subsequent sync operations; recovery evidence is retained.";
|
|
388
|
+
ctx.ui.notify(saveStatus, "info");
|
|
330
389
|
return { kind: "stay" };
|
|
331
390
|
} catch (error) {
|
|
332
391
|
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
@@ -405,6 +464,18 @@ export async function showSyncSettings(
|
|
|
405
464
|
return { kind: "rejected" };
|
|
406
465
|
}
|
|
407
466
|
},
|
|
467
|
+
debug: async ({ value, signal: actionSignal }) => {
|
|
468
|
+
const debug = value === "On";
|
|
469
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
470
|
+
try {
|
|
471
|
+
await updateLocalConfig((settings) => ({ ...settings, debug }), mutationSignal);
|
|
472
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
473
|
+
return { kind: "stay" };
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
476
|
+
return { kind: "rejected" };
|
|
477
|
+
}
|
|
478
|
+
},
|
|
408
479
|
"on-switch": async ({ value, signal: actionSignal }) => {
|
|
409
480
|
const action = value ? setupSwitchActionFromLabel(value) : undefined;
|
|
410
481
|
if (!action) return { kind: "rejected" };
|
|
@@ -444,6 +515,33 @@ export async function showSyncSettings(
|
|
|
444
515
|
},
|
|
445
516
|
},
|
|
446
517
|
});
|
|
518
|
+
for (const id of [
|
|
519
|
+
"automatic",
|
|
520
|
+
"automatic-transfer",
|
|
521
|
+
"merge-settings",
|
|
522
|
+
"skip-secret-scan",
|
|
523
|
+
"show-status",
|
|
524
|
+
"debug",
|
|
525
|
+
"on-switch",
|
|
526
|
+
] as const) {
|
|
527
|
+
const save = menu.actions[id];
|
|
528
|
+
menu.actions[id] = async (action) => {
|
|
529
|
+
const validate = captureMutationOwner(ctx, action.signal);
|
|
530
|
+
validate();
|
|
531
|
+
ctx.ui.notify("Saving Pi Sync setting…", "info");
|
|
532
|
+
const result = await save(action);
|
|
533
|
+
if (action.signal.aborted || signal?.aborted) return { kind: "rejected" };
|
|
534
|
+
validate();
|
|
535
|
+
saveStatus =
|
|
536
|
+
result?.kind === "rejected"
|
|
537
|
+
? "Save failed or rejected. Previous value retained; retry is available."
|
|
538
|
+
: id === "automatic-transfer" || id === "automatic" || id === "debug"
|
|
539
|
+
? "Saved. Enabling startup work applies next session start; completed transfers are not undone."
|
|
540
|
+
: "Saved. Applies to subsequent operations; status visibility applies immediately.";
|
|
541
|
+
ctx.ui.notify(saveStatus, result?.kind === "rejected" ? "warning" : "info");
|
|
542
|
+
return result;
|
|
543
|
+
};
|
|
544
|
+
}
|
|
447
545
|
async function resumeSettings(editorSignal: AbortSignal) {
|
|
448
546
|
if (editorSignal.aborted) return { kind: "close" as const };
|
|
449
547
|
try {
|
|
@@ -455,7 +553,7 @@ export async function showSyncSettings(
|
|
|
455
553
|
return { kind: "close" as const };
|
|
456
554
|
}
|
|
457
555
|
}
|
|
458
|
-
await runMenu(ctx, menu, {
|
|
556
|
+
await runMenu({ ...ctx, ui: boundedSettingsUI(ctx) }, menu, {
|
|
459
557
|
getState: () => loadConfig(setupName),
|
|
460
558
|
signal,
|
|
461
559
|
isCurrent: () => !signal?.aborted,
|
|
@@ -2,7 +2,11 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { readLocalConfigObject } from "../../settings/settings-store.js";
|
|
3
3
|
import { validateConfigName } from "../../settings/settings-validation.js";
|
|
4
4
|
import { DEFAULT_SYNC_INCLUDE } from "../../sync/sync-policy.js";
|
|
5
|
+
import { AUTOMATIC_SYNC_DESCRIPTION } from "../sync-behavior.js";
|
|
5
6
|
import { safeTerminalText } from "../terminal-text.js";
|
|
7
|
+
|
|
8
|
+
export { AUTOMATIC_SYNC_DESCRIPTION } from "../sync-behavior.js";
|
|
9
|
+
|
|
6
10
|
import { promptTextInput } from "./text-input.js";
|
|
7
11
|
|
|
8
12
|
export async function readSetupConnection(name: string, signal?: AbortSignal) {
|
|
@@ -12,9 +16,6 @@ export async function readSetupConnection(name: string, signal?: AbortSignal) {
|
|
|
12
16
|
return connection;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export const AUTOMATIC_SYNC_DESCRIPTION =
|
|
16
|
-
"Check the current setup in the background at startup; review changes in /sync before transferring. At shutdown, automatically push selected content only when sessions are included. No startup dialogs.";
|
|
17
|
-
|
|
18
19
|
export async function chooseSetupContent(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
19
20
|
const choice = await ctx.ui.select(
|
|
20
21
|
"Choose included content\n\nRecommended: Pi settings, instructions, skills, prompts, themes, and extensions.\nMinimal: settings.json and AGENTS.md.\nReview the exact paths before saving.",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const AUTOMATIC_SYNC_DESCRIPTION =
|
|
2
|
+
"This setup: check remote state once at startup in TUI/RPC. At shutdown, push all selected content only when sessions are included; reload skips that push. Saves apply to subsequent operations; a new check waits for the next session start. This does not enable startup transfer.";
|
|
3
|
+
|
|
4
|
+
export const STARTUP_TRANSFER_DESCRIPTION =
|
|
5
|
+
"This setup: separate opt-in for one idle startup transfer in TUI/RPC. Requires an accepted baseline, matching remote included paths, and conditional or lease-protected publication (Git, verified WebDAV, or R2/S3 with verified conditional writes). May upload, replace, or remove selected files. Never reloads resources. Enabling applies next session start; opening Settings cancels pending work. Completed transfers are not undone.";
|
|
6
|
+
|
|
7
|
+
export function startupTransferSummary(config: {
|
|
8
|
+
automaticTransfer?: boolean;
|
|
9
|
+
backend: { type: "s3" | "git" | "webdav" };
|
|
10
|
+
}) {
|
|
11
|
+
if (!config.automaticTransfer) return "Off";
|
|
12
|
+
return config.backend.type === "s3"
|
|
13
|
+
? "On (next idle startup; R2/S3 conditional-write verification required)"
|
|
14
|
+
: "On (next idle startup; baseline and remote policy checks required)";
|
|
15
|
+
}
|
package/src/ui/sync-format.ts
CHANGED
|
@@ -5,12 +5,17 @@ import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
|
5
5
|
import { inspectRemoteSelection } from "../sync/sync-policy.js";
|
|
6
6
|
import { fileHashMap } from "../sync/sync-state.js";
|
|
7
7
|
import { safeTerminalText, snapshotPathLabel } from "./terminal-text.js";
|
|
8
|
+
import { transferChanges, transferCounts } from "./transfer-changes.js";
|
|
8
9
|
|
|
9
10
|
export function formatDiff(local: Snapshot, remote: Snapshot) {
|
|
10
11
|
const localMap = fileHashMap(local);
|
|
11
12
|
const remoteMap = fileHashMap(remote);
|
|
12
13
|
const allPaths = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
13
|
-
const lines = [
|
|
14
|
+
const lines = [
|
|
15
|
+
`local: ${local.files.length} files`,
|
|
16
|
+
`remote: ${safeTerminalText(remote.id)} (${remote.files.length} files)`,
|
|
17
|
+
"",
|
|
18
|
+
];
|
|
14
19
|
let changed = 0;
|
|
15
20
|
for (const filePath of allPaths) {
|
|
16
21
|
if (!Object.hasOwn(localMap, filePath)) {
|
|
@@ -29,9 +34,10 @@ export function formatDiff(local: Snapshot, remote: Snapshot) {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function formatSnapshotOnlyDiff(title: string, snapshot: Snapshot) {
|
|
32
|
-
return [
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
return [
|
|
38
|
+
`${safeTerminalText(title)}: ${safeTerminalText(snapshot.id)}`,
|
|
39
|
+
...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`),
|
|
40
|
+
].join("\n");
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function formatPushSummary(
|
|
@@ -45,26 +51,40 @@ export function formatPushSummary(
|
|
|
45
51
|
return [
|
|
46
52
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
47
53
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
48
|
-
`
|
|
54
|
+
`Direction: Local → Remote. Source: ${safeTerminalText(agentDir())}.`,
|
|
55
|
+
`Publish a snapshot containing ${upload.files.length} files; unchanged and preserved files are not counted as changes.`,
|
|
49
56
|
`Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
50
|
-
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
51
|
-
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
57
|
+
head ? `Remote latest: ${safeTerminalText(head.snapshotId)}` : "Remote latest: empty",
|
|
58
|
+
"Publication effect: the backend's active head will reference the new immutable snapshot. Removed paths leave the active snapshot; retained history is not erased.",
|
|
52
59
|
...(remote && inspectRemoteSelection(config.include, remote).kind === "different"
|
|
53
60
|
? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."]
|
|
54
61
|
: []),
|
|
55
62
|
formatPublicationPreview(remote, upload),
|
|
56
|
-
|
|
57
|
-
?
|
|
58
|
-
:
|
|
63
|
+
config.skipSecretScan
|
|
64
|
+
? "Warning: possible-secret scanning is disabled for this push."
|
|
65
|
+
: preservedRemoteFileCount > 0
|
|
66
|
+
? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
|
|
67
|
+
: "Possible secrets were scanned before this prompt.",
|
|
59
68
|
].join("\n");
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
export function formatApplyPreview(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
export function formatApplyPreview(
|
|
72
|
+
local: Snapshot,
|
|
73
|
+
remote: Snapshot,
|
|
74
|
+
protectedPaths: ReadonlySet<string> = new Set(),
|
|
75
|
+
additionalRemovals: readonly string[] = [],
|
|
76
|
+
) {
|
|
77
|
+
return formatDirectionalChanges(
|
|
78
|
+
local,
|
|
79
|
+
remote,
|
|
80
|
+
{
|
|
81
|
+
add: "Add locally",
|
|
82
|
+
update: "Update locally",
|
|
83
|
+
remove: "Remove locally",
|
|
84
|
+
},
|
|
85
|
+
protectedPaths,
|
|
86
|
+
additionalRemovals,
|
|
87
|
+
);
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
export function formatPullSummary(
|
|
@@ -72,15 +92,20 @@ export function formatPullSummary(
|
|
|
72
92
|
destination: string,
|
|
73
93
|
local: Snapshot,
|
|
74
94
|
remote: Snapshot,
|
|
75
|
-
|
|
95
|
+
protectedPaths: ReadonlySet<string>,
|
|
96
|
+
additionalRemovals: readonly string[] = [],
|
|
76
97
|
) {
|
|
77
98
|
return [
|
|
78
99
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
79
100
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
101
|
+
"Direction: Remote → Local.",
|
|
80
102
|
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
81
103
|
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
82
|
-
`Protected live sessions: ${
|
|
83
|
-
formatApplyPreview(local, remote),
|
|
104
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
105
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
106
|
+
...(additionalRemovals.length
|
|
107
|
+
? ["Case-variant aliases listed above are removed before writing canonical paths."]
|
|
108
|
+
: []),
|
|
84
109
|
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged.",
|
|
85
110
|
].join("\n");
|
|
86
111
|
}
|
|
@@ -91,25 +116,25 @@ export function formatRollbackSummary(
|
|
|
91
116
|
local: Snapshot,
|
|
92
117
|
remote: Snapshot,
|
|
93
118
|
requestedSnapshot: string,
|
|
94
|
-
|
|
119
|
+
protectedPaths: ReadonlySet<string>,
|
|
120
|
+
additionalRemovals: readonly string[] = [],
|
|
95
121
|
) {
|
|
96
122
|
return [
|
|
97
123
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
98
124
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
125
|
+
"Direction: Historical snapshot → Local and remote active head.",
|
|
99
126
|
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
100
127
|
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
101
|
-
`Protected live sessions: ${
|
|
102
|
-
formatApplyPreview(local, remote),
|
|
128
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
129
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
130
|
+
...(additionalRemovals.length
|
|
131
|
+
? ["Case-variant aliases listed above are removed before writing canonical paths."]
|
|
132
|
+
: []),
|
|
103
133
|
"A local backup is created before applying; the backend's active head will change.",
|
|
104
134
|
].join("\n");
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
export function formatPublicationPreview(remote: Snapshot | undefined, upload: Snapshot) {
|
|
108
|
-
if (!remote) {
|
|
109
|
-
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
|
|
110
|
-
"\n",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
138
|
return formatDirectionalChanges(remote, upload, {
|
|
114
139
|
add: "Add remotely",
|
|
115
140
|
update: "Update remotely",
|
|
@@ -118,21 +143,27 @@ export function formatPublicationPreview(remote: Snapshot | undefined, upload: S
|
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
function formatDirectionalChanges(
|
|
121
|
-
before: Snapshot,
|
|
146
|
+
before: Snapshot | undefined,
|
|
122
147
|
after: Snapshot,
|
|
123
148
|
labels: { add: string; update: string; remove: string },
|
|
149
|
+
protectedPaths: ReadonlySet<string> = new Set(),
|
|
150
|
+
additionalRemovals: readonly string[] = [],
|
|
124
151
|
) {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
|
|
152
|
+
const changes = transferChanges(before, after, protectedPaths);
|
|
153
|
+
const known = new Set(changes.map((change) => change.path));
|
|
154
|
+
for (const filePath of additionalRemovals) {
|
|
155
|
+
if (!known.has(filePath) && !protectedPaths.has(filePath)) {
|
|
156
|
+
changes.push({ path: filePath, action: "Delete" });
|
|
157
|
+
known.add(filePath);
|
|
158
|
+
}
|
|
133
159
|
}
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
const counts = transferCounts(changes);
|
|
161
|
+
const label = { Add: labels.add, Update: labels.update, Delete: labels.remove };
|
|
162
|
+
return [
|
|
163
|
+
`Changes: ${counts.add} add · ${counts.update} update · ${counts.remove} remove`,
|
|
164
|
+
...changes.map((change) => `${label[change.action]}: ${snapshotPathLabel(change.path)}`),
|
|
165
|
+
...(changes.length ? [] : ["No file changes."]),
|
|
166
|
+
].join("\n");
|
|
136
167
|
}
|
|
137
168
|
|
|
138
169
|
export function countPreservedRemoteFiles(local: Snapshot, upload: Snapshot) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
2
|
+
import { fileHashMap } from "../sync/sync-state.js";
|
|
3
|
+
|
|
4
|
+
export interface TransferChange {
|
|
5
|
+
path: string;
|
|
6
|
+
action: "Add" | "Update" | "Delete";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Compare effective before/after snapshots, not raw planner decisions. */
|
|
10
|
+
export function transferChanges(
|
|
11
|
+
before: Snapshot | undefined,
|
|
12
|
+
after: Snapshot,
|
|
13
|
+
protectedPaths: ReadonlySet<string> = new Set(),
|
|
14
|
+
): TransferChange[] {
|
|
15
|
+
const beforeMap = before ? fileHashMap(before) : {};
|
|
16
|
+
const afterMap = fileHashMap(after);
|
|
17
|
+
return [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])]
|
|
18
|
+
.sort()
|
|
19
|
+
.flatMap((filePath): TransferChange[] => {
|
|
20
|
+
if (protectedPaths.has(filePath)) return [];
|
|
21
|
+
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
22
|
+
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
23
|
+
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function transferCounts(changes: readonly TransferChange[]) {
|
|
28
|
+
return {
|
|
29
|
+
add: changes.filter((change) => change.action === "Add").length,
|
|
30
|
+
update: changes.filter((change) => change.action === "Update").length,
|
|
31
|
+
remove: changes.filter((change) => change.action === "Delete").length,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { confirmMergeReview } from "./merge-review.js";
|
|
3
|
+
|
|
4
|
+
/** Reuse the scrollable safe-default review in TUI; retain RPC's confirmation protocol. */
|
|
5
|
+
export async function confirmTransferReview(
|
|
6
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
7
|
+
title: string,
|
|
8
|
+
content: string,
|
|
9
|
+
signal: AbortSignal | undefined,
|
|
10
|
+
validate: () => void,
|
|
11
|
+
) {
|
|
12
|
+
validate();
|
|
13
|
+
const isCurrent = () => {
|
|
14
|
+
try {
|
|
15
|
+
validate();
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const confirmed =
|
|
22
|
+
ctx.mode === "tui" && ctx.hasUI
|
|
23
|
+
? await confirmMergeReview(ctx, title, content, signal, isCurrent, "apply reviewed transfer", content.split("\n"))
|
|
24
|
+
: await ctx.ui.confirm(title, content, { signal });
|
|
25
|
+
validate();
|
|
26
|
+
return confirmed;
|
|
27
|
+
}
|