pi-lean-dimension 0.2.4 → 0.3.1

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 +35 -7
  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
@@ -2,457 +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: "pi-lean-dimension.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:pi-lean-dimension.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: "pi-lean-dimension.web-learn",
37
+ names: new Set(["web-learn"]),
38
+ persistKey: "toolset-state:pi-lean-dimension.web-learn",
39
+ defaultEnabled: false,
40
+ requires: ["pi-lean-dimension.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 — but only
146
- // within the *currently active* set. Using getActiveTools() (not
147
- // getAllTools()) is critical for peer-toggle composition: /web off
148
- // must not re-enable tools that another toggle (e.g. host's /api off)
149
- // has already disabled. getAllTools() would silently re-activate
150
- // every registered tool outside our own set.
151
- const current = pi.getActiveTools();
152
- pi.setActiveTools(current.filter((name) => !registered.has(name)));
153
- }
154
- }
155
-
156
- /**
157
- * Persist the current toggle state into the session for branch-aware
158
- * restoration across /reload, /resume, /fork, and /tree navigation.
159
- */
160
- function persistState(pi: ExtensionAPI, state: BrowserToggleState): void {
161
- pi.appendEntry<BrowserToggleState>("web-toggle-state", state);
66
+ interface ProfileState {
67
+ defaultProfile: string;
162
68
  }
163
69
 
164
- // ---- Branch-aware restoration ----------------------------------
70
+ let _conversationDefaultProfile: string | undefined;
165
71
 
166
- function restoreFromBranch(pi: ExtensionAPI, ctx: ExtensionContext): boolean {
167
- const registered = getRegisteredIn(pi, BROWSER_TOOL_NAMES);
168
- if (registered.length === 0) return false;
72
+ export function getConversationDefaultProfile(): string | undefined {
73
+ return _conversationDefaultProfile;
74
+ }
169
75
 
170
- let savedState: BrowserToggleState | undefined;
76
+ function persistProfile(pi: ExtensionAPI, profile: string): void {
77
+ pi.appendEntry<ProfileState>(PROFILE_PERSIST_KEY, {
78
+ defaultProfile: profile,
79
+ });
80
+ }
171
81
 
82
+ function restoreProfile(_pi: ExtensionAPI, ctx: ExtensionContext): void {
172
83
  for (const entry of ctx.sessionManager.getBranch()) {
173
- if (entry.type === "custom" && entry.customType === "web-toggle-state") {
174
- const data = entry.data as Record<string, unknown>;
175
- if (data && typeof data.browserToolsEnabled === "boolean") {
176
- 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;
177
89
  }
178
90
  }
179
91
  }
180
-
181
- if (savedState !== undefined) {
182
- applyBrowserState(pi, savedState.browserToolsEnabled);
183
- applyLearnState(pi, savedState.learnToolsEnabled);
184
- // Restore conversation-scoped default profile
185
- if (savedState.defaultProfile && savedState.defaultProfile !== "none") {
186
- _conversationDefaultProfile = savedState.defaultProfile;
187
- }
188
- return true;
189
- }
190
-
191
- return false;
192
- }
193
-
194
- /**
195
- * Apply the config-file default on a fresh session (no branch state found).
196
- * Reads `browserToggle.defaultEnabled` from pi's settings.json files.
197
- * Learn always starts disabled by default.
198
- */
199
- function applyConfigDefault(pi: ExtensionAPI): void {
200
- const enabled = readBrowserToggleConfig();
201
- applyBrowserState(pi, enabled);
202
- applyLearnState(pi, false);
203
- // Persist so subsequent branch navigation sees this as the initial state
204
- persistState(pi, {
205
- browserToolsEnabled: enabled,
206
- learnToolsEnabled: false,
207
- defaultProfile: "none",
208
- });
209
92
  }
210
93
 
211
- // ---- Unit-test exports -----------------------------------------
212
- // Helper functions are exported for testing while the default export
213
- // wires the toggle into pi-lean-portal's runtime.
214
-
215
- /**
216
- * Return the last known toggle state (true = enabled, false = disabled).
217
- * Returns true initially at startup (browser tools start enabled).
218
- */
219
- export function getToggleState(): boolean {
220
- return _lastToggleState;
221
- }
222
-
223
- /**
224
- * Return the last known learn state (true = learn mode enabled).
225
- * Used by the status bar to color the glyph differently.
226
- */
227
- export function getLearnState(): boolean {
228
- return _lastLearnState;
229
- }
94
+ // ---- Test helpers -------------------------------------------------
230
95
 
231
- /** @internal Reset module-level state to defaults (test helper). */
96
+ /** @internal Reset cached state to defaults (test helper). */
232
97
  function _resetToggleStateForTest(): void {
233
98
  _lastToggleState = true;
234
99
  _lastLearnState = false;
100
+ _lastCtx = null;
101
+ _conversationDefaultProfile = undefined;
235
102
  }
236
103
 
237
- /**
238
- * Reset all module-level toggle state to defaults.
239
- *
240
- * Called at the start of the extension entry function to ensure safe
241
- * re-invocation when pi reuses the cached module factory (e.g.
242
- * during /resume to the same working directory).
243
- * Resets both the toggle status and conversation-scoped profile.
244
- */
245
- export function resetToggleModuleState(): void {
104
+ /** @internal Reset cached state (called from index.ts on re-entry). */
105
+ function resetToggleModuleState(): void {
246
106
  _lastToggleState = true;
247
107
  _lastLearnState = false;
108
+ _lastCtx = null;
248
109
  _conversationDefaultProfile = undefined;
249
110
  }
250
111
 
251
- // ---- Config-file support -----------------------------------------
252
-
253
- /**
254
- * Read the browser toggle default from pi's settings.json files.
255
- *
256
- * Looks for `browserToggle.defaultEnabled` in:
257
- * 1. `~/.pi/agent/settings.json` (global)
258
- * 2. `.pi/settings.json` (project-local, overrides global)
259
- *
260
- * Returns the config value, or `true` if unset (backward-compatible default).
261
- * Silently returns `true` on missing files, parse errors, or type mismatches.
262
- */
263
- function readBrowserToggleConfig(): boolean {
264
- const merged = readMergedSettings();
265
- const segment = merged[CONFIG_KEY];
266
-
267
- if (
268
- segment &&
269
- typeof segment === "object" &&
270
- !Array.isArray(segment) &&
271
- typeof (segment as Record<string, unknown>).defaultEnabled === "boolean"
272
- ) {
273
- return (segment as Record<string, unknown>).defaultEnabled as boolean;
274
- }
275
-
276
- return true; // default: enabled
277
- }
278
-
279
- /**
280
- * Check whether learn tools are currently active.
281
- */
282
- function isLearnEnabled(pi: ExtensionAPI): boolean {
283
- const registered = getRegisteredIn(pi, LEARN_TOOL_NAMES);
284
- const active = new Set(pi.getActiveTools());
285
- return registered.some((name) => active.has(name));
286
- }
112
+ // ---- Exports for testing -----------------------------------------
287
113
 
288
- /**
289
- * Apply a learn-tool state without persisting.
290
- * Uses getActiveTools() (not getAllTools()) to avoid accidentally activating
291
- * learn tools that are not currently registered.
292
- */
293
- function applyLearnState(pi: ExtensionAPI, enable: boolean): void {
294
- _lastLearnState = enable;
295
- const registered = new Set(getRegisteredIn(pi, LEARN_TOOL_NAMES));
296
- if (registered.size === 0) return;
297
- if (enable) {
298
- const current = pi.getActiveTools();
299
- pi.setActiveTools([...new Set([...current, ...registered])]);
300
- } else {
301
- // Remove learn tools from the *currently active* set
302
- const current = pi.getActiveTools();
303
- pi.setActiveTools(current.filter((name) => !registered.has(name)));
304
- }
305
- }
114
+ export { _resetToggleStateForTest, resetToggleModuleState };
306
115
 
307
- // ── Test-only exports ──────────────────────────────────
308
- // These are exported solely for unit testing.
309
- // Do not import them directly from production code.
310
- /** @internal */
311
- export {
312
- getRegisteredIn,
313
- isBrowserEnabled,
314
- applyBrowserState,
315
- persistState,
316
- restoreFromBranch,
317
- readBrowserToggleConfig,
318
- applyConfigDefault,
319
- isLearnEnabled,
320
- applyLearnState,
321
- _resetToggleStateForTest,
322
- };
323
- export type { BrowserToggleState };
116
+ // ---- Glyph helpers -----------------------------------------------
324
117
 
325
- // ---- Profile & Cookies (extracted) ------------------------------
326
-
327
- import { handleProfileSubcommand } from "./browser-profile.js";
328
- import { handleCookiesSubcommand } from "./browser-cookies.js";
329
- import { handleStatusSubcommand } from "./browser-status.js";
330
-
331
- // ---- Internal helpers (used by delegated handlers) ---------------
332
-
333
- /**
334
- * Set the conversation-scoped default profile.
335
- * Affects this conversation only; survives /reload, /resume, /fork.
336
- * "none" resets to the config-file default.
337
- */
338
- function setConversationDefaultProfile(
339
- pi: ExtensionAPI,
340
- 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,
341
127
  ): void {
342
- if (profile === "none") {
343
- _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");
344
134
  } else {
345
- _conversationDefaultProfile = profile;
135
+ ctx.ui.setStatus("browser", ctx.ui.theme.fg("accent", "●") + " idle");
346
136
  }
347
-
348
- // Persist so it survives /reload, /resume, /fork
349
- const currentState = getCurrentState(pi);
350
- persistState(pi, {
351
- ...currentState,
352
- defaultProfile: profile,
353
- });
354
137
  }
355
138
 
356
- /** Read the current in-memory toggle state. */
357
- function getCurrentState(
358
- pi: ExtensionAPI,
359
- ): Pick<BrowserToggleState, "browserToolsEnabled" | "learnToolsEnabled"> {
360
- return {
361
- browserToolsEnabled: isBrowserEnabled(pi),
362
- learnToolsEnabled: isLearnEnabled(pi),
363
- };
364
- }
139
+ // ---- Toggle initializer ------------------------------------------
365
140
 
366
- // ---- Toggle initializer ----------------------------------------
367
-
368
- /**
369
- * Register the /web command and session-restoration hooks.
370
- * Called from pi-lean-portal's index.ts entry point.
371
- */
372
141
  export default function initBrowserToggle(pi: ExtensionAPI) {
373
- // ── 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
+ };
374
170
 
171
+ pi.events.on(TOOLSET_EVENTS.changed, syncCachedState);
172
+ pi.events.on(TOOLSET_EVENTS.restored, syncCachedState);
173
+
174
+ // ── /web command ──────────────────────────────────────────
375
175
  pi.registerCommand("web", {
376
176
  description:
377
177
  "Enable/disable browser automation tools. " +
378
178
  "Usage: /web on | off | learn | status",
379
179
  handler: async (args, ctx) => {
380
180
  const cmd = args.trim().toLowerCase();
381
- const hasBrowserTools = getRegisteredIn(pi, BROWSER_TOOL_NAMES).length > 0;
382
181
 
383
- 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
+ ) {
384
191
  ctx.ui.notify(
385
- "🌐 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.",
386
193
  "warning",
387
194
  );
388
195
  return;
389
196
  }
390
197
 
391
198
  if (cmd === "on") {
392
- applyBrowserState(pi, true);
393
- applyLearnState(pi, false);
394
- persistState(pi, {
395
- browserToolsEnabled: true,
396
- learnToolsEnabled: false,
397
- defaultProfile: _conversationDefaultProfile ?? "none",
398
- });
399
- ctx.ui.setStatus("browser", ctx.ui.theme.fg("accent", "●") + " idle");
400
-
401
- setSearchSlot(pi, ctx, ctx.ui.theme.fg("accent", "●") + " searxng");
402
-
199
+ webToolset.enable(pi);
200
+ learnToolset.disable(pi);
403
201
  ctx.ui.notify(
404
202
  "🌐 Browser tools enabled. /web learn to make web-learn available.",
405
203
  "info",
406
204
  );
407
205
  } else if (cmd === "learn") {
408
- applyBrowserState(pi, true);
409
- applyLearnState(pi, true);
410
- persistState(pi, {
411
- browserToolsEnabled: true,
412
- learnToolsEnabled: true,
413
- defaultProfile: _conversationDefaultProfile ?? "none",
414
- });
415
- ctx.ui.setStatus("browser", ctx.ui.theme.fg("success", "●") + " idle");
416
-
417
- setSearchSlot(pi, ctx, ctx.ui.theme.fg("accent", "●") + " searxng");
418
-
206
+ learnToolset.enable(pi); // cascades web on via requires
419
207
  ctx.ui.notify(
420
208
  "📖 web-learn tool is now available. Agent will save/update guides when asked.",
421
209
  "info",
422
210
  );
423
211
  } else if (cmd === "off") {
424
- applyBrowserState(pi, false);
425
- applyLearnState(pi, false);
426
- persistState(pi, {
427
- browserToolsEnabled: false,
428
- learnToolsEnabled: false,
429
- defaultProfile: _conversationDefaultProfile ?? "none",
430
- });
431
- ctx.ui.setStatus("browser", "○ web off");
432
-
433
- setSearchSlot(pi, ctx, "○ searxng");
434
-
212
+ webToolset.disable(pi); // cascades learn off via requires
435
213
  ctx.ui.notify(
436
214
  "🌐 Browser tools disabled. /web on to re-enable.",
437
215
  "info",
438
216
  );
439
217
  } else if (cmd === "profile" || cmd.startsWith("profile ")) {
440
218
  const sub = cmd.slice("profile".length).trim();
219
+ const { handleProfileSubcommand } = await import(
220
+ "./browser-profile.js"
221
+ );
441
222
  await handleProfileSubcommand(sub, ctx, pi, (profile: string) => {
442
- setConversationDefaultProfile(pi, profile);
223
+ if (profile === "none") {
224
+ _conversationDefaultProfile = undefined;
225
+ } else {
226
+ _conversationDefaultProfile = profile;
227
+ }
228
+ persistProfile(pi, profile);
443
229
  });
444
230
  } else if (cmd === "cookies" || cmd.startsWith("cookies ")) {
445
231
  const sub = cmd.slice("cookies".length).trim();
232
+ const { handleCookiesSubcommand } = await import(
233
+ "./browser-cookies.js"
234
+ );
446
235
  await handleCookiesSubcommand(sub, ctx);
447
236
  } else if (cmd === "status") {
448
- 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
+ );
449
243
  } else {
450
244
  // Default: show status
451
- const browserStatus = isBrowserEnabled(pi) ? "✅ on" : "❌ off";
452
- const learnStatus = isLearnEnabled(pi) ? "✅ on" : "❌ off";
245
+ const webOn = webToolset.isEnabled(pi) ? "✅ on" : "❌ off";
246
+ const learnOn = learnToolset.isEnabled(pi) ? "✅ on" : "❌ off";
453
247
  ctx.ui.notify(
454
- `🌐 Browser tools: ${browserStatus}\n` +
455
- `📖 Learn mode: ${learnStatus}\n` +
248
+ `🌐 Browser tools: ${webOn}\n` +
249
+ `📖 Learn mode: ${learnOn}\n` +
456
250
  ` /web profile manage browser profiles\n` +
457
251
  ` /web cookies inspect or clear session cookies\n` +
458
252
  ` /web off disable all browser tools\n` +
@@ -466,31 +260,20 @@ export default function initBrowserToggle(pi: ExtensionAPI) {
466
260
  },
467
261
  });
468
262
 
469
- // ── State restoration across session boundaries ────────────
470
- // Ensures the toggle survives /reload, /resume, /fork, and
471
- // /tree navigation without "forgetting" whether we're on or off.
472
- //
473
- // For fresh sessions (no branch state), falls back to the config
474
- // default from settings.json (`browserToggle.defaultEnabled`).
475
-
263
+ // ── Session handlers: restore profile + render glyph ─────
476
264
  pi.on("session_start", async (_event, ctx) => {
477
- const restored = restoreFromBranch(pi, ctx);
478
- if (!restored) {
479
- // No persisted state in this branch → apply config file default
480
- applyConfigDefault(pi);
481
- }
482
-
483
- // After state restoration, ensure the search slot reflects the actual
484
- // toggle state. Without this, a race between portal's session_start
485
- // (async — restores/applies state) and search's session_start (probes
486
- // health) can leave the search glyph showing blue when tools are off.
487
- const hasSiblingTools = getRegisteredIn(pi, SIBLING_TOOL_NAMES).length > 0;
488
- if (hasSiblingTools && !isBrowserEnabled(pi)) {
489
- ctx.ui.setStatus("search", "○ searxng");
490
- }
265
+ restoreProfile(pi, ctx);
266
+ _lastCtx = ctx;
267
+ syncCachedState();
491
268
  });
492
269
 
493
270
  pi.on("session_tree", async (_event, ctx) => {
494
- restoreFromBranch(pi, ctx);
271
+ restoreProfile(pi, ctx);
272
+ _lastCtx = ctx;
273
+ syncCachedState();
274
+ });
275
+
276
+ pi.on("session_shutdown", async () => {
277
+ _lastCtx = null;
495
278
  });
496
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