pi-lean-dimension 0.2.4 → 0.3.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 (159) hide show
  1. package/README.md +1 -1
  2. package/node_modules/pi-lean-portal/AGENTS.md +15 -23
  3. package/node_modules/pi-lean-portal/README.md +31 -1
  4. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
  5. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1126
  6. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
  7. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
  8. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
  9. package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
  10. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
  11. package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
  12. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
  13. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
  15. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
  16. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
  17. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
  18. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
  19. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  20. package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
  21. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
  22. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  23. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  25. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  26. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
  27. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  28. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
  29. package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
  30. package/node_modules/pi-lean-portal/browser-toggle.ts +168 -385
  31. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
  32. package/node_modules/pi-lean-portal/contributed/README.md +65 -6
  33. package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
  34. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
  35. package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
  36. package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
  37. package/node_modules/pi-lean-portal/core/guides.ts +14 -18
  38. package/node_modules/pi-lean-portal/core/router.ts +2 -20
  39. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
  40. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
  41. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
  42. package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
  43. package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
  44. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
  45. package/node_modules/pi-lean-portal/index.ts +8 -1
  46. package/node_modules/pi-lean-portal/package.json +2 -1
  47. package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
  48. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
  49. package/node_modules/pi-lean-search/index.ts +134 -64
  50. package/node_modules/pi-lean-search/package.json +2 -1
  51. package/node_modules/playwright/README.md +3 -3
  52. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
  53. package/node_modules/playwright/lib/cli/reportActions.js +2 -2
  54. package/node_modules/playwright/lib/common/index.js +77 -52
  55. package/node_modules/playwright/lib/common/index.js.txt +10 -11
  56. package/node_modules/playwright/lib/errorContext.js +2 -3
  57. package/node_modules/playwright/lib/index.js +30 -8
  58. package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
  59. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
  60. package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
  61. package/node_modules/playwright/lib/program.js +19 -0
  62. package/node_modules/playwright/lib/runner/index.js +309 -153
  63. package/node_modules/playwright/lib/runner/index.js.txt +13 -12
  64. package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
  65. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
  66. package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
  67. package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
  68. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
  69. package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
  70. package/node_modules/playwright/lib/util.js +2 -29
  71. package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
  72. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
  73. package/node_modules/playwright/package.json +3 -6
  74. package/node_modules/playwright/types/test.d.ts +391 -4
  75. package/node_modules/playwright/types/testReporter.d.ts +79 -4
  76. package/node_modules/playwright-core/browsers.json +12 -18
  77. package/node_modules/playwright-core/lib/bootstrap.js +1 -1
  78. package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
  79. package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
  80. package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
  81. package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
  82. package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
  83. package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
  84. package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
  85. package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
  86. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  87. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  88. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  89. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  90. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  91. package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
  92. package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
  93. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
  94. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  95. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  96. package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
  97. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
  98. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
  99. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  100. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  101. package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  102. package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  103. package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
  104. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
  105. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
  106. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  107. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
  108. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  109. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  110. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  111. package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
  112. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
  113. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  114. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
  115. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
  116. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  117. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
  118. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
  119. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  120. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  121. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  122. package/node_modules/playwright-core/lib/xdg-open +338 -137
  123. package/node_modules/playwright-core/package.json +2 -2
  124. package/node_modules/playwright-core/types/protocol.d.ts +249 -36
  125. package/node_modules/playwright-core/types/structs.d.ts +3 -1
  126. package/node_modules/playwright-core/types/types.d.ts +2009 -266
  127. package/package.json +3 -3
  128. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
  129. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
  130. package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
  131. package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
  132. package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
  133. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
  134. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
  135. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
  136. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
  137. package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
  138. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
  139. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
  140. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
  141. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
  142. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
  143. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
  144. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
  145. package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
  146. package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
  147. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
  148. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
  149. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
  150. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
  151. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
  152. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
  153. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
  154. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
  155. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
  156. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
  157. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
  158. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
  159. /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
@@ -30,7 +30,7 @@ export interface NavigationSettlePage {
30
30
  }
31
31
 
32
32
  /** Options for `waitForNavigationSettle`. */
