@narumitw/pi-sync 0.50.1 → 0.50.2
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 +5 -4
- package/dist/chunks/{chunk-S7YBZR42.ts → chunk-5B3FXBLE.ts} +29 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +7 -0
- package/dist/chunks/{manager-ui-R3XPUFFS.ts → manager-ui-5KLKVMRC.ts} +2 -2
- package/dist/index.ts +2 -2
- package/package.json +1 -1
- package/src/ui/sync-attention.ts +37 -13
- package/dist/chunks/chunk-S7YBZR42.ts.map +0 -7
- /package/dist/chunks/{manager-ui-R3XPUFFS.ts.map → manager-ui-5KLKVMRC.ts.map} +0 -0
package/README.md
CHANGED
|
@@ -131,14 +131,15 @@ Then choose one reviewed direction:
|
|
|
131
131
|
Cancelling a preparation or confirmation returns to conflict resolution with no side effects.
|
|
132
132
|
Back returns to the sync manager, and Ctrl+C closes the complete flow.
|
|
133
133
|
|
|
134
|
-
Startup checks never open a dialog.
|
|
134
|
+
Startup checks never open a dialog. With a sync baseline and no content-list mismatch, ordinary one-sided changes appear only as **local changes pending** or **remote changes pending** in status. A persistent widget above the editor is reserved for both sides changing, first sync with an existing remote snapshot, a differing content list (including order-only differences), or a missing remote snapshot despite an existing baseline. Both sides changing is a reason to review, not a confirmed file conflict.
|
|
135
|
+
No selected content and first sync with an empty remote stay quiet outside `/sync`, which provides setup or initialization guidance. Legacy remote metadata without an authoritative content list does not independently trigger a widget; the baseline and change conditions still apply. An included-content mismatch puts **Review synced content (recommended)** in the manager, where a fresh review verifies the remote snapshot before offering changes.
|
|
135
136
|
Check results are advisory observations against the last sync baseline, not proof of a file conflict or current equality. Opening the manager uses local information and shows when the check completed; it does not contact remote storage. Transfer actions recheck current content.
|
|
136
|
-
Attention stays in memory and is invalidated by relevant settings/state changes or a foreground transfer's commit boundary, and cleared on session replacement or shutdown. Cancelling a review or a failure before commit preserves a still-valid
|
|
137
|
+
The widget has no expiry timer. Attention stays in memory and is invalidated by relevant settings/state changes or a foreground transfer's commit boundary, and cleared on session replacement or shutdown. A newer observation replaces the presentation, clearing the widget when only status or no reminder is needed. Cancelling a review or a failure before commit preserves a still-valid observation and its appropriate presentation.
|
|
137
138
|
|
|
138
139
|
Interactive TUI `/sync sync`, `/sync pull`, and `/sync push` routes without `--yes` open the same review flow when they detect the mismatch.
|
|
139
140
|
Explicit `--yes` routes remain non-interactive and report exact remote-only, device-only, or order-only guidance while leaving visible attention for later review.
|
|
140
141
|
Shutdown automatic sync never opens a dialog because Pi is exiting.
|
|
141
|
-
RPC startup
|
|
142
|
+
RPC startup checks use status for one-sided changes and nonblocking warning notifications for review conditions; included-content review remains read-only.
|
|
142
143
|
Print and JSON modes do not support `/sync` because UI output is not observable there.
|
|
143
144
|
|
|
144
145
|
## ⚙️ Settings
|
|
@@ -149,7 +150,7 @@ Missing settings stay unconfigured without creating files or locks.
|
|
|
149
150
|
|
|
150
151
|
### Background startup checks
|
|
151
152
|
|
|
152
|
-
With **Automatic sync** enabled, session startup schedules a background check instead of waiting for a transfer. Pi remains usable while Git, WebDAV, R2, or S3 checks local hashes and remote metadata.
|
|
153
|
+
With **Automatic sync** enabled, session startup schedules a background check instead of waiting for a transfer. Pi remains usable while Git, WebDAV, R2, or S3 checks local hashes and remote metadata. Results follow the [status and review classifications](#resolve-conflicts-in-the-manager); check failures provide `/sync` guidance without opening a dialog. Use **Sync now** to start a reviewed transfer, or `/sync status` to retry a check. Foreground `/sync` cancels and drains the background check before starting.
|
|
153
154
|
|
|
154
155
|
Checks run once per session start, including `/reload`, new, resumed, and forked sessions, in TUI and RPC only. Print/JSON skip startup checks. There is no polling or automatic check retry loop. The overall deadline is 30 seconds, followed by underlying cleanup where needed; Git process termination and temporary-ref cleanup can take additional time. Git checks can still fetch objects and update the extension's private bare cache, but never push, apply managed files, update the sync baseline, or reload Pi.
|
|
155
156
|
|
|
@@ -36,7 +36,7 @@ function createSyncAttentionController() {
|
|
|
36
36
|
observation = void 0;
|
|
37
37
|
},
|
|
38
38
|
notifyObservation(ctx) {
|
|
39
|
-
if (observation && observationNeedsAttention(observation)) {
|
|
39
|
+
if (ctx.hasUI && observation && observationNeedsAttention(observation)) {
|
|
40
40
|
ctx.ui.notify(observationLines(observation).join("\n"), "warning");
|
|
41
41
|
}
|
|
42
42
|
},
|
|
@@ -65,20 +65,33 @@ function createSyncAttentionController() {
|
|
|
65
65
|
},
|
|
66
66
|
async publish(ctx, signal) {
|
|
67
67
|
const currentGeneration = ++generation;
|
|
68
|
-
if (signal?.aborted) return;
|
|
69
|
-
|
|
68
|
+
if (signal?.aborted || !ctx.hasUI) return;
|
|
69
|
+
const classification = observation ? classifyObservation(observation) : "none";
|
|
70
|
+
if (!state && (classification === "none" || classification === "guidance")) {
|
|
70
71
|
clearAttentionPresentation(ctx);
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
74
|
+
if (!state && classification === "status") {
|
|
75
|
+
ctx.ui.setWidget(WIDGET_KEY, void 0);
|
|
76
|
+
ctx.ui.setStatus(
|
|
77
|
+
STATUS_KEY,
|
|
78
|
+
observation?.inspection.remoteChanged ? "remote changes pending" : "local changes pending"
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
73
82
|
const presentation = state ? attentionPresentation(state.decision) : {
|
|
74
83
|
status: "changes to review",
|
|
75
84
|
lines: observationLines(observation)
|
|
76
85
|
};
|
|
77
86
|
if (ctx.mode !== "tui") {
|
|
87
|
+
ctx.ui.setWidget(WIDGET_KEY, void 0);
|
|
78
88
|
ctx.ui.setStatus(STATUS_KEY, presentation.status);
|
|
79
89
|
return;
|
|
80
90
|
}
|
|
81
|
-
const { EditorStatusWidget } = await import("./attention-widget-Z7G3SYYJ.ts")
|
|
91
|
+
const { EditorStatusWidget } = await import("./attention-widget-Z7G3SYYJ.ts").catch((error) => {
|
|
92
|
+
if (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
82
95
|
if (generation !== currentGeneration || signal?.aborted) return;
|
|
83
96
|
ctx.ui.setStatus(STATUS_KEY, presentation.status);
|
|
84
97
|
ctx.ui.setWidget(
|
|
@@ -121,9 +134,17 @@ function observationSummary(observation) {
|
|
|
121
134
|
return "Legacy remote has no authoritative content list";
|
|
122
135
|
return "No changes detected since last sync";
|
|
123
136
|
}
|
|
124
|
-
function
|
|
137
|
+
function classifyObservation(observation) {
|
|
125
138
|
const result = observation.inspection;
|
|
126
|
-
|
|
139
|
+
if (result.emptyInclude) return "guidance";
|
|
140
|
+
if (result.selectionState?.kind === "different") return "review";
|
|
141
|
+
if (result.firstSync) return result.head ? "review" : "guidance";
|
|
142
|
+
if (!result.head || result.localChanged && result.remoteChanged) return "review";
|
|
143
|
+
if (result.localChanged || result.remoteChanged) return "status";
|
|
144
|
+
return "none";
|
|
145
|
+
}
|
|
146
|
+
function observationNeedsAttention(observation) {
|
|
147
|
+
return classifyObservation(observation) === "review";
|
|
127
148
|
}
|
|
128
149
|
function observationLines(observation) {
|
|
129
150
|
return [
|
|
@@ -133,6 +154,7 @@ function observationLines(observation) {
|
|
|
133
154
|
];
|
|
134
155
|
}
|
|
135
156
|
function clearAttentionPresentation(ctx) {
|
|
157
|
+
if (!ctx.hasUI) return;
|
|
136
158
|
ctx.ui.setStatus(STATUS_KEY, void 0);
|
|
137
159
|
ctx.ui.setWidget(WIDGET_KEY, void 0);
|
|
138
160
|
}
|
|
@@ -143,4 +165,4 @@ export {
|
|
|
143
165
|
observationMatchesConfig,
|
|
144
166
|
observationSummary
|
|
145
167
|
};
|
|
146
|
-
//# sourceMappingURL=chunk-
|
|
168
|
+
//# sourceMappingURL=chunk-5B3FXBLE.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/sync-attention.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { syncCheckConfigFingerprint, syncConfigReviewFingerprint } from \"../settings/config.js\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { StartupObservation } from \"../sync/sync-inspection.js\";\nimport {\n\tcompareSyncInclude,\n\ttype RemoteSelectionDecision,\n\tsameSyncInclude,\n} from \"../sync/sync-policy.js\";\nimport { safeTerminalText } from \"./terminal-text.js\";\n\nconst STATUS_KEY = \"sync\";\nconst WIDGET_KEY = \"sync:attention\";\n\nexport type SyncAttentionOrigin = \"sync\" | \"pull\" | \"push\";\n\nexport interface SyncAttentionState {\n\tdecision: RemoteSelectionDecision;\n\torigin: SyncAttentionOrigin;\n\toffered: boolean;\n}\n\nexport function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {\n\treturn (\n\t\tattention.decision.setupName === config.setupName &&\n\t\tattention.decision.configIdentity === syncConfigReviewFingerprint(config) &&\n\t\tsameSyncInclude(attention.decision.localInclude, config.include)\n\t);\n}\n\nexport interface SyncAttentionController {\n\tobserve(observation: StartupObservation): void;\n\tobservation(): StartupObservation | undefined;\n\tclearObservation(): void;\n\tnotifyObservation(ctx: ExtensionContext): void;\n\tset(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;\n\tcurrent(): SyncAttentionState | undefined;\n\tmarkOffered(): boolean;\n\tclear(ctx: ExtensionContext): void;\n\treset(ctx: ExtensionContext): void;\n\tpublish(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;\n}\n\nexport function createSyncAttentionController(): SyncAttentionController {\n\tlet state: SyncAttentionState | undefined;\n\tlet observation: StartupObservation | undefined;\n\tlet generation = 0;\n\n\treturn {\n\t\tobserve(value) {\n\t\t\tgeneration++;\n\t\t\tobservation = value;\n\t\t},\n\t\tobservation() {\n\t\t\treturn observation;\n\t\t},\n\t\tclearObservation() {\n\t\t\tgeneration++;\n\t\t\tobservation = undefined;\n\t\t},\n\t\tnotifyObservation(ctx) {\n\t\t\tif (ctx.hasUI && observation && observationNeedsAttention(observation)) {\n\t\t\t\tctx.ui.notify(observationLines(observation).join(\"\\n\"), \"warning\");\n\t\t\t}\n\t\t},\n\t\tset(decision, origin) {\n\t\t\tgeneration++;\n\t\t\tstate = { decision, origin, offered: false };\n\t\t},\n\t\tcurrent() {\n\t\t\treturn state;\n\t\t},\n\t\tmarkOffered() {\n\t\t\tif (!state || state.offered) return false;\n\t\t\tstate = { ...state, offered: true };\n\t\t\treturn true;\n\t\t},\n\t\tclear(ctx) {\n\t\t\tgeneration++;\n\t\t\tstate = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\treset(ctx) {\n\t\t\tgeneration++;\n\t\t\tstate = undefined;\n\t\t\tobservation = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\tasync publish(ctx, signal) {\n\t\t\tconst currentGeneration = ++generation;\n\t\t\tif (signal?.aborted || !ctx.hasUI) return;\n\t\t\tconst classification = observation ? classifyObservation(observation) : \"none\";\n\t\t\tif (!state && (classification === \"none\" || classification === \"guidance\")) {\n\t\t\t\tclearAttentionPresentation(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!state && classification === \"status\") {\n\t\t\t\tctx.ui.setWidget(WIDGET_KEY, undefined);\n\t\t\t\tctx.ui.setStatus(\n\t\t\t\t\tSTATUS_KEY,\n\t\t\t\t\tobservation?.inspection.remoteChanged\n\t\t\t\t\t\t? \"remote changes pending\"\n\t\t\t\t\t\t: \"local changes pending\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst presentation = state\n\t\t\t\t? attentionPresentation(state.decision)\n\t\t\t\t: {\n\t\t\t\t\t\tstatus: \"changes to review\",\n\t\t\t\t\t\tlines: observationLines(observation as StartupObservation),\n\t\t\t\t\t};\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\tctx.ui.setWidget(WIDGET_KEY, undefined);\n\t\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Keep Kit outside the eager startup graph; module loading owns no cancellable resources.\n\t\t\tconst { EditorStatusWidget } = await import(\"./attention-widget.js\").catch((error) => {\n\t\t\t\tif (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tif (generation !== currentGeneration || signal?.aborted) return;\n\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\tctx.ui.setWidget(\n\t\t\t\tWIDGET_KEY,\n\t\t\t\t(_tui, theme) =>\n\t\t\t\t\tnew EditorStatusWidget({\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\trenderBody: (width) =>\n\t\t\t\t\t\t\tpresentation.lines.map((line, index) =>\n\t\t\t\t\t\t\t\twidth === 0\n\t\t\t\t\t\t\t\t\t? \"\"\n\t\t\t\t\t\t\t\t\t: truncateToWidth(theme.fg(index === 0 ? \"warning\" : \"muted\", line), width, \"\u2026\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t};\n}\n\nfunction attentionPresentation(decision: RemoteSelectionDecision) {\n\tconst setupName = safeTerminalText(decision.setupName);\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\tconst difference =\n\t\tcomparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? \"Only list order differs\"\n\t\t\t: `Remote ${comparison.remoteOnly.length} \u00B7 Device ${comparison.localOnly.length}`;\n\treturn {\n\t\tstatus: \"review needed\",\n\t\tlines: [`Pi Sync needs review \u00B7 ${setupName}`, difference, \"No changes \u00B7 Run /sync to review\"],\n\t};\n}\n\nexport function observationMatchesConfig(observation: StartupObservation, config: AnySyncConfig) {\n\treturn observation.configIdentity === syncCheckConfigFingerprint(config);\n}\n\nexport function observationSummary(observation: StartupObservation) {\n\tconst result = observation.inspection;\n\tif (result.emptyInclude) return \"No included content selected\";\n\tif (result.selectionState?.kind === \"different\")\n\t\treturn \"Synced-content list differs; review needed\";\n\tif (result.firstSync)\n\t\treturn result.head\n\t\t\t? \"No sync baseline; review remote and local content\"\n\t\t\t: \"Remote empty; no sync baseline\";\n\tif (!result.head) return \"Remote snapshot missing; review needed\";\n\tif (result.localChanged && result.remoteChanged)\n\t\treturn \"Local and remote changed since last sync; review needed\";\n\tif (result.remoteChanged) return \"Remote changed since last sync\";\n\tif (result.localChanged) return \"Local content changed since last sync\";\n\tif (result.selectionState?.kind === \"legacy\")\n\t\treturn \"Legacy remote has no authoritative content list\";\n\treturn \"No changes detected since last sync\";\n}\n\nexport type ObservationClassification = \"none\" | \"guidance\" | \"status\" | \"review\";\n\n/** Presentation only: never discard an observation or authorize a transfer here. */\nexport function classifyObservation(observation: StartupObservation): ObservationClassification {\n\tconst result = observation.inspection;\n\t// Sync now exits before reading remote content when nothing is selected.\n\tif (result.emptyInclude) return \"guidance\";\n\t// Ordering is part of the existing transfer policy, not merely display order.\n\tif (result.selectionState?.kind === \"different\") return \"review\";\n\tif (result.firstSync) return result.head ? \"review\" : \"guidance\";\n\tif (!result.head || (result.localChanged && result.remoteChanged)) return \"review\";\n\tif (result.localChanged || result.remoteChanged) return \"status\";\n\t// Legacy metadata alone requires no scope confirmation in transfer operations.\n\treturn \"none\";\n}\n\nexport function observationNeedsAttention(observation: StartupObservation) {\n\treturn classifyObservation(observation) === \"review\";\n}\n\nfunction observationLines(observation: StartupObservation) {\n\treturn [\n\t\t`Pi Sync check \u00B7 ${safeTerminalText(observation.setupName)}`,\n\t\tobservationSummary(observation),\n\t\t\"No startup transfer \u00B7 Run /sync to review\",\n\t];\n}\n\nfunction clearAttentionPresentation(ctx: ExtensionContext) {\n\tif (!ctx.hasUI) return;\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
observationMatchesConfig,
|
|
20
20
|
observationSummary,
|
|
21
21
|
syncAttentionMatchesConfig
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-5B3FXBLE.ts";
|
|
23
23
|
import {
|
|
24
24
|
inspectLock,
|
|
25
25
|
isLockGuardHeld,
|
|
@@ -1677,4 +1677,4 @@ function syncMainMenuItem(label) {
|
|
|
1677
1677
|
export {
|
|
1678
1678
|
showSyncManager
|
|
1679
1679
|
};
|
|
1680
|
-
//# sourceMappingURL=manager-ui-
|
|
1680
|
+
//# sourceMappingURL=manager-ui-5KLKVMRC.ts.map
|
package/dist/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createSyncAttentionController,
|
|
5
5
|
observationMatchesConfig,
|
|
6
6
|
syncAttentionMatchesConfig
|
|
7
|
-
} from "./chunks/chunk-
|
|
7
|
+
} from "./chunks/chunk-5B3FXBLE.ts";
|
|
8
8
|
import {
|
|
9
9
|
recoverSnapshotTransactionsOnStartup
|
|
10
10
|
} from "./chunks/chunk-IJ6FJFA5.ts";
|
|
@@ -368,7 +368,7 @@ async function handleCommand(rawArgs, ctx, sessionSignal, loaders, attention) {
|
|
|
368
368
|
);
|
|
369
369
|
if (!rawArgs.trim()) {
|
|
370
370
|
try {
|
|
371
|
-
const { showSyncManager } = await import("./chunks/manager-ui-
|
|
371
|
+
const { showSyncManager } = await import("./chunks/manager-ui-5KLKVMRC.ts");
|
|
372
372
|
if (sessionSignal.aborted) return;
|
|
373
373
|
await showSyncManager(ctx, run, sessionSignal, {
|
|
374
374
|
getAttention: () => attention.current(),
|
package/package.json
CHANGED
package/src/ui/sync-attention.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function createSyncAttentionController(): SyncAttentionController {
|
|
|
60
60
|
observation = undefined;
|
|
61
61
|
},
|
|
62
62
|
notifyObservation(ctx) {
|
|
63
|
-
if (observation && observationNeedsAttention(observation)) {
|
|
63
|
+
if (ctx.hasUI && observation && observationNeedsAttention(observation)) {
|
|
64
64
|
ctx.ui.notify(observationLines(observation).join("\n"), "warning");
|
|
65
65
|
}
|
|
66
66
|
},
|
|
@@ -89,11 +89,22 @@ export function createSyncAttentionController(): SyncAttentionController {
|
|
|
89
89
|
},
|
|
90
90
|
async publish(ctx, signal) {
|
|
91
91
|
const currentGeneration = ++generation;
|
|
92
|
-
if (signal?.aborted) return;
|
|
93
|
-
|
|
92
|
+
if (signal?.aborted || !ctx.hasUI) return;
|
|
93
|
+
const classification = observation ? classifyObservation(observation) : "none";
|
|
94
|
+
if (!state && (classification === "none" || classification === "guidance")) {
|
|
94
95
|
clearAttentionPresentation(ctx);
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
98
|
+
if (!state && classification === "status") {
|
|
99
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
100
|
+
ctx.ui.setStatus(
|
|
101
|
+
STATUS_KEY,
|
|
102
|
+
observation?.inspection.remoteChanged
|
|
103
|
+
? "remote changes pending"
|
|
104
|
+
: "local changes pending",
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
97
108
|
const presentation = state
|
|
98
109
|
? attentionPresentation(state.decision)
|
|
99
110
|
: {
|
|
@@ -101,11 +112,15 @@ export function createSyncAttentionController(): SyncAttentionController {
|
|
|
101
112
|
lines: observationLines(observation as StartupObservation),
|
|
102
113
|
};
|
|
103
114
|
if (ctx.mode !== "tui") {
|
|
115
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
104
116
|
ctx.ui.setStatus(STATUS_KEY, presentation.status);
|
|
105
117
|
return;
|
|
106
118
|
}
|
|
107
119
|
// Keep Kit outside the eager startup graph; module loading owns no cancellable resources.
|
|
108
|
-
const { EditorStatusWidget } = await import("./attention-widget.js")
|
|
120
|
+
const { EditorStatusWidget } = await import("./attention-widget.js").catch((error) => {
|
|
121
|
+
if (generation === currentGeneration && !signal?.aborted) clearAttentionPresentation(ctx);
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
109
124
|
if (generation !== currentGeneration || signal?.aborted) return;
|
|
110
125
|
ctx.ui.setStatus(STATUS_KEY, presentation.status);
|
|
111
126
|
ctx.ui.setWidget(
|
|
@@ -161,16 +176,24 @@ export function observationSummary(observation: StartupObservation) {
|
|
|
161
176
|
return "No changes detected since last sync";
|
|
162
177
|
}
|
|
163
178
|
|
|
164
|
-
export
|
|
179
|
+
export type ObservationClassification = "none" | "guidance" | "status" | "review";
|
|
180
|
+
|
|
181
|
+
/** Presentation only: never discard an observation or authorize a transfer here. */
|
|
182
|
+
export function classifyObservation(observation: StartupObservation): ObservationClassification {
|
|
165
183
|
const result = observation.inspection;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
// Sync now exits before reading remote content when nothing is selected.
|
|
185
|
+
if (result.emptyInclude) return "guidance";
|
|
186
|
+
// Ordering is part of the existing transfer policy, not merely display order.
|
|
187
|
+
if (result.selectionState?.kind === "different") return "review";
|
|
188
|
+
if (result.firstSync) return result.head ? "review" : "guidance";
|
|
189
|
+
if (!result.head || (result.localChanged && result.remoteChanged)) return "review";
|
|
190
|
+
if (result.localChanged || result.remoteChanged) return "status";
|
|
191
|
+
// Legacy metadata alone requires no scope confirmation in transfer operations.
|
|
192
|
+
return "none";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function observationNeedsAttention(observation: StartupObservation) {
|
|
196
|
+
return classifyObservation(observation) === "review";
|
|
174
197
|
}
|
|
175
198
|
|
|
176
199
|
function observationLines(observation: StartupObservation) {
|
|
@@ -182,6 +205,7 @@ function observationLines(observation: StartupObservation) {
|
|
|
182
205
|
}
|
|
183
206
|
|
|
184
207
|
function clearAttentionPresentation(ctx: ExtensionContext) {
|
|
208
|
+
if (!ctx.hasUI) return;
|
|
185
209
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
186
210
|
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
187
211
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/ui/sync-attention.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { syncCheckConfigFingerprint, syncConfigReviewFingerprint } from \"../settings/config.js\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { StartupObservation } from \"../sync/sync-inspection.js\";\nimport {\n\tcompareSyncInclude,\n\ttype RemoteSelectionDecision,\n\tsameSyncInclude,\n} from \"../sync/sync-policy.js\";\nimport { safeTerminalText } from \"./terminal-text.js\";\n\nconst STATUS_KEY = \"sync\";\nconst WIDGET_KEY = \"sync:attention\";\n\nexport type SyncAttentionOrigin = \"sync\" | \"pull\" | \"push\";\n\nexport interface SyncAttentionState {\n\tdecision: RemoteSelectionDecision;\n\torigin: SyncAttentionOrigin;\n\toffered: boolean;\n}\n\nexport function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {\n\treturn (\n\t\tattention.decision.setupName === config.setupName &&\n\t\tattention.decision.configIdentity === syncConfigReviewFingerprint(config) &&\n\t\tsameSyncInclude(attention.decision.localInclude, config.include)\n\t);\n}\n\nexport interface SyncAttentionController {\n\tobserve(observation: StartupObservation): void;\n\tobservation(): StartupObservation | undefined;\n\tclearObservation(): void;\n\tnotifyObservation(ctx: ExtensionContext): void;\n\tset(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;\n\tcurrent(): SyncAttentionState | undefined;\n\tmarkOffered(): boolean;\n\tclear(ctx: ExtensionContext): void;\n\treset(ctx: ExtensionContext): void;\n\tpublish(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;\n}\n\nexport function createSyncAttentionController(): SyncAttentionController {\n\tlet state: SyncAttentionState | undefined;\n\tlet observation: StartupObservation | undefined;\n\tlet generation = 0;\n\n\treturn {\n\t\tobserve(value) {\n\t\t\tgeneration++;\n\t\t\tobservation = value;\n\t\t},\n\t\tobservation() {\n\t\t\treturn observation;\n\t\t},\n\t\tclearObservation() {\n\t\t\tgeneration++;\n\t\t\tobservation = undefined;\n\t\t},\n\t\tnotifyObservation(ctx) {\n\t\t\tif (observation && observationNeedsAttention(observation)) {\n\t\t\t\tctx.ui.notify(observationLines(observation).join(\"\\n\"), \"warning\");\n\t\t\t}\n\t\t},\n\t\tset(decision, origin) {\n\t\t\tgeneration++;\n\t\t\tstate = { decision, origin, offered: false };\n\t\t},\n\t\tcurrent() {\n\t\t\treturn state;\n\t\t},\n\t\tmarkOffered() {\n\t\t\tif (!state || state.offered) return false;\n\t\t\tstate = { ...state, offered: true };\n\t\t\treturn true;\n\t\t},\n\t\tclear(ctx) {\n\t\t\tgeneration++;\n\t\t\tstate = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\treset(ctx) {\n\t\t\tgeneration++;\n\t\t\tstate = undefined;\n\t\t\tobservation = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\tasync publish(ctx, signal) {\n\t\t\tconst currentGeneration = ++generation;\n\t\t\tif (signal?.aborted) return;\n\t\t\tif (!state && (!observation || !observationNeedsAttention(observation))) {\n\t\t\t\tclearAttentionPresentation(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst presentation = state\n\t\t\t\t? attentionPresentation(state.decision)\n\t\t\t\t: {\n\t\t\t\t\t\tstatus: \"changes to review\",\n\t\t\t\t\t\tlines: observationLines(observation as StartupObservation),\n\t\t\t\t\t};\n\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Keep Kit outside the eager startup graph; module loading owns no cancellable resources.\n\t\t\tconst { EditorStatusWidget } = await import(\"./attention-widget.js\");\n\t\t\tif (generation !== currentGeneration || signal?.aborted) return;\n\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\tctx.ui.setWidget(\n\t\t\t\tWIDGET_KEY,\n\t\t\t\t(_tui, theme) =>\n\t\t\t\t\tnew EditorStatusWidget({\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\trenderBody: (width) =>\n\t\t\t\t\t\t\tpresentation.lines.map((line, index) =>\n\t\t\t\t\t\t\t\twidth === 0\n\t\t\t\t\t\t\t\t\t? \"\"\n\t\t\t\t\t\t\t\t\t: truncateToWidth(theme.fg(index === 0 ? \"warning\" : \"muted\", line), width, \"\u2026\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t};\n}\n\nfunction attentionPresentation(decision: RemoteSelectionDecision) {\n\tconst setupName = safeTerminalText(decision.setupName);\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\tconst difference =\n\t\tcomparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? \"Only list order differs\"\n\t\t\t: `Remote ${comparison.remoteOnly.length} \u00B7 Device ${comparison.localOnly.length}`;\n\treturn {\n\t\tstatus: \"review needed\",\n\t\tlines: [`Pi Sync needs review \u00B7 ${setupName}`, difference, \"No changes \u00B7 Run /sync to review\"],\n\t};\n}\n\nexport function observationMatchesConfig(observation: StartupObservation, config: AnySyncConfig) {\n\treturn observation.configIdentity === syncCheckConfigFingerprint(config);\n}\n\nexport function observationSummary(observation: StartupObservation) {\n\tconst result = observation.inspection;\n\tif (result.emptyInclude) return \"No included content selected\";\n\tif (result.selectionState?.kind === \"different\")\n\t\treturn \"Synced-content list differs; review needed\";\n\tif (result.firstSync)\n\t\treturn result.head\n\t\t\t? \"No sync baseline; review remote and local content\"\n\t\t\t: \"Remote empty; no sync baseline\";\n\tif (!result.head) return \"Remote snapshot missing; review needed\";\n\tif (result.localChanged && result.remoteChanged)\n\t\treturn \"Local and remote changed since last sync; review needed\";\n\tif (result.remoteChanged) return \"Remote changed since last sync\";\n\tif (result.localChanged) return \"Local content changed since last sync\";\n\tif (result.selectionState?.kind === \"legacy\")\n\t\treturn \"Legacy remote has no authoritative content list\";\n\treturn \"No changes detected since last sync\";\n}\n\nexport function observationNeedsAttention(observation: StartupObservation) {\n\tconst result = observation.inspection;\n\treturn (\n\t\tresult.emptyInclude ||\n\t\tresult.firstSync ||\n\t\t!result.head ||\n\t\tresult.localChanged ||\n\t\tresult.remoteChanged ||\n\t\tresult.selectionState?.kind !== \"same\"\n\t);\n}\n\nfunction observationLines(observation: StartupObservation) {\n\treturn [\n\t\t`Pi Sync check \u00B7 ${safeTerminalText(observation.setupName)}`,\n\t\tobservationSummary(observation),\n\t\t\"No startup transfer \u00B7 Run /sync to review\",\n\t];\n}\n\nfunction clearAttentionPresentation(ctx: ExtensionContext) {\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;AACA,SAAS,uBAAuB;AAWhC,IAAM,aAAa;AACnB,IAAM,aAAa;AAUZ,SAAS,2BAA2B,WAA+B,QAAuB;AAChG,SACC,UAAU,SAAS,cAAc,OAAO,aACxC,UAAU,SAAS,mBAAmB,4BAA4B,MAAM,KACxE,gBAAgB,UAAU,SAAS,cAAc,OAAO,OAAO;AAEjE;AAeO,SAAS,gCAAyD;AACxE,MAAI;AACJ,MAAI;AACJ,MAAI,aAAa;AAEjB,SAAO;AAAA,IACN,QAAQ,OAAO;AACd;AACA,oBAAc;AAAA,IACf;AAAA,IACA,cAAc;AACb,aAAO;AAAA,IACR;AAAA,IACA,mBAAmB;AAClB;AACA,oBAAc;AAAA,IACf;AAAA,IACA,kBAAkB,KAAK;AACtB,UAAI,eAAe,0BAA0B,WAAW,GAAG;AAC1D,YAAI,GAAG,OAAO,iBAAiB,WAAW,EAAE,KAAK,IAAI,GAAG,SAAS;AAAA,MAClE;AAAA,IACD;AAAA,IACA,IAAI,UAAU,QAAQ;AACrB;AACA,cAAQ,EAAE,UAAU,QAAQ,SAAS,MAAM;AAAA,IAC5C;AAAA,IACA,UAAU;AACT,aAAO;AAAA,IACR;AAAA,IACA,cAAc;AACb,UAAI,CAAC,SAAS,MAAM,QAAS,QAAO;AACpC,cAAQ,EAAE,GAAG,OAAO,SAAS,KAAK;AAClC,aAAO;AAAA,IACR;AAAA,IACA,MAAM,KAAK;AACV;AACA,cAAQ;AACR,iCAA2B,GAAG;AAAA,IAC/B;AAAA,IACA,MAAM,KAAK;AACV;AACA,cAAQ;AACR,oBAAc;AACd,iCAA2B,GAAG;AAAA,IAC/B;AAAA,IACA,MAAM,QAAQ,KAAK,QAAQ;AAC1B,YAAM,oBAAoB,EAAE;AAC5B,UAAI,QAAQ,QAAS;AACrB,UAAI,CAAC,UAAU,CAAC,eAAe,CAAC,0BAA0B,WAAW,IAAI;AACxE,mCAA2B,GAAG;AAC9B;AAAA,MACD;AACA,YAAM,eAAe,QAClB,sBAAsB,MAAM,QAAQ,IACpC;AAAA,QACA,QAAQ;AAAA,QACR,OAAO,iBAAiB,WAAiC;AAAA,MAC1D;AACF,UAAI,IAAI,SAAS,OAAO;AACvB,YAAI,GAAG,UAAU,YAAY,aAAa,MAAM;AAChD;AAAA,MACD;AAEA,YAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,gCAAuB;AACnE,UAAI,eAAe,qBAAqB,QAAQ,QAAS;AACzD,UAAI,GAAG,UAAU,YAAY,aAAa,MAAM;AAChD,UAAI,GAAG;AAAA,QACN;AAAA,QACA,CAAC,MAAM,UACN,IAAI,mBAAmB;AAAA,UACtB;AAAA,UACA,YAAY,CAAC,UACZ,aAAa,MAAM;AAAA,YAAI,CAAC,MAAM,UAC7B,UAAU,IACP,KACA,gBAAgB,MAAM,GAAG,UAAU,IAAI,YAAY,SAAS,IAAI,GAAG,OAAO,QAAG;AAAA,UACjF;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACD;AAAA,EACD;AACD;AAEA,SAAS,sBAAsB,UAAmC;AACjE,QAAM,YAAY,iBAAiB,SAAS,SAAS;AACrD,QAAM,aAAa,mBAAmB,SAAS,cAAc,SAAS,aAAa;AACnF,QAAM,aACL,WAAW,WAAW,WAAW,KAAK,WAAW,UAAU,WAAW,IACnE,4BACA,UAAU,WAAW,WAAW,MAAM,gBAAa,WAAW,UAAU,MAAM;AAClF,SAAO;AAAA,IACN,QAAQ;AAAA,IACR,OAAO,CAAC,6BAA0B,SAAS,IAAI,YAAY,qCAAkC;AAAA,EAC9F;AACD;AAEO,SAAS,yBAAyB,aAAiC,QAAuB;AAChG,SAAO,YAAY,mBAAmB,2BAA2B,MAAM;AACxE;AAEO,SAAS,mBAAmB,aAAiC;AACnE,QAAM,SAAS,YAAY;AAC3B,MAAI,OAAO,aAAc,QAAO;AAChC,MAAI,OAAO,gBAAgB,SAAS;AACnC,WAAO;AACR,MAAI,OAAO;AACV,WAAO,OAAO,OACX,sDACA;AACJ,MAAI,CAAC,OAAO,KAAM,QAAO;AACzB,MAAI,OAAO,gBAAgB,OAAO;AACjC,WAAO;AACR,MAAI,OAAO,cAAe,QAAO;AACjC,MAAI,OAAO,aAAc,QAAO;AAChC,MAAI,OAAO,gBAAgB,SAAS;AACnC,WAAO;AACR,SAAO;AACR;AAEO,SAAS,0BAA0B,aAAiC;AAC1E,QAAM,SAAS,YAAY;AAC3B,SACC,OAAO,gBACP,OAAO,aACP,CAAC,OAAO,QACR,OAAO,gBACP,OAAO,iBACP,OAAO,gBAAgB,SAAS;AAElC;AAEA,SAAS,iBAAiB,aAAiC;AAC1D,SAAO;AAAA,IACN,sBAAmB,iBAAiB,YAAY,SAAS,CAAC;AAAA,IAC1D,mBAAmB,WAAW;AAAA,IAC9B;AAAA,EACD;AACD;AAEA,SAAS,2BAA2B,KAAuB;AAC1D,MAAI,GAAG,UAAU,YAAY,MAAS;AACtC,MAAI,GAAG,UAAU,YAAY,MAAS;AACvC;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
File without changes
|