pi-lean-search 0.6.0 → 0.7.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.
Files changed (2) hide show
  1. package/index.ts +65 -16
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -19,9 +19,12 @@ import type {
19
19
  import {
20
20
  defineToolset,
21
21
  TOOLSET_EVENTS,
22
- getDefaultResolutionMode,
23
22
  } from "pi-tool-masking";
24
- import type { ToolsetSpec, ToolsetChangedEvent } from "pi-tool-masking";
23
+ import type {
24
+ ToolsetSpec,
25
+ ToolsetChangedEvent,
26
+ BranchReader,
27
+ } from "pi-tool-masking";
25
28
  import { readSearxngUrl } from "./search-config.js";
26
29
  import { webSearchTool, normalizeBaseUrl } from "./web-search-tool.js";
27
30
 
@@ -51,6 +54,21 @@ let _lastDegraded = false;
51
54
  /** Cached ExtensionContext for event-driven glyph rendering. */
52
55
  let _lastCtx: ExtensionContext | null = null;
53
56
 
57
+ /** Live sessionManager reference for the co-activation mirror — stored at
58
+ * session_start; the library invokes getBranch() fresh per apply, so the
59
+ * stored reference stays correct across cascades and external writes. */
60
+ let _sessionManager: BranchReader | null = null;
61
+
62
+ /** Unsubscribe for the co-activation mirror (registered inside session_start,
63
+ * torn down in session_shutdown so re-fires don't stack listeners). */
64
+ let _offMirror: (() => void) | null = null;
65
+
66
+ /** Unsubscribers for the factory-time glyph-sync listeners — pi re-invokes
67
+ * the cached extension factory per load pass, so these are re-registered
68
+ * each time and torn down in the reset block / session_shutdown to avoid
69
+ * stacking duplicate listeners on the shared event bus. */
70
+ let _offSync: (() => void)[] = [];
71
+
54
72
  // ─── Health probes ───────────────────────────────────────────────
55
73
 
56
74
  /**
@@ -170,6 +188,8 @@ function _resetStateForTest(): void {
170
188
  _lastHealth = null;
171
189
  _lastDegraded = false;
172
190
  _lastCtx = null;
191
+ _sessionManager = null;
192
+ _offMirror = null;
173
193
  }
174
194
 
175
195
  export { _resetStateForTest };
@@ -177,6 +197,17 @@ export { _resetStateForTest };
177
197
  // ─── Extension entry point ───────────────────────────────────────
178
198
 
179
199
  export default function (pi: ExtensionAPI) {
200
+ // --- Ensure idempotent re-invocation ----------------------------
201
+ // pi reuses the cached extension factory on /resume (same cwd),
202
+ // which re-invokes this function with the same module-level
203
+ // singletons. Reset the mirror state so the second load doesn't
204
+ // skip re-registration behind a stale _offMirror.
205
+ _offMirror?.();
206
+ _offMirror = null;
207
+ _sessionManager = null;
208
+ for (const off of _offSync) off();
209
+ _offSync = [];
210
+
180
211
  // ── Register the web-search tool ─────────────────────────
181
212
  pi.registerTool(webSearchTool);
182
213
 
@@ -185,23 +216,27 @@ export default function (pi: ExtensionAPI) {
185
216
  const searchToolset = defineToolset(pi, SEARCH_WEB_SPEC);
186
217
 
187
218
  // ── Co-activation: mirror pi-lean-dimension.web changed events ─
188
- // Listen on changed ONLY, not restored.
189
- //
190
- // Focus-mode guard: while allowlist focus holds the line, skip
191
- // co-activation. The focus set is authoritative, so a web `changed` event
192
- // — including one a stale library `doRestore` emits during resume — must
193
- // not disable search or write a focus-indistinguishable {enabled} entry.
194
- pi.events.on(TOOLSET_EVENTS.changed, (data: unknown) => {
219
+ // Listen on changed ONLY, not restored. Registered inside session_start
220
+ // (after _sessionManager is stored) so the reader always exists when the
221
+ // mirror runs — there is no reader-less window.
222
+ const mirrorWebChanged = (data: unknown) => {
195
223
  const event = data as ToolsetChangedEvent;
196
- if (event.id === "pi-lean-dimension.web") {
197
- if (getDefaultResolutionMode() === "allowlist") return;
224
+ if (event.id !== "pi-lean-dimension.web") return;
225
+ // Catch AllowlistModeError by name, never `instanceof` — the handle
226
+ // may come from a different library copy via the shared registry;
227
+ // refusal means "nothing changed" — skip co-activation.
228
+ try {
198
229
  if (event.enabled) {
199
- searchToolset.enable(pi);
230
+ searchToolset.enable(pi, _sessionManager!);
200
231
  } else {
201
- searchToolset.disable(pi);
232
+ searchToolset.disable(pi, _sessionManager!);
233
+ }
234
+ } catch (err) {
235
+ if ((err as { name?: string } | undefined)?.name !== "AllowlistModeError") {
236
+ throw err;
202
237
  }
203
238
  }
204
- });
239
+ };
205
240
 
206
241
  // ── Keep cached state in sync with library events ────────
207
242
  const syncSearchState = (data: unknown) => {
@@ -211,13 +246,22 @@ export default function (pi: ExtensionAPI) {
211
246
  if (_lastCtx) renderSearchGlyph(_lastCtx);
212
247
  }
213
248
  };
214
- pi.events.on(TOOLSET_EVENTS.changed, syncSearchState);
215
- pi.events.on(TOOLSET_EVENTS.restored, syncSearchState);
249
+ _offSync = [
250
+ pi.events.on(TOOLSET_EVENTS.changed, syncSearchState),
251
+ pi.events.on(TOOLSET_EVENTS.restored, syncSearchState),
252
+ ];
216
253
 
217
254
  // ── Session start: health probe + glyph ──────────────────
218
255
  pi.on("session_start", async (event, ctx) => {
256
+ _sessionManager = ctx.sessionManager;
219
257
  _lastCtx = ctx;
220
258
 
259
+ // Register the co-activation mirror once, after the reader is stored
260
+ // (dedup across re-fires of session_start for the same instance).
261
+ if (!_offMirror) {
262
+ _offMirror = pi.events.on(TOOLSET_EVENTS.changed, mirrorWebChanged);
263
+ }
264
+
221
265
  // Re-read config in case it changed between sessions
222
266
  _searxngUrl = readSearxngUrl();
223
267
 
@@ -271,7 +315,12 @@ export default function (pi: ExtensionAPI) {
271
315
 
272
316
  // ── Session shutdown: clean up ───────────────────────────
273
317
  pi.on("session_shutdown", async (_event, ctx) => {
318
+ _offMirror?.();
319
+ _offMirror = null;
320
+ for (const off of _offSync) off();
321
+ _offSync = [];
274
322
  _lastCtx = null;
323
+ _sessionManager = null;
275
324
  try {
276
325
  ctx?.ui?.setStatus?.("search", "");
277
326
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lean-search",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "SearXNG web search for Pi. Self-hosted or public instance, no API keys or quotas; returns a setup message instead of erroring when unreachable. Pairs with pi-lean-portal's /web toggle or works standalone.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -36,7 +36,7 @@
36
36
  "test": "vitest run"
37
37
  },
38
38
  "dependencies": {
39
- "pi-tool-masking": "^1.3.0"
39
+ "pi-tool-masking": "^2.0.1"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@earendil-works/pi-ai": "*",