pi-lean-dimension 0.2.3 → 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 (176) 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 -1102
  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/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  20. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  21. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  22. package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
  23. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
  24. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  26. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  27. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  28. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  29. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
  30. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  31. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  32. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  33. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  34. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  35. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  36. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
  37. package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
  38. package/node_modules/pi-lean-portal/browser-toggle.ts +168 -380
  39. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
  40. package/node_modules/pi-lean-portal/contributed/README.md +65 -6
  41. package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
  42. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
  43. package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
  44. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +1 -1
  45. package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
  46. package/node_modules/pi-lean-portal/core/guides.ts +14 -18
  47. package/node_modules/pi-lean-portal/core/router.ts +2 -20
  48. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
  49. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
  50. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
  51. package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
  52. package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
  53. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
  54. package/node_modules/pi-lean-portal/index.ts +8 -1
  55. package/node_modules/pi-lean-portal/package.json +2 -1
  56. package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
  57. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
  58. package/node_modules/pi-lean-search/index.ts +134 -64
  59. package/node_modules/pi-lean-search/package.json +2 -1
  60. package/node_modules/playwright/README.md +3 -3
  61. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
  62. package/node_modules/playwright/lib/cli/reportActions.js +2 -2
  63. package/node_modules/playwright/lib/common/index.js +77 -52
  64. package/node_modules/playwright/lib/common/index.js.txt +10 -11
  65. package/node_modules/playwright/lib/errorContext.js +2 -3
  66. package/node_modules/playwright/lib/index.js +30 -8
  67. package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
  68. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
  69. package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
  70. package/node_modules/playwright/lib/program.js +19 -0
  71. package/node_modules/playwright/lib/runner/index.js +309 -153
  72. package/node_modules/playwright/lib/runner/index.js.txt +13 -12
  73. package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
  74. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
  75. package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
  76. package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
  77. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
  78. package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
  79. package/node_modules/playwright/lib/util.js +2 -29
  80. package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
  81. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
  82. package/node_modules/playwright/package.json +3 -6
  83. package/node_modules/playwright/types/test.d.ts +391 -4
  84. package/node_modules/playwright/types/testReporter.d.ts +79 -4
  85. package/node_modules/playwright-core/browsers.json +12 -18
  86. package/node_modules/playwright-core/lib/bootstrap.js +1 -1
  87. package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
  88. package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
  89. package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
  90. package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
  91. package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
  92. package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
  93. package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
  94. package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
  95. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  96. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  97. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  98. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  99. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  100. package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
  101. package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
  102. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
  103. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  104. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  105. package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
  106. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
  107. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
  108. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  109. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  110. package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  111. package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  112. package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
  113. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
  114. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
  115. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  116. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
  117. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  118. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  119. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  120. package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
  121. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
  122. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  123. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
  124. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
  125. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  126. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
  127. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
  128. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  129. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  130. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  131. package/node_modules/playwright-core/lib/xdg-open +338 -137
  132. package/node_modules/playwright-core/package.json +2 -2
  133. package/node_modules/playwright-core/types/protocol.d.ts +249 -36
  134. package/node_modules/playwright-core/types/structs.d.ts +3 -1
  135. package/node_modules/playwright-core/types/types.d.ts +2009 -266
  136. package/package.json +3 -3
  137. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
  138. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  139. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  140. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
  141. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  142. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  143. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  144. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  145. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  146. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  147. package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
  148. package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
  149. package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
  150. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
  151. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
  152. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
  153. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
  154. package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
  155. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
  156. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
  157. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
  158. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
  159. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
  160. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
  161. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
  162. package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
  163. package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
  164. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
  165. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
  166. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
  167. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
  168. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
  169. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
  170. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
  171. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
  172. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
  173. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
  174. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
  175. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
  176. /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
@@ -2,452 +2,251 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionCommandContext,
4
4
  ExtensionContext,
5
+ ThemeColor,
5
6
  } from "@earendil-works/pi-coding-agent";