33
- export interface NavigationSettleOptions {
33
+ interface NavigationSettleOptions {
34
34
  /**
35
35
  * Maximum time (ms) to wait for page readiness (load + networkidle)
36
36
  * when a navigation is detected. Shared budget for both waits.
@@ -45,7 +45,7 @@ export interface NavigationSettleOptions {
45
45
  }
46
46
 
47
47
  /** Result of `waitForNavigationSettle`. */
48
- export interface NavigationSettleResult {
48
+ interface NavigationSettleResult {
49
49
  /** Whether a `framenavigated` event was observed on the main frame. */
50
50
  navigated: boolean;
51
51
  /** The page URL after settling (guaranteed consistent with DOM). */
@@ -13,8 +13,12 @@ import { tmpdir, homedir } from "node:os";
13
13
  import { mkdirSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
 
16
- /** Base temp directory for all pi-lean-portal ephemeral files. */
17
- export const BROWSER_TEMP_DIR = `${tmpdir()}/pi-lean-portal`;
16
+ /**
17
+ * Base temp directory for all pi-lean-portal ephemeral files.
18
+ * Override via PI_BROWSER_TEMP_DIR env var (used by test isolation).
19
+ */
20
+ export const BROWSER_TEMP_DIR =
21
+ process.env.PI_BROWSER_TEMP_DIR ?? `${tmpdir()}/pi-lean-portal`;
18
22
 
19
23
  /**
20
24
  * Portal-owned data root under ~/.pi/agent/, namespaced by package name.
@@ -57,3 +61,13 @@ export function ensureBrowserTempDir(): void {
57
61
  /* best-effort */
58
62
  }
59
63
  }
64
+
65
+ /**
66
+ * Format a byte count as a human-readable string (B, KB, MB).
67
+ * Uses the same ladder as browser-profile's profileStateSize.
68
+ */
69
+ export function formatBytes(bytes: number): string {
70
+ if (bytes < 1024) return `${bytes} B`;
71
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
72
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
73
+ }
@@ -15,15 +15,10 @@ import { join } from "node:path";
15
15
  // ─── Config paths ─────────────────────────────────────────────────
16
16
 
17
17
  /** Global pi settings path. */
18
- export const GLOBAL_SETTINGS_PATH = join(
19
- homedir(),
20
- ".pi",
21
- "agent",
22
- "settings.json",
23
- );
18
+ const GLOBAL_SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
24
19
 
25
20
  /** Project pi settings path (relative to cwd). */
26
- export const PROJECT_SETTINGS_PATH = ".pi/settings.json";
21
+ const PROJECT_SETTINGS_PATH = ".pi/settings.json";
27
22
 
28
23
  // ─── Reader ────────────────────────────────────────────────────────
29
24
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Snapshot Disk Cache — Phase 1 of the browser-intelligence plan.
2
+ * Snapshot Disk Cache — disk-backed snapshot caching.
3
3
  *
4
4
  * When compactSnapshot() truncates a page's accessibility tree, the full
5
5
  * tree is written to /tmp/pi-lean-portal/snapshot-*.txt so the agent can read
@@ -53,7 +53,7 @@ export default function (pi: ExtensionAPI) {
53
53
  // --- Plugin registration ----------------------------------------
54
54
  // Resolve plugin `dir` values against the shipped backends root first,
55
55
  // then the user-writable `~/.pi/agent/pi-lean-portal/user-backends/`
56
- // tree (Phase 0b). An absolute `dir` short-circuits both roots.
56
+ // tree. An absolute `dir` short-circuits both roots.
57
57
  const { plugins: pluginConfigs, errors: configErrors } = loadFullConfig(
58
58
  DEFAULT_BACKEND_ROOTS,
59
59
  ).plugins;
@@ -260,6 +260,13 @@ export default function (pi: ExtensionAPI) {
260
260
  updateFooterStatus(ctx);
261
261
  });
262
262
 
263
+ // Sync the status-bar glyph after /tree navigation. browser-toggle's
264
+ // session_tree handler restores the toggle state + active tools first
265
+ // (registered earlier, runs first); this repaints the glyph to match.
266
+ pi.on("session_tree", async (_event, ctx) => {
267
+ updateFooterStatus(ctx);
268
+ });
269
+
263
270
  // --- Cleanup ----------------------------------------------------
264
271
  pi.on("session_shutdown", async (_event, ctx) => {
265
272
  setLastCtx(null);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lean-portal",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension. Interactive web browsing for Pi — a /web toggle removes the tools from context when switched off, Playwright Chromium/Firefox deliver accessibility-tree snapshots, persistent profiles, cookies, and domain-matched guides; custom/stealth backends (Camoufox) plug in when a site blocks the shipped browsers.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -63,6 +63,7 @@
63
63
  "@earendil-works/pi-ai": "*",
64
64
  "@earendil-works/pi-coding-agent": "*",
65
65
  "@earendil-works/pi-tui": "*",
66
+ "pi-tool-masking": "^1.0.0",
66
67
  "typebox": "*"
67
68
  }
68
69
  }
@@ -7,7 +7,7 @@ import { Type } from "@earendil-works/pi-ai";
7
7
  import { Text } from "@earendil-works/pi-tui";
8
8
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { USER_GUIDES_DIR, invalidateGuideContent } from "../core/guides.js";
10
+ import { USER_GUIDES_DIR } from "../core/guides.js";
11
11
 
12
12
  export const webLearnTool = defineTool({
13
13
  name: "web-learn",
@@ -96,9 +96,6 @@ export const webLearnTool = defineTool({
96
96
  const isUpdate = existsSync(filepath);
97
97
  writeFileSync(filepath, fileContent, "utf-8");
98
98
 
99
- // ── Invalidate cache ────────────────────────────────────
100
- invalidateGuideContent();
101
-
102
99
  const verb = isUpdate ? "Updated" : "Created";
103
100
  return {
104
101
  content: [
@@ -6,8 +6,56 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
+ import { EventEmitter } from "node:events";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { TOOLSET_EVENTS } from "pi-tool-masking";
9
12
  import { readSearxngUrl } from "../search-config.js";
10
13
  import { webSearchTool } from "../web-search-tool.js";
14
+ import searchExtension, { _resetStateForTest } from "../index.js";
15
+
16
+ // Mock Pi used for integration-style tests (toolset wiring, glyph rendering).
17
+ // ─── Mock helpers ─────────────────────────────────────────────────────
18
+
19
+ const REGISTRY_KEY = "__piToolMaskingRegistry";
20
+
21
+ function mockSearchPi(initialActive?: string[]) {
22
+ let active = initialActive ?? ["web-search"];
23
+ const eventEmitter = new EventEmitter();
24
+ const handlers = new Map<string, Array<(...args: any[]) => void>>();
25
+ const entryCalls: Array<{ customType: string; data: unknown }> = [];
26
+
27
+ // A minimal mock of the ExtensionAPI. We intentionally don't mock
28
+ // _everything_ — only the methods the extension code actually calls.
29
+ const pi = {
30
+ getAllTools: vi.fn(() => [{ name: "web-search", description: "search" }]),
31
+ getActiveTools: vi.fn(() => [...active]),
32
+ setActiveTools: vi.fn((names: string[]) => {
33
+ active = [...names];
34
+ }),
35
+ appendEntry: vi.fn((customType: string, data?: unknown) => {
36
+ entryCalls.push({ customType, data });
37
+ }),
38
+ registerCommand: vi.fn(),
39
+ registerTool: vi.fn(),
40
+ on: vi.fn(<T>(event: string, handler: (event: T, ctx: any) => void) => {
41
+ if (!handlers.has(event)) handlers.set(event, []);
42
+ handlers.get(event)!.push(handler as any);
43
+ return () => {};
44
+ }),
45
+ get events() {
46
+ return {
47
+ emit: (channel: string, data: unknown) =>
48
+ eventEmitter.emit(channel, data),
49
+ on: (channel: string, handler: (data: unknown) => void) => {
50
+ eventEmitter.on(channel, handler);
51
+ return () => eventEmitter.off(channel, handler);
52
+ },
53
+ };
54
+ },
55
+ } as unknown as ExtensionAPI;
56
+
57
+ return { pi, events: eventEmitter, handlers, entryCalls };
58
+ }
11
59
 
12
60
  // ─── Config reader (mocked fs) ──────────────────────────────────
13
61
 
@@ -391,3 +439,42 @@ describe("execute answer rendering", () => {
391
439
  expect(r.content[0].text).toContain("Result 1");
392
440
  });
393
441
  });
442
+
443
+ // Clean module-level state between tests.
444
+ beforeEach(() => {
445
+ _resetStateForTest();
446
+ delete (globalThis as any)[REGISTRY_KEY];
447
+ });
448
+
449
+ // ==================================================================
450
+ // Toolset wiring: portal.web ↔ search.web co-activation mirror
451
+ // ==================================================================
452
+ describe("portal.web co-activation mirror", () => {
453
+ it("disables search.web when portal.web changed fires with enabled: false", async () => {
454
+ const { pi, events } = mockSearchPi();
455
+ searchExtension(pi);
456
+
457
+ events.emit(TOOLSET_EVENTS.changed, {
458
+ id: "portal.web",
459
+ enabled: false,
460
+ });
461
+
462
+ expect(pi.setActiveTools).toHaveBeenCalledWith(
463
+ expect.not.arrayContaining(["web-search"]),
464
+ );
465
+ });
466
+
467
+ it("enables search.web when portal.web changed fires with enabled: true", async () => {
468
+ const { pi, events } = mockSearchPi([]);
469
+ searchExtension(pi);
470
+
471
+ events.emit(TOOLSET_EVENTS.changed, {
472
+ id: "portal.web",
473
+ enabled: true,
474
+ });
475
+
476
+ expect(pi.setActiveTools).toHaveBeenCalledWith(
477
+ expect.arrayContaining(["web-search"]),
478
+ );
479
+ });
480
+ });
@@ -4,28 +4,49 @@
4
4
  * Registers the `web-search` tool and a `/searxng-status` diagnostic command.
5
5
  * Manages the `search` status bar slot with health-colored glyphs.
6
6
  *
7
- * On session_start, probes SearXNG reachability and updates the search slot:
8
- * ● searxng (accent/blue) — healthy and reachable
9
- * ● searxng (warning/yellow) — server up but pipeline degraded
10
- * ● searxng (error/red) — unreachable
11
- *
12
- * Portal owns the `search` slot's "off" state: when `/web off` is called,
13
- * portal writes `○ searxng` (open circle). Search overrides with the
14
- * health-colored glyph on session_start.
7
+ * Owns the `search.web` toolset (co-activated off `portal.web`):
8
+ * ● searxng (accent/blue) — healthy and reachable
9
+ * ● searxng (warning/yellow) — server up but pipeline degraded
10
+ * ● searxng (error/red) — unreachable
11
+ * ○ searxng — search tools off
15
12
  */
16
13
 
17
14
  import type {
18
15
  ExtensionAPI,
19
16
  ExtensionContext,
17
+ ThemeColor,
20
18
  } from "@earendil-works/pi-coding-agent";
19
+ import { defineToolset, TOOLSET_EVENTS } from "pi-tool-masking";
20
+ import type { ToolsetSpec, ToolsetChangedEvent } from "pi-tool-masking";
21
21
  import { readSearxngUrl } from "./search-config.js";
22
22
  import { webSearchTool } from "./web-search-tool.js";
23
23
 
24
- // ─── Module-level state ───────────────────────────────────────────
24
+ // ─── Toolset spec ────────────────────────────────────────────────
25
+
26
+ const SEARCH_WEB_SPEC: ToolsetSpec = {
27
+ id: "search.web",
28
+ names: new Set(["web-search"]),
29
+ persistKey: "toolset-state:search.web",
30
+ defaultEnabled: true,
31
+ };
32
+
33
+ // ─── Module-level state ──────────────────────────────────────────
25
34
 
26
35
  /** Cached SearXNG URL (read once at startup, stable mid-session). */
27
36
  let _searxngUrl: string | undefined;
28
37
 
38
+ /** Last known search.web enabled state (updated by library events). */
39
+ let _lastSearchEnabled = true;
40
+
41
+ /** Last known health state: true=healthy, false=unreachable, null=unconfigured. */
42
+ let _lastHealth: boolean | null = null;
43
+
44
+ /** Whether the search pipeline is degraded (server up, aggregation broken). */
45
+ let _lastDegraded = false;
46
+
47
+ /** Cached ExtensionContext for event-driven glyph rendering. */
48
+ let _lastCtx: ExtensionContext | null = null;
49
+
29
50
  // ─── Health probes ───────────────────────────────────────────────
30
51
 
31
52
  /**
@@ -95,7 +116,7 @@ async function checkSearchReachable(
95
116
  }
96
117
  }
97
118
 
98
- // ─── Status slot helpers ──────────────────────────────────────────
119
+ // ─── Status slot helpers ─────────────────────────────────────────
99
120
 
100
121
  /**
101
122
  * Normalize the SearXNG URL for display (strip protocol prefix for brevity).
@@ -105,60 +126,65 @@ function displayUrl(url: string): string {
105
126
  }
106
127
 
107
128
  /**
108
- * Set the `search` status slot with a health-colored glyph.
109
- *
110
- * Coloring:
111
- * - Healthy: accent (blue) — same as browser on
112
- * - Degraded: warning (yellow/gold) — server up but pipeline broken
113
- * - Unhealthy: error (red) — unreachable or unconfigured
129
+ * Render the `search` status slot based on toolset state + health.
114
130
  *
115
- * Portal writes `○ searxng` on /web off; search overrides here on
116
- * session_start or /searxng-status.
131
+ * search.web off → ○ searxng
132
+ * search.web on + healthy → ● searxng (accent/blue)
133
+ * search.web on + degraded → ● searxng (warning/yellow)
134
+ * search.web on + unreachable → ● searxng (error/red)
135
+ * unconfigured → (clear slot)
117
136
  */
118
- function setSearchStatus(
119
- ctx: ExtensionContext,
120
- healthy: boolean | null,
121
- degraded: boolean,
122
- ): void {
123
- if (healthy === null) {
124
- // Unconfigured — no glyph
137
+ function renderSearchGlyph(ctx: {
138
+ ui: {
139
+ setStatus: (key: string, label: string) => void;
140
+ theme: { fg: (c: ThemeColor, t: string) => string };
141
+ };
142
+ }): void {
143
+ const safeSetStatus = (text: string) => {
125
144
  try {
126
- ctx.ui.setStatus("search", "");
145
+ ctx.ui.setStatus("search", text);
127
146
  } catch {
128
- // ctx.ui may be unavailable during shutdown
147
+ /* ctx.ui may be unavailable */
129
148
  }
149
+ };
150
+
151
+ // Unconfigured — clear slot entirely
152
+ if (_lastHealth === null) {
153
+ safeSetStatus("");
130
154
  return;
131
155
  }
132
156
 
133
- if (!healthy) {
134
- // Unreachable
135
- try {
136
- ctx.ui.setStatus("search", ctx.ui.theme.fg("error", "●") + " searxng");
137
- } catch {
138
- // ignore
139
- }
157
+ // search.web disabled — show off state
158
+ if (!_lastSearchEnabled) {
159
+ safeSetStatus("○ searxng");
140
160
  return;
141
161
  }
142
162
 
143
- if (degraded) {
144
- // Server up but pipeline broken
145
- try {
146
- ctx.ui.setStatus("search", ctx.ui.theme.fg("warning", "●") + " searxng");
147
- } catch {
148
- // ignore
149
- }
163
+ // search.web enabled — show health-colored glyph
164
+ if (!_lastHealth) {
165
+ safeSetStatus(ctx.ui.theme.fg("error", "●") + " searxng");
150
166
  return;
151
167
  }
152
-
153
- // Healthy
154
- try {
155
- ctx.ui.setStatus("search", ctx.ui.theme.fg("accent", "●") + " searxng");
156
- } catch {
157
- // ignore
168
+ if (_lastDegraded) {
169
+ safeSetStatus(ctx.ui.theme.fg("warning", "●") + " searxng");
170
+ return;
158
171
  }
172
+ safeSetStatus(ctx.ui.theme.fg("accent", "●") + " searxng");
159
173
  }
160
174
 
161
- // ─── Extension entry point ────────────────────────────────────────
175
+ // ─── Test helpers ─────────────────────────────────────────────────
176
+
177
+ /** @internal Reset cached state to defaults (test helper). */
178
+ function _resetStateForTest(): void {
179
+ _lastSearchEnabled = true;
180
+ _lastHealth = null;
181
+ _lastDegraded = false;
182
+ _lastCtx = null;
183
+ }
184
+
185
+ export { _resetStateForTest };
186
+
187
+ // ─── Extension entry point ───────────────────────────────────────
162
188
 
163
189
  export default function (pi: ExtensionAPI) {
164
190
  // Read config at startup
@@ -167,48 +193,83 @@ export default function (pi: ExtensionAPI) {
167
193
  // ── Register the web-search tool ─────────────────────────
168
194
  pi.registerTool(webSearchTool);
169
195
 
170
- // ── Session start: health probe + status update ──────────
196
+ // ── Define the search.web toolset ────────────────────────
197
+ // Registers restore handler on session_start / session_tree.
198
+ const searchToolset = defineToolset(pi, SEARCH_WEB_SPEC);
199
+
200
+ // ── Co-activation: mirror portal.web changed events ──────
201
+ // Listen on changed ONLY, not restored (§10.1).
202
+ pi.events.on(TOOLSET_EVENTS.changed, (data: unknown) => {
203
+ const event = data as ToolsetChangedEvent;
204
+ if (event.id === "portal.web") {
205
+ if (event.enabled) {
206
+ searchToolset.enable(pi);
207
+ } else {
208
+ searchToolset.disable(pi);
209
+ }
210
+ }
211
+ });
212
+
213
+ // ── Keep cached state in sync with library events ────────
214
+ const syncSearchState = (data: unknown) => {
215
+ const event = data as ToolsetChangedEvent;
216
+ if (event.id === "search.web") {
217
+ _lastSearchEnabled = event.enabled;
218
+ if (_lastCtx) renderSearchGlyph(_lastCtx);
219
+ }
220
+ };
221
+ pi.events.on(TOOLSET_EVENTS.changed, syncSearchState);
222
+ pi.events.on(TOOLSET_EVENTS.restored, syncSearchState);
223
+
224
+ // ── Session start: health probe + glyph ──────────────────
171
225
  pi.on("session_start", async (_event, ctx) => {
226
+ _lastCtx = ctx;
227
+
172
228
  // Re-read config in case it changed between sessions
173
229
  _searxngUrl = readSearxngUrl();
174
230
 
175
231
  if (!_searxngUrl) {
176
- // Unconfigured — clear the slot
177
- setSearchStatus(ctx, null, false);
232
+ _lastHealth = null;
233
+ renderSearchGlyph(ctx);
178
234
  return;
179
235
  }
180
236
 
181
- // Check if web-search tools are currently active (portal
182
- // restores toggle state before search's session_start fires).
183
- const activeTools = pi.getActiveTools();
184
- if (!activeTools.includes("web-search")) {
185
- // Tools are off — don't override portal's ○ searxng
186
- return;
187
- }
237
+ // Note: by now the library's restore handler has already
238
+ // fired (registered by defineToolset), so _lastSearchEnabled
239
+ // reflects the restored toolset state.
188
240
 
189
241
  const reachable = await checkServerReachable(
190
242
  _searxngUrl,
191
243
  ctx.signal ?? undefined,
192
244
  );
245
+ _lastHealth = reachable;
246
+ _lastDegraded = false;
193
247
 
194
248
  if (reachable) {
195
- setSearchStatus(ctx, true, false);
196
249
  ctx.ui.notify(
197
250
  `🔍 SearXNG at ${displayUrl(_searxngUrl)} is available`,
198
251
  "info",
199
252
  );
200
253
  } else {
201
- setSearchStatus(ctx, false, false);
202
254
  ctx.ui.notify(
203
255
  `⚠ SearXNG at ${displayUrl(_searxngUrl)} is unreachable. ` +
204
256
  "Web search will degrade gracefully with error messages.",
205
257
  "warning",
206
258
  );
207
259
  }
260
+
261
+ renderSearchGlyph(ctx);
262
+ });
263
+
264
+ // ── Session tree: re-render with cached state ────────────
265
+ pi.on("session_tree", async (_event, ctx) => {
266
+ _lastCtx = ctx;
267
+ renderSearchGlyph(ctx);
208
268
  });
209
269
 
210
270
  // ── Session shutdown: clean up ───────────────────────────
211
271
  pi.on("session_shutdown", async (_event, ctx) => {
272
+ _lastCtx = null;
212
273
  try {
213
274
  ctx?.ui?.setStatus?.("search", "");
214
275
  } catch {
@@ -223,13 +284,16 @@ export default function (pi: ExtensionAPI) {
223
284
  "and update the status bar. Use when web-search returns " +
224
285
  "errors or when you've just started/restarted SearXNG.",
225
286
  handler: async (_args, ctx) => {
287
+ _lastCtx = ctx;
288
+
226
289
  if (!_searxngUrl) {
227
290
  ctx.ui.notify(
228
291
  "❌ SearXNG is not configured. " +
229
292
  "Set `searxng.url` in your Pi settings.json.",
230
293
  "error",
231
294
  );
232
- setSearchStatus(ctx, false, false);
295
+ _lastHealth = false;
296
+ renderSearchGlyph(ctx);
233
297
  return;
234
298
  }
235
299
 
@@ -240,7 +304,9 @@ export default function (pi: ExtensionAPI) {
240
304
  );
241
305
 
242
306
  if (searchOk) {
243
- setSearchStatus(ctx, true, false);
307
+ _lastHealth = true;
308
+ _lastDegraded = false;
309
+ renderSearchGlyph(ctx);
244
310
  ctx.ui.notify(
245
311
  `✅ SearXNG search pipeline is working at ${displayUrl(_searxngUrl)}`,
246
312
  "info",
@@ -255,7 +321,9 @@ export default function (pi: ExtensionAPI) {
255
321
  );
256
322
 
257
323
  if (serverOk) {
258
- setSearchStatus(ctx, true, true); // degraded
324
+ _lastHealth = true;
325
+ _lastDegraded = true;
326
+ renderSearchGlyph(ctx);
259
327
  ctx.ui.notify(
260
328
  "⚠ SearXNG server is up but the search pipeline " +
261
329
  "may be broken (aggregation failed). " +
@@ -263,7 +331,9 @@ export default function (pi: ExtensionAPI) {
263
331
  "warning",
264
332
  );
265
333
  } else {
266
- setSearchStatus(ctx, false, false);
334
+ _lastHealth = false;
335
+ _lastDegraded = false;
336
+ renderSearchGlyph(ctx);
267
337
  ctx.ui.notify(
268
338
  `❌ SearXNG at ${displayUrl(_searxngUrl)} is not responding. ` +
269
339
  "Check that the SearXNG service is running.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lean-search",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension. SearXNG search tool for Pi — pairs with pi-lean-portal's /web toggle; search-only installs are valid, or add it to a portal install for the full web-tools suite.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -42,6 +42,7 @@
42
42
  "@earendil-works/pi-coding-agent": "*",
43
43
  "@earendil-works/pi-tui": "*",
44
44
  "pi-lean-portal": "*",
45
+ "pi-tool-masking": "^1.0.0",
45
46
  "typebox": "*"
46
47
  },
47
48
  "peerDependenciesMeta": {
@@ -1,6 +1,6 @@
1
1
  # 🎭 Playwright
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-149.0.7827.55-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-151.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.5-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
3
+ [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-151.0.7922.34-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-153.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.5-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
4
4
 
5
5
  ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
6
6
 
@@ -296,9 +296,9 @@ The [Playwright VS Code extension](https://marketplace.visualstudio.com/items?it
296
296
 
297
297
  | | Linux | macOS | Windows |
298
298
  | :--- | :---: | :---: | :---: |
299
- | Chromium<sup>1</sup> <!-- GEN:chromium-version -->149.0.7827.55<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
299
+ | Chromium<sup>1</sup> <!-- GEN:chromium-version -->151.0.7922.34<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
300
300
  | WebKit <!-- GEN:webkit-version -->26.5<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
301
- | Firefox <!-- GEN:firefox-version -->151.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
301
+ | Firefox <!-- GEN:firefox-version -->153.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
302
302
 
303
303
  Headless and headed execution on all platforms. <sup>1</sup> Uses [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing) by default.
304
304
 
@@ -17,9 +17,9 @@ jobs:
17
17
  contents: read
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v6
21
21
 
22
- - uses: actions/setup-node@v4
22
+ - uses: actions/setup-node@v6
23
23
  with:
24
24
  node-version: lts/*
25
25
 
@@ -55,8 +55,8 @@ async function mergeReports(reportDir, opts) {
55
55
  if (!reporterDescriptions)
56
56
  reporterDescriptions = [[import_common.config.defaultReporter]];
57
57
  const rootDirOverride = configFile ? config.config.rootDir : void 0;
58
- await import_runner.merge.createMergedReport(config, dir, reporterDescriptions, rootDirOverride);
59
- gracefullyProcessExitDoNotHang(0);
58
+ const result = await import_runner.merge.createMergedReport(config, dir, reporterDescriptions, rootDirOverride);
59
+ gracefullyProcessExitDoNotHang(result === "failed" ? 1 : 0);
60
60
  }
61
61
  function resolveReporterOption(reporter) {
62
62
  if (!reporter || !reporter.length)