@demigodmode/pi-web-agent 0.2.1 → 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.
- package/README.md +65 -145
- package/dist/commands/web-agent-config.d.ts +23 -0
- package/dist/commands/web-agent-config.js +254 -0
- package/dist/extension.js +113 -4
- package/dist/presentation/config-store.d.ts +23 -0
- package/dist/presentation/config-store.js +64 -0
- package/dist/presentation/config.d.ts +7 -0
- package/dist/presentation/config.js +44 -0
- package/dist/presentation/explore-presentation.d.ts +3 -0
- package/dist/presentation/explore-presentation.js +34 -0
- package/dist/presentation/fetch-presentation.d.ts +5 -0
- package/dist/presentation/fetch-presentation.js +40 -0
- package/dist/presentation/search-presentation.d.ts +3 -0
- package/dist/presentation/search-presentation.js +30 -0
- package/dist/presentation/select-view.d.ts +2 -0
- package/dist/presentation/select-view.js +12 -0
- package/dist/presentation/types.d.ts +50 -0
- package/dist/presentation/types.js +1 -0
- package/dist/search/duckduckgo.d.ts +6 -1
- package/dist/search/duckduckgo.js +11 -1
- package/dist/tools/web-explore.d.ts +6 -16
- package/dist/tools/web-explore.js +12 -10
- package/dist/tools/web-fetch-headless.js +11 -2
- package/dist/tools/web-fetch.js +11 -2
- package/dist/tools/web-search.js +99 -12
- package/dist/types.d.ts +15 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,199 +1,119 @@
|
|
|
1
1
|
# pi-web-agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/demigodmode/pi-web-agent/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@demigodmode/pi-web-agent)
|
|
5
|
+
[](https://demigodmode.github.io/pi-web-agent/)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`@demigodmode/pi-web-agent` is a Pi package for web access.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
The whole point is keeping the boundaries straight:
|
|
8
10
|
|
|
9
|
-
|
|
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
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
- `web_fetch` fetches a specific page over plain HTTP and tries to extract readable content
|
|
13
|
-
- `web_fetch_headless` is the explicit browser-based path for pages that need rendering
|
|
14
|
-
|
|
15
|
-
The boundary between those tools is intentional.
|
|
16
|
-
|
|
17
|
-
`web_search` is for discovery. It should not imply that a page was fetched.
|
|
18
|
-
|
|
19
|
-
`web_fetch` is for reading a page over HTTP. If the result looks weak, incomplete, blocked, or too script-heavy, it should return `needs_headless` instead of bluffing.
|
|
20
|
-
|
|
21
|
-
`web_fetch_headless` exists for the cases where a browser really is required. It is opt-in only.
|
|
22
|
-
|
|
23
|
-
## Why this exists
|
|
24
|
-
|
|
25
|
-
A lot of web tooling in coding agents gets fuzzy in exactly the wrong places. Search results get treated like page reads. Browser fallback happens behind the scenes. Failures get softened into fake confidence.
|
|
26
|
-
|
|
27
|
-
This package is trying to do the opposite.
|
|
28
|
-
|
|
29
|
-
The rules are straightforward:
|
|
30
|
-
|
|
31
|
-
- no hidden browser launch
|
|
32
|
-
- no automatic HTTP-to-headless fallback
|
|
33
|
-
- no claiming a page was read when only snippets were available
|
|
34
|
-
- explicit structured failure when the result is incomplete or blocked
|
|
35
|
-
|
|
36
|
-
## What makes it different
|
|
37
|
-
|
|
38
|
-
The main thing is the contract.
|
|
39
|
-
|
|
40
|
-
`web_search` discovers sources.
|
|
41
|
-
|
|
42
|
-
`web_fetch` reads over HTTP only.
|
|
43
|
-
|
|
44
|
-
`web_fetch_headless` is the explicit browser path.
|
|
45
|
-
|
|
46
|
-
That separation is the whole point. It makes failures easier to reason about and avoids the weird behavior where a tool quietly changes execution mode behind your back.
|
|
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.
|
|
47
17
|
|
|
48
18
|
## Install
|
|
49
19
|
|
|
50
|
-
Install it through Pi:
|
|
51
|
-
|
|
52
20
|
```bash
|
|
53
21
|
pi install npm:@demigodmode/pi-web-agent
|
|
54
22
|
```
|
|
55
23
|
|
|
56
|
-
|
|
24
|
+
Later on, update installed packages with:
|
|
57
25
|
|
|
58
26
|
```bash
|
|
59
27
|
pi update
|
|
60
28
|
```
|
|
61
29
|
|
|
62
|
-
|
|
30
|
+
## Docs
|
|
63
31
|
|
|
64
|
-
|
|
65
|
-
npm view @demigodmode/pi-web-agent
|
|
66
|
-
```
|
|
32
|
+
Docs site:
|
|
67
33
|
|
|
68
|
-
|
|
34
|
+
- https://demigodmode.github.io/pi-web-agent/
|
|
69
35
|
|
|
70
|
-
|
|
36
|
+
Work on the docs locally:
|
|
71
37
|
|
|
72
|
-
|
|
38
|
+
```bash
|
|
39
|
+
npm run docs:dev
|
|
40
|
+
```
|
|
73
41
|
|
|
74
|
-
|
|
75
|
-
- shared result and status contracts
|
|
76
|
-
- a DuckDuckGo HTML parser for `web_search`
|
|
77
|
-
- an HTTP fetch path with readability-based extraction and conservative escalation to `needs_headless`
|
|
78
|
-
- a real browser-backed `web_fetch_headless` implementation with local browser resolution
|
|
79
|
-
- repo-local Pi extension wiring for development
|
|
80
|
-
- a test suite around parser behavior, contracts, extraction, caching, and tool adapters
|
|
81
|
-
- optional smoke coverage for local installed browsers
|
|
42
|
+
Build the docs:
|
|
82
43
|
|
|
83
|
-
|
|
44
|
+
```bash
|
|
45
|
+
npm run docs:build
|
|
46
|
+
```
|
|
84
47
|
|
|
85
|
-
##
|
|
48
|
+
## Presentation modes
|
|
86
49
|
|
|
87
|
-
|
|
50
|
+
`pi-web-agent` renders web tool output in one visible mode at a time:
|
|
88
51
|
|
|
89
|
-
|
|
52
|
+
- `compact` — short summary, default everywhere
|
|
53
|
+
- `preview` — slightly richer bounded view
|
|
54
|
+
- `verbose` — fuller bounded view
|
|
90
55
|
|
|
91
|
-
|
|
56
|
+
## Settings
|
|
92
57
|
|
|
93
|
-
|
|
58
|
+
Primary UI:
|
|
94
59
|
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
60
|
+
```text
|
|
61
|
+
/web-agent settings
|
|
62
|
+
```
|
|
98
63
|
|
|
99
|
-
|
|
64
|
+
Helper commands:
|
|
100
65
|
|
|
101
|
-
|
|
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
|
+
```
|
|
102
74
|
|
|
103
|
-
|
|
75
|
+
Config files:
|
|
104
76
|
|
|
105
|
-
|
|
77
|
+
```text
|
|
78
|
+
Global: ~/.pi/agent/extensions/pi-web-agent/config.json
|
|
79
|
+
Project: .pi/extensions/pi-web-agent/config.json
|
|
80
|
+
```
|
|
106
81
|
|
|
107
|
-
|
|
82
|
+
Precedence:
|
|
108
83
|
|
|
109
|
-
|
|
84
|
+
- built-in defaults
|
|
85
|
+
- global config
|
|
86
|
+
- project config
|
|
110
87
|
|
|
111
|
-
|
|
88
|
+
Project config overrides global config.
|
|
112
89
|
|
|
113
|
-
|
|
90
|
+
Example:
|
|
114
91
|
|
|
115
|
-
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"presentation": {
|
|
95
|
+
"defaultMode": "compact",
|
|
96
|
+
"tools": {
|
|
97
|
+
"web_search": { "mode": "preview" },
|
|
98
|
+
"web_explore": { "mode": "verbose" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
116
103
|
|
|
117
104
|
## Local development
|
|
118
105
|
|
|
119
|
-
Install dependencies:
|
|
120
|
-
|
|
121
106
|
```bash
|
|
122
107
|
npm install
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Run tests with coverage:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
108
|
npm test
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Run the typecheck used as lint:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
109
|
npm run lint
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Build the project:
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
110
|
npm run build
|
|
141
111
|
```
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Coverage is now part of the normal `npm test` flow. Vitest prints a text summary in the terminal and writes the full HTML report to `coverage/`.
|
|
146
|
-
|
|
147
|
-
### Trying it in Pi locally
|
|
148
|
-
|
|
149
|
-
This repo includes a project-local Pi extension entrypoint at `.pi/extensions/pi-web-agent.ts` for development and hot reload.
|
|
113
|
+
For local Pi work, this repo includes `.pi/extensions/pi-web-agent.ts`.
|
|
150
114
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
After starting Pi in this project, use `/reload` if you change the extension code and want Pi to pick up the latest version.
|
|
154
|
-
|
|
155
|
-
## Project layout
|
|
156
|
-
|
|
157
|
-
The code is split into small modules on purpose.
|
|
158
|
-
|
|
159
|
-
- `src/extension.ts` - package entry surface
|
|
160
|
-
- `src/tools/` - thin tool adapters
|
|
161
|
-
- `src/search/` - search backend logic
|
|
162
|
-
- `src/fetch/` - HTTP and headless fetch logic
|
|
163
|
-
- `src/extract/` - readable-content extraction
|
|
164
|
-
- `src/cache/` - small cache utilities
|
|
165
|
-
- `src/types.ts` - shared contracts
|
|
166
|
-
- `tests/` - parser, contract, extraction, fetch, and adapter tests
|
|
115
|
+
If Pi is already running, use `/reload` after changes.
|
|
167
116
|
|
|
168
117
|
## License
|
|
169
118
|
|
|
170
119
|
AGPL-3.0-only. See `LICENSE`.
|
|
171
|
-
|
|
172
|
-
## Release process
|
|
173
|
-
|
|
174
|
-
1. Update `CHANGELOG.md` under `## Unreleased`.
|
|
175
|
-
2. Run `npm run release:dry-run` to preview the next version.
|
|
176
|
-
3. Run `npm run release` to bump version, rewrite the changelog release heading, create a release commit, and create a tag.
|
|
177
|
-
4. Push the branch and tag.
|
|
178
|
-
5. GitHub Actions publishes the tagged release to npm.
|
|
179
|
-
|
|
180
|
-
## Maintainer release notes
|
|
181
|
-
|
|
182
|
-
This repo is set up for npm Trusted Publishing from GitHub Actions.
|
|
183
|
-
|
|
184
|
-
In npm package settings, add a trusted publisher for:
|
|
185
|
-
- package: `@demigodmode/pi-web-agent`
|
|
186
|
-
- provider: GitHub Actions
|
|
187
|
-
- repository: `demigodmode/pi-web-agent`
|
|
188
|
-
|
|
189
|
-
That replaces the old `NPM_TOKEN` secret flow.
|
|
190
|
-
|
|
191
|
-
## Near-term next steps
|
|
192
|
-
|
|
193
|
-
The next chunk of work is pretty clear:
|
|
194
|
-
|
|
195
|
-
- keep tightening weak-content escalation on tricky HTTP targets
|
|
196
|
-
- improve cleanup of noisy rendered-page extraction on busy sites
|
|
197
|
-
- expand fixtures and end-to-end coverage
|
|
198
|
-
- add alternate search backends behind a first-class provider abstraction
|
|
199
|
-
|
|
@@ -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
|
+
}
|