@demigodmode/pi-web-agent 0.2.2 → 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 (101) hide show
  1. package/README.md +119 -63
  2. package/dist/commands/web-agent-config.d.ts +23 -0
  3. package/dist/commands/web-agent-config.js +254 -0
  4. package/dist/extension.js +113 -4
  5. package/dist/presentation/config-store.d.ts +23 -0
  6. package/dist/presentation/config-store.js +64 -0
  7. package/dist/presentation/config.d.ts +7 -0
  8. package/dist/presentation/config.js +44 -0
  9. package/dist/presentation/explore-presentation.d.ts +3 -0
  10. package/dist/presentation/explore-presentation.js +34 -0
  11. package/dist/presentation/fetch-presentation.d.ts +5 -0
  12. package/dist/presentation/fetch-presentation.js +40 -0
  13. package/dist/presentation/search-presentation.d.ts +3 -0
  14. package/dist/presentation/search-presentation.js +30 -0
  15. package/dist/presentation/select-view.d.ts +2 -0
  16. package/dist/presentation/select-view.js +12 -0
  17. package/dist/presentation/types.d.ts +50 -0
  18. package/dist/presentation/types.js +1 -0
  19. package/dist/search/duckduckgo.d.ts +6 -1
  20. package/dist/search/duckduckgo.js +11 -1
  21. package/dist/tools/web-explore.d.ts +6 -16
  22. package/dist/tools/web-explore.js +12 -10
  23. package/dist/tools/web-fetch-headless.js +11 -2
  24. package/dist/tools/web-fetch.js +11 -2
  25. package/dist/tools/web-search.js +99 -12
  26. package/dist/types.d.ts +15 -0
  27. package/package.json +1 -1
  28. package/dist/scripts/live-web-eval.d.ts +0 -1
  29. package/dist/scripts/live-web-eval.js +0 -411
  30. package/dist/src/cache/ttl-cache.d.ts +0 -8
  31. package/dist/src/cache/ttl-cache.js +0 -21
  32. package/dist/src/extension.d.ts +0 -2
  33. package/dist/src/extension.js +0 -155
  34. package/dist/src/extract/readability.d.ts +0 -8
  35. package/dist/src/extract/readability.js +0 -93
  36. package/dist/src/fetch/browser-resolution.d.ts +0 -15
  37. package/dist/src/fetch/browser-resolution.js +0 -55
  38. package/dist/src/fetch/headless-fetch.d.ts +0 -18
  39. package/dist/src/fetch/headless-fetch.js +0 -87
  40. package/dist/src/fetch/http-fetch.d.ts +0 -4
  41. package/dist/src/fetch/http-fetch.js +0 -50
  42. package/dist/src/orchestration/index.d.ts +0 -41
  43. package/dist/src/orchestration/index.js +0 -9
  44. package/dist/src/orchestration/research-orchestrator.d.ts +0 -43
  45. package/dist/src/orchestration/research-orchestrator.js +0 -87
  46. package/dist/src/orchestration/research-types.d.ts +0 -41
  47. package/dist/src/orchestration/research-types.js +0 -1
  48. package/dist/src/orchestration/research-worker.d.ts +0 -16
  49. package/dist/src/orchestration/research-worker.js +0 -131
  50. package/dist/src/search/duckduckgo.d.ts +0 -9
  51. package/dist/src/search/duckduckgo.js +0 -52
  52. package/dist/src/tools/web-explore.d.ts +0 -44
  53. package/dist/src/tools/web-explore.js +0 -50
  54. package/dist/src/tools/web-fetch-headless.d.ts +0 -6
  55. package/dist/src/tools/web-fetch-headless.js +0 -14
  56. package/dist/src/tools/web-fetch.d.ts +0 -6
  57. package/dist/src/tools/web-fetch.js +0 -14
  58. package/dist/src/tools/web-search.d.ts +0 -10
  59. package/dist/src/tools/web-search.js +0 -103
  60. package/dist/src/types.d.ts +0 -48
  61. package/dist/src/types.js +0 -7
  62. package/dist/tests/cache/ttl-cache.test.d.ts +0 -1
  63. package/dist/tests/cache/ttl-cache.test.js +0 -19
  64. package/dist/tests/contracts.test.d.ts +0 -1
  65. package/dist/tests/contracts.test.js +0 -65
  66. package/dist/tests/extension.test.d.ts +0 -1
  67. package/dist/tests/extension.test.js +0 -123
  68. package/dist/tests/extract/readability.test.d.ts +0 -1
  69. package/dist/tests/extract/readability.test.js +0 -79
  70. package/dist/tests/fetch/browser-resolution.test.d.ts +0 -1
  71. package/dist/tests/fetch/browser-resolution.test.js +0 -37
  72. package/dist/tests/fetch/headless-fetch.smoke.test.d.ts +0 -1
  73. package/dist/tests/fetch/headless-fetch.smoke.test.js +0 -17
  74. package/dist/tests/fetch/headless-fetch.test.d.ts +0 -1
  75. package/dist/tests/fetch/headless-fetch.test.js +0 -150
  76. package/dist/tests/fetch/http-fetch.test.d.ts +0 -1
  77. package/dist/tests/fetch/http-fetch.test.js +0 -129
  78. package/dist/tests/orchestration/research-orchestrator.test.d.ts +0 -1
  79. package/dist/tests/orchestration/research-orchestrator.test.js +0 -298
  80. package/dist/tests/orchestration/research-worker.test.d.ts +0 -1
  81. package/dist/tests/orchestration/research-worker.test.js +0 -171
  82. package/dist/tests/orchestration/research-workflow.test.d.ts +0 -1
  83. package/dist/tests/orchestration/research-workflow.test.js +0 -119
  84. package/dist/tests/package-manifest.test.d.ts +0 -1
  85. package/dist/tests/package-manifest.test.js +0 -29
  86. package/dist/tests/release-foundation.test.d.ts +0 -1
  87. package/dist/tests/release-foundation.test.js +0 -16
  88. package/dist/tests/release-script.test.d.ts +0 -1
  89. package/dist/tests/release-script.test.js +0 -72
  90. package/dist/tests/search/duckduckgo.test.d.ts +0 -1
  91. package/dist/tests/search/duckduckgo.test.js +0 -103
  92. package/dist/tests/tools/web-explore.test.d.ts +0 -1
  93. package/dist/tests/tools/web-explore.test.js +0 -163
  94. package/dist/tests/tools/web-fetch-headless.test.d.ts +0 -1
  95. package/dist/tests/tools/web-fetch-headless.test.js +0 -31
  96. package/dist/tests/tools/web-fetch.test.d.ts +0 -1
  97. package/dist/tests/tools/web-fetch.test.js +0 -27
  98. package/dist/tests/tools/web-search.test.d.ts +0 -1
  99. package/dist/tests/tools/web-search.test.js +0 -125
  100. package/dist/vitest.config.d.ts +0 -2
  101. package/dist/vitest.config.js +0 -13