6
- // fs functions used by profile/cookies handlers are in their respective modules
7
-
8
- /**
9
- * Browser Toggle — integrated into pi-lean-portal extension.
10
- *
11
- * Provides /web on, /web off, /web learn, and /web status commands to enable / disable
12
- * browser automation tools (browsing tools) and learn tools (web-learn) in the system prompt.
13
- *
14
- * Three states: on (browsing only), learn (browsing + web-learn), off (all disabled).
15
- * Internally stores two independent booleans (browserToolsEnabled, learnToolsEnabled).
16
- *
17
- * When browser tools are disabled, the LLM cannot see them — no descriptions,
18
- * parameter schemas, prompt snippets, or guidelines are included in the context,
19
- * saving roughly 1500–2000 tokens per turn.
20
- *
21
- * ── Stateless fallback ─────────────────────────────────────────
22
- *
23
- * web-fetch is a stateless HTTP tool (no session, no interactivity). If you
24
- * only need quick page content and never use the interactive browser tools,
25
- * you can leave browser tools disabled and rely on web-fetch alone.
26
- */
27
-
7
+ import {
8
+ defineToolset,
9
+ TOOLSET_EVENTS,
10
+ getDefaultResolutionMode,
11
+ } from "pi-tool-masking";
12
+ import type { ToolsetSpec } from "pi-tool-masking";
28
13
  import { readMergedSettings } from "./core/shared/settings-reader.js";
29
14
 
30
- // ---- Constants -------------------------------------------------
31
-
32
- /** Default toggle config key */
33
- const CONFIG_KEY = "browserToggle";
34
-
35
- /** Names of every browser browsing tool registered by pi-lean-portal's index.ts (excludes learn tools). */
36
- const BROWSER_TOOL_NAMES = new Set([
37
- "web-fetch",
38
- "browser-navigate",
39
- "browser-snapshot",
40
- "browser-click",
41
- "browser-type",
42
- "browser-scroll",
43
- "browser-back",
44
- "browser-press",
45
- "browser-console",
46
- "browser-inspect",
47
- "web-guide",
48
- ]);
49
-
50
- /** Names of learn tools (web-learn) that require /web learn to be active. */
51
- const LEARN_TOOL_NAMES = new Set(["web-learn"]);
52
-
53
- /**
54
- * Names of sibling-package tools that are toggled alongside browser tools
55
- * by /web on|off|learn.
56
- *
57
- * - "web-search": SearXNG search from pi-lean-search (Sprint 4).
58
- *
59
- * Exact-name `Set.has()` membership — NO regex (avoids false positives
60
- * on third-party web-* tools).
61
- */
62
- const SIBLING_TOOL_NAMES = new Set<string>(["web-search"]);
63
-
64
- /**
65
- * Update the search status bar glyph if sibling tools are installed.
66
- * Used by ``/web on|learn|off`` to keep the ``search`` slot in sync.
67
- */
68
- function setSearchSlot(
69
- pi: ExtensionAPI,
70
- ctx: ExtensionCommandContext,
71
- glyph: string,
72
- ): void {
73
- const hasSiblingTools = getRegisteredIn(pi, SIBLING_TOOL_NAMES).length > 0;
74
- if (hasSiblingTools) {
75
- ctx.ui.setStatus("search", glyph);
76
- }
77
- }
15
+ // ---- Toolset specs -----------------------------------------------
16
+
17
+ const PORTAL_WEB_SPEC: ToolsetSpec = {
18
+ id: "portal.web",
19
+ names: new Set([
20
+ "web-fetch",
21
+ "browser-navigate",
22
+ "browser-snapshot",
23
+ "browser-click",
24
+ "browser-type",
25
+ "browser-scroll",
26
+ "browser-back",
27
+ "browser-press",
28
+ "browser-console",
29
+ "browser-inspect",
30
+ "web-guide",
31
+ ]),
32
+ persistKey: "toolset-state:portal.web",
33
+ };
78
34
 
