pi-lean-portal 0.1.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.
- package/LICENSE +661 -0
- package/README.md +608 -0
- package/backends/chromium/index.ts +50 -0
- package/backends/chromium-py/bridge.py +67 -0
- package/backends/firefox/index.ts +60 -0
- package/backends/firefox-py/bridge.py +64 -0
- package/backends/playwright-base/playwright-plugin.ts +1294 -0
- package/backends/python-adapter.ts +1141 -0
- package/backends/python-base/pi_browser_bridge/__init__.py +71 -0
- package/backends/python-base/pi_browser_bridge/accessibility.py +408 -0
- package/backends/python-base/pi_browser_bridge/bot_detection.py +115 -0
- package/backends/python-base/pi_browser_bridge/bridge.py +598 -0
- package/backends/python-base/pi_browser_bridge/playwright_base.py +1222 -0
- package/backends/python-base/pi_browser_bridge/transport.py +167 -0
- package/backends/python-base/pyproject.toml +15 -0
- package/browser-cookies.ts +88 -0
- package/browser-profile.ts +260 -0
- package/browser-status.ts +84 -0
- package/browser-toggle.ts +527 -0
- package/core/fetch-backend.ts +466 -0
- package/core/guides.ts +467 -0
- package/core/plugin-api.ts +302 -0
- package/core/plugin-config.ts +388 -0
- package/core/plugin-registry.ts +263 -0
- package/core/router.ts +1186 -0
- package/core/shared/accessibility-tree.ts +408 -0
- package/core/shared/bot-detection.ts +187 -0
- package/core/shared/browser-events.ts +111 -0
- package/core/shared/dom-extractor.ts +550 -0
- package/core/shared/nav-settle.ts +187 -0
- package/core/shared/paths.ts +56 -0
- package/core/shared/session-manager.ts +258 -0
- package/core/shared/settings-reader.ts +63 -0
- package/core/shared/snapshot-cache.ts +231 -0
- package/core/shared/storage-state.ts +560 -0
- package/core/shared/task-id.ts +77 -0
- package/core/shared/url-safety.ts +164 -0
- package/index.ts +253 -0
- package/package.json +63 -0
- package/ship-manifest.test.ts +12 -0
- package/tools/browser-back.ts +50 -0
- package/tools/browser-click.ts +74 -0
- package/tools/browser-console.ts +160 -0
- package/tools/browser-inspect.ts +136 -0
- package/tools/browser-navigate.ts +254 -0
- package/tools/browser-press.ts +80 -0
- package/tools/browser-scroll.ts +56 -0
- package/tools/browser-snapshot.ts +90 -0
- package/tools/browser-type.ts +60 -0
- package/tools/index.ts +19 -0
- package/tools/utils.ts +157 -0
- package/tools/web-fetch.ts +147 -0
- package/tools/web-guide.ts +55 -0
- package/tools/web-learn.ts +128 -0
- package/verify-ship-manifest.ts +126 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
// fs functions used by profile/cookies handlers are in their respective modules
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Browser Toggle — integrated into pi-lean-portal extension.
|
|
9
|
+
*
|
|
10
|
+
* Provides /web on, /web off, /web learn, and /web status commands to enable / disable
|
|
11
|
+
* browser automation tools (browsing tools) and learn tools (web-learn) in the system prompt.
|
|
12
|
+
*
|
|
13
|
+
* Three states: on (browsing only), learn (browsing + web-learn), off (all disabled).
|
|
14
|
+
* Internally stores two independent booleans (browserToolsEnabled, learnToolsEnabled).
|
|
15
|
+
*
|
|
16
|
+
* When browser tools are disabled, the LLM cannot see them — no descriptions,
|
|
17
|
+
* parameter schemas, prompt snippets, or guidelines are included in the context,
|
|
18
|
+
* saving roughly 1500–2000 tokens per turn.
|
|
19
|
+
*
|
|
20
|
+
* ── Stateless fallback ─────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* web-fetch is a stateless HTTP tool (no session, no interactivity). If you
|
|
23
|
+
* only need quick page content and never use the interactive browser tools,
|
|
24
|
+
* you can leave browser tools disabled and rely on web-fetch alone.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readMergedSettings } from "./core/shared/settings-reader.js";
|
|
28
|
+
|
|
29
|
+
// ---- Constants -------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Default toggle config key */
|
|
32
|
+
const CONFIG_KEY = "browserToggle";
|
|
33
|
+
|
|
34
|
+
/** Names of every browser browsing tool registered by pi-lean-portal's index.ts (excludes learn tools). */
|
|
35
|
+
const BROWSER_TOOL_NAMES = new Set([
|
|
36
|
+
"web-fetch",
|
|
37
|
+
"browser-navigate",
|
|
38
|
+
"browser-snapshot",
|
|
39
|
+
"browser-click",
|
|
40
|
+
"browser-type",
|
|
41
|
+
"browser-scroll",
|
|
42
|
+
"browser-back",
|
|
43
|
+
"browser-press",
|
|
44
|
+
"browser-console",
|
|
45
|
+
"browser-inspect",
|
|
46
|
+
"web-guide",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** Names of learn tools (web-learn) that require /web learn to be active. */
|
|
50
|
+
const LEARN_TOOL_NAMES = new Set(["web-learn"]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Names of sibling-package tools that are toggled alongside browser tools
|
|
54
|
+
* by /web on|off|learn.
|
|
55
|
+
*
|
|
56
|
+
* - "web-search": SearXNG search from pi-lean-search (Sprint 4).
|
|
57
|
+
*
|
|
58
|
+
* Exact-name `Set.has()` membership — NO regex (avoids false positives
|
|
59
|
+
* on third-party web-* tools).
|
|
60
|
+
*/
|
|
61
|
+
const SIBLING_TOOL_NAMES = new Set<string>(["web-search"]);
|
|
62
|
+
|
|
63
|
+
/** Persisted state shape — two independent booleans plus conversation-scoped default profile. */
|
|
64
|
+
interface BrowserToggleState {
|
|
65
|
+
browserToolsEnabled: boolean;
|
|
66
|
+
learnToolsEnabled: boolean;
|
|
67
|
+
/** Default profile for the current conversation. "none" means no override. */
|
|
68
|
+
defaultProfile: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Last known toggle state — used by status bar */
|
|
72
|
+
let _lastToggleState = true;
|
|
73
|
+
|
|
74
|
+
/** Last known learn state — used by status bar for glyph coloring */
|
|
75
|
+
let _lastLearnState = false;
|
|
76
|
+
|
|
77
|
+
/** Conversation-scoped default profile. "none" means: read from config file. */
|
|
78
|
+
let _conversationDefaultProfile: string | undefined;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the effective default profile. Returns the conversation-scoped override
|
|
82
|
+
* if set, otherwise falls back to "none" (the router reads browser.defaultProfile
|
|
83
|
+
* from settings.json as its own fallback).
|
|
84
|
+
*/
|
|
85
|
+
export function getConversationDefaultProfile(): string | undefined {
|
|
86
|
+
return _conversationDefaultProfile;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Helpers ---------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Return the subset of BROWSER_TOOL_NAMES that are actually registered.
|
|
93
|
+
* (With the toggle integrated into pi-lean-portal, this is always non-empty
|
|
94
|
+
* when pi-lean-portal is loaded, but the helper remains for robustness.)
|
|
95
|
+
*/
|
|
96
|
+
function getRegisteredBrowserTools(pi: ExtensionAPI): string[] {
|
|
97
|
+
return pi
|
|
98
|
+
.getAllTools()
|
|
99
|
+
.map((t) => t.name)
|
|
100
|
+
.filter((n) => BROWSER_TOOL_NAMES.has(n));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check whether browser tools are currently active in the system prompt.
|
|
105
|
+
* Returns true when no browser tools exist (nothing to toggle).
|
|
106
|
+
*/
|
|
107
|
+
function isBrowserEnabled(pi: ExtensionAPI): boolean {
|
|
108
|
+
const registered = getRegisteredBrowserTools(pi);
|
|
109
|
+
if (registered.length === 0) return true; // nothing registered → vacuously "enabled"
|
|
110
|
+
|
|
111
|
+
const active = new Set(pi.getActiveTools());
|
|
112
|
+
return registered.some((name) => active.has(name));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Apply a browser-tool state without persisting.
|
|
117
|
+
* @param pi ExtensionAPI
|
|
118
|
+
* @param enable true = add browser tools to the active set, false = remove them
|
|
119
|
+
*/
|
|
120
|
+
function applyBrowserState(pi: ExtensionAPI, enable: boolean): void {
|
|
121
|
+
// Combine browser tools + sibling tools into one toggle set.
|
|
122
|
+
// (/web on enables both; /web off disables both.)
|
|
123
|
+
const registered = new Set([
|
|
124
|
+
...getRegisteredBrowserTools(pi),
|
|
125
|
+
...getRegisteredSiblingTools(pi),
|
|
126
|
+
]);
|
|
127
|
+
if (registered.size === 0) return;
|
|
128
|
+
_lastToggleState = enable;
|
|
129
|
+
|
|
130
|
+
if (enable) {
|
|
131
|
+
// Merge browser+sibling tools back into whatever is currently active
|
|
132
|
+
const current = pi.getActiveTools();
|
|
133
|
+
pi.setActiveTools([...new Set([...current, ...registered])]);
|
|
134
|
+
} else {
|
|
135
|
+
// Keep everything that is NOT a browser+sibling tool
|
|
136
|
+
const all = pi.getAllTools().map((t) => t.name);
|
|
137
|
+
pi.setActiveTools(all.filter((name) => !registered.has(name)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Persist the current toggle state into the session for branch-aware
|
|
143
|
+
* restoration across /reload, /resume, /fork, and /tree navigation.
|
|
144
|
+
*/
|
|
145
|
+
function persistState(pi: ExtensionAPI, state: BrowserToggleState): void {
|
|
146
|
+
pi.appendEntry<BrowserToggleState>("web-toggle-state", state);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- Branch-aware restoration ----------------------------------
|
|
150
|
+
|
|
151
|
+
function restoreFromBranch(pi: ExtensionAPI, ctx: ExtensionContext): boolean {
|
|
152
|
+
const registered = getRegisteredBrowserTools(pi);
|
|
153
|
+
if (registered.length === 0) return false;
|
|
154
|
+
|
|
155
|
+
let savedState: BrowserToggleState | undefined;
|
|
156
|
+
|
|
157
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
158
|
+
if (entry.type === "custom" && entry.customType === "web-toggle-state") {
|
|
159
|
+
const data = entry.data as Record<string, unknown>;
|
|
160
|
+
if (data && typeof data.browserToolsEnabled === "boolean") {
|
|
161
|
+
savedState = data as unknown as BrowserToggleState;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (savedState !== undefined) {
|
|
167
|
+
applyBrowserState(pi, savedState.browserToolsEnabled);
|
|
168
|
+
applyLearnState(pi, savedState.learnToolsEnabled);
|
|
169
|
+
// Restore conversation-scoped default profile
|
|
170
|
+
if (savedState.defaultProfile && savedState.defaultProfile !== "none") {
|
|
171
|
+
_conversationDefaultProfile = savedState.defaultProfile;
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Apply the config-file default on a fresh session (no branch state found).
|
|
181
|
+
* Reads `browserToggle.defaultEnabled` from pi's settings.json files.
|
|
182
|
+
* Learn always starts disabled by default.
|
|
183
|
+
*/
|
|
184
|
+
function applyConfigDefault(pi: ExtensionAPI): void {
|
|
185
|
+
const enabled = readBrowserToggleConfig();
|
|
186
|
+
applyBrowserState(pi, enabled);
|
|
187
|
+
applyLearnState(pi, false);
|
|
188
|
+
// Persist so subsequent branch navigation sees this as the initial state
|
|
189
|
+
persistState(pi, {
|
|
190
|
+
browserToolsEnabled: enabled,
|
|
191
|
+
learnToolsEnabled: false,
|
|
192
|
+
defaultProfile: "none",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- Unit-test exports -----------------------------------------
|
|
197
|
+
// Helper functions are exported for testing while the default export
|
|
198
|
+
// wires the toggle into pi-lean-portal's runtime.
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Return the last known toggle state (true = enabled, false = disabled).
|
|
202
|
+
* Returns true initially at startup (browser tools start enabled).
|
|
203
|
+
*/
|
|
204
|
+
export function getToggleState(): boolean {
|
|
205
|
+
return _lastToggleState;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Return the last known learn state (true = learn mode enabled).
|
|
210
|
+
* Used by the status bar to color the glyph differently.
|
|
211
|
+
*/
|
|
212
|
+
export function getLearnState(): boolean {
|
|
213
|
+
return _lastLearnState;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** @internal Reset module-level state to defaults (test helper). */
|
|
217
|
+
function _resetToggleStateForTest(): void {
|
|
218
|
+
_lastToggleState = true;
|
|
219
|
+
_lastLearnState = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reset all module-level toggle state to defaults.
|
|
224
|
+
*
|
|
225
|
+
* Called at the start of the extension entry function to ensure safe
|
|
226
|
+
* re-invocation when pi reuses the cached module factory (e.g.
|
|
227
|
+
* during /resume to the same working directory).
|
|
228
|
+
* Resets both the toggle status and conversation-scoped profile.
|
|
229
|
+
*/
|
|
230
|
+
export function resetToggleModuleState(): void {
|
|
231
|
+
_lastToggleState = true;
|
|
232
|
+
_lastLearnState = false;
|
|
233
|
+
_conversationDefaultProfile = undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- Config-file support -----------------------------------------
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read the browser toggle default from pi's settings.json files.
|
|
240
|
+
*
|
|
241
|
+
* Looks for `browserToggle.defaultEnabled` in:
|
|
242
|
+
* 1. `~/.pi/agent/settings.json` (global)
|
|
243
|
+
* 2. `.pi/settings.json` (project-local, overrides global)
|
|
244
|
+
*
|
|
245
|
+
* Returns the config value, or `true` if unset (backward-compatible default).
|
|
246
|
+
* Silently returns `true` on missing files, parse errors, or type mismatches.
|
|
247
|
+
*/
|
|
248
|
+
function readBrowserToggleConfig(): boolean {
|
|
249
|
+
const merged = readMergedSettings();
|
|
250
|
+
const segment = merged[CONFIG_KEY];
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
segment &&
|
|
254
|
+
typeof segment === "object" &&
|
|
255
|
+
!Array.isArray(segment) &&
|
|
256
|
+
typeof (segment as Record<string, unknown>).defaultEnabled === "boolean"
|
|
257
|
+
) {
|
|
258
|
+
return (segment as Record<string, unknown>).defaultEnabled as boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return true; // default: enabled
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Return the subset of LEARN_TOOL_NAMES that are actually registered.
|
|
266
|
+
*/
|
|
267
|
+
function getRegisteredLearnTools(pi: ExtensionAPI): string[] {
|
|
268
|
+
return pi
|
|
269
|
+
.getAllTools()
|
|
270
|
+
.map((t) => t.name)
|
|
271
|
+
.filter((n) => LEARN_TOOL_NAMES.has(n));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Return the subset of SIBLING_TOOL_NAMES that are actually registered.
|
|
276
|
+
* Used by applyBrowserState to include sibling tools in toggle operations.
|
|
277
|
+
*/
|
|
278
|
+
function getRegisteredSiblingTools(pi: ExtensionAPI): string[] {
|
|
279
|
+
return pi
|
|
280
|
+
.getAllTools()
|
|
281
|
+
.map((t) => t.name)
|
|
282
|
+
.filter((n) => SIBLING_TOOL_NAMES.has(n));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check whether learn tools are currently active.
|
|
287
|
+
* Returns true when no learn tools exist (vacuously enabled).
|
|
288
|
+
*/
|
|
289
|
+
function isLearnEnabled(pi: ExtensionAPI): boolean {
|
|
290
|
+
const registered = getRegisteredLearnTools(pi);
|
|
291
|
+
if (registered.length === 0) return true;
|
|
292
|
+
const active = new Set(pi.getActiveTools());
|
|
293
|
+
return registered.some((name) => active.has(name));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Apply a learn-tool state without persisting.
|
|
298
|
+
* Uses getActiveTools() (not getAllTools()) to avoid accidentally activating
|
|
299
|
+
* learn tools that are not currently registered.
|
|
300
|
+
*/
|
|
301
|
+
function applyLearnState(pi: ExtensionAPI, enable: boolean): void {
|
|
302
|
+
_lastLearnState = enable;
|
|
303
|
+
const registered = new Set(getRegisteredLearnTools(pi));
|
|
304
|
+
if (registered.size === 0) return;
|
|
305
|
+
if (enable) {
|
|
306
|
+
const current = pi.getActiveTools();
|
|
307
|
+
pi.setActiveTools([...new Set([...current, ...registered])]);
|
|
308
|
+
} else {
|
|
309
|
+
// Remove learn tools from the *currently active* set
|
|
310
|
+
const current = pi.getActiveTools();
|
|
311
|
+
pi.setActiveTools(current.filter((name) => !registered.has(name)));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Test-only exports ──────────────────────────────────
|
|
316
|
+
// These are exported solely for unit testing via
|
|
317
|
+
// __tests__/helpers/toggle-test-utils.ts. Do not import
|
|
318
|
+
// them directly from production code.
|
|
319
|
+
/** @internal */
|
|
320
|
+
export {
|
|
321
|
+
getRegisteredBrowserTools,
|
|
322
|
+
isBrowserEnabled,
|
|
323
|
+
applyBrowserState,
|
|
324
|
+
persistState,
|
|
325
|
+
restoreFromBranch,
|
|
326
|
+
readBrowserToggleConfig,
|
|
327
|
+
applyConfigDefault,
|
|
328
|
+
getRegisteredLearnTools,
|
|
329
|
+
getRegisteredSiblingTools,
|
|
330
|
+
isLearnEnabled,
|
|
331
|
+
applyLearnState,
|
|
332
|
+
_resetToggleStateForTest,
|
|
333
|
+
};
|
|
334
|
+
export type { BrowserToggleState };
|
|
335
|
+
|
|
336
|
+
// ---- Profile & Cookies (extracted) ------------------------------
|
|
337
|
+
|
|
338
|
+
import { handleProfileSubcommand } from "./browser-profile.js";
|
|
339
|
+
import { handleCookiesSubcommand } from "./browser-cookies.js";
|
|
340
|
+
import { handleStatusSubcommand } from "./browser-status.js";
|
|
341
|
+
|
|
342
|
+
// ---- Internal helpers (used by delegated handlers) ---------------
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Set the conversation-scoped default profile.
|
|
346
|
+
* Affects this conversation only; survives /reload, /resume, /fork.
|
|
347
|
+
* "none" resets to the config-file default.
|
|
348
|
+
*/
|
|
349
|
+
function setConversationDefaultProfile(
|
|
350
|
+
pi: ExtensionAPI,
|
|
351
|
+
profile: string,
|
|
352
|
+
): void {
|
|
353
|
+
if (profile === "none") {
|
|
354
|
+
_conversationDefaultProfile = undefined;
|
|
355
|
+
} else {
|
|
356
|
+
_conversationDefaultProfile = profile;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Persist so it survives /reload, /resume, /fork
|
|
360
|
+
const currentState = getCurrentState(pi);
|
|
361
|
+
persistState(pi, {
|
|
362
|
+
...currentState,
|
|
363
|
+
defaultProfile: profile,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Read the current in-memory toggle state. */
|
|
368
|
+
function getCurrentState(
|
|
369
|
+
pi: ExtensionAPI,
|
|
370
|
+
): Pick<BrowserToggleState, "browserToolsEnabled" | "learnToolsEnabled"> {
|
|
371
|
+
return {
|
|
372
|
+
browserToolsEnabled: isBrowserEnabled(pi),
|
|
373
|
+
learnToolsEnabled: isLearnEnabled(pi),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- Toggle initializer ----------------------------------------
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Register the /web command and session-restoration hooks.
|
|
381
|
+
* Called from pi-lean-portal's index.ts entry point.
|
|
382
|
+
*/
|
|
383
|
+
export default function initBrowserToggle(pi: ExtensionAPI) {
|
|
384
|
+
// ── Commands ──────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
pi.registerCommand("web", {
|
|
387
|
+
description:
|
|
388
|
+
"Enable/disable browser automation tools. " +
|
|
389
|
+
"Usage: /web on | off | learn | status",
|
|
390
|
+
handler: async (args, ctx) => {
|
|
391
|
+
const cmd = args.trim().toLowerCase();
|
|
392
|
+
const hasBrowserTools = getRegisteredBrowserTools(pi).length > 0;
|
|
393
|
+
|
|
394
|
+
if (!hasBrowserTools) {
|
|
395
|
+
ctx.ui.notify(
|
|
396
|
+
"🌐 pi-lean-portal extension not detected. Nothing to toggle.",
|
|
397
|
+
"warning",
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (cmd === "on") {
|
|
403
|
+
applyBrowserState(pi, true);
|
|
404
|
+
applyLearnState(pi, false);
|
|
405
|
+
persistState(pi, {
|
|
406
|
+
browserToolsEnabled: true,
|
|
407
|
+
learnToolsEnabled: false,
|
|
408
|
+
defaultProfile: _conversationDefaultProfile ?? "none",
|
|
409
|
+
});
|
|
410
|
+
ctx.ui.setStatus("browser", ctx.ui.theme.fg("accent", "●") + " idle");
|
|
411
|
+
|
|
412
|
+
// Clear any stale search off-glyph — sibling tools are now enabled.
|
|
413
|
+
const hasSiblingTools = getRegisteredSiblingTools(pi).length > 0;
|
|
414
|
+
if (hasSiblingTools) {
|
|
415
|
+
ctx.ui.setStatus(
|
|
416
|
+
"search",
|
|
417
|
+
ctx.ui.theme.fg("accent", "●") + " searxng",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
ctx.ui.notify(
|
|
422
|
+
"🌐 Browser tools enabled. /web learn to make web-learn available.",
|
|
423
|
+
"info",
|
|
424
|
+
);
|
|
425
|
+
} else if (cmd === "learn") {
|
|
426
|
+
applyBrowserState(pi, true);
|
|
427
|
+
applyLearnState(pi, true);
|
|
428
|
+
persistState(pi, {
|
|
429
|
+
browserToolsEnabled: true,
|
|
430
|
+
learnToolsEnabled: true,
|
|
431
|
+
defaultProfile: _conversationDefaultProfile ?? "none",
|
|
432
|
+
});
|
|
433
|
+
ctx.ui.setStatus("browser", ctx.ui.theme.fg("success", "●") + " idle");
|
|
434
|
+
|
|
435
|
+
// Clear any stale search off-glyph — sibling tools are now enabled.
|
|
436
|
+
const hasSiblingTools = getRegisteredSiblingTools(pi).length > 0;
|
|
437
|
+
if (hasSiblingTools) {
|
|
438
|
+
ctx.ui.setStatus(
|
|
439
|
+
"search",
|
|
440
|
+
ctx.ui.theme.fg("accent", "●") + " searxng",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
ctx.ui.notify(
|
|
445
|
+
"📖 web-learn tool is now available. Agent will save/update guides when asked.",
|
|
446
|
+
"info",
|
|
447
|
+
);
|
|
448
|
+
} else if (cmd === "off") {
|
|
449
|
+
applyBrowserState(pi, false);
|
|
450
|
+
applyLearnState(pi, false);
|
|
451
|
+
persistState(pi, {
|
|
452
|
+
browserToolsEnabled: false,
|
|
453
|
+
learnToolsEnabled: false,
|
|
454
|
+
defaultProfile: _conversationDefaultProfile ?? "none",
|
|
455
|
+
});
|
|
456
|
+
ctx.ui.setStatus("browser", "○ web off");
|
|
457
|
+
|
|
458
|
+
// Set the search tool status to off if sibling apps are present.
|
|
459
|
+
// Search owns the colored glyph for on states; portal only writes
|
|
460
|
+
// the off state when tools are explicitly disabled.
|
|
461
|
+
const hasSiblingTools = getRegisteredSiblingTools(pi).length > 0;
|
|
462
|
+
if (hasSiblingTools) {
|
|
463
|
+
ctx.ui.setStatus("search", "○ searxng");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
ctx.ui.notify(
|
|
467
|
+
"🌐 Browser tools disabled. /web on to re-enable.",
|
|
468
|
+
"info",
|
|
469
|
+
);
|
|
470
|
+
} else if (cmd === "profile" || cmd.startsWith("profile ")) {
|
|
471
|
+
const sub = cmd.slice("profile".length).trim();
|
|
472
|
+
await handleProfileSubcommand(sub, ctx, pi, (profile: string) => {
|
|
473
|
+
setConversationDefaultProfile(pi, profile);
|
|
474
|
+
});
|
|
475
|
+
} else if (cmd === "cookies" || cmd.startsWith("cookies ")) {
|
|
476
|
+
const sub = cmd.slice("cookies".length).trim();
|
|
477
|
+
await handleCookiesSubcommand(sub, ctx);
|
|
478
|
+
} else if (cmd === "status") {
|
|
479
|
+
handleStatusSubcommand(ctx, isBrowserEnabled(pi), isLearnEnabled(pi));
|
|
480
|
+
} else {
|
|
481
|
+
// Default: show status
|
|
482
|
+
const browserStatus = isBrowserEnabled(pi) ? "✅ on" : "❌ off";
|
|
483
|
+
const learnStatus = isLearnEnabled(pi) ? "✅ on" : "❌ off";
|
|
484
|
+
ctx.ui.notify(
|
|
485
|
+
`🌐 Browser tools: ${browserStatus}\n` +
|
|
486
|
+
`📖 Learn mode: ${learnStatus}\n` +
|
|
487
|
+
` /web profile manage browser profiles\n` +
|
|
488
|
+
` /web cookies inspect or clear session cookies\n` +
|
|
489
|
+
` /web off disable all browser tools\n` +
|
|
490
|
+
` /web on enable browsing only\n` +
|
|
491
|
+
` /web learn enable browsing + guide-saving\n` +
|
|
492
|
+
` /web status detailed runtime status (sessions, plugins, profiles)\n` +
|
|
493
|
+
` /web show this status`,
|
|
494
|
+
"info",
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ── State restoration across session boundaries ────────────
|
|
501
|
+
// Ensures the toggle survives /reload, /resume, /fork, and
|
|
502
|
+
// /tree navigation without "forgetting" whether we're on or off.
|
|
503
|
+
//
|
|
504
|
+
// For fresh sessions (no branch state), falls back to the config
|
|
505
|
+
// default from settings.json (`browserToggle.defaultEnabled`).
|
|
506
|
+
|
|
507
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
508
|
+
const restored = restoreFromBranch(pi, ctx);
|
|
509
|
+
if (!restored) {
|
|
510
|
+
// No persisted state in this branch → apply config file default
|
|
511
|
+
applyConfigDefault(pi);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// After state restoration, ensure the search slot reflects the actual
|
|
515
|
+
// toggle state. Without this, a race between portal's session_start
|
|
516
|
+
// (async — restores/applies state) and search's session_start (probes
|
|
517
|
+
// health) can leave the search glyph showing blue when tools are off.
|
|
518
|
+
const hasSiblingTools = getRegisteredSiblingTools(pi).length > 0;
|
|
519
|
+
if (hasSiblingTools && !isBrowserEnabled(pi)) {
|
|
520
|
+
ctx.ui.setStatus("search", "○ searxng");
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
525
|
+
restoreFromBranch(pi, ctx);
|
|
526
|
+
});
|
|
527
|
+
}
|