package/README.md CHANGED
@@ -1,63 +1,119 @@
1
- # pi-web-agent
2
-
3
- [![CI](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/@demigodmode/pi-web-agent)](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
5
- [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://demigodmode.github.io/pi-web-agent/)
6
-
7
- `@demigodmode/pi-web-agent` is a Pi package for web access.
8
-
9
- The whole point is keeping the boundaries straight:
10
-
11
- - `web_search` is for discovery
12
- - `web_fetch` is for plain HTTP reads
13
- - `web_fetch_headless` is the explicit browser path
14
- - `web_explore` is the bounded research path
15
-
16
- That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust.
17
-
18
- ## Install
19
-
20
- ```bash
21
- pi install npm:@demigodmode/pi-web-agent
22
- ```
23
-
24
- Later on, update installed packages with:
25
-
26
- ```bash
27
- pi update
28
- ```
29
-
30
- ## Docs
31
-
32
- Docs site:
33
-
34
- - https://demigodmode.github.io/pi-web-agent/
35
-
36
- Work on the docs locally:
37
-
38
- ```bash
39
- npm run docs:dev
40
- ```
41
-
42
- Build the docs:
43
-
44
- ```bash
45
- npm run docs:build
46
- ```
47
-
48
- ## Local development
49
-
50
- ```bash
51
- npm install
52
- npm test
53
- npm run lint
54
- npm run build
55
- ```
56
-
57
- For local Pi work, this repo includes `.pi/extensions/pi-web-agent.ts`.
58
-
59
- If Pi is already running, use `/reload` after changes.
60
-
61
- ## License
62
-
63
- AGPL-3.0-only. See `LICENSE`.
1
+ # pi-web-agent
2
+
3
+ [![CI](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@demigodmode/pi-web-agent)](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
5
+ [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://demigodmode.github.io/pi-web-agent/)
6
+
7
+ `@demigodmode/pi-web-agent` is a Pi package for web access.
8
+
9
+ The whole point is keeping the boundaries straight:
10
+
11
+ - `web_search` is for discovery
12
+ - `web_fetch` is for plain HTTP reads
13
+ - `web_fetch_headless` is the explicit browser path
14
+ - `web_explore` is the bounded research path
15
+
16
+ That sounds obvious, but a lot of agent tooling gets fuzzy right there. This package is meant to be stricter about what it actually did and more willing to say when a read was not good enough to trust.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pi install npm:@demigodmode/pi-web-agent
22
+ ```
23
+
24
+ Later on, update installed packages with:
25
+
26
+ ```bash
27
+ pi update
28
+ ```
29
+
30
+ ## Docs
31
+
32
+ Docs site:
33
+
34
+ - https://demigodmode.github.io/pi-web-agent/
35
+
36
+ Work on the docs locally:
37
+
38
+ ```bash
39
+ npm run docs:dev
40
+ ```
41
+
42
+ Build the docs:
43
+
44
+ ```bash
45
+ npm run docs:build
46
+ ```
47
+
48
+ ## Presentation modes
49
+
50
+ `pi-web-agent` renders web tool output in one visible mode at a time:
51
+
52
+ - `compact` — short summary, default everywhere
53
+ - `preview` — slightly richer bounded view
54
+ - `verbose` — fuller bounded view
55
+
56
+ ## Settings
57
+
58
+ Primary UI:
59
+
60
+ ```text
61
+ /web-agent settings
62
+ ```
63
+
64
+ Helper commands:
65
+
66
+ ```text
67
+ /web-agent show
68
+ /web-agent reset project
69
+ /web-agent reset global
70
+ /web-agent mode preview
71
+ /web-agent mode web_search verbose
72
+ /web-agent mode web_search inherit
73
+ ```
74
+
75
+ Config files:
76
+
77
+ ```text
78
+ Global: ~/.pi/agent/extensions/pi-web-agent/config.json
79
+ Project: .pi/extensions/pi-web-agent/config.json
80
+ ```
81
+
82
+ Precedence:
83
+
84
+ - built-in defaults
85
+ - global config
86
+ - project config
87
+
88
+ Project config overrides global config.
89
+
90
+ Example:
91
+
92
+ ```json
93
+ {
94
+ "presentation": {
95
+ "defaultMode": "compact",
96
+ "tools": {
97
+ "web_search": { "mode": "preview" },
98
+ "web_explore": { "mode": "verbose" }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Local development
105
+
106
+ ```bash
107
+ npm install
108
+ npm test
109
+ npm run lint
110
+ npm run build
111
+ ```
112
+
113
+ For local Pi work, this repo includes `.pi/extensions/pi-web-agent.ts`.
114
+
115
+ If Pi is already running, use `/reload` after changes.
116
+
117
+ ## License
118
+
119
+ AGPL-3.0-only. See `LICENSE`.
@@ -0,0 +1,23 @@
1
+ import { type ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
+ import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
3
+ import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
4
+ type CommandDeps = {
5
+ load?: () => ReturnType<typeof loadPresentationConfigLayers>;
6
+ save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
7
+ reset?: (scope: PresentationScope) => Promise<void>;
8
+ };
9
+ export type SettingsDraftState = {
10
+ scope: PresentationScope;
11
+ drafts: Record<PresentationScope, PresentationConfig>;
12
+ config: PresentationConfig;
13
+ };
14
+ export declare function getInheritedConfigForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
15
+ export declare function getScopeDisplayConfig(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
16
+ export declare function createSettingsDraftState(loaded: Awaited<LoadedPresentationConfig>, initialScope: PresentationScope): SettingsDraftState;
17
+ export declare function applySettingsValue(state: SettingsDraftState, id: string, newValue: string): SettingsDraftState;
18
+ export declare function collapsePresentationConfigToOverride(config: PresentationConfig, inheritedConfig: PresentationConfig): PresentationConfigOverride;
19
+ export declare function handleSettingsShortcut(data: string): {
20
+ action: 'cancel' | 'reset' | 'save';
21
+ } | undefined;
22
+ export declare function registerWebAgentConfigCommands(pi: ExtensionAPI, deps?: CommandDeps): void;
23
+ export {};
@@ -0,0 +1,254 @@
1
+ import { getSettingsListTheme } from '@mariozechner/pi-coding-agent';
2
+ import { Container, SettingsList, Text } from '@mariozechner/pi-tui';
3
+ import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
4
+ import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
5
+ const PRESENTATION_TOOL_NAMES = [
6
+ 'web_search',
7
+ 'web_fetch',
8
+ 'web_fetch_headless',
9
+ 'web_explore'
10
+ ];
11
+ function parseScopeToken(token) {
12
+ return token === 'global' || token === 'project' ? token : undefined;
13
+ }
14
+ function clonePresentationConfig(config) {
15
+ return {
16
+ defaultMode: config.defaultMode,
17
+ tools: { ...config.tools }
18
+ };
19
+ }
20
+ function formatConfigSummary(config) {
21
+ const lines = [`defaultMode: ${config.defaultMode}`];
22
+ for (const toolName of PRESENTATION_TOOL_NAMES) {
23
+ lines.push(`${toolName}: ${config.tools[toolName]?.mode ?? 'inherit'}`);
24
+ }
25
+ return lines.join('\n');
26
+ }
27
+ function buildSettingsItems(scope, config) {
28
+ return [
29
+ {
30
+ id: 'scope',
31
+ label: 'Write scope',
32
+ currentValue: scope,
33
+ values: ['project', 'global']
34
+ },
35
+ {
36
+ id: 'defaultMode',
37
+ label: 'Default mode',
38
+ currentValue: config.defaultMode,
39
+ values: ['compact', 'preview', 'verbose']
40
+ },
41
+ ...PRESENTATION_TOOL_NAMES.map((toolName) => ({
42
+ id: `tool:${toolName}`,
43
+ label: toolName,
44
+ currentValue: config.tools[toolName]?.mode ?? 'inherit',
45
+ values: ['inherit', 'compact', 'preview', 'verbose']
46
+ }))
47
+ ];
48
+ }
49
+ function isToolName(value) {
50
+ return PRESENTATION_TOOL_NAMES.includes(value);
51
+ }
52
+ function isModeOrInherit(value) {
53
+ return ['inherit', 'compact', 'preview', 'verbose'].includes(value);
54
+ }
55
+ export function getInheritedConfigForScope(loaded, scope) {
56
+ if (scope === 'global') {
57
+ return DEFAULT_PRESENTATION_CONFIG;
58
+ }
59
+ return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
60
+ }
61
+ export function getScopeDisplayConfig(loaded, scope) {
62
+ if (scope === 'global') {
63
+ return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
64
+ }
65
+ return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig, loaded.project.rawConfig);
66
+ }
67
+ export function createSettingsDraftState(loaded, initialScope) {
68
+ const drafts = {
69
+ global: getScopeDisplayConfig(loaded, 'global'),
70
+ project: getScopeDisplayConfig(loaded, 'project')
71
+ };
72
+ return {
73
+ scope: initialScope,
74
+ drafts,
75
+ config: clonePresentationConfig(drafts[initialScope])
76
+ };
77
+ }
78
+ export function applySettingsValue(state, id, newValue) {
79
+ const nextDrafts = {
80
+ global: clonePresentationConfig(state.drafts.global),
81
+ project: clonePresentationConfig(state.drafts.project)
82
+ };
83
+ let nextScope = state.scope;
84
+ if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
85
+ nextScope = newValue;
86
+ return {
87
+ scope: nextScope,
88
+ drafts: nextDrafts,
89
+ config: clonePresentationConfig(nextDrafts[nextScope])
90
+ };
91
+ }
92
+ const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
93
+ if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
94
+ currentDraft.defaultMode = newValue;
95
+ }
96
+ if (id.startsWith('tool:')) {
97
+ const toolName = id.slice('tool:'.length);
98
+ const nextTools = { ...currentDraft.tools };
99
+ if (newValue === 'inherit') {
100
+ delete nextTools[toolName];
101
+ }
102
+ else if (newValue === 'compact' ||
103
+ newValue === 'preview' ||
104
+ newValue === 'verbose') {
105
+ nextTools[toolName] = { mode: newValue };
106
+ }
107
+ currentDraft.tools = nextTools;
108
+ }
109
+ nextDrafts[nextScope] = currentDraft;
110
+ return {
111
+ scope: nextScope,
112
+ drafts: nextDrafts,
113
+ config: clonePresentationConfig(nextDrafts[nextScope])
114
+ };
115
+ }
116
+ export function collapsePresentationConfigToOverride(config, inheritedConfig) {
117
+ const tools = Object.fromEntries(PRESENTATION_TOOL_NAMES.flatMap((toolName) => {
118
+ const configuredMode = config.tools[toolName]?.mode;
119
+ if (!configuredMode) {
120
+ return [];
121
+ }
122
+ const inheritedMode = resolvePresentationMode(toolName, inheritedConfig);
123
+ if (configuredMode === inheritedMode) {
124
+ return [];
125
+ }
126
+ return [[toolName, { mode: configuredMode }]];
127
+ }));
128
+ return {
129
+ defaultMode: config.defaultMode === inheritedConfig.defaultMode ? undefined : config.defaultMode,
130
+ tools
131
+ };
132
+ }
133
+ export function handleSettingsShortcut(data) {
134
+ if (data === '\u001b') {
135
+ return { action: 'cancel' };
136
+ }
137
+ if (data === '\u0012') {
138
+ return { action: 'reset' };
139
+ }
140
+ if (data === '\u0013') {
141
+ return { action: 'save' };
142
+ }
143
+ return undefined;
144
+ }
145
+ async function openSettingsUi(ctx, loaded, initialScope) {
146
+ return ctx.ui.custom((_tui, theme, _kb, done) => {
147
+ let state = createSettingsDraftState(loaded, initialScope);
148
+ let settingsList;
149
+ const container = new Container();
150
+ container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent settings')), 1, 1));
151
+ container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'), 1, 2));
152
+ const rebuildSettingsList = () => {
153
+ if (settingsList) {
154
+ container.removeChild(settingsList);
155
+ }
156
+ settingsList = new SettingsList(buildSettingsItems(state.scope, state.config), Math.min(PRESENTATION_TOOL_NAMES.length + 8, 18), getSettingsListTheme(), (id, newValue) => {
157
+ state = applySettingsValue(state, id, newValue);
158
+ rebuildSettingsList();
159
+ container.invalidate();
160
+ }, () => done({ action: 'save', scope: state.scope, config: state.config }), { enableSearch: true });
161
+ container.addChild(settingsList);
162
+ };
163
+ rebuildSettingsList();
164
+ return {
165
+ render: (width) => container.render(width),
166
+ invalidate: () => container.invalidate(),
167
+ handleInput: (data) => {
168
+ const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
169
+ if (shortcut?.action === 'cancel') {
170
+ done({ action: 'cancel' });
171
+ return;
172
+ }
173
+ if (shortcut?.action === 'reset') {
174
+ done({ action: 'reset', scope: state.scope });
175
+ return;
176
+ }
177
+ if (shortcut?.action === 'save') {
178
+ done({ action: 'save', scope: state.scope, config: state.config });
179
+ return;
180
+ }
181
+ settingsList.handleInput?.(data);
182
+ }
183
+ };
184
+ });
185
+ }
186
+ export function registerWebAgentConfigCommands(pi, deps = {}) {
187
+ const load = deps.load ?? (() => loadPresentationConfigLayers());
188
+ const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
189
+ const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
190
+ pi.registerCommand('web-agent', {
191
+ description: 'Open settings or manage pi-web-agent presentation config',
192
+ handler: async (args, ctx) => {
193
+ const [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
194
+ if (action === 'show') {
195
+ const loaded = await load();
196
+ ctx.ui.notify([
197
+ formatConfigSummary(loaded.effectiveConfig),
198
+ `global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
199
+ `project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
200
+ ].join('\n'), 'info');
201
+ return;
202
+ }
203
+ if (action === 'reset') {
204
+ const scope = parseScopeToken(maybeScope) ?? 'project';
205
+ await reset(scope);
206
+ ctx.ui.notify(`Reset ${scope} config`, 'info');
207
+ return;
208
+ }
209
+ if (action === 'mode') {
210
+ const [, first, second] = (args ?? '').trim().split(/\s+/).filter(Boolean);
211
+ const loaded = await load();
212
+ const scope = 'project';
213
+ const baseConfig = getScopeDisplayConfig(loaded, scope);
214
+ const inheritedConfig = getInheritedConfigForScope(loaded, scope);
215
+ if (first && isModeOrInherit(first) && first !== 'inherit') {
216
+ await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, defaultMode: first }, inheritedConfig));
217
+ ctx.ui.notify(`Saved project default mode = ${first}`, 'info');
218
+ return;
219
+ }
220
+ if (first && second && isToolName(first) && isModeOrInherit(second)) {
221
+ const nextTools = { ...baseConfig.tools };
222
+ if (second === 'inherit') {
223
+ delete nextTools[first];
224
+ }
225
+ else {
226
+ nextTools[first] = { mode: second };
227
+ }
228
+ await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, tools: nextTools }, inheritedConfig));
229
+ ctx.ui.notify(`Saved project ${first} = ${second}`, 'info');
230
+ return;
231
+ }
232
+ ctx.ui.notify('Usage: /web-agent mode <compact|preview|verbose> or /web-agent mode <tool> <inherit|compact|preview|verbose>', 'info');
233
+ return;
234
+ }
235
+ if (!action || action === 'settings') {
236
+ const loaded = await load();
237
+ const initialScope = 'project';
238
+ const result = await openSettingsUi(ctx, loaded, initialScope);
239
+ if (!result || result.action === 'cancel') {
240
+ return;
241
+ }
242
+ if (result.action === 'reset') {
243
+ await reset(result.scope);
244
+ ctx.ui.notify(`Reset ${result.scope} config`, 'info');
245
+ return;
246
+ }
247
+ await save(result.scope, collapsePresentationConfigToOverride(result.config, getInheritedConfigForScope(loaded, result.scope)));
248
+ ctx.ui.notify(`Saved ${result.scope} config`, 'info');
249
+ return;
250
+ }
251
+ ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent reset project, or /web-agent settings', 'info');
252
+ }
253
+ });
254
+ }
package/dist/extension.js CHANGED
@@ -1,14 +1,111 @@
1
1
  import { Type } from '@sinclair/typebox';
2
+ import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
3
+ import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
4
+ import { loadPresentationConfigLayers } from './presentation/config-store.js';
5
+ import { selectPresentationView } from './presentation/select-view.js';
2
6
  import { createWebExploreTool } from './tools/web-explore.js';
3
7
  import { createWebFetchTool } from './tools/web-fetch.js';
4
8
  import { createWebFetchHeadlessTool } from './tools/web-fetch-headless.js';
5
9
  import { createWebSearchTool } from './tools/web-search.js';
10
+ async function getEffectivePresentationConfig(pi) {
11
+ const store = pi.__presentationConfigStore;
12
+ try {
13
+ const loaded = await (store?.load?.() ?? loadPresentationConfigLayers());
14
+ return loaded.effectiveConfig;
15
+ }
16
+ catch {
17
+ return DEFAULT_PRESENTATION_CONFIG;
18
+ }
19
+ }
20
+ async function renderToolText(pi, toolName, details) {
21
+ const config = await getEffectivePresentationConfig(pi);
22
+ const mode = resolvePresentationMode(toolName, config);
23
+ return selectPresentationView(details.presentation, mode) ?? JSON.stringify(details, null, 2);
24
+ }
6
25
  export default function extension(pi) {
26
+ registerWebAgentConfigCommands(pi);
7
27
  const webSearch = createWebSearchTool();
8
28
  const webFetch = createWebFetchTool();
9
29
  const webFetchHeadless = createWebFetchHeadlessTool();
10
30
  const webExplore = createWebExploreTool();
31
+ let webExploreUsedInCurrentFlow = false;
32
+ const postWebExploreGuardError = {
33
+ code: 'POST_WEB_EXPLORE_GUARD',
34
+ message: 'web_explore already ran for this research task. Only use low-level web tools if there is a specific unresolved gap.'
35
+ };
36
+ async function guardSearchResponse() {
37
+ const result = {
38
+ status: 'error',
39
+ results: [],
40
+ metadata: {
41
+ backend: 'duckduckgo',
42
+ cacheHit: false
43
+ },
44
+ error: postWebExploreGuardError,
45
+ presentation: {
46
+ mode: 'compact',
47
+ views: {
48
+ compact: `Search failed: ${postWebExploreGuardError.message}`
49
+ }
50
+ }
51
+ };
52
+ return {
53
+ content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
54
+ details: result,
55
+ isError: true
56
+ };
57
+ }
58
+ async function guardFetchResponse(url) {
59
+ const result = {
60
+ status: 'error',
61
+ url,
62
+ metadata: {
63
+ method: 'http',
64
+ cacheHit: false
65
+ },
66
+ error: postWebExploreGuardError,
67
+ presentation: {
68
+ mode: 'compact',
69
+ views: {
70
+ compact: `Fetch failed: ${postWebExploreGuardError.message}`
71
+ }
72
+ }
73
+ };
74
+ return {
75
+ content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
76
+ details: result,
77
+ isError: true
78
+ };
79
+ }
80
+ async function guardHeadlessResponse(url) {
81
+ const result = {
82
+ status: 'error',
83
+ url,
84
+ metadata: {
85
+ method: 'headless',
86
+ cacheHit: false
87
+ },
88
+ error: postWebExploreGuardError,
89
+ presentation: {
90
+ mode: 'compact',
91
+ views: {
92
+ compact: `Fetch failed: ${postWebExploreGuardError.message}`
93
+ }
94
+ }
95
+ };
96
+ return {
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: await renderToolText(pi, 'web_fetch_headless', result)
101
+ }
102
+ ],
103
+ details: result,
104
+ isError: true
105
+ };
106
+ }
11
107
  pi.on('before_agent_start', async (event) => {
108
+ webExploreUsedInCurrentFlow = false;
12
109
  return {
13
110
  systemPrompt: `${event.systemPrompt}\n\n` +
14
111
  'For web research questions that require finding and comparing multiple sources, prefer web_explore. ' +
@@ -25,9 +122,12 @@ export default function extension(pi) {
25
122
  query: Type.String({ description: 'Search query.' })
26
123
  }),
27
124
  async execute(_toolCallId, params) {
125
+ if (webExploreUsedInCurrentFlow) {
126
+ return guardSearchResponse();
127
+ }
28
128
  const result = await webSearch({ query: params.query });
29
129
  return {
30
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
130
+ content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
31
131
  details: result,
32
132
  isError: result.status === 'error'
33
133
  };
@@ -41,9 +141,12 @@ export default function extension(pi) {
41
141
  url: Type.String({ description: 'HTTP or HTTPS URL to fetch.' })
42
142
  }),
43
143
  async execute(_toolCallId, params) {
144
+ if (webExploreUsedInCurrentFlow) {
145
+ return guardFetchResponse(params.url);
146
+ }
44
147
  const result = await webFetch({ url: params.url });
45
148
  return {
46
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
149
+ content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
47
150
  details: result,
48
151
  isError: result.status === 'error'
49
152
  };
@@ -57,9 +160,12 @@ export default function extension(pi) {
57
160
  url: Type.String({ description: 'HTTP or HTTPS URL to fetch in headless mode.' })
58
161
  }),
59
162
  async execute(_toolCallId, params) {
163
+ if (webExploreUsedInCurrentFlow) {
164
+ return guardHeadlessResponse(params.url);
165
+ }
60
166
  const result = await webFetchHeadless({ url: params.url });
61
167
  return {
62
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
168
+ content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch_headless', result) }],
63
169
  details: result,
64
170
  isError: result.status === 'error'
65
171
  };
@@ -74,11 +180,14 @@ export default function extension(pi) {
74
180
  }),
75
181
  async execute(_toolCallId, params) {
76
182
  const result = await webExplore({ query: params.query });
183
+ if (result.status === 'ok') {
184
+ webExploreUsedInCurrentFlow = true;
185
+ }
77
186
  return {
78
187
  content: [
79
188
  {
80
189
  type: 'text',
81
- text: result.status === 'ok' ? result.text : JSON.stringify(result, null, 2)
190
+ text: await renderToolText(pi, 'web_explore', result)
82
191
  }
83
192
  ],
84
193
  details: result,
@@ -0,0 +1,23 @@
1
+ import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from './types.js';
2
+ export type PresentationConfigStoreOptions = {
3
+ homeDir?: string;
4
+ projectDir?: string;
5
+ };
6
+ export type PresentationConfigLayer = {
7
+ path: string;
8
+ exists: boolean;
9
+ rawConfig?: PresentationConfigOverride;
10
+ error?: string;
11
+ };
12
+ export type LoadedPresentationConfig = {
13
+ global: PresentationConfigLayer;
14
+ project: PresentationConfigLayer;
15
+ effectiveConfig: PresentationConfig;
16
+ };
17
+ export declare function getPresentationConfigPaths(options?: PresentationConfigStoreOptions): {
18
+ globalPath: string;
19
+ projectPath: string;
20
+ };
21
+ export declare function loadPresentationConfigLayers(options?: PresentationConfigStoreOptions): Promise<LoadedPresentationConfig>;
22
+ export declare function savePresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope, config: PresentationConfigOverride): Promise<void>;
23
+ export declare function resetPresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope): Promise<void>;