79
- /** Persisted state shape — two independent booleans plus conversation-scoped default profile. */
80
- interface BrowserToggleState {
81
- browserToolsEnabled: boolean;
82
- learnToolsEnabled: boolean;
83
- /** Default profile for the current conversation. "none" means no override. */
84
- defaultProfile: string;
85
- }
35
+ const PORTAL_LEARN_SPEC: ToolsetSpec = {
36
+ id: "portal.learn",
37
+ names: new Set(["web-learn"]),
38
+ persistKey: "toolset-state:portal.learn",
39
+ defaultEnabled: false,
40
+ requires: ["portal.web"],
41
+ };
42
+
43
+ // ---- Status bar cached state (derived from library events) ------
86
44
 
87
- /** Last known toggle state — used by status bar */
45
+ /** @internal Last known web-toggle state for status bar rendering. */
88
46
  let _lastToggleState = true;
89
47
 
90
- /** Last known learn state — used by status bar for glyph coloring */
48
+ /** @internal Last known learn state for status bar coloring. */
91
49
  let _lastLearnState = false;
92
50
 
93
- /** Conversation-scoped default profile. "none" means: read from config file. */
94
- let _conversationDefaultProfile: string | undefined;
51
+ /** @internal Last captured ExtensionContext for event-driven glyph rendering. */
52
+ let _lastCtx: ExtensionContext | null = null;
95
53
 
96
- /**
97
- * Get the effective default profile. Returns the conversation-scoped override
98
- * if set, otherwise falls back to "none" (the router reads browser.defaultProfile
99
- * from settings.json as its own fallback).
100
- */
101
- export function getConversationDefaultProfile(): string | undefined {
102
- return _conversationDefaultProfile;
54
+ export function getToggleState(): boolean {
55
+ return _lastToggleState;
103
56
  }
104
57
 
105
- // ---- Helpers ---------------------------------------------------
106
-
107
- /** Return the subset of a tool-name set that is actually registered. */
108
- function getRegisteredIn(pi: ExtensionAPI, names: Set<string>): string[] {
109
- return pi
110
- .getAllTools()
111
- .map((t) => t.name)
112
- .filter((n) => names.has(n));
58
+ export function getLearnState(): boolean {
59
+ return _lastLearnState;
113
60
  }
114
61
 
115
- /**
116
- * Check whether browser tools are currently active in the system prompt.
117
- */
118
- function isBrowserEnabled(pi: ExtensionAPI): boolean {
119
- const registered = getRegisteredIn(pi, BROWSER_TOOL_NAMES);
62
+ // ---- Conversation-scoped default profile -------------------------
120
63
 
121
- const active = new Set(pi.getActiveTools());
122
- return registered.some((name) => active.has(name));
123
- }
64
+ const PROFILE_PERSIST_KEY = "portal-conversation-state";
124
65
 
125
- /**
126
- * Apply a browser-tool state without persisting.
127
- * @param pi ExtensionAPI
128
- * @param enable true = add browser tools to the active set, false = remove them
129
- */
130
- function applyBrowserState(pi: ExtensionAPI, enable: boolean): void {
131
- // Combine browser tools + sibling tools into one toggle set.
132
- // (/web on enables both; /web off disables both.)
133
- const registered = new Set([
134
- ...getRegisteredIn(pi, BROWSER_TOOL_NAMES),
135
- ...getRegisteredIn(pi, SIBLING_TOOL_NAMES),
136
- ]);
137
- if (registered.size === 0) return;
138
- _lastToggleState = enable;
139
-
140
- if (enable) {
141
- // Merge browser+sibling tools back into whatever is currently active
142
- const current = pi.getActiveTools();
143
- pi.setActiveTools([...new Set([...current, ...registered])]);
144
- } else {
145
- // Keep everything that is NOT a browser+sibling tool
146
- const all = pi.getAllTools().map((t) => t.name);
147
- pi.setActiveTools(all.filter((name) => !registered.has(name)));
148
- }
149
- }
150
-
151
- /**
152
- * Persist the current toggle state into the session for branch-aware
153
- * restoration across /reload, /resume, /fork, and /tree navigation.
154
- */
155
- function persistState(pi: ExtensionAPI, state: BrowserToggleState): void {
156
- pi.appendEntry<BrowserToggleState>("web-toggle-state", state);
66
+ interface ProfileState {
67
+ defaultProfile: string;
157
68
  }
158
69
 
159
- // ---- Branch-aware restoration ----------------------------------
70
+ let _conversationDefaultProfile: string | undefined;
160
71
 
161
- function restoreFromBranch(pi: ExtensionAPI, ctx: ExtensionContext): boolean {
162
- const registered = getRegisteredIn(pi, BROWSER_TOOL_NAMES);
163
- if (registered.length === 0) return false;
72
+ export function getConversationDefaultProfile(): string | undefined {
73
+ return _conversationDefaultProfile;
74
+ }
164
75
 
165
- let savedState: BrowserToggleState | undefined;
76
+ function persistProfile(pi: ExtensionAPI, profile: string): void {
77
+ pi.appendEntry<ProfileState>(PROFILE_PERSIST_KEY, {
78
+ defaultProfile: profile,
79
+ });
80
+ }
166
81
 
82
+ function restoreProfile(_pi: ExtensionAPI, ctx: ExtensionContext): void {
167
83
  for (const entry of ctx.sessionManager.getBranch()) {
168
- if (entry.type === "custom" && entry.customType === "web-toggle-state") {
169
- const data = entry.data as Record<string, unknown>;
170
- if (data && typeof data.browserToolsEnabled === "boolean") {
171
- savedState = data as unknown as BrowserToggleState;
84
+ if (entry.type === "custom" && entry.customType === PROFILE_PERSIST_KEY) {
85
+ const data = entry.data as Record<string, unknown> | undefined;
86
+ if (data && typeof data.defaultProfile === "string") {
87
+ _conversationDefaultProfile = data.defaultProfile;
88
+ return;
172
89
  }
173
90
  }
174
91
  }
175
-
176
- if (savedState !== undefined) {
177
- applyBrowserState(pi, savedState.browserToolsEnabled);
178
- applyLearnState(pi, savedState.learnToolsEnabled);
179
- // Restore conversation-scoped default profile
180
- if (savedState.defaultProfile && savedState.defaultProfile !== "none") {
181
- _conversationDefaultProfile = savedState.defaultProfile;
182
- }
183
- return true;
184
- }
185
-
186
- return false;
187
- }
188
-
189
- /**
190
- * Apply the config-file default on a fresh session (no branch state found).
191
- * Reads `browserToggle.defaultEnabled` from pi's settings.json files.
192
- * Learn always starts disabled by default.
193
- */
194
- function applyConfigDefault(pi: ExtensionAPI): void {
195
- const enabled = readBrowserToggleConfig();
196
- applyBrowserState(pi, enabled);
197
- applyLearnState(pi, false);
198
- // Persist so subsequent branch navigation sees this as the initial state
199
- persistState(pi, {
200
- browserToolsEnabled: enabled,
201
- learnToolsEnabled: false,
202
- defaultProfile: "none",
203
- });
204
92
  }
205
93
 
206
- // ---- Unit-test exports -----------------------------------------
207
- // Helper functions are exported for testing while the default export
208
- // wires the toggle into pi-lean-portal's runtime.
209
-
210
- /**
211
- * Return the last known toggle state (true = enabled, false = disabled).
212
- * Returns true initially at startup (browser tools start enabled).
213
- */
214
- export function getToggleState(): boolean {
215
- return _lastToggleState;
216
- }
217
-
218
- /**
219
- * Return the last known learn state (true = learn mode enabled).
220
- * Used by the status bar to color the glyph differently.
221
- */
222
- export function getLearnState(): boolean {
223
- return _lastLearnState;
224
- }
94
+ // ---- Test helpers -------------------------------------------------
225
95
 
226
- /** @internal Reset module-level state to defaults (test helper). */
96
+ /** @internal Reset cached state to defaults (test helper). */
227
97
  function _resetToggleStateForTest(): void {
228
98
  _lastToggleState = true;
229
99
  _lastLearnState = false;
100
+ _lastCtx = null;
101
+ _conversationDefaultProfile = undefined;
230
102
  }
231
103
 
232
- /**
233
- * Reset all module-level toggle state to defaults.
234
- *
235
- * Called at the start of the extension entry function to ensure safe
236
- * re-invocation when pi reuses the cached module factory (e.g.
237
- * during /resume to the same working directory).
238
- * Resets both the toggle status and conversation-scoped profile.
239
- */
240
- export function resetToggleModuleState(): void {
104
+ /** @internal Reset cached state (called from index.ts on re-entry). */
105
+ function resetToggleModuleState(): void {
241
106
  _lastToggleState = true;
242
107
  _lastLearnState = false;
108
+ _lastCtx = null;
243
109
  _conversationDefaultProfile = undefined;
244
110
  }
245
111
 
246
- // ---- Config-file support -----------------------------------------
247
-
248
- /**
249
- * Read the browser toggle default from pi's settings.json files.
250
- *
251
- * Looks for `browserToggle.defaultEnabled` in:
252
- * 1. `~/.pi/agent/settings.json` (global)
253
- * 2. `.pi/settings.json` (project-local, overrides global)
254
- *
255
- * Returns the config value, or `true` if unset (backward-compatible default).
256
- * Silently returns `true` on missing files, parse errors, or type mismatches.
257
- */
258
- function readBrowserToggleConfig(): boolean {
259
- const merged = readMergedSettings();
260
- const segment = merged[CONFIG_KEY];
261
-
262
- if (
263
- segment &&
264
- typeof segment === "object" &&
265
- !Array.isArray(segment) &&
266
- typeof (segment as Record<string, unknown>).defaultEnabled === "boolean"
267
- ) {
268
- return (segment as Record<string, unknown>).defaultEnabled as boolean;
269
- }
270
-
271
- return true; // default: enabled
272
- }
273
-
274
- /**
275
- * Check whether learn tools are currently active.
276
- */
277
- function isLearnEnabled(pi: ExtensionAPI): boolean {
278
- const registered = getRegisteredIn(pi, LEARN_TOOL_NAMES);
279
- const active = new Set(pi.getActiveTools());
280
- return registered.some((name) => active.has(name));
281
- }
112
+ // ---- Exports for testing -----------------------------------------
282
113
 
283
- /**
284
- * Apply a learn-tool state without persisting.
285
- * Uses getActiveTools() (not getAllTools()) to avoid accidentally activating
286
- * learn tools that are not currently registered.
287
- */
288
- function applyLearnState(pi: ExtensionAPI, enable: boolean): void {
289
- _lastLearnState = enable;
290
- const registered = new Set(getRegisteredIn(pi, LEARN_TOOL_NAMES));
291
- if (registered.size === 0) return;
292
- if (enable) {
293
- const current = pi.getActiveTools();
294
- pi.setActiveTools([...new Set([...current, ...registered])]);
295
- } else {
296
- // Remove learn tools from the *currently active* set
297
- const current = pi.getActiveTools();
298
- pi.setActiveTools(current.filter((name) => !registered.has(name)));
299
- }
300
- }
114
+ export { _resetToggleStateForTest, resetToggleModuleState };
301
115
 
302
- // ── Test-only exports ──────────────────────────────────
303
- // These are exported solely for unit testing.
304
- // Do not import them directly from production code.
305
- /** @internal */
306
- export {
307
- getRegisteredIn,
308
- isBrowserEnabled,
309
- applyBrowserState,
310
- persistState,
311
- restoreFromBranch,
312
- readBrowserToggleConfig,
313
- applyConfigDefault,
314
- isLearnEnabled,
315
- applyLearnState,
316
- _resetToggleStateForTest,
317
- };
318
- export type { BrowserToggleState };
116
+ // ---- Glyph helpers -----------------------------------------------
319
117
 
320
- // ---- Profile & Cookies (extracted) ------------------------------
321
-
322
- import { handleProfileSubcommand } from "./browser-profile.js";
323
- import { handleCookiesSubcommand } from "./browser-cookies.js";
324
- import { handleStatusSubcommand } from "./browser-status.js";
325
-
326
- // ---- Internal helpers (used by delegated handlers) ---------------
327
-
328
- /**
329
- * Set the conversation-scoped default profile.
330
- * Affects this conversation only; survives /reload, /resume, /fork.
331
- * "none" resets to the config-file default.
332
- */
333
- function setConversationDefaultProfile(
334
- pi: ExtensionAPI,
335
- profile: string,
118
+ function renderBrowserGlyph(
119
+ ctx: {
120
+ ui: {
121
+ setStatus: (key: string, label: string) => void;
122
+ theme: { fg: (c: ThemeColor, t: string) => string };
123
+ };
124
+ },
125
+ webEnabled: boolean,
126
+ learnEnabled: boolean,
336
127
  ): void {
337
- if (profile === "none") {
338
- _conversationDefaultProfile = undefined;
128
+ if (!webEnabled) {
129
+ ctx.ui.setStatus("browser", "○ web off");
130
+ return;
131
+ }
132
+ if (learnEnabled) {
133
+ ctx.ui.setStatus("browser", ctx.ui.theme.fg("success", "●") + " idle");
339
134
  } else {
340
- _conversationDefaultProfile = profile;
135
+ ctx.ui.setStatus("browser", ctx.ui.theme.fg("accent", "●") + " idle");
341
136
  }
342
-
343
- // Persist so it survives /reload, /resume, /fork
344
- const currentState = getCurrentState(pi);
345
- persistState(pi, {
346
- ...currentState,
347
- defaultProfile: profile,
348
- });
349
137
  }
350
138
 
351
- /** Read the current in-memory toggle state. */
352
- function getCurrentState(
353
- pi: ExtensionAPI,
354
- ): Pick<BrowserToggleState, "browserToolsEnabled" | "learnToolsEnabled"> {
355
- return {
356
- browserToolsEnabled: isBrowserEnabled(pi),
357
- learnToolsEnabled: isLearnEnabled(pi),
358
- };
359
- }
139
+ // ---- Toggle initializer ------------------------------------------
360
140
 
361
- // ---- Toggle initializer ----------------------------------------
362
-
363
- /**
364
- * Register the /web command and session-restoration hooks.
365
- * Called from pi-lean-portal's index.ts entry point.
366
- */
367
141
  export default function initBrowserToggle(pi: ExtensionAPI) {
368
- // ── Commands ──────────────────────────────────────────────
142
+ const merged = readMergedSettings();
143
+ const browserToggleSegment = (merged as Record<string, unknown>)[
144
+ "browserToggle"
145
+ ] as Record<string, unknown> | undefined;
146
+ const webDefault =
147
+ browserToggleSegment &&
148
+ typeof browserToggleSegment["defaultEnabled"] === "boolean"
149
+ ? (browserToggleSegment["defaultEnabled"] as boolean)
150
+ : true;
151
+
152
+ const webSpec: ToolsetSpec = {
153
+ ...PORTAL_WEB_SPEC,
154
+ defaultEnabled: webDefault,
155
+ };
156
+ const webToolset = defineToolset(pi, webSpec);
157
+ const learnToolset = defineToolset(pi, PORTAL_LEARN_SPEC);
158
+
159
+ // ── Keep cached state in sync with library events ─────────
160
+ // Re-render the glyph on every change/restore so external callers
161
+ // (e.g. pi-tbox's `/tbox all off`) keep the slot in sync — the cached
162
+ // flags alone don't update the status bar.
163
+ const syncCachedState = () => {
164
+ _lastToggleState = webToolset.isEnabled(pi);
165
+ _lastLearnState = learnToolset.isEnabled(pi);
166
+ if (_lastCtx) {
167
+ renderBrowserGlyph(_lastCtx, _lastToggleState, _lastLearnState);
168
+ }
169
+ };
369
170
 
171
+ pi.events.on(TOOLSET_EVENTS.changed, syncCachedState);
172
+ pi.events.on(TOOLSET_EVENTS.restored, syncCachedState);
173
+
174
+ // ── /web command ──────────────────────────────────────────
370
175
  pi.registerCommand("web", {
371
176
  description:
372
177
  "Enable/disable browser automation tools. " +
373
178
  "Usage: /web on | off | learn | status",
374
179
  handler: async (args, ctx) => {
375
180
  const cmd = args.trim().toLowerCase();
376
- const hasBrowserTools = getRegisteredIn(pi, BROWSER_TOOL_NAMES).length > 0;
377
181
 
378
- if (!hasBrowserTools) {
182
+ // Focus-mode guard (§13.2): refuse actuating subcommands while the
183
+ // library holds the line in inclusion mode, so a sibling toggle
184
+ // can't write a focus-indistinguishable {enabled} entry. Read-only
185
+ // subcommands (status/profile/cookies/bare /web) stay unguarded,
186
+ // matching tbox's treatment of its own read-only commands.
187
+ if (
188
+ ["on", "off", "learn"].includes(cmd) &&
189
+ getDefaultResolutionMode() === "inclusion"
190
+ ) {
379
191
  ctx.ui.notify(
380
- "🌐 pi-lean-portal extension not detected. Nothing to toggle.",
192
+ "Another plugin has active inclusion mode — this toolset can't be toggled while inclusion is holding the line. Deactivate it there first.",
381
193
  "warning",
382
194
  );
383
195
  return;
384
196
  }
385
197
 
386
198
  if (cmd === "on") {
387
- applyBrowserState(pi, true);
388
- applyLearnState(pi, false);
389
- persistState(pi, {
390
- browserToolsEnabled: true,
391
- learnToolsEnabled: false,
392
- defaultProfile: _conversationDefaultProfile ?? "none",
393
- });
394
- ctx.ui.setStatus("browser", ctx.ui.theme.fg("accent", "●") + " idle");
395
-
396
- setSearchSlot(pi, ctx, ctx.ui.theme.fg("accent", "●") + " searxng");
397
-
199
+ webToolset.enable(pi);
200
+ learnToolset.disable(pi);
398
201
  ctx.ui.notify(
399
202
  "🌐 Browser tools enabled. /web learn to make web-learn available.",
400
203
  "info",
401
204
  );
402
205
  } else if (cmd === "learn") {
403
- applyBrowserState(pi, true);
404
- applyLearnState(pi, true);
405
- persistState(pi, {
406
- browserToolsEnabled: true,
407
- learnToolsEnabled: true,
408
- defaultProfile: _conversationDefaultProfile ?? "none",
409
- });
410
- ctx.ui.setStatus("browser", ctx.ui.theme.fg("success", "●") + " idle");
411
-
412
- setSearchSlot(pi, ctx, ctx.ui.theme.fg("accent", "●") + " searxng");
413
-
206
+ learnToolset.enable(pi); // cascades web on via requires
414
207
  ctx.ui.notify(
415
208
  "📖 web-learn tool is now available. Agent will save/update guides when asked.",
416
209
  "info",
417
210
  );
418
211
  } else if (cmd === "off") {
419
- applyBrowserState(pi, false);
420
- applyLearnState(pi, false);
421
- persistState(pi, {
422
- browserToolsEnabled: false,
423
- learnToolsEnabled: false,
424
- defaultProfile: _conversationDefaultProfile ?? "none",
425
- });
426
- ctx.ui.setStatus("browser", "○ web off");
427
-
428
- setSearchSlot(pi, ctx, "○ searxng");
429
-
212
+ webToolset.disable(pi); // cascades learn off via requires
430
213
  ctx.ui.notify(
431
214
  "🌐 Browser tools disabled. /web on to re-enable.",
432
215
  "info",
433
216
  );
434
217
  } else if (cmd === "profile" || cmd.startsWith("profile ")) {
435
218
  const sub = cmd.slice("profile".length).trim();
219
+ const { handleProfileSubcommand } = await import(
220
+ "./browser-profile.js"
221
+ );
436
222
  await handleProfileSubcommand(sub, ctx, pi, (profile: string) => {
437
- setConversationDefaultProfile(pi, profile);
223
+ if (profile === "none") {
224
+ _conversationDefaultProfile = undefined;
225
+ } else {
226
+ _conversationDefaultProfile = profile;
227
+ }
228
+ persistProfile(pi, profile);
438
229
  });
439
230
  } else if (cmd === "cookies" || cmd.startsWith("cookies ")) {
440
231
  const sub = cmd.slice("cookies".length).trim();
232
+ const { handleCookiesSubcommand } = await import(
233
+ "./browser-cookies.js"
234
+ );
441
235
  await handleCookiesSubcommand(sub, ctx);
442
236
  } else if (cmd === "status") {
443
- handleStatusSubcommand(ctx, isBrowserEnabled(pi), isLearnEnabled(pi));
237
+ const { handleStatusSubcommand } = await import("./browser-status.js");
238
+ handleStatusSubcommand(
239
+ ctx,
240
+ webToolset.isEnabled(pi),
241
+ learnToolset.isEnabled(pi),
242
+ );
444
243
  } else {
445
244
  // Default: show status
446
- const browserStatus = isBrowserEnabled(pi) ? "✅ on" : "❌ off";
447
- const learnStatus = isLearnEnabled(pi) ? "✅ on" : "❌ off";
245
+ const webOn = webToolset.isEnabled(pi) ? "✅ on" : "❌ off";
246
+ const learnOn = learnToolset.isEnabled(pi) ? "✅ on" : "❌ off";
448
247
  ctx.ui.notify(
449
- `🌐 Browser tools: ${browserStatus}\n` +
450
- `📖 Learn mode: ${learnStatus}\n` +
248
+ `🌐 Browser tools: ${webOn}\n` +
249
+ `📖 Learn mode: ${learnOn}\n` +
451
250
  ` /web profile manage browser profiles\n` +
452
251
  ` /web cookies inspect or clear session cookies\n` +
453
252
  ` /web off disable all browser tools\n` +
@@ -461,31 +260,20 @@ export default function initBrowserToggle(pi: ExtensionAPI) {
461
260
  },
462
261
  });
463
262
 
464
- // ── State restoration across session boundaries ────────────
465
- // Ensures the toggle survives /reload, /resume, /fork, and
466
- // /tree navigation without "forgetting" whether we're on or off.
467
- //
468
- // For fresh sessions (no branch state), falls back to the config
469
- // default from settings.json (`browserToggle.defaultEnabled`).
470
-
263
+ // ── Session handlers: restore profile + render glyph ─────
471
264
  pi.on("session_start", async (_event, ctx) => {
472
- const restored = restoreFromBranch(pi, ctx);
473
- if (!restored) {
474
- // No persisted state in this branch → apply config file default
475
- applyConfigDefault(pi);
476
- }
477
-
478
- // After state restoration, ensure the search slot reflects the actual
479
- // toggle state. Without this, a race between portal's session_start
480
- // (async — restores/applies state) and search's session_start (probes
481
- // health) can leave the search glyph showing blue when tools are off.
482
- const hasSiblingTools = getRegisteredIn(pi, SIBLING_TOOL_NAMES).length > 0;
483
- if (hasSiblingTools && !isBrowserEnabled(pi)) {
484
- ctx.ui.setStatus("search", "○ searxng");
485
- }
265
+ restoreProfile(pi, ctx);
266
+ _lastCtx = ctx;
267
+ syncCachedState();
486
268
  });
487
269
 
488
270
  pi.on("session_tree", async (_event, ctx) => {
489
- restoreFromBranch(pi, ctx);
271
+ restoreProfile(pi, ctx);
272
+ _lastCtx = ctx;
273
+ syncCachedState();
274
+ });
275
+
276
+ pi.on("session_shutdown", async () => {
277
+ _lastCtx = null;
490
278
  });
491
279
  }
@@ -118,8 +118,8 @@ rather than failing opaquely.
118
118
  executes stealth backends automatically. `user-backends/` is trusted
119
119
  user code — you wrote or audited it.
120
120
  - **Not shipped in the npm tarball.** The templates live in the source
121
- repo under `contributed/`; `docs/` is excluded from
122
- `package.json` `files`. You need the git repo (or a copy of the
121
+ repo under `contributed/`; `contributed/` is not included in the
122
+ `package.json` `files` allow-list. You need the git repo (or a copy of the
123
123
  files) to install a stealth backend.
124
124
  - **Not in the default fallback list.** Stealth backends are loaded
125
125
  only when explicitly listed in `browser.plugins`. A fresh install