@narumitw/pi-sync 0.54.2 → 0.56.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 +40 -15
- package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
- package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
- package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
- package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
- package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
- package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
- package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
- package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
- package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
- package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
- package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
- package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
- package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
- package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
- package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
- package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
- package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
- package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
- package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
- package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
- package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
- package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
- package/dist/chunks/chunk-QGK445K7.ts +21 -0
- package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
- package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
- package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
- package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
- package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
- package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
- package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
- package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
- package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
- package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
- package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
- package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
- package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
- package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
- package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
- package/dist/chunks/config-KGIYTTXQ.ts +37 -0
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
- package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
- package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
- package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
- package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
- package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
- package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
- package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
- package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
- package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
- package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
- package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
- package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
- package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
- package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
- package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
- package/dist/index.ts +246 -131
- package/dist/index.ts.map +3 -3
- package/docs/agent-mode-verification.md +99 -0
- package/docs/continuous-sync-verification.md +87 -0
- package/docs/settings.md +131 -11
- package/docs/tui-contract.md +61 -0
- package/package.json +3 -2
- package/src/backends/git/git-backend.ts +1 -4
- package/src/backends/git/git-runner.ts +1 -5
- package/src/backends/s3/s3-backend.ts +3 -25
- package/src/backends/s3/s3-client.ts +14 -2
- package/src/backends/snapshot-pointer.ts +27 -0
- package/src/backends/webdav/webdav-backend.ts +3 -29
- package/src/backends/webdav/webdav-client.ts +11 -7
- package/src/commands/command-execution.ts +7 -3
- package/src/commands/command-handler.ts +15 -3
- package/src/commands/command-types.ts +10 -0
- package/src/commands/command.ts +4 -1
- package/src/settings/config.ts +20 -10
- package/src/settings/settings-store.ts +1 -34
- package/src/settings/settings-types.ts +10 -0
- package/src/settings/settings-validation.ts +4 -1
- package/src/snapshot/snapshot-codec.ts +1 -5
- package/src/state/lock.ts +13 -10
- package/src/state/merge-baseline-store.ts +19 -1
- package/src/state/state-directory.ts +8 -1
- package/src/sync/agent-candidate.ts +251 -0
- package/src/sync/agent-plan.ts +87 -0
- package/src/sync/agent-policy.ts +13 -0
- package/src/sync/agent-request.ts +17 -0
- package/src/sync/agent-runner.ts +188 -0
- package/src/sync/agent-store.ts +151 -0
- package/src/sync/agent-sync.ts +91 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +46 -33
- package/src/sync/continuous-errors.ts +85 -0
- package/src/sync/continuous-lease.ts +221 -0
- package/src/sync/continuous-record.ts +179 -0
- package/src/sync/continuous-storage.ts +55 -0
- package/src/sync/continuous-sync.ts +516 -0
- package/src/sync/continuous-watch.ts +59 -0
- package/src/sync/merged-sync.ts +268 -112
- package/src/sync/remote-snapshot.ts +2 -3
- package/src/sync/settings-merge.ts +19 -3
- package/src/sync/setup-switch.ts +1 -5
- package/src/sync/startup-check.ts +51 -29
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +16 -5
- package/src/sync/sync-mutations.ts +55 -28
- package/src/sync/sync-queries.ts +19 -3
- package/src/sync/text-merge.ts +3 -3
- package/src/sync-extension.ts +75 -19
- package/src/ui/agent-mode-setting.ts +91 -0
- package/src/ui/manager-state.ts +12 -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/remote-selection-ui.ts +5 -5
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +223 -43
- package/src/ui/setup/s3-credentials-ui.ts +1 -5
- package/src/ui/setup/setup-name-ui.ts +2 -5
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/setup/setup-switcher.ts +2 -0
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/sync-setups-ui.ts +3 -5
- 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-5ZWQ33XL.ts.map +0 -7
- package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
- package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
- package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
- package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
- package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
- package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
- package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
- package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
- package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
- package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
- package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
package/src/sync-extension.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { completeSyncArguments, splitArgs } from "./commands/command.js";
|
|
3
3
|
import { handleCommand } from "./commands/command-handler.js";
|
|
4
4
|
import type { AnySyncConfig } from "./settings/settings-types.js";
|
|
@@ -19,6 +19,31 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
19
19
|
let sessionAbort = new AbortController();
|
|
20
20
|
let shutdownAbort: AbortController | undefined;
|
|
21
21
|
let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
|
|
22
|
+
let continuous: ReturnType<typeof import("./sync/continuous-sync.js")["createContinuousSync"]> | undefined;
|
|
23
|
+
let owner: ExtensionContext["sessionManager"] | undefined;
|
|
24
|
+
let continuousPossible = false;
|
|
25
|
+
const foreground = new Map<ExtensionContext["sessionManager"], number>();
|
|
26
|
+
async function resumeContinuous(ctx: ExtensionContext, signal: AbortSignal, retry?: { setup?: string }) {
|
|
27
|
+
const isCurrent = () => !signal.aborted && !foreground.has(ctx.sessionManager) && owner === ctx.sessionManager;
|
|
28
|
+
if (!isCurrent() || !ctx.hasUI) return;
|
|
29
|
+
try {
|
|
30
|
+
const { loadConfigForCheck } = await import("./settings/config.js");
|
|
31
|
+
if (!isCurrent()) return;
|
|
32
|
+
const config = await loadConfigForCheck();
|
|
33
|
+
if (!isCurrent()) return;
|
|
34
|
+
continuousPossible = Boolean(config.continuous && config.agentMode);
|
|
35
|
+
if (!continuousPossible) {
|
|
36
|
+
await continuous?.stop(ctx.sessionManager);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { createContinuousSync } = await import("./sync/continuous-sync.js");
|
|
40
|
+
if (!isCurrent()) return;
|
|
41
|
+
continuous ??= createContinuousSync(loaders);
|
|
42
|
+
continuous.start(ctx, signal, Boolean(retry && (retry.setup === undefined || retry.setup === config.setupName)));
|
|
43
|
+
} catch {
|
|
44
|
+
/* Existing initialization/command paths report settings errors. */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
pi.registerCommand("sync", {
|
|
24
49
|
description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
|
|
@@ -28,23 +53,39 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
28
53
|
throw new Error("/sync requires TUI or RPC mode so results and safety prompts are observable.");
|
|
29
54
|
}
|
|
30
55
|
const signal = sessionAbort.signal;
|
|
31
|
-
const
|
|
32
|
-
if (signal.aborted) return;
|
|
33
|
-
await check.stop();
|
|
34
|
-
if (signal.aborted) return;
|
|
56
|
+
const manager = ctx.sessionManager;
|
|
35
57
|
const command = splitArgs(args)[0];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
let retry: { setup?: string } | undefined;
|
|
59
|
+
let mayResume = false;
|
|
60
|
+
foreground.set(manager, (foreground.get(manager) ?? 0) + 1);
|
|
61
|
+
try {
|
|
62
|
+
const ready = await initialization;
|
|
63
|
+
mayResume = ready !== false;
|
|
64
|
+
if (signal.aborted) return;
|
|
65
|
+
await continuous?.stop(manager);
|
|
66
|
+
if (signal.aborted) return;
|
|
67
|
+
await check.stop();
|
|
68
|
+
if (signal.aborted) return;
|
|
69
|
+
if (ready === false && command !== "help" && command !== "unlock" && command !== "migrate-state") {
|
|
70
|
+
ctx.ui.notify(
|
|
71
|
+
"pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
|
|
72
|
+
"error",
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const run = () =>
|
|
77
|
+
handleCommand(args, ctx, signal, loaders, attention, (setup) => {
|
|
78
|
+
retry = { setup };
|
|
79
|
+
});
|
|
80
|
+
// Drain background children before foreground guards. Help remains reachable during repair.
|
|
81
|
+
if (command === "migrate-state" || command === "help") await run();
|
|
82
|
+
else await withStateDirectoryAccess(run);
|
|
83
|
+
} finally {
|
|
84
|
+
const count = (foreground.get(manager) ?? 1) - 1;
|
|
85
|
+
if (count) foreground.set(manager, count);
|
|
86
|
+
else foreground.delete(manager);
|
|
87
|
+
if (mayResume) await resumeContinuous(ctx, signal, retry);
|
|
42
88
|
}
|
|
43
|
-
const run = () => handleCommand(args, ctx, signal, loaders, attention);
|
|
44
|
-
// Drain background cache/child cleanup before entering any foreground guard.
|
|
45
|
-
// Help must remain reachable when the state roots themselves need repair.
|
|
46
|
-
if (command === "migrate-state" || command === "help") await run();
|
|
47
|
-
else await withStateDirectoryAccess(run);
|
|
48
89
|
},
|
|
49
90
|
});
|
|
50
91
|
|
|
@@ -54,11 +95,15 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
54
95
|
sessionAbort.abort(new DOMException("Session replaced", "AbortError"));
|
|
55
96
|
sessionAbort = new AbortController();
|
|
56
97
|
const signal = sessionAbort.signal;
|
|
98
|
+
owner = ctx.sessionManager;
|
|
99
|
+
continuousPossible = false;
|
|
57
100
|
attention.reset(ctx);
|
|
58
101
|
const previous = initialization;
|
|
59
102
|
initialization = (async () => {
|
|
60
103
|
await previous;
|
|
61
104
|
if (signal.aborted) return false;
|
|
105
|
+
await continuous?.stop();
|
|
106
|
+
if (signal.aborted) return false;
|
|
62
107
|
await check.stop();
|
|
63
108
|
if (signal.aborted) return false;
|
|
64
109
|
try {
|
|
@@ -72,18 +117,27 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
72
117
|
}
|
|
73
118
|
})();
|
|
74
119
|
const ready = await initialization;
|
|
75
|
-
if (ready && !signal.aborted)
|
|
120
|
+
if (ready && !signal.aborted) {
|
|
121
|
+
if (ready.continuous && ready.agentMode) await resumeContinuous(ctx, signal);
|
|
122
|
+
else check.start(ctx, signal, ready);
|
|
123
|
+
}
|
|
76
124
|
});
|
|
77
125
|
|
|
78
126
|
pi.on("agent_start", async (_event, ctx) => {
|
|
127
|
+
await continuous?.stop(ctx.sessionManager);
|
|
79
128
|
await check.interrupt(ctx);
|
|
80
129
|
});
|
|
81
130
|
|
|
82
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
83
|
-
|
|
131
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
132
|
+
const signal = sessionAbort.signal;
|
|
133
|
+
if (!signal.aborted) {
|
|
134
|
+
check.settled(ctx);
|
|
135
|
+
if (continuousPossible) await resumeContinuous(ctx, signal);
|
|
136
|
+
}
|
|
84
137
|
});
|
|
85
138
|
|
|
86
139
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
140
|
+
if (continuous && owner && owner !== ctx.sessionManager) return;
|
|
87
141
|
sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
|
|
88
142
|
attention.reset(ctx);
|
|
89
143
|
shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError"));
|
|
@@ -94,6 +148,8 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
94
148
|
try {
|
|
95
149
|
const ready = await initialization;
|
|
96
150
|
if (signal.aborted) return;
|
|
151
|
+
await continuous?.stop(ctx.sessionManager);
|
|
152
|
+
if (signal.aborted) return;
|
|
97
153
|
await check.stop();
|
|
98
154
|
if (signal.aborted) return;
|
|
99
155
|
if (ready !== false && reason !== "reload") {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { requireSettings, updateLocalConfig } from "../settings/settings-store.js";
|
|
4
|
+
import { captureMutationOwner } from "../sync/sync-local.js";
|
|
5
|
+
import { confirmMergeReview } from "./merge-review.js";
|
|
6
|
+
import { safeTerminalText } from "./terminal-text.js";
|
|
7
|
+
|
|
8
|
+
export const AGENT_MODE_DESCRIPTION =
|
|
9
|
+
"This setup, experimental: one autonomous merged sync next startup and on Sync now. Sends eligible conflict text to the selected Pi model; may incur costs and replace/delete selected local and remote content without transfer prompts. No automatic reload. Unsupported conflicts pause.";
|
|
10
|
+
|
|
11
|
+
export const CONTINUOUS_DESCRIPTION =
|
|
12
|
+
"This setup, experimental: recurring sync while Pi is open. Requires Agent mode. Watches selected files, checks the remote every 60–66 seconds, and retries temporary failures. Local changes and retries can cause extra requests between polls. May disclose conflict text and incur model/storage costs. No automatic reload.";
|
|
13
|
+
|
|
14
|
+
export function saveContinuousMode(
|
|
15
|
+
ctx: ExtensionCommandContext,
|
|
16
|
+
setupName: string,
|
|
17
|
+
enabled: boolean,
|
|
18
|
+
signal?: AbortSignal,
|
|
19
|
+
) {
|
|
20
|
+
return saveAgentMode(ctx, setupName, enabled, signal, "continuous");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveAgentMode(
|
|
24
|
+
ctx: ExtensionCommandContext,
|
|
25
|
+
setupName: string,
|
|
26
|
+
enabled: boolean,
|
|
27
|
+
signal?: AbortSignal,
|
|
28
|
+
preference: "agentMode" | "continuous" = "agentMode",
|
|
29
|
+
) {
|
|
30
|
+
const validate = captureMutationOwner(ctx, signal);
|
|
31
|
+
const settings = await requireSettings();
|
|
32
|
+
validate();
|
|
33
|
+
const setup = settings.syncSetups[setupName];
|
|
34
|
+
if (!setup || settings.activeSyncSetup !== setupName) throw new Error("Current sync setup changed; reopen Settings.");
|
|
35
|
+
const continuous = preference === "continuous";
|
|
36
|
+
const label = continuous ? "Continuous sync" : "Agent mode";
|
|
37
|
+
if (enabled && continuous && !setup.sync.agentMode) throw new Error("Enable Agent mode before Continuous sync.");
|
|
38
|
+
if (Boolean(setup.sync[preference]) === enabled) return true;
|
|
39
|
+
if (enabled) {
|
|
40
|
+
const summary = [
|
|
41
|
+
`Setup: ${safeTerminalText(setupName)}`,
|
|
42
|
+
`Storage connection: ${safeTerminalText(setup.storage.connection)}`,
|
|
43
|
+
`Storage path: ${safeTerminalText(setup.storage.path)}`,
|
|
44
|
+
...(setup.storage.bucket ? [`Bucket: ${safeTerminalText(String(setup.storage.bucket))}`] : []),
|
|
45
|
+
...(setup.storage.branch ? [`Branch: ${safeTerminalText(String(setup.storage.branch))}`] : []),
|
|
46
|
+
`Included paths: ${setup.sync.include.map(safeTerminalText).join(", ")}`,
|
|
47
|
+
continuous ? CONTINUOUS_DESCRIPTION : AGENT_MODE_DESCRIPTION,
|
|
48
|
+
...(continuous
|
|
49
|
+
? [
|
|
50
|
+
"Upgrade all Pi processes sharing this agent directory before enabling Continuous sync. Monitoring starts after the /sync command or manager closes. An elected idle local session uses its selected Pi model; another session can use a different provider/model. Each computer is a separate replica.",
|
|
51
|
+
"Automatic requests are limited to two per candidate and two per replica binding in a rolling 15-minute window, across restarts. Network/storage checks and requests may incur costs. Retries start at five seconds and grow to five minutes. These are request bounds, not a currency cap.",
|
|
52
|
+
]
|
|
53
|
+
: setup.sync.continuous
|
|
54
|
+
? ["The saved Continuous sync preference will resume monitoring after the /sync command or manager closes."]
|
|
55
|
+
: []),
|
|
56
|
+
"Only bounded Markdown/plain-text conflicts and atomic global settings choices are eligible. Model errors can change intended behavior. Session histories, code, deletion conflicts and path collisions are not model-resolved.",
|
|
57
|
+
"The existing baseline, selection, secret-scan and publication checks remain. Pi Sync cannot exclude arbitrary concurrent local writers. Disabling does not undo completed transfers or provider charges. No transfer starts from Settings.",
|
|
58
|
+
].join("\n\n");
|
|
59
|
+
if (
|
|
60
|
+
!(await confirmMergeReview(
|
|
61
|
+
ctx,
|
|
62
|
+
`Enable experimental ${label}?`,
|
|
63
|
+
summary,
|
|
64
|
+
signal,
|
|
65
|
+
() => !signal?.aborted,
|
|
66
|
+
`Enable ${label}`,
|
|
67
|
+
summary.split("\n"),
|
|
68
|
+
))
|
|
69
|
+
)
|
|
70
|
+
return false;
|
|
71
|
+
validate();
|
|
72
|
+
}
|
|
73
|
+
await updateLocalConfig((current) => {
|
|
74
|
+
validate();
|
|
75
|
+
if (
|
|
76
|
+
current.activeSyncSetup !== setupName ||
|
|
77
|
+
!isDeepStrictEqual(current.syncSetups[setupName], setup) ||
|
|
78
|
+
!isDeepStrictEqual(
|
|
79
|
+
current.storageConnections[setup.storage.connection],
|
|
80
|
+
settings.storageConnections[setup.storage.connection],
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
throw new Error("Sync setup or connection changed during Agent mode review; reopen Settings.");
|
|
84
|
+
return {
|
|
85
|
+
...current,
|
|
86
|
+
syncSetups: { ...current.syncSetups, [setupName]: { ...setup, sync: { ...setup.sync, [preference]: enabled } } },
|
|
87
|
+
};
|
|
88
|
+
}, signal);
|
|
89
|
+
// The atomic writer has committed. Later cancellation cannot undo this save.
|
|
90
|
+
return true;
|
|
91
|
+
}
|
package/src/ui/manager-state.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type SyncAttentionState,
|
|
22
22
|
syncAttentionMatchesConfig,
|
|
23
23
|
} from "./sync-attention.js";
|
|
24
|
+
import { startupTransferSummary } from "./sync-behavior.js";
|
|
24
25
|
import { countValidSyncSetups } from "./sync-setups-ui.js";
|
|
25
26
|
import { safeTerminalText } from "./terminal-text.js";
|
|
26
27
|
|
|
@@ -28,6 +29,8 @@ export const MAIN_MENU_ACTIONS = [
|
|
|
28
29
|
"Sync now",
|
|
29
30
|
"Pull from remote…",
|
|
30
31
|
"Push to remote…",
|
|
32
|
+
"View differences",
|
|
33
|
+
"Check remote status",
|
|
31
34
|
"Settings",
|
|
32
35
|
"History",
|
|
33
36
|
"Diagnostics",
|
|
@@ -121,7 +124,7 @@ export async function describeManagerState(
|
|
|
121
124
|
const lastAppliedSnapshot = changesBlocked
|
|
122
125
|
? "Unavailable while operations are locked"
|
|
123
126
|
: syncState?.lastAppliedSnapshot
|
|
124
|
-
? safeTerminalText(syncState.lastAppliedSnapshot)
|
|
127
|
+
? `${truncateToWidth(safeTerminalText(syncState.lastAppliedSnapshot), 20, "…")} (application time unavailable; full ID in History)`
|
|
125
128
|
: syncState
|
|
126
129
|
? "Never synced"
|
|
127
130
|
: "Unavailable";
|
|
@@ -134,10 +137,7 @@ export async function describeManagerState(
|
|
|
134
137
|
"",
|
|
135
138
|
`Current sync setup: ${safeTerminalText(config.setupName)}`,
|
|
136
139
|
`Storage: ${backendStorageDescription(config)}`,
|
|
137
|
-
|
|
138
|
-
`Automatic sync: ${automaticSyncSummary(config.automatic)}`,
|
|
139
|
-
`Automatic transfer at startup: ${config.automaticTransfer ? "On — upload/replace/delete at idle; no reload" : "Off"}`,
|
|
140
|
-
`Last applied: ${lastAppliedSnapshot}`,
|
|
140
|
+
"CURRENT STATE",
|
|
141
141
|
...(syncState?.unresolved?.length
|
|
142
142
|
? [
|
|
143
143
|
`Unresolved: ${syncState.unresolved.length} dependency groups · choose Review unresolved conflicts`,
|
|
@@ -160,11 +160,17 @@ export async function describeManagerState(
|
|
|
160
160
|
"Check-time observation only; synchronization rechecks current content.",
|
|
161
161
|
]
|
|
162
162
|
: ["Remote status: Not checked"]),
|
|
163
|
+
`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"}`,
|
|
164
|
+
`Automatic sync: ${automaticSyncSummary(config.automatic)}`,
|
|
165
|
+
`Startup transfer: ${startupTransferSummary(config)}`,
|
|
166
|
+
`Agent mode: ${config.agentMode ? "On (experimental; autonomous merged sync)" : "Off"}`,
|
|
167
|
+
...(config.continuous ? await (await import("../sync/continuous-record.js")).continuousStatusLines(config) : []),
|
|
168
|
+
`Last applied: ${lastAppliedSnapshot}`,
|
|
163
169
|
...(noSyncedContent
|
|
164
170
|
? ["", "No included content is selected. Choose included content in Settings before syncing."]
|
|
165
171
|
: []),
|
|
166
172
|
"",
|
|
167
|
-
"
|
|
173
|
+
"ACTIONS · transfer, inspect, or manage",
|
|
168
174
|
];
|
|
169
175
|
return {
|
|
170
176
|
title: (changesBlocked ? ["Manage sync", ...operationStatusLines(operation)] : ordinaryTitle).join("\n"),
|
package/src/ui/manager-ui.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
|
-
import { loadConfig } from "../settings/config.js";
|
|
3
|
+
import { loadConfig, syncCheckConfigFingerprint } from "../settings/config.js";
|
|
4
4
|
import { operationBlocksChanges } from "../state/operation-availability.js";
|
|
5
|
+
import { captureMutationOwner } from "../sync/sync-local.js";
|
|
5
6
|
import { type RunRoute, runCancellableOperation } from "./cancellable-operation.js";
|
|
6
7
|
import {
|
|
7
8
|
attentionMainMenuItems,
|
|
@@ -11,12 +12,13 @@ import {
|
|
|
11
12
|
} from "./manager-attention.js";
|
|
12
13
|
import { recoverSyncAccess } from "./manager-recovery.js";
|
|
13
14
|
import { dispatchManagerResult } from "./manager-result-dispatcher.js";
|
|
14
|
-
import { describeManagerState } from "./manager-state.js";
|
|
15
|
+
import { backendStorageDescription, describeManagerState } from "./manager-state.js";
|
|
15
16
|
import { showSyncSettings } from "./settings-ui.js";
|
|
16
17
|
import { showSyncSetupManager } from "./setup/setup-actions.js";
|
|
17
18
|
import { showSetupSwitcher } from "./setup/setup-switcher.js";
|
|
18
19
|
import { showStorageConnections } from "./storage-connections-ui.js";
|
|
19
20
|
import { observationMatchesConfig } from "./sync-attention.js";
|
|
21
|
+
import { safeTerminalText } from "./terminal-text.js";
|
|
20
22
|
|
|
21
23
|
export async function showSyncManager(
|
|
22
24
|
ctx: ExtensionCommandContext,
|
|
@@ -36,6 +38,8 @@ export async function showSyncManager(
|
|
|
36
38
|
| "settings"
|
|
37
39
|
| "pull"
|
|
38
40
|
| "push"
|
|
41
|
+
| "diff"
|
|
42
|
+
| "status"
|
|
39
43
|
| "setups"
|
|
40
44
|
| "connections"
|
|
41
45
|
| "history"
|
|
@@ -167,12 +171,43 @@ export async function showSyncManager(
|
|
|
167
171
|
});
|
|
168
172
|
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
169
173
|
},
|
|
170
|
-
|
|
171
|
-
const result = await runCancellableOperation(ctx, "
|
|
174
|
+
diff: async () => {
|
|
175
|
+
const result = await runCancellableOperation(ctx, "Comparing local and remote…", "diff", runRoute, {
|
|
176
|
+
signal: sessionSignal,
|
|
177
|
+
});
|
|
178
|
+
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
179
|
+
},
|
|
180
|
+
status: async () => {
|
|
181
|
+
const result = await runCancellableOperation(ctx, "Checking remote status…", "status", runRoute, {
|
|
172
182
|
signal: sessionSignal,
|
|
173
183
|
});
|
|
174
184
|
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
175
185
|
},
|
|
186
|
+
doctor: async ({ signal: actionSignal }) => {
|
|
187
|
+
const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
|
|
188
|
+
const validate = captureMutationOwner(ctx, signal);
|
|
189
|
+
const config = await loadConfig();
|
|
190
|
+
validate();
|
|
191
|
+
const confirmed = await ctx.ui.confirm(
|
|
192
|
+
"Run diagnostics?",
|
|
193
|
+
`Setup: ${safeTerminalText(config.setupName)}\nStorage: ${backendStorageDescription(config)}\nS3/R2 reads the latest pointer without writing. Git reads remote snapshots and checks its cache. WebDAV probes conditional writes and cleanup, and can repair a missing history entry. Possible-secret checks still run. This is not the read-only Check remote status action.`,
|
|
194
|
+
{ signal },
|
|
195
|
+
);
|
|
196
|
+
if (signal.aborted) return { kind: "close" };
|
|
197
|
+
validate();
|
|
198
|
+
if (!confirmed) return { kind: "stay" };
|
|
199
|
+
const latest = await loadConfig();
|
|
200
|
+
validate();
|
|
201
|
+
if (syncCheckConfigFingerprint(config) !== syncCheckConfigFingerprint(latest)) {
|
|
202
|
+
ctx.ui.notify("Setup changed during diagnostics review; reopen Diagnostics.", "warning");
|
|
203
|
+
return { kind: "stay" };
|
|
204
|
+
}
|
|
205
|
+
const result = await runCancellableOperation(ctx, "Checking setup access…", "doctor", runRoute, {
|
|
206
|
+
signal,
|
|
207
|
+
target: config.setupName,
|
|
208
|
+
});
|
|
209
|
+
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
210
|
+
},
|
|
176
211
|
recover: async ({ state, signal: actionSignal }) => {
|
|
177
212
|
const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
|
|
178
213
|
return { kind: result === "close" ? "close" : "stay" };
|
|
@@ -228,6 +263,8 @@ function syncMainMenuItem(
|
|
|
228
263
|
| "sync"
|
|
229
264
|
| "pull"
|
|
230
265
|
| "push"
|
|
266
|
+
| "diff"
|
|
267
|
+
| "status"
|
|
231
268
|
| "switch"
|
|
232
269
|
| "settings"
|
|
233
270
|
| "setups"
|
|
@@ -244,6 +281,8 @@ function syncMainMenuItem(
|
|
|
244
281
|
"Sync now": "sync",
|
|
245
282
|
"Pull from remote…": "pull",
|
|
246
283
|
"Push to remote…": "push",
|
|
284
|
+
"View differences": "diff",
|
|
285
|
+
"Check remote status": "status",
|
|
247
286
|
"Switch sync setup": "switch",
|
|
248
287
|
Settings: "settings",
|
|
249
288
|
History: "history",
|
|
@@ -263,8 +302,19 @@ function syncMainMenuItem(
|
|
|
263
302
|
id: action,
|
|
264
303
|
label,
|
|
265
304
|
action,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
305
|
+
description:
|
|
306
|
+
action === "doctor"
|
|
307
|
+
? "Manage: review backend checks before running."
|
|
308
|
+
: action === "settings" || action === "history" || action === "switch"
|
|
309
|
+
? "Manage"
|
|
310
|
+
: action === "sync"
|
|
311
|
+
? "Transfer: choose a safe direction or review conflicts."
|
|
312
|
+
: action === "pull"
|
|
313
|
+
? "Transfer: remote → local; review writes and removals."
|
|
314
|
+
: action === "push"
|
|
315
|
+
? "Transfer: local → remote active snapshot; review publication."
|
|
316
|
+
: action === "status" || action === "diff"
|
|
317
|
+
? "Inspect: read current local and remote state without transferring files."
|
|
318
|
+
: undefined,
|
|
269
319
|
};
|
|
270
320
|
}
|
package/src/ui/merge-review.ts
CHANGED
|
@@ -60,9 +60,9 @@ export async function confirmMergeReview(
|
|
|
60
60
|
})
|
|
61
61
|
: await runMenu(ctx, menu, { getState: () => undefined, signal, isCurrent, onError: () => {} });
|
|
62
62
|
if (!isCurrent() || signal?.aborted) return false;
|
|
63
|
-
if (result.kind === "error") throw new Error("
|
|
63
|
+
if (result.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
|
|
64
64
|
if (result.kind === "unsupported")
|
|
65
|
-
throw new Error("
|
|
65
|
+
throw new Error("Sync review requires observable TUI or RPC; review the plan before using --yes.");
|
|
66
66
|
if (result.kind === "completed") choice.value = result.value;
|
|
67
67
|
else if (result.kind !== "closed") return false;
|
|
68
68
|
// Keep authorization separate from UI termination in both adapters.
|
|
@@ -79,7 +79,7 @@ export async function confirmMergeReview(
|
|
|
79
79
|
onError: () => {},
|
|
80
80
|
});
|
|
81
81
|
if (!isCurrent() || signal?.aborted) return false;
|
|
82
|
-
if (detail.kind === "error") throw new Error("
|
|
82
|
+
if (detail.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
|
|
83
83
|
if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
|
|
84
84
|
// A fresh menu discards remembered selection: returning from details always selects No.
|
|
85
85
|
// Unlike Pi's native Yes-first dialog, approval requires moving off the safe default.
|
|
@@ -2,13 +2,8 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { PublicationCapability } from "../backends/sync-backend.js";
|
|
3
3
|
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
4
4
|
import type { FileMergeDecision } from "../sync/file-merge-planner.js";
|
|
5
|
-
import { fileHashMap } from "../sync/sync-state.js";
|
|
6
5
|
import { safeTerminalText, snapshotPathLabel } from "./terminal-text.js";
|
|
7
|
-
|
|
8
|
-
interface FileChange {
|
|
9
|
-
path: string;
|
|
10
|
-
action: "Add" | "Update" | "Delete";
|
|
11
|
-
}
|
|
6
|
+
import { transferChanges as changes, type TransferChange as FileChange } from "./transfer-changes.js";
|
|
12
7
|
|
|
13
8
|
export interface MergedSyncSummaryInput {
|
|
14
9
|
setupName: string;
|
|
@@ -78,6 +73,7 @@ export function mergedSyncSummary(input: MergedSyncSummaryInput) {
|
|
|
78
73
|
const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
|
|
79
74
|
const safety = [
|
|
80
75
|
`Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
|
|
76
|
+
"Remote removals affect the active snapshot; retained history is not erased.",
|
|
81
77
|
`Sessions: ${input.sessions ? "included — may contain private conversations" : "not included"}`,
|
|
82
78
|
input.groups.length
|
|
83
79
|
? `Unresolved groups: ${input.groups.length} — these paths stay unchanged on each side`
|
|
@@ -114,15 +110,3 @@ export function mergedSyncSummary(input: MergedSyncSummaryInput) {
|
|
|
114
110
|
].join("\n"),
|
|
115
111
|
};
|
|
116
112
|
}
|
|
117
|
-
|
|
118
|
-
function changes(before: Snapshot, after: Snapshot): FileChange[] {
|
|
119
|
-
const beforeMap = fileHashMap(before);
|
|
120
|
-
const afterMap = fileHashMap(after);
|
|
121
|
-
return [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])]
|
|
122
|
-
.sort()
|
|
123
|
-
.flatMap((filePath): FileChange[] => {
|
|
124
|
-
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
125
|
-
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
126
|
-
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
127
|
-
});
|
|
128
|
-
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { SyncSetupReviewChangedError, updateSyncSetup } from "../settings/settings-management.js";
|
|
12
12
|
import type { AnySyncConfig, PartialConfig } from "../settings/settings-types.js";
|
|
13
13
|
import { readSnapshotForHead } from "../sync/remote-snapshot.js";
|
|
14
|
+
import { throwIfAborted } from "../sync/signals.js";
|
|
14
15
|
import { errorMessage } from "../sync/sync-errors.js";
|
|
15
16
|
import {
|
|
16
17
|
compareSyncInclude,
|
|
@@ -240,6 +241,8 @@ async function showSelectionDifference(
|
|
|
240
241
|
include: [...state.decision.remoteInclude],
|
|
241
242
|
automatic: review.config.automatic,
|
|
242
243
|
automaticTransfer: review.config.automaticTransfer,
|
|
244
|
+
agentMode: review.config.agentMode,
|
|
245
|
+
continuous: review.config.continuous,
|
|
243
246
|
onSwitch: review.config.onSwitch,
|
|
244
247
|
showStatus: review.config.showStatus,
|
|
245
248
|
};
|
|
@@ -592,6 +595,8 @@ function sameContinuationReview(left: PartialConfig, right: PartialConfig) {
|
|
|
592
595
|
left.storagePath === right.storagePath &&
|
|
593
596
|
left.bucket === right.bucket &&
|
|
594
597
|
left.branch === right.branch &&
|
|
598
|
+
Boolean(left.agentMode) === Boolean(right.agentMode) &&
|
|
599
|
+
Boolean(left.continuous) === Boolean(right.continuous) &&
|
|
595
600
|
sameSyncInclude(left.include, right.include)
|
|
596
601
|
);
|
|
597
602
|
}
|
|
@@ -639,8 +644,3 @@ function runWithOptionalStateAccess<T>(
|
|
|
639
644
|
function combineSignals(sessionSignal: AbortSignal | undefined, actionSignal: AbortSignal) {
|
|
640
645
|
return sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
|
|
641
646
|
}
|
|
642
|
-
|
|
643
|
-
function throwIfAborted(signal: AbortSignal) {
|
|
644
|
-
if (!signal.aborted) return;
|
|
645
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
646
|
-
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/** Adapt only public rendering/input boundaries; Kit still owns search, saves and focus. */
|
|
5
|
+
export function boundedSettingsUI(ctx: ExtensionCommandContext): ExtensionCommandContext["ui"] {
|
|
6
|
+
return {
|
|
7
|
+
...ctx.ui,
|
|
8
|
+
custom: (factory, options) =>
|
|
9
|
+
ctx.ui.custom((tui, ...args) => {
|
|
10
|
+
// Kit pins search before the selected setting at a one-row budget. Render its
|
|
11
|
+
// smallest complete frame, then retain the active row when the real budget is smaller.
|
|
12
|
+
const terminal = new Proxy(tui.terminal, {
|
|
13
|
+
get: (target, key) => (key === "rows" ? Math.max(8, target.rows) : Reflect.get(target, key)),
|
|
14
|
+
});
|
|
15
|
+
const screenTui = new Proxy(tui, {
|
|
16
|
+
get(target, key) {
|
|
17
|
+
if (key === "terminal") return terminal;
|
|
18
|
+
const value = Reflect.get(target, key);
|
|
19
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const guard = <
|
|
23
|
+
T extends {
|
|
24
|
+
render(width: number): string[];
|
|
25
|
+
handleMouse?: import("@earendil-works/pi-tui").Component["handleMouse"];
|
|
26
|
+
},
|
|
27
|
+
>(
|
|
28
|
+
component: T,
|
|
29
|
+
): T => {
|
|
30
|
+
const render = component.render.bind(component);
|
|
31
|
+
const handleMouse = component.handleMouse?.bind(component);
|
|
32
|
+
let sourceRow = 0;
|
|
33
|
+
const compact = () => tui.terminal.rows < 8;
|
|
34
|
+
component.render = (width) => {
|
|
35
|
+
if (width <= 0) return [];
|
|
36
|
+
const lines = render(width);
|
|
37
|
+
if (!compact()) return lines;
|
|
38
|
+
const active = lines.findIndex((line) => stripVTControlCharacters(line).startsWith("→"));
|
|
39
|
+
const search = lines.findIndex((line) => stripVTControlCharacters(line).startsWith(">"));
|
|
40
|
+
sourceRow = active < 0 ? Math.max(0, search) : active;
|
|
41
|
+
return lines.slice(sourceRow, sourceRow + 1);
|
|
42
|
+
};
|
|
43
|
+
if (handleMouse)
|
|
44
|
+
component.handleMouse = (event) => {
|
|
45
|
+
if (event.width <= 0 || (compact() && event.y !== 0)) return undefined;
|
|
46
|
+
component.render(event.width);
|
|
47
|
+
return handleMouse(compact() ? { ...event, y: event.y + sourceRow } : event);
|
|
48
|
+
};
|
|
49
|
+
return component;
|
|
50
|
+
};
|
|
51
|
+
const component = factory(screenTui, ...args);
|
|
52
|
+
return component instanceof Promise ? component.then(guard) : guard(component);
|
|
53
|
+
}, options),
|
|
54
|
+
};
|
|
55
|
+
}
|