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