@selesai/code 0.13.3 → 0.13.4
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/CHANGELOG.md +11 -0
- package/dist/defaults/models.json +17 -1
- package/dist/extensions/cost-reconcile.test.ts +24 -5
- package/dist/extensions/cost-reconcile.ts +14 -7
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +4 -5
- package/dist/extensions/pi-intercom/config.test.ts +3 -31
- package/dist/extensions/pi-intercom/config.ts +0 -15
- package/dist/extensions/pi-intercom/index.ts +9 -46
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
- package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
- package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
- package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
- package/dist/extensions/pi-web-agent/README.md +131 -0
- package/dist/extensions/pi-web-agent/package.json +4 -2
- package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
- package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
- package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
- package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
- package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
- package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
- package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
- package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
- package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
- package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
- package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
- package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
- package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
- package/dist/extensions/pi-web-agent/src/types.ts +19 -4
- package/package.json +1 -1
|
@@ -2,16 +2,27 @@ import {
|
|
|
2
2
|
DEFAULT_BACKEND_CONFIG,
|
|
3
3
|
mergeBackendConfigLayers,
|
|
4
4
|
validateBackendConfig,
|
|
5
|
+
usableSearchProviders,
|
|
5
6
|
type BackendConfig,
|
|
6
7
|
type BackendConfigOverride
|
|
7
8
|
} from '../backends/config.js';
|
|
8
9
|
import { checkBackendHealth } from '../backends/doctor.js';
|
|
10
|
+
import type { SearchProviderName } from '../types.js';
|
|
9
11
|
import {
|
|
10
12
|
DynamicBorder,
|
|
11
13
|
getSettingsListTheme,
|
|
12
14
|
type ExtensionAPI
|
|
13
15
|
} from '@selesai/code';
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Container,
|
|
18
|
+
Input,
|
|
19
|
+
SelectList,
|
|
20
|
+
SettingsList,
|
|
21
|
+
Text,
|
|
22
|
+
type Component,
|
|
23
|
+
type SelectItem,
|
|
24
|
+
type SettingItem
|
|
25
|
+
} from '@earendil-works/pi-tui';
|
|
15
26
|
import {
|
|
16
27
|
DEFAULT_PRESENTATION_CONFIG,
|
|
17
28
|
mergePresentationConfigLayers,
|
|
@@ -78,7 +89,10 @@ function cloneBackendConfig(config: BackendConfig): BackendConfig {
|
|
|
78
89
|
return {
|
|
79
90
|
search: {
|
|
80
91
|
...config.search,
|
|
81
|
-
options: config.search.options ? { ...config.search.options } : undefined
|
|
92
|
+
options: config.search.options ? { ...config.search.options } : undefined,
|
|
93
|
+
fanout: config.search.fanout
|
|
94
|
+
? { mode: config.search.fanout.mode, providers: config.search.fanout.providers ? [...config.search.fanout.providers] : undefined }
|
|
95
|
+
: undefined
|
|
82
96
|
},
|
|
83
97
|
fetch: {
|
|
84
98
|
...config.fetch,
|
|
@@ -115,12 +129,17 @@ async function defaultCheckTypebox(): Promise<boolean> {
|
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
function formatSearchOptions(config: BackendConfig['search']) {
|
|
118
|
-
|
|
132
|
+
const parts = [
|
|
119
133
|
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
120
134
|
config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
|
|
121
135
|
config.options?.language ? `language ${config.options.language}` : undefined,
|
|
122
|
-
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
|
|
123
|
-
|
|
136
|
+
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined,
|
|
137
|
+
config.fanout && config.fanout.mode !== 'off'
|
|
138
|
+
? `fanout ${config.fanout.mode} (${(config.fanout.providers || usableSearchProviders(config)).join(', ')})`
|
|
139
|
+
: undefined
|
|
140
|
+
].filter(Boolean);
|
|
141
|
+
|
|
142
|
+
return parts.join(' ');
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
function formatFetchOptions(config: BackendConfig['fetch']) {
|
|
@@ -181,7 +200,73 @@ function buildPresentationSettingsItems(scope: PresentationScope, config: Presen
|
|
|
181
200
|
];
|
|
182
201
|
}
|
|
183
202
|
|
|
184
|
-
function
|
|
203
|
+
export function createBackendUrlEditor(
|
|
204
|
+
theme: any,
|
|
205
|
+
label: string,
|
|
206
|
+
placeholderUrl: string,
|
|
207
|
+
onOpenChange?: (open: boolean) => void
|
|
208
|
+
) {
|
|
209
|
+
return (currentValue: string, done: (selectedValue?: string) => void): Component => {
|
|
210
|
+
onOpenChange?.(true);
|
|
211
|
+
|
|
212
|
+
const initialValue = currentValue && currentValue !== 'not set' ? currentValue : placeholderUrl;
|
|
213
|
+
const container = new Container();
|
|
214
|
+
const hint = new Text(theme.fg('muted', `${label} · enter to save · empty clears · esc cancels`), 1, 0);
|
|
215
|
+
const input = new Input();
|
|
216
|
+
input.setValue(initialValue);
|
|
217
|
+
input.handleInput('\x1b[F'); // move cursor to end; setValue leaves it at 0
|
|
218
|
+
input.focused = true;
|
|
219
|
+
const errorText = new Text('', 1, 0);
|
|
220
|
+
|
|
221
|
+
const finish = (selectedValue?: string) => {
|
|
222
|
+
onOpenChange?.(false);
|
|
223
|
+
done(selectedValue);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const showError = (message: string) => {
|
|
227
|
+
errorText.setText(theme.fg('warning', message));
|
|
228
|
+
container.invalidate();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
input.onSubmit = (value: string) => {
|
|
232
|
+
if (!value.trim()) {
|
|
233
|
+
finish('');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const validated = validateBackendUrl(value);
|
|
238
|
+
if (!validated.ok) {
|
|
239
|
+
showError(validated.message);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
finish(validated.value);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
input.onEscape = () => finish(undefined);
|
|
247
|
+
|
|
248
|
+
container.addChild(hint);
|
|
249
|
+
container.addChild(input);
|
|
250
|
+
container.addChild(errorText);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
render: (width: number) => container.render(width),
|
|
254
|
+
invalidate: () => container.invalidate(),
|
|
255
|
+
handleInput: (data: string) => input.handleInput(data)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildBackendSettingsItems(
|
|
261
|
+
scope: PresentationScope,
|
|
262
|
+
backends: BackendConfig,
|
|
263
|
+
theme: any,
|
|
264
|
+
onUrlEditorOpenChange?: (open: boolean) => void
|
|
265
|
+
): SettingItem[] {
|
|
266
|
+
const usable = usableSearchProviders(backends.search);
|
|
267
|
+
const fanoutMode = backends.search.fanout?.mode ?? 'off';
|
|
268
|
+
const fanoutProviders = backends.search.fanout?.providers ?? usable;
|
|
269
|
+
|
|
185
270
|
return [
|
|
186
271
|
{
|
|
187
272
|
id: 'scope',
|
|
@@ -193,26 +278,56 @@ function buildBackendSettingsItems(scope: PresentationScope, backends: BackendCo
|
|
|
193
278
|
id: 'backend:search:provider',
|
|
194
279
|
label: 'Search backend',
|
|
195
280
|
currentValue: backends.search.provider,
|
|
196
|
-
values: ['duckduckgo', 'searxng', 'brave']
|
|
281
|
+
values: ['duckduckgo', 'searxng', 'brave', 'youcom', 'exa', 'tavily']
|
|
197
282
|
},
|
|
198
283
|
{
|
|
199
284
|
id: 'backend:search:baseUrl',
|
|
200
285
|
label: 'SearXNG URL',
|
|
201
286
|
currentValue: backends.search.baseUrl ?? 'not set',
|
|
202
|
-
|
|
287
|
+
submenu: createBackendUrlEditor(theme, 'SearXNG base URL', 'http://localhost:8080', onUrlEditorOpenChange)
|
|
203
288
|
},
|
|
204
289
|
{
|
|
205
290
|
id: 'backend:search:fallback',
|
|
206
291
|
label: 'Search fallback',
|
|
207
|
-
currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? backends.search.fallback ?? 'off' : 'off',
|
|
208
|
-
values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? ['off', 'duckduckgo'] : ['off']
|
|
292
|
+
currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' || backends.search.provider === 'exa' || backends.search.provider === 'tavily' ? backends.search.fallback ?? 'off' : 'off',
|
|
293
|
+
values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' || backends.search.provider === 'exa' || backends.search.provider === 'tavily' ? ['off', 'duckduckgo'] : ['off']
|
|
209
294
|
},
|
|
295
|
+
{
|
|
296
|
+
id: 'backend:search:fanout:mode',
|
|
297
|
+
label: 'Search fanout',
|
|
298
|
+
currentValue: fanoutMode,
|
|
299
|
+
values: ['off', 'on', 'auto']
|
|
300
|
+
},
|
|
301
|
+
...(fanoutMode !== 'off' ? usable.map((provider) => ({
|
|
302
|
+
id: `backend:search:fanout:provider:${provider}`,
|
|
303
|
+
label: ` ${provider}`,
|
|
304
|
+
currentValue: fanoutProviders.includes(provider) ? 'included' : 'excluded',
|
|
305
|
+
values: ['included', 'excluded']
|
|
306
|
+
})) : []),
|
|
210
307
|
{
|
|
211
308
|
id: 'backend:secret:brave',
|
|
212
309
|
label: 'Brave API key',
|
|
213
310
|
currentValue: 'env var',
|
|
214
311
|
values: ['env var']
|
|
215
312
|
},
|
|
313
|
+
{
|
|
314
|
+
id: 'backend:secret:youcom',
|
|
315
|
+
label: 'You.com API key',
|
|
316
|
+
currentValue: 'env var',
|
|
317
|
+
values: ['env var']
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'backend:secret:exa',
|
|
321
|
+
label: 'Exa API key',
|
|
322
|
+
currentValue: 'env var',
|
|
323
|
+
values: ['env var']
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: 'backend:secret:tavily',
|
|
327
|
+
label: 'Tavily API key',
|
|
328
|
+
currentValue: 'env var',
|
|
329
|
+
values: ['env var']
|
|
330
|
+
},
|
|
216
331
|
{
|
|
217
332
|
id: 'backend:fetch:provider',
|
|
218
333
|
label: 'Fetch backend',
|
|
@@ -223,7 +338,7 @@ function buildBackendSettingsItems(scope: PresentationScope, backends: BackendCo
|
|
|
223
338
|
id: 'backend:fetch:baseUrl',
|
|
224
339
|
label: 'Firecrawl URL',
|
|
225
340
|
currentValue: backends.fetch.baseUrl ?? 'not set',
|
|
226
|
-
|
|
341
|
+
submenu: createBackendUrlEditor(theme, 'Firecrawl base URL', 'http://localhost:3002', onUrlEditorOpenChange)
|
|
227
342
|
},
|
|
228
343
|
{
|
|
229
344
|
id: 'backend:fetch:fallback',
|
|
@@ -373,7 +488,7 @@ export function applySettingsValue(
|
|
|
373
488
|
currentDraft.tools = nextTools;
|
|
374
489
|
}
|
|
375
490
|
|
|
376
|
-
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave')) {
|
|
491
|
+
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave' || newValue === 'youcom' || newValue === 'exa' || newValue === 'tavily')) {
|
|
377
492
|
currentBackends.search.provider = newValue;
|
|
378
493
|
if (newValue !== 'searxng') {
|
|
379
494
|
delete currentBackends.search.baseUrl;
|
|
@@ -385,15 +500,53 @@ export function applySettingsValue(
|
|
|
385
500
|
}
|
|
386
501
|
|
|
387
502
|
if (id === 'backend:search:fallback') {
|
|
388
|
-
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave')) {
|
|
503
|
+
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave' || currentBackends.search.provider === 'youcom' || currentBackends.search.provider === 'exa' || currentBackends.search.provider === 'tavily')) {
|
|
389
504
|
currentBackends.search.fallback = 'duckduckgo';
|
|
390
505
|
} else {
|
|
391
506
|
delete currentBackends.search.fallback;
|
|
392
507
|
}
|
|
393
508
|
}
|
|
394
509
|
|
|
510
|
+
if (id === 'backend:search:fanout:mode') {
|
|
511
|
+
if (newValue === 'off') {
|
|
512
|
+
currentBackends.search.fanout = { mode: 'off' };
|
|
513
|
+
} else if (newValue === 'on' || newValue === 'auto') {
|
|
514
|
+
currentBackends.search.fanout = {
|
|
515
|
+
mode: newValue,
|
|
516
|
+
providers: undefined
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (id.startsWith('backend:search:fanout:provider:')) {
|
|
522
|
+
const providerName = id.slice('backend:search:fanout:provider:'.length) as SearchProviderName;
|
|
523
|
+
const usableProviders = usableSearchProviders(currentBackends.search);
|
|
524
|
+
|
|
525
|
+
// Ensure fanout exists
|
|
526
|
+
if (!currentBackends.search.fanout) {
|
|
527
|
+
currentBackends.search.fanout = { mode: 'on', providers: undefined };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Materialize the provider list if it's currently undefined; use usable providers instead of all
|
|
531
|
+
const currentProviders = currentBackends.search.fanout.providers ?? usableProviders;
|
|
532
|
+
|
|
533
|
+
if (newValue === 'excluded') {
|
|
534
|
+
// Remove the provider from the list
|
|
535
|
+
const filtered: SearchProviderName[] = currentProviders.filter((p) => p !== providerName);
|
|
536
|
+
// never allow an empty set: keep at least one provider (empty would be read as "all")
|
|
537
|
+
if (filtered.length > 0) {
|
|
538
|
+
currentBackends.search.fanout.providers = filtered;
|
|
539
|
+
}
|
|
540
|
+
} else if (newValue === 'included') {
|
|
541
|
+
// Add the provider back, maintaining canonical order using usable providers
|
|
542
|
+
const result: SearchProviderName[] = usableProviders.filter((p) => currentProviders.includes(p) || p === providerName);
|
|
543
|
+
currentBackends.search.fanout.providers = result;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
395
547
|
if (id === 'backend:search:baseUrl') {
|
|
396
|
-
if (
|
|
548
|
+
if (newValue.trim()) {
|
|
549
|
+
currentBackends.search.provider = 'searxng';
|
|
397
550
|
currentBackends.search.baseUrl = newValue.trim();
|
|
398
551
|
} else {
|
|
399
552
|
delete currentBackends.search.baseUrl;
|
|
@@ -417,6 +570,7 @@ export function applySettingsValue(
|
|
|
417
570
|
|
|
418
571
|
if (id === 'backend:fetch:baseUrl') {
|
|
419
572
|
if (newValue.trim()) {
|
|
573
|
+
currentBackends.fetch.provider = 'firecrawl';
|
|
420
574
|
currentBackends.fetch.baseUrl = newValue.trim();
|
|
421
575
|
} else {
|
|
422
576
|
delete currentBackends.fetch.baseUrl;
|
|
@@ -474,7 +628,8 @@ export function collapseBackendConfigToOverride(
|
|
|
474
628
|
: {
|
|
475
629
|
...(config.search.baseUrl !== inheritedConfig.search.baseUrl ? { baseUrl: config.search.baseUrl } : {}),
|
|
476
630
|
...(config.search.fallback !== inheritedConfig.search.fallback ? { fallback: config.search.fallback } : {}),
|
|
477
|
-
...(!sameJson(config.search.options, inheritedConfig.search.options) ? { options: config.search.options } : {})
|
|
631
|
+
...(!sameJson(config.search.options, inheritedConfig.search.options) ? { options: config.search.options } : {}),
|
|
632
|
+
...(!sameJson(config.search.fanout, inheritedConfig.search.fanout) ? { fanout: config.search.fanout } : {})
|
|
478
633
|
};
|
|
479
634
|
|
|
480
635
|
if (config.search.provider !== inheritedConfig.search.provider) {
|
|
@@ -675,9 +830,10 @@ async function openBackendSettingsUi(
|
|
|
675
830
|
loaded: Awaited<LoadedPresentationConfig>,
|
|
676
831
|
initialScope: PresentationScope
|
|
677
832
|
): Promise<SettingsUiResult | undefined> {
|
|
678
|
-
return ctx.ui.custom((
|
|
833
|
+
return ctx.ui.custom((_tui: unknown, theme: any, _kb: unknown, done: (value: SettingsUiResult) => void) => {
|
|
679
834
|
let state = createSettingsDraftState(loaded, initialScope);
|
|
680
835
|
let settingsList: SettingsList;
|
|
836
|
+
let urlEditorOpen = false;
|
|
681
837
|
|
|
682
838
|
const container = new Container();
|
|
683
839
|
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
|
|
@@ -689,46 +845,18 @@ async function openBackendSettingsUi(
|
|
|
689
845
|
)
|
|
690
846
|
);
|
|
691
847
|
|
|
692
|
-
const editUrl = async (id: string) => {
|
|
693
|
-
const isSearchUrl = id === 'backend:search:baseUrl';
|
|
694
|
-
const label = isSearchUrl ? 'SearXNG base URL' : 'Firecrawl base URL';
|
|
695
|
-
const currentValue = isSearchUrl ? state.backends.search.baseUrl : state.backends.fetch.baseUrl;
|
|
696
|
-
const entered = await ctx.ui.input(label, currentValue ?? (isSearchUrl ? 'http://localhost:8080' : 'http://localhost:3002'));
|
|
697
|
-
if (entered === undefined) return;
|
|
698
|
-
|
|
699
|
-
if (!entered.trim()) {
|
|
700
|
-
state = applySettingsValue(state, id, '');
|
|
701
|
-
rebuildSettingsList();
|
|
702
|
-
tui.requestRender?.();
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const validated = validateBackendUrl(entered);
|
|
707
|
-
if (!validated.ok) {
|
|
708
|
-
ctx.ui.notify(validated.message, 'warning');
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
state = applySettingsValue(state, id, validated.value);
|
|
713
|
-
rebuildSettingsList();
|
|
714
|
-
tui.requestRender?.();
|
|
715
|
-
};
|
|
716
|
-
|
|
717
848
|
const rebuildSettingsList = () => {
|
|
718
849
|
if (settingsList) {
|
|
719
850
|
container.removeChild(settingsList);
|
|
720
851
|
}
|
|
721
852
|
|
|
722
853
|
settingsList = new SettingsList(
|
|
723
|
-
buildBackendSettingsItems(state.scope, state.backends)
|
|
854
|
+
buildBackendSettingsItems(state.scope, state.backends, theme, (open) => {
|
|
855
|
+
urlEditorOpen = open;
|
|
856
|
+
}),
|
|
724
857
|
12,
|
|
725
858
|
getSettingsListTheme(),
|
|
726
859
|
(id, newValue) => {
|
|
727
|
-
if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
|
|
728
|
-
void editUrl(id);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
860
|
state = applySettingsValue(state, id, newValue);
|
|
733
861
|
rebuildSettingsList();
|
|
734
862
|
container.invalidate();
|
|
@@ -746,6 +874,14 @@ async function openBackendSettingsUi(
|
|
|
746
874
|
render: (width: number) => container.render(width),
|
|
747
875
|
invalidate: () => container.invalidate(),
|
|
748
876
|
handleInput: (data: string) => {
|
|
877
|
+
// While the inline URL editor is open, let it (and its own Escape/Enter
|
|
878
|
+
// handling) consume input first — otherwise these shortcuts close the
|
|
879
|
+
// whole settings UI out from under an in-progress edit.
|
|
880
|
+
if (urlEditorOpen) {
|
|
881
|
+
settingsList.handleInput?.(data);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
749
885
|
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
750
886
|
|
|
751
887
|
if (shortcut?.action === 'cancel') {
|
|
@@ -6,18 +6,35 @@ import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presenta
|
|
|
6
6
|
import { createResearchWorkflow } from './orchestration/index.js';
|
|
7
7
|
import { loadPresentationConfigLayers } from './presentation/config-store.js';
|
|
8
8
|
import { selectPresentationView } from './presentation/select-view.js';
|
|
9
|
-
import type {
|
|
10
|
-
PresentationConfig,
|
|
11
|
-
PresentationEnvelope,
|
|
12
|
-
PresentationToolName
|
|
13
|
-
} from './presentation/types.js';
|
|
9
|
+
import type { PresentationConfig } from './presentation/types.js';
|
|
14
10
|
import { createWebExploreTool } from './tools/web-explore.js';
|
|
15
11
|
import type { WebExploreResponse } from './types.js';
|
|
16
12
|
import { getUpdateChangelogNotice } from './changelog-notice.js';
|
|
13
|
+
import { Text } from '@earendil-works/pi-tui';
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
/**
|
|
16
|
+
* What the model receives as the tool result: the actual synthesized findings (which for a
|
|
17
|
+
* direct GitHub/PDF/YouTube read is the full extracted content), plus source citations and any
|
|
18
|
+
* caveat. This is separate from the terminal display (see renderResult), so the model always
|
|
19
|
+
* gets the substance regardless of the user's compact/preview/verbose presentation setting.
|
|
20
|
+
*/
|
|
21
|
+
function serializeForModel(result: WebExploreResponse): string {
|
|
22
|
+
if (result.status === 'error') {
|
|
23
|
+
return `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`;
|
|
24
|
+
}
|
|
25
|
+
if (result.findings.length === 0 && result.sources.length === 0) {
|
|
26
|
+
return 'No usable evidence found.';
|
|
27
|
+
}
|
|
28
|
+
return [
|
|
29
|
+
result.findings.join('\n\n'),
|
|
30
|
+
result.sources.length
|
|
31
|
+
? `Sources:\n${result.sources.map((source) => `- ${source.title}: ${source.url}`).join('\n')}`
|
|
32
|
+
: undefined,
|
|
33
|
+
result.caveat
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join('\n\n');
|
|
37
|
+
}
|
|
21
38
|
|
|
22
39
|
async function loadWebAgentConfig(pi: ExtensionAPI) {
|
|
23
40
|
const store = (
|
|
@@ -49,16 +66,6 @@ async function getEffectiveBackendConfig(pi: ExtensionAPI): Promise<BackendConfi
|
|
|
49
66
|
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
async function renderToolText(
|
|
53
|
-
pi: ExtensionAPI,
|
|
54
|
-
toolName: PresentationToolName,
|
|
55
|
-
details: ToolResultWithPresentation
|
|
56
|
-
): Promise<string> {
|
|
57
|
-
const config = await getEffectivePresentationConfig(pi);
|
|
58
|
-
const mode = resolvePresentationMode(toolName, config);
|
|
59
|
-
return selectPresentationView(details.presentation, mode) ?? JSON.stringify(details, null, 2);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
69
|
export default function extension(pi: ExtensionAPI) {
|
|
63
70
|
registerWebAgentConfigCommands(pi);
|
|
64
71
|
|
|
@@ -111,16 +118,46 @@ export default function extension(pi: ExtensionAPI) {
|
|
|
111
118
|
async execute(_toolCallId, params) {
|
|
112
119
|
const webExplore = await getConfiguredWebExplore();
|
|
113
120
|
const result: WebExploreResponse = await webExplore({ query: params.query });
|
|
121
|
+
|
|
122
|
+
// Terminal display honors the user's presentation mode; the model gets the full findings.
|
|
123
|
+
// The fallback must stay terse: never fall back to serializeForModel here, or a missing
|
|
124
|
+
// presentation would dump the full findings into the terminal.
|
|
125
|
+
const mode = resolvePresentationMode('web_explore', await getEffectivePresentationConfig(pi));
|
|
126
|
+
const terseFallback = 'web_explore result';
|
|
127
|
+
const terminalText = selectPresentationView(result.presentation, mode) ?? terseFallback;
|
|
128
|
+
const terminalTextExpanded = selectPresentationView(result.presentation, 'verbose') ?? terminalText;
|
|
129
|
+
|
|
114
130
|
return {
|
|
115
|
-
content: [
|
|
116
|
-
|
|
117
|
-
type: 'text',
|
|
118
|
-
text: await renderToolText(pi, 'web_explore', result)
|
|
119
|
-
}
|
|
120
|
-
],
|
|
121
|
-
details: result,
|
|
131
|
+
content: [{ type: 'text', text: serializeForModel(result) }],
|
|
132
|
+
details: { ...result, terminalText, terminalTextExpanded },
|
|
122
133
|
isError: result.status === 'error'
|
|
123
134
|
};
|
|
135
|
+
},
|
|
136
|
+
renderResult(toolResult, options) {
|
|
137
|
+
const details = toolResult.details as
|
|
138
|
+
| {
|
|
139
|
+
terminalText?: string;
|
|
140
|
+
terminalTextExpanded?: string;
|
|
141
|
+
presentation?: { views?: { compact?: string; verbose?: string } };
|
|
142
|
+
}
|
|
143
|
+
| undefined;
|
|
144
|
+
try {
|
|
145
|
+
// Legacy fallback: results persisted before this change carry `presentation` but no
|
|
146
|
+
// `terminalText`, so old sessions stay readable instead of rendering blank.
|
|
147
|
+
const legacy = options.expanded
|
|
148
|
+
? details?.presentation?.views?.verbose ?? details?.presentation?.views?.compact
|
|
149
|
+
: details?.presentation?.views?.compact;
|
|
150
|
+
const text =
|
|
151
|
+
(options.expanded ? details?.terminalTextExpanded : details?.terminalText) ??
|
|
152
|
+
details?.terminalText ??
|
|
153
|
+
legacy ??
|
|
154
|
+
'web_explore result';
|
|
155
|
+
return new Text(text, 0, 0);
|
|
156
|
+
} catch {
|
|
157
|
+
// Never throw: a thrown renderer makes Pi fall back to raw content, which would dump
|
|
158
|
+
// the full model-facing findings into the terminal.
|
|
159
|
+
return new Text('web_explore result', 0, 0);
|
|
160
|
+
}
|
|
124
161
|
}
|
|
125
162
|
});
|
|
126
163
|
}
|
|
@@ -41,18 +41,26 @@ export function extractReadableContent(html: string, maxLength = 4000): Extracte
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
45
|
+
' ': ' ',
|
|
46
|
+
'&': '&',
|
|
47
|
+
'<': '<',
|
|
48
|
+
'>': '>',
|
|
49
|
+
'"': '"'
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
function decodeHtmlEntities(text: string): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Single pass so each entity is decoded exactly once. A multi-pass decode
|
|
54
|
+
// rescans its own output, which double-unescapes either &lt; -> < or
|
|
55
|
+
// &amp; -> & depending on the pass order. One pass avoids both.
|
|
56
|
+
return text.replace(
|
|
57
|
+
/&(?:#(\d+)|#x([\da-f]+)|nbsp|amp|lt|gt|quot);/gi,
|
|
58
|
+
(match, dec, hex) => {
|
|
59
|
+
if (dec !== undefined) return String.fromCharCode(Number(dec));
|
|
60
|
+
if (hex !== undefined) return String.fromCharCode(parseInt(hex, 16));
|
|
61
|
+
return NAMED_ENTITIES[match.toLowerCase()] ?? match;
|
|
62
|
+
}
|
|
63
|
+
);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
function extractTitle(html: string): string | undefined {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { SearchResult } from '../types.js';
|
|
2
|
-
import { classifySourceProfile } from './source-profile.js';
|
|
2
|
+
import { classifySourceProfile, hostOf } from './source-profile.js';
|
|
3
3
|
|
|
4
4
|
function wantsDiscussionSources(query = '') {
|
|
5
5
|
return /reddit|forum|forums|discussion|thread|comments|community|user experience|people recommend/i.test(query);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function candidateScore(result: SearchResult, query?: string) {
|
|
9
|
-
const
|
|
9
|
+
const host = hostOf(result.url);
|
|
10
|
+
const isGithub = host === 'github.com' || (host?.endsWith('.github.com') ?? false);
|
|
10
11
|
const profile = classifySourceProfile(result.url);
|
|
11
12
|
const wantsThreads = wantsDiscussionSources(query);
|
|
12
13
|
|
|
@@ -16,13 +17,13 @@ function candidateScore(result: SearchResult, query?: string) {
|
|
|
16
17
|
if (wantsThreads) {
|
|
17
18
|
if (profile.kind === 'forum-thread') return 2;
|
|
18
19
|
if (profile.kind === 'issue-thread') return 3;
|
|
19
|
-
if (
|
|
20
|
+
if (isGithub) return 4;
|
|
20
21
|
if (profile.kind === 'package-page') return 6;
|
|
21
22
|
return 5;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
if (profile.kind === 'issue-thread') return 2;
|
|
25
|
-
if (
|
|
26
|
+
if (isGithub) return 3;
|
|
26
27
|
if (profile.kind === 'package-page') return 5;
|
|
27
28
|
return 4;
|
|
28
29
|
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
'utm_source',
|
|
3
|
-
'utm_medium',
|
|
4
|
-
'utm_campaign',
|
|
5
|
-
'utm_term',
|
|
6
|
-
'utm_content',
|
|
7
|
-
'utm_name',
|
|
8
|
-
'fbclid',
|
|
9
|
-
'gclid'
|
|
10
|
-
]);
|
|
1
|
+
import { canonicalizeUrl } from './url.js';
|
|
11
2
|
|
|
12
3
|
function stripTrailingPunctuation(raw: string): string {
|
|
13
4
|
let next = raw.trim();
|
|
@@ -22,21 +13,7 @@ function stripTrailingPunctuation(raw: string): string {
|
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
function normalizeDirectUrl(raw: string): string | undefined {
|
|
25
|
-
|
|
26
|
-
const url = new URL(stripTrailingPunctuation(raw));
|
|
27
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined;
|
|
28
|
-
|
|
29
|
-
for (const key of [...url.searchParams.keys()]) {
|
|
30
|
-
if (TRACKING_PARAMS.has(key.toLowerCase())) {
|
|
31
|
-
url.searchParams.delete(key);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
url.hash = '';
|
|
36
|
-
return url.toString().replace(/\/$/, '');
|
|
37
|
-
} catch {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
16
|
+
return canonicalizeUrl(stripTrailingPunctuation(raw), { canonicalizeHost: false });
|
|
40
17
|
}
|
|
41
18
|
|
|
42
19
|
export function extractDirectUrls(query: string): string[] {
|
|
@@ -15,6 +15,7 @@ export type EvidenceQualityReport = {
|
|
|
15
15
|
community: number;
|
|
16
16
|
thread: number;
|
|
17
17
|
packagePage: number;
|
|
18
|
+
primaryContent: number;
|
|
18
19
|
distinctHosts: number;
|
|
19
20
|
};
|
|
20
21
|
flags: {
|
|
@@ -65,11 +66,12 @@ export function analyzeEvidenceQuality({
|
|
|
65
66
|
const community = evidence.filter((item) => item.sourceKind === 'community').length;
|
|
66
67
|
const thread = evidence.filter((item) => item.sourceKind === 'issue-thread' || item.sourceKind === 'official-discussion').length;
|
|
67
68
|
const packagePage = evidence.filter((item) => item.sourceKind === 'package-page').length;
|
|
69
|
+
const primaryContent = evidence.filter((item) => item.sourceKind === 'primary-content').length;
|
|
68
70
|
const distinctHosts = new Set(evidence.map((item) => hostname(item.url))).size;
|
|
69
71
|
|
|
70
72
|
const hasOfficialEvidence = official > 0;
|
|
71
|
-
const hasOnlyCommunityEvidence = evidence.length > 0 && official === 0;
|
|
72
|
-
const hasLowDiversity = evidence.length > 1 && distinctHosts <= 1;
|
|
73
|
+
const hasOnlyCommunityEvidence = evidence.length > 0 && official === 0 && primaryContent === 0;
|
|
74
|
+
const hasLowDiversity = evidence.length > 1 && distinctHosts <= 1 && primaryContent === 0;
|
|
73
75
|
const hasUnreadableDirectSource = gaps.some((gap) => /Direct URL could not be read reliably/i.test(gap.message));
|
|
74
76
|
const hasUnreadableThreadSource = gaps.some((gap) => /Thread source could not be read reliably/i.test(gap.message));
|
|
75
77
|
const hasPossibleConflict = hasConflictMarkers(evidence);
|
|
@@ -90,6 +92,7 @@ export function analyzeEvidenceQuality({
|
|
|
90
92
|
community,
|
|
91
93
|
thread,
|
|
92
94
|
packagePage,
|
|
95
|
+
primaryContent,
|
|
93
96
|
distinctHosts
|
|
94
97
|
},
|
|
95
98
|
flags: {
|