@narumitw/pi-sync 0.50.2 → 0.51.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 +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-4CHTZMEO.ts → chunk-7KHKWL3Q.ts} +37 -55
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-G4RNQQZS.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-KW7YQEZN.ts → chunk-UA7RXOIT.ts} +48 -100
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-5KLKVMRC.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-IUB6ESEU.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +92 -112
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +198 -214
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-4CHTZMEO.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-G4RNQQZS.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-KW7YQEZN.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +0 -22
- package/dist/chunks/manager-ui-5KLKVMRC.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-FKIBM3GO.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-D7RBRJNL.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
package/src/ui/sync-attention.ts
CHANGED
|
@@ -3,209 +3,193 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { syncCheckConfigFingerprint, syncConfigReviewFingerprint } from "../settings/config.js";
|
|
4
4
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
5
5
|
import type { StartupObservation } from "../sync/sync-inspection.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
type RemoteSelectionDecision,
|
|
9
|
-
sameSyncInclude,
|
|
10
|
-
} from "../sync/sync-policy.js";
|
|
6
|
+
import { compareSyncInclude, type RemoteSelectionDecision, sameSyncInclude } from "../sync/sync-policy.js";
|
|
7
|
+
import { setSyncStatus } from "./sync-status.js";
|
|
11
8
|
import { safeTerminalText } from "./terminal-text.js";
|
|
12
9
|
|
|
13
|
-
const STATUS_KEY = "sync";
|
|
14
10
|
const WIDGET_KEY = "sync:attention";
|
|
15
11
|
|
|
16
12
|
export type SyncAttentionOrigin = "sync" | "pull" | "push";
|
|
17
13
|
|
|
18
14
|
export interface SyncAttentionState {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
decision: RemoteSelectionDecision;
|
|
16
|
+
origin: SyncAttentionOrigin;
|
|
17
|
+
offered: boolean;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
export function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
return (
|
|
22
|
+
attention.decision.setupName === config.setupName &&
|
|
23
|
+
attention.decision.configIdentity === syncConfigReviewFingerprint(config) &&
|
|
24
|
+
sameSyncInclude(attention.decision.localInclude, config.include)
|
|
25
|
+
);
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
export interface SyncAttentionController {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
observe(observation: StartupObservation): void;
|
|
30
|
+
observation(): StartupObservation | undefined;
|
|
31
|
+
clearObservation(): void;
|
|
32
|
+
notifyObservation(ctx: ExtensionContext): void;
|
|
33
|
+
set(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;
|
|
34
|
+
current(): SyncAttentionState | undefined;
|
|
35
|
+
markOffered(): boolean;
|
|
36
|
+
clear(ctx: ExtensionContext): void;
|
|
37
|
+
reset(ctx: ExtensionContext): void;
|
|
38
|
+
publish(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
export function createSyncAttentionController(): SyncAttentionController {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
let state: SyncAttentionState | undefined;
|
|
43
|
+
let observation: StartupObservation | undefined;
|
|
44
|
+
let generation = 0;
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? ""
|
|
135
|
-
: truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), width, "…"),
|
|
136
|
-
),
|
|
137
|
-
}),
|
|
138
|
-
);
|
|
139
|
-
},
|
|
140
|
-
};
|
|
46
|
+
return {
|
|
47
|
+
observe(value) {
|
|
48
|
+
generation++;
|
|
49
|
+
observation = value;
|
|
50
|
+
},
|
|
51
|
+
observation() {
|
|
52
|
+
return observation;
|
|
53
|
+
},
|
|
54
|
+
clearObservation() {
|
|
55
|
+
generation++;
|
|
56
|
+
observation = undefined;
|
|
57
|
+
},
|
|
58
|
+
notifyObservation(ctx) {
|
|
59
|
+
if (ctx.hasUI && observation && observationNeedsAttention(observation)) {
|
|
60
|
+
ctx.ui.notify(observationLines(observation).join("\n"), "warning");
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
set(decision, origin) {
|
|
64
|
+
generation++;
|
|
65
|
+
state = { decision, origin, offered: false };
|
|
66
|
+
},
|
|
67
|
+
current() {
|
|
68
|
+
return state;
|
|
69
|
+
},
|
|
70
|
+
markOffered() {
|
|
71
|
+
if (!state || state.offered) return false;
|
|
72
|
+
state = { ...state, offered: true };
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
clear(ctx) {
|
|
76
|
+
generation++;
|
|
77
|
+
state = undefined;
|
|
78
|
+
clearAttentionPresentation(ctx);
|
|
79
|
+
},
|
|
80
|
+
reset(ctx) {
|
|
81
|
+
generation++;
|
|
82
|
+
state = undefined;
|
|
83
|
+
observation = undefined;
|
|
84
|
+
clearAttentionPresentation(ctx);
|
|
85
|
+
},
|
|
86
|
+
async publish(ctx, signal) {
|
|
87
|
+
const currentGeneration = ++generation;
|
|
88
|
+
if (signal?.aborted || !ctx.hasUI) return;
|
|
89
|
+
const classification = observation ? classifyObservation(observation) : "none";
|
|
90
|
+
if (!state && (classification === "none" || classification === "guidance")) {
|
|
91
|
+
clearAttentionPresentation(ctx);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!state && classification === "status") {
|
|
95
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
96
|
+
setSyncStatus(ctx, observation?.inspection.remoteChanged ? "sync ⇣" : "sync ⇡");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const presentation = state
|
|
100
|
+
? attentionPresentation(state.decision)
|
|
101
|
+
: {
|
|
102
|
+
status: "sync ⇕",
|
|
103
|
+
lines: observationLines(observation as StartupObservation),
|
|
104
|
+
};
|
|
105
|
+
if (ctx.mode !== "tui") {
|
|
106
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
107
|
+
setSyncStatus(ctx, presentation.status);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Keep Kit outside the eager startup graph; module loading owns no cancellable resources.
|
|
111
|
+
const { EditorStatusWidget } = await import("./attention-widget.js").catch((error) => {
|
|
112
|
+
if (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
if (generation !== currentGeneration || signal?.aborted) return;
|
|
116
|
+
setSyncStatus(ctx, presentation.status);
|
|
117
|
+
ctx.ui.setWidget(
|
|
118
|
+
WIDGET_KEY,
|
|
119
|
+
(_tui, theme) =>
|
|
120
|
+
new EditorStatusWidget({
|
|
121
|
+
theme,
|
|
122
|
+
renderBody: (width) =>
|
|
123
|
+
presentation.lines.map((line, index) =>
|
|
124
|
+
width === 0 ? "" : truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), width, "…"),
|
|
125
|
+
),
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
141
130
|
}
|
|
142
131
|
|
|
143
132
|
function attentionPresentation(decision: RemoteSelectionDecision) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
const setupName = safeTerminalText(decision.setupName);
|
|
134
|
+
const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
|
|
135
|
+
const difference =
|
|
136
|
+
comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
|
|
137
|
+
? "Only list order differs"
|
|
138
|
+
: `Remote ${comparison.remoteOnly.length} · Device ${comparison.localOnly.length}`;
|
|
139
|
+
return {
|
|
140
|
+
status: "sync ⇕",
|
|
141
|
+
lines: [`Pi Sync needs review · ${setupName}`, difference, "No changes · Run /sync to review"],
|
|
142
|
+
};
|
|
154
143
|
}
|
|
155
144
|
|
|
156
145
|
export function observationMatchesConfig(observation: StartupObservation, config: AnySyncConfig) {
|
|
157
|
-
|
|
146
|
+
return observation.configIdentity === syncCheckConfigFingerprint(config);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
export function observationSummary(observation: StartupObservation) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (result.remoteChanged) return "Remote changed since last sync";
|
|
173
|
-
if (result.localChanged) return "Local content changed since last sync";
|
|
174
|
-
if (result.selectionState?.kind === "legacy")
|
|
175
|
-
return "Legacy remote has no authoritative content list";
|
|
176
|
-
return "No changes detected since last sync";
|
|
150
|
+
const result = observation.inspection;
|
|
151
|
+
if (result.emptyInclude) return "No included content selected";
|
|
152
|
+
if (result.selectionState?.kind === "different") return "Synced-content list differs; review needed";
|
|
153
|
+
if (result.firstSync)
|
|
154
|
+
return result.head ? "No sync baseline; review remote and local content" : "Remote empty; no sync baseline";
|
|
155
|
+
if (!result.head) return "Remote snapshot missing; review needed";
|
|
156
|
+
if (result.localChanged && result.remoteChanged) return "Local and remote changed since last sync; review needed";
|
|
157
|
+
if (result.remoteChanged) return "Remote changed since last sync";
|
|
158
|
+
if (result.localChanged) return "Local content changed since last sync";
|
|
159
|
+
if (result.selectionState?.kind === "legacy") return "Legacy remote has no authoritative content list";
|
|
160
|
+
return "No changes detected since last sync";
|
|
177
161
|
}
|
|
178
162
|
|
|
179
163
|
export type ObservationClassification = "none" | "guidance" | "status" | "review";
|
|
180
164
|
|
|
181
165
|
/** Presentation only: never discard an observation or authorize a transfer here. */
|
|
182
166
|
export function classifyObservation(observation: StartupObservation): ObservationClassification {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
167
|
+
const result = observation.inspection;
|
|
168
|
+
// Sync now exits before reading remote content when nothing is selected.
|
|
169
|
+
if (result.emptyInclude) return "guidance";
|
|
170
|
+
// Ordering is part of the existing transfer policy, not merely display order.
|
|
171
|
+
if (result.selectionState?.kind === "different") return "review";
|
|
172
|
+
if (result.firstSync) return result.head ? "review" : "guidance";
|
|
173
|
+
if (!result.head || (result.localChanged && result.remoteChanged)) return "review";
|
|
174
|
+
if (result.localChanged || result.remoteChanged) return "status";
|
|
175
|
+
// Legacy metadata alone requires no scope confirmation in transfer operations.
|
|
176
|
+
return "none";
|
|
193
177
|
}
|
|
194
178
|
|
|
195
179
|
export function observationNeedsAttention(observation: StartupObservation) {
|
|
196
|
-
|
|
180
|
+
return classifyObservation(observation) === "review";
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
function observationLines(observation: StartupObservation) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
184
|
+
return [
|
|
185
|
+
`Pi Sync check · ${safeTerminalText(observation.setupName)}`,
|
|
186
|
+
observationSummary(observation),
|
|
187
|
+
"No startup transfer · Run /sync to review",
|
|
188
|
+
];
|
|
205
189
|
}
|
|
206
190
|
|
|
207
191
|
function clearAttentionPresentation(ctx: ExtensionContext) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
192
|
+
if (!ctx.hasUI) return;
|
|
193
|
+
setSyncStatus(ctx, undefined);
|
|
194
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
211
195
|
}
|
package/src/ui/sync-format.ts
CHANGED
|
@@ -7,159 +7,148 @@ import { fileHashMap } from "../sync/sync-state.js";
|
|
|
7
7
|
import { safeTerminalText } from "./terminal-text.js";
|
|
8
8
|
|
|
9
9
|
export function formatDiff(local: Snapshot, remote: Snapshot) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (changed === 0) lines.push("No file differences.");
|
|
32
|
-
return lines.join("\n");
|
|
10
|
+
const localMap = fileHashMap(local);
|
|
11
|
+
const remoteMap = fileHashMap(remote);
|
|
12
|
+
const allPaths = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
13
|
+
const lines = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
|
|
14
|
+
let changed = 0;
|
|
15
|
+
for (const filePath of allPaths) {
|
|
16
|
+
if (!localMap[filePath]) {
|
|
17
|
+
lines.push(`Remote only: ${filePath}`);
|
|
18
|
+
changed += 1;
|
|
19
|
+
} else if (!remoteMap[filePath]) {
|
|
20
|
+
lines.push(`Local only: ${filePath}`);
|
|
21
|
+
changed += 1;
|
|
22
|
+
} else if (localMap[filePath] !== remoteMap[filePath]) {
|
|
23
|
+
lines.push(`Different: ${filePath}`);
|
|
24
|
+
changed += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (changed === 0) lines.push("No file differences.");
|
|
28
|
+
return lines.join("\n");
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
export function formatSnapshotOnlyDiff(title: string, snapshot: Snapshot) {
|
|
36
|
-
|
|
37
|
-
"\n",
|
|
38
|
-
);
|
|
32
|
+
return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${file.path}`)].join("\n");
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
export function formatPushSummary(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
config: CommonSyncConfig,
|
|
37
|
+
destination: string,
|
|
38
|
+
upload: Snapshot,
|
|
39
|
+
head: RemoteHead | undefined,
|
|
40
|
+
preservedRemoteFileCount = 0,
|
|
41
|
+
remote?: Snapshot,
|
|
48
42
|
) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: "Possible secrets were scanned before this prompt.",
|
|
65
|
-
].join("\n");
|
|
43
|
+
return [
|
|
44
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
45
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
46
|
+
`Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
|
|
47
|
+
`Sessions: ${upload.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
48
|
+
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
49
|
+
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
50
|
+
...(remote && inspectRemoteSelection(config.include, remote).kind === "different"
|
|
51
|
+
? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."]
|
|
52
|
+
: []),
|
|
53
|
+
formatPublicationPreview(remote, upload),
|
|
54
|
+
preservedRemoteFileCount > 0
|
|
55
|
+
? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.`
|
|
56
|
+
: "Possible secrets were scanned before this prompt.",
|
|
57
|
+
].join("\n");
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
export function formatApplyPreview(local: Snapshot, remote: Snapshot) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
return formatDirectionalChanges(local, remote, {
|
|
62
|
+
add: "Add locally",
|
|
63
|
+
update: "Update locally",
|
|
64
|
+
remove: "Remove locally",
|
|
65
|
+
});
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
export function formatPullSummary(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
config: CommonSyncConfig,
|
|
70
|
+
destination: string,
|
|
71
|
+
local: Snapshot,
|
|
72
|
+
remote: Snapshot,
|
|
73
|
+
protectedSessionCount: number,
|
|
82
74
|
) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
75
|
+
return [
|
|
76
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
77
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
78
|
+
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
79
|
+
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
80
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
81
|
+
formatApplyPreview(local, remote),
|
|
82
|
+
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged.",
|
|
83
|
+
].join("\n");
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
export function formatRollbackSummary(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
config: CommonSyncConfig,
|
|
88
|
+
destination: string,
|
|
89
|
+
local: Snapshot,
|
|
90
|
+
remote: Snapshot,
|
|
91
|
+
requestedSnapshot: string,
|
|
92
|
+
protectedSessionCount: number,
|
|
101
93
|
) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
94
|
+
return [
|
|
95
|
+
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
96
|
+
`Storage location: ${safeTerminalText(destination)}`,
|
|
97
|
+
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
98
|
+
`Sessions: ${remote.syncSessions ? "included — may contain private conversations" : "not included"}`,
|
|
99
|
+
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
100
|
+
formatApplyPreview(local, remote),
|
|
101
|
+
"A local backup is created before applying; the backend's active head will change.",
|
|
102
|
+
].join("\n");
|
|
111
103
|
}
|
|
112
104
|
|
|
113
105
|
function formatPublicationPreview(remote: Snapshot | undefined, upload: Snapshot) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
remove: "Remove remotely",
|
|
123
|
-
});
|
|
106
|
+
if (!remote) {
|
|
107
|
+
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${file.path}`)].join("\n");
|
|
108
|
+
}
|
|
109
|
+
return formatDirectionalChanges(remote, upload, {
|
|
110
|
+
add: "Add remotely",
|
|
111
|
+
update: "Update remotely",
|
|
112
|
+
remove: "Remove remotely",
|
|
113
|
+
});
|
|
124
114
|
}
|
|
125
115
|
|
|
126
116
|
function formatDirectionalChanges(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
before: Snapshot,
|
|
118
|
+
after: Snapshot,
|
|
119
|
+
labels: { add: string; update: string; remove: string },
|
|
130
120
|
) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return lines.join("\n");
|
|
121
|
+
const beforeMap = fileHashMap(before);
|
|
122
|
+
const afterMap = fileHashMap(after);
|
|
123
|
+
const paths = [...new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
|
|
124
|
+
const lines: string[] = [];
|
|
125
|
+
for (const filePath of paths) {
|
|
126
|
+
if (!beforeMap[filePath]) lines.push(`${labels.add}: ${filePath}`);
|
|
127
|
+
else if (!afterMap[filePath]) lines.push(`${labels.remove}: ${filePath}`);
|
|
128
|
+
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${filePath}`);
|
|
129
|
+
}
|
|
130
|
+
if (lines.length === 0) lines.push("No file changes.");
|
|
131
|
+
return lines.join("\n");
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
export function countPreservedRemoteFiles(local: Snapshot, upload: Snapshot) {
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
const localPaths = new Set(local.files.map((file) => file.path));
|
|
136
|
+
return upload.files.filter((file) => !localPaths.has(file.path)).length;
|
|
148
137
|
}
|
|
149
138
|
|
|
150
139
|
export function publicationCapabilityDescription(capability: PublicationCapability) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
140
|
+
switch (capability) {
|
|
141
|
+
case "lease-protected":
|
|
142
|
+
return "lease-protected (exact expected-ref update)";
|
|
143
|
+
case "atomic-conditional":
|
|
144
|
+
return "atomic-conditional (verified atomic precondition)";
|
|
145
|
+
case "conditional-required":
|
|
146
|
+
return "conditional-required (read-only until atomic preconditions are verified)";
|
|
147
|
+
case "read-check-write-verify":
|
|
148
|
+
return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
|
|
149
|
+
}
|
|
161
150
|
}
|
|
162
151
|
|
|
163
152
|
export function redact(value: string) {
|
|
164
|
-
|
|
153
|
+
return value.length <= 8 ? "configured" : `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
165
154
|
}
|