@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 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. Detected changes leave a compact hint above the editor; open `/sync` when ready. An included-content mismatch puts **Review synced content (recommended)** in the manager, where a fresh review verifies the remote snapshot before offering changes.
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 hint.
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 check results use nonblocking notifications; included-content review remains read-only.
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. Unchanged results are quiet; changes or 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
+ 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
- if (!state && (!observation || !observationNeedsAttention(observation))) {
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 observationNeedsAttention(observation) {
137
+ function classifyObservation(observation) {
125
138
  const result = observation.inspection;
126
- return result.emptyInclude || result.firstSync || !result.head || result.localChanged || result.remoteChanged || result.selectionState?.kind !== "same";
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-S7YBZR42.ts.map
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-S7YBZR42.ts";
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-R3XPUFFS.ts.map
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-S7YBZR42.ts";
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-R3XPUFFS.ts");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.50.1",
3
+ "version": "0.50.2",
4
4
  "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- if (!state && (!observation || !observationNeedsAttention(observation))) {
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 function observationNeedsAttention(observation: StartupObservation) {
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
- return (
167
- result.emptyInclude ||
168
- result.firstSync ||
169
- !result.head ||
170
- result.localChanged ||
171
- result.remoteChanged ||
172
- result.selectionState?.kind !== "same"
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": "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",
6
- "names": []
7
- }