@selesai/code 0.9.9 → 0.9.11
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/cli/args.js +13 -2
- package/dist/cli/args.test.js +8 -0
- package/dist/cli/credential-print.js +4 -4
- package/dist/cli.js +0 -0
- package/dist/core/agent-session-auto-handoff.test.js +24 -1
- package/dist/core/agent-session.d.ts +11 -4
- package/dist/core/agent-session.js +99 -39
- package/dist/core/compaction/branch-summarization.js +30 -30
- package/dist/core/compaction/compaction.js +81 -81
- package/dist/core/compaction/utils.js +2 -2
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.js +9 -0
- package/dist/core/export-html/template.css +1066 -1066
- package/dist/core/export-html/template.html +55 -55
- package/dist/core/export-html/template.js +1864 -1864
- package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
- package/dist/core/export-html/vendor/marked.min.js +78 -78
- package/dist/core/extensions/loader.js +108 -35
- package/dist/core/extensions/loader.test.d.ts +1 -0
- package/dist/core/extensions/loader.test.js +21 -0
- package/dist/core/extensions/types.d.ts +12 -2
- package/dist/core/keybindings.d.ts +2 -2
- package/dist/core/messages.js +7 -7
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.js +10 -4
- package/dist/core/package-manager.js +4 -4
- package/dist/core/sdk.d.ts +5 -5
- package/dist/core/sdk.js +16 -4
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.js +22 -0
- package/dist/core/tools/bash.d.ts +18 -1
- package/dist/core/tools/bash.js +41 -23
- package/dist/core/tools/index.d.ts +4 -1
- package/dist/core/tools/index.js +9 -1
- package/dist/core/tools/powershell.d.ts +15 -0
- package/dist/core/tools/powershell.js +38 -0
- package/dist/core/tools/powershell.test.d.ts +1 -0
- package/dist/core/tools/powershell.test.js +20 -0
- package/dist/extensions/context-compaction-reminder.test.ts +82 -82
- package/dist/extensions/context-compaction-reminder.ts +28 -28
- package/dist/extensions/copy-turn.test.ts +24 -1
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +15 -2
- package/dist/extensions/handoff-new.ts +44 -30
- package/dist/extensions/pi-intercom/LICENSE +21 -21
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -387
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -153
- package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
- package/dist/extensions/pi-intercom/cwd.ts +31 -31
- package/dist/extensions/pi-intercom/extension-api.ts +44 -44
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
- package/dist/extensions/pi-intercom/format-context.ts +32 -32
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
- package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
- package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
- package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +286 -286
- package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +108 -108
- package/dist/extensions/pi-powerline-footer/separators.ts +57 -57
- package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +47 -47
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -39
- package/dist/extensions/pi-powerline-footer/theme.example.json +24 -24
- package/dist/extensions/pi-powerline-footer/theme.json +12 -12
- package/dist/extensions/pi-powerline-footer/tps.ts +345 -345
- package/dist/extensions/pi-rewind-hook/README.md +245 -245
- package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
- package/dist/extensions/pi-rewind-hook/package.json +29 -29
- package/dist/extensions/pi-subagents/install.mjs +0 -0
- package/dist/extensions/pi-web-agent/package.json +31 -31
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
- package/dist/extensions/pi-web-agent/src/types.ts +88 -88
- package/dist/extensions/ponytail/package.json +8 -8
- package/dist/extensions/question/batch.ts +103 -103
- package/dist/extensions/question/constants.ts +30 -30
- package/dist/extensions/question/helpers.ts +58 -58
- package/dist/extensions/question/navigation.ts +14 -14
- package/dist/extensions/question/package.json +19 -19
- package/dist/extensions/question/schemas.ts +43 -43
- package/dist/extensions/question/selection-mode.ts +46 -46
- package/dist/extensions/question/shortcuts.ts +44 -44
- package/dist/extensions/question/types.ts +132 -132
- package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
- package/dist/extensions/test-resolve-hook.mjs +6 -6
- package/dist/extensions/web-agent-onboarding.ts +222 -222
- package/dist/extensions/workflow/package.json +17 -17
- package/dist/package-manager-cli.js +71 -71
- package/dist/rpc-entry.js +0 -0
- package/dist/skills/agent-browser/SKILL.md +52 -52
- package/dist/skills/batch-grill-me/SKILL.md +19 -19
- package/dist/skills/grill-me/SKILL.md +10 -10
- package/dist/skills/handoff/SKILL.md +16 -16
- package/dist/skills/handoff-text/SKILL.md +14 -14
- package/dist/skills/implanger/SKILL.md +69 -69
- package/dist/skills/improve-codebase/REFERENCE.md +78 -78
- package/dist/skills/improve-codebase/SKILL.md +178 -178
- package/dist/skills/planger/SKILL.md +166 -166
- package/dist/skills/ponytail/SKILL.md +116 -116
- package/dist/skills/ponytail-audit/SKILL.md +42 -42
- package/dist/skills/ponytail-debt/SKILL.md +45 -45
- package/dist/skills/ponytail-gain/SKILL.md +51 -51
- package/dist/skills/ponytail-help/SKILL.md +70 -70
- package/dist/skills/ponytail-review/SKILL.md +58 -58
- package/dist/skills/selesai-handoff/SKILL.md +20 -20
- package/dist/skills/workflow-creation/SKILL.md +73 -73
- package/dist/themes/powerline-footer/theme.json +33 -33
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.js +17 -5
- package/docs/compaction.md +396 -396
- package/docs/containerization.md +111 -111
- package/docs/development.md +71 -71
- package/docs/docs.json +164 -164
- package/docs/environment-variables.md +86 -86
- package/docs/index.md +83 -83
- package/docs/json.md +82 -82
- package/docs/models.md +502 -502
- package/docs/packages.md +227 -227
- package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
- package/docs/prompt-templates.md +95 -95
- package/docs/providers.md +293 -293
- package/docs/sdk.md +1144 -1143
- package/docs/security.md +59 -59
- package/docs/session-format.md +414 -414
- package/docs/sessions.md +145 -145
- package/docs/shared-host-extensions.md +109 -109
- package/docs/shell-aliases.md +13 -13
- package/docs/skills.md +231 -231
- package/docs/terminal-setup.md +142 -142
- package/docs/termux.md +127 -127
- package/docs/themes.md +295 -295
- package/docs/tmux.md +63 -63
- package/docs/tui.md +927 -927
- package/docs/windows.md +17 -17
- package/examples/README.md +25 -25
- package/examples/extensions/README.md +211 -211
- package/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/examples/extensions/bash-spawn-hook.ts +30 -30
- package/examples/extensions/bookmark.ts +50 -50
- package/examples/extensions/border-status-editor.ts +150 -150
- package/examples/extensions/built-in-tool-renderer.ts +249 -249
- package/examples/extensions/claude-rules.ts +86 -86
- package/examples/extensions/commands.ts +72 -72
- package/examples/extensions/confirm-destructive.ts +59 -59
- package/examples/extensions/custom-compaction.ts +130 -130
- package/examples/extensions/custom-footer.ts +64 -64
- package/examples/extensions/custom-header.ts +73 -73
- package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
- package/examples/extensions/custom-provider-anthropic/package.json +19 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
- package/examples/extensions/dirty-repo-guard.ts +56 -56
- package/examples/extensions/doom-overlay/README.md +46 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
- package/examples/extensions/doom-overlay/doom-component.ts +132 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
- package/examples/extensions/doom-overlay/index.ts +74 -74
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
- package/examples/extensions/dynamic-resources/SKILL.md +8 -8
- package/examples/extensions/dynamic-resources/dynamic.json +79 -79
- package/examples/extensions/dynamic-resources/dynamic.md +5 -5
- package/examples/extensions/dynamic-resources/index.ts +15 -15
- package/examples/extensions/dynamic-tools.ts +74 -74
- package/examples/extensions/event-bus.ts +43 -43
- package/examples/extensions/file-trigger.ts +41 -41
- package/examples/extensions/git-checkpoint.ts +53 -53
- package/examples/extensions/git-merge-and-resolve.ts +115 -115
- package/examples/extensions/github-issue-autocomplete.ts +185 -185
- package/examples/extensions/gondolin/index.ts +531 -531
- package/examples/extensions/gondolin/package-lock.json +185 -185
- package/examples/extensions/gondolin/package.json +19 -19
- package/examples/extensions/handoff.ts +199 -199
- package/examples/extensions/hello.ts +26 -26
- package/examples/extensions/hidden-thinking-label.ts +53 -53
- package/examples/extensions/inline-bash.ts +94 -94
- package/examples/extensions/input-transform-streaming.ts +39 -39
- package/examples/extensions/input-transform.ts +43 -43
- package/examples/extensions/interactive-shell.ts +196 -196
- package/examples/extensions/mac-system-theme.ts +47 -47
- package/examples/extensions/message-renderer.ts +59 -59
- package/examples/extensions/minimal-mode.ts +426 -426
- package/examples/extensions/modal-editor.ts +85 -85
- package/examples/extensions/model-status.ts +31 -31
- package/examples/extensions/notify.ts +55 -55
- package/examples/extensions/overlay-qa-tests.ts +1450 -1450
- package/examples/extensions/overlay-test.ts +153 -153
- package/examples/extensions/permission-gate.ts +34 -34
- package/examples/extensions/pirate.ts +47 -47
- package/examples/extensions/plan-mode/README.md +66 -66
- package/examples/extensions/plan-mode/index.ts +390 -390
- package/examples/extensions/plan-mode/utils.ts +168 -168
- package/examples/extensions/preset.ts +436 -436
- package/examples/extensions/project-trust.ts +64 -64
- package/examples/extensions/prompt-customizer.ts +97 -97
- package/examples/extensions/protected-paths.ts +30 -30
- package/examples/extensions/provider-payload.ts +18 -18
- package/examples/extensions/qna.ts +122 -122
- package/examples/extensions/question.ts +285 -285
- package/examples/extensions/questionnaire.ts +448 -448
- package/examples/extensions/rainbow-editor.ts +88 -88
- package/examples/extensions/reload-runtime.ts +37 -37
- package/examples/extensions/rpc-demo.ts +118 -118
- package/examples/extensions/sandbox/index.ts +321 -321
- package/examples/extensions/sandbox/package-lock.json +92 -92
- package/examples/extensions/sandbox/package.json +19 -19
- package/examples/extensions/send-user-message.ts +97 -97
- package/examples/extensions/session-name.ts +27 -27
- package/examples/extensions/shutdown-command.ts +63 -63
- package/examples/extensions/snake.ts +343 -343
- package/examples/extensions/space-invaders.ts +560 -560
- package/examples/extensions/ssh.ts +220 -220
- package/examples/extensions/status-line.ts +32 -32
- package/examples/extensions/structured-output.ts +65 -65
- package/examples/extensions/subagent/README.md +175 -175
- package/examples/extensions/subagent/agents/planner.md +37 -37
- package/examples/extensions/subagent/agents/reviewer.md +35 -35
- package/examples/extensions/subagent/agents/scout.md +50 -50
- package/examples/extensions/subagent/agents/worker.md +24 -24
- package/examples/extensions/subagent/agents.ts +126 -126
- package/examples/extensions/subagent/index.ts +1015 -1015
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
- package/examples/extensions/subagent/prompts/implement.md +10 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
- package/examples/extensions/summarize.ts +209 -209
- package/examples/extensions/system-prompt-header.ts +17 -17
- package/examples/extensions/tic-tac-toe.ts +1008 -1008
- package/examples/extensions/timed-confirm.ts +70 -70
- package/examples/extensions/titlebar-spinner.ts +58 -58
- package/examples/extensions/todo.ts +297 -297
- package/examples/extensions/tool-override.ts +144 -144
- package/examples/extensions/tools.ts +146 -146
- package/examples/extensions/trigger-compact.ts +50 -50
- package/examples/extensions/truncated-tool.ts +195 -195
- package/examples/extensions/widget-placement.ts +9 -9
- package/examples/extensions/with-deps/index.ts +32 -32
- package/examples/extensions/with-deps/package-lock.json +31 -31
- package/examples/extensions/with-deps/package.json +22 -22
- package/examples/extensions/working-indicator.ts +123 -123
- package/examples/extensions/working-message-test.ts +25 -25
- package/examples/rpc-extension-ui.ts +632 -632
- package/examples/sdk/01-minimal.ts +26 -26
- package/examples/sdk/02-custom-model.ts +53 -53
- package/examples/sdk/03-custom-prompt.ts +75 -75
- package/examples/sdk/04-skills.ts +55 -55
- package/examples/sdk/05-tools.ts +48 -48
- package/examples/sdk/06-extensions.ts +99 -99
- package/examples/sdk/07-context-files.ts +47 -47
- package/examples/sdk/08-prompt-templates.ts +51 -51
- package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
- package/examples/sdk/10-settings.ts +53 -53
- package/examples/sdk/11-sessions.ts +52 -52
- package/examples/sdk/12-full-control.ts +79 -79
- package/examples/sdk/13-session-runtime.ts +67 -67
- package/examples/sdk/README.md +144 -144
- package/package.json +4 -4
|
@@ -1,946 +1,946 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_BACKEND_CONFIG,
|
|
3
|
-
mergeBackendConfigLayers,
|
|
4
|
-
validateBackendConfig,
|
|
5
|
-
type BackendConfig,
|
|
6
|
-
type BackendConfigOverride
|
|
7
|
-
} from '../backends/config.js';
|
|
8
|
-
import { checkBackendHealth } from '../backends/doctor.js';
|
|
9
|
-
import {
|
|
10
|
-
DynamicBorder,
|
|
11
|
-
getSettingsListTheme,
|
|
12
|
-
type ExtensionAPI
|
|
13
|
-
} from '@selesai/code';
|
|
14
|
-
import { Container, SelectList, SettingsList, Text, type SelectItem, type SettingItem } from '@earendil-works/pi-tui';
|
|
15
|
-
import {
|
|
16
|
-
DEFAULT_PRESENTATION_CONFIG,
|
|
17
|
-
mergePresentationConfigLayers,
|
|
18
|
-
resolvePresentationMode
|
|
19
|
-
} from '../presentation/config.js';
|
|
20
|
-
import {
|
|
21
|
-
loadPresentationConfigLayers,
|
|
22
|
-
resetPresentationConfigScope,
|
|
23
|
-
saveBackendConfigScope,
|
|
24
|
-
savePresentationConfigScope,
|
|
25
|
-
type LoadedPresentationConfig
|
|
26
|
-
} from '../presentation/config-store.js';
|
|
27
|
-
import { resolveBrowserExecutable, type BrowserResolutionResult } from '../fetch/browser-resolution.js';
|
|
28
|
-
import { getLatestChangelogEntry } from '../changelog-notice.js';
|
|
29
|
-
import type {
|
|
30
|
-
PresentationConfig,
|
|
31
|
-
PresentationConfigOverride,
|
|
32
|
-
PresentationScope,
|
|
33
|
-
PresentationToolName
|
|
34
|
-
} from '../presentation/types.js';
|
|
35
|
-
|
|
36
|
-
type CommandDeps = {
|
|
37
|
-
load?: () => ReturnType<typeof loadPresentationConfigLayers>;
|
|
38
|
-
save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
|
|
39
|
-
saveBackends?: (scope: PresentationScope, config: BackendConfigOverride) => Promise<void>;
|
|
40
|
-
reset?: (scope: PresentationScope) => Promise<void>;
|
|
41
|
-
resolveBrowser?: () => Promise<BrowserResolutionResult>;
|
|
42
|
-
runtime?: { nodeVersion: string; platform: string; arch: string };
|
|
43
|
-
checkTypebox?: () => Promise<boolean>;
|
|
44
|
-
checkBackends?: (config: BackendConfig) => Promise<string[]>;
|
|
45
|
-
getChangelog?: () => Promise<string | undefined>;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type SettingsUiResult =
|
|
49
|
-
| { action: 'cancel' }
|
|
50
|
-
| { action: 'reset'; scope: PresentationScope }
|
|
51
|
-
| { action: 'save'; scope: PresentationScope; config: PresentationConfig; backends: BackendConfig };
|
|
52
|
-
|
|
53
|
-
type WebAgentAction = 'settings' | 'show' | 'doctor' | 'changelog' | 'reset-project' | 'reset-global';
|
|
54
|
-
type SettingsSection = 'presentation' | 'backends';
|
|
55
|
-
|
|
56
|
-
export type SettingsDraftState = {
|
|
57
|
-
scope: PresentationScope;
|
|
58
|
-
drafts: Record<PresentationScope, PresentationConfig>;
|
|
59
|
-
backendDrafts: Record<PresentationScope, BackendConfig>;
|
|
60
|
-
config: PresentationConfig;
|
|
61
|
-
backends: BackendConfig;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const PRESENTATION_TOOL_NAMES: PresentationToolName[] = ['web_explore'];
|
|
65
|
-
|
|
66
|
-
function parseScopeToken(token: string | undefined): PresentationScope | undefined {
|
|
67
|
-
return token === 'global' || token === 'project' ? token : undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function clonePresentationConfig(config: PresentationConfig): PresentationConfig {
|
|
71
|
-
return {
|
|
72
|
-
defaultMode: config.defaultMode,
|
|
73
|
-
tools: { ...config.tools }
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function cloneBackendConfig(config: BackendConfig): BackendConfig {
|
|
78
|
-
return {
|
|
79
|
-
search: {
|
|
80
|
-
...config.search,
|
|
81
|
-
options: config.search.options ? { ...config.search.options } : undefined
|
|
82
|
-
},
|
|
83
|
-
fetch: {
|
|
84
|
-
...config.fetch,
|
|
85
|
-
options: config.fetch.options ? { ...config.fetch.options } : undefined
|
|
86
|
-
},
|
|
87
|
-
headless: { ...config.headless }
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function sameJson(left: unknown, right: unknown): boolean {
|
|
92
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function validateBackendUrl(value: string): { ok: true; value: string } | { ok: false; message: string } {
|
|
96
|
-
try {
|
|
97
|
-
const url = new URL(value.trim());
|
|
98
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
99
|
-
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { ok: true, value: url.toString().replace(/\/$/, '') };
|
|
103
|
-
} catch {
|
|
104
|
-
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function defaultCheckTypebox(): Promise<boolean> {
|
|
109
|
-
try {
|
|
110
|
-
await import('typebox');
|
|
111
|
-
return true;
|
|
112
|
-
} catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function formatSearchOptions(config: BackendConfig['search']) {
|
|
118
|
-
return [
|
|
119
|
-
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
120
|
-
config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
|
|
121
|
-
config.options?.language ? `language ${config.options.language}` : undefined,
|
|
122
|
-
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
|
|
123
|
-
].filter(Boolean).join(' ');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatFetchOptions(config: BackendConfig['fetch']) {
|
|
127
|
-
return [
|
|
128
|
-
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
129
|
-
config.options?.formats?.length ? `formats ${config.options.formats.join(',')}` : undefined,
|
|
130
|
-
config.options?.onlyMainContent !== undefined ? `onlyMainContent ${config.options.onlyMainContent}` : undefined
|
|
131
|
-
].filter(Boolean).join(' ');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function formatBackendSummary(config: BackendConfig = DEFAULT_BACKEND_CONFIG) {
|
|
135
|
-
const searchSuffix = formatSearchOptions(config.search);
|
|
136
|
-
const fetchSuffix = formatFetchOptions(config.fetch);
|
|
137
|
-
const searchBase = config.search.baseUrl
|
|
138
|
-
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
139
|
-
: `search: ${config.search.provider}`;
|
|
140
|
-
const fetchBase = config.fetch.baseUrl
|
|
141
|
-
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
142
|
-
: `fetch: ${config.fetch.provider}`;
|
|
143
|
-
|
|
144
|
-
return [
|
|
145
|
-
searchSuffix ? `${searchBase} ${searchSuffix}` : searchBase,
|
|
146
|
-
fetchSuffix ? `${fetchBase} ${fetchSuffix}` : fetchBase,
|
|
147
|
-
`headless: ${config.headless.provider}`
|
|
148
|
-
].join('\n');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function formatConfigSummary(config: PresentationConfig) {
|
|
152
|
-
const lines = [`defaultMode: ${config.defaultMode}`];
|
|
153
|
-
|
|
154
|
-
for (const toolName of PRESENTATION_TOOL_NAMES) {
|
|
155
|
-
lines.push(`${toolName}: ${config.tools[toolName]?.mode ?? 'inherit'}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return lines.join('\n');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function buildPresentationSettingsItems(scope: PresentationScope, config: PresentationConfig): SettingItem[] {
|
|
162
|
-
return [
|
|
163
|
-
{
|
|
164
|
-
id: 'scope',
|
|
165
|
-
label: 'Write scope',
|
|
166
|
-
currentValue: scope,
|
|
167
|
-
values: ['project', 'global']
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: 'defaultMode',
|
|
171
|
-
label: 'Default mode',
|
|
172
|
-
currentValue: config.defaultMode,
|
|
173
|
-
values: ['compact', 'preview', 'verbose']
|
|
174
|
-
},
|
|
175
|
-
...PRESENTATION_TOOL_NAMES.map((toolName) => ({
|
|
176
|
-
id: `tool:${toolName}`,
|
|
177
|
-
label: toolName,
|
|
178
|
-
currentValue: config.tools[toolName]?.mode ?? 'inherit',
|
|
179
|
-
values: ['inherit', 'compact', 'preview', 'verbose']
|
|
180
|
-
}))
|
|
181
|
-
];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function buildBackendSettingsItems(scope: PresentationScope, backends: BackendConfig): SettingItem[] {
|
|
185
|
-
return [
|
|
186
|
-
{
|
|
187
|
-
id: 'scope',
|
|
188
|
-
label: 'Write scope',
|
|
189
|
-
currentValue: scope,
|
|
190
|
-
values: ['project', 'global']
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
id: 'backend:search:provider',
|
|
194
|
-
label: 'Search backend',
|
|
195
|
-
currentValue: backends.search.provider,
|
|
196
|
-
values: ['duckduckgo', 'searxng', 'brave']
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
id: 'backend:search:baseUrl',
|
|
200
|
-
label: 'SearXNG URL',
|
|
201
|
-
currentValue: backends.search.baseUrl ?? 'not set',
|
|
202
|
-
values: ['edit']
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
id: 'backend:search:fallback',
|
|
206
|
-
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']
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
id: 'backend:secret:brave',
|
|
212
|
-
label: 'Brave API key',
|
|
213
|
-
currentValue: 'env var',
|
|
214
|
-
values: ['env var']
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
id: 'backend:fetch:provider',
|
|
218
|
-
label: 'Fetch backend',
|
|
219
|
-
currentValue: backends.fetch.provider,
|
|
220
|
-
values: ['http', 'firecrawl']
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
id: 'backend:fetch:baseUrl',
|
|
224
|
-
label: 'Firecrawl URL',
|
|
225
|
-
currentValue: backends.fetch.baseUrl ?? 'not set',
|
|
226
|
-
values: ['edit']
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
id: 'backend:fetch:fallback',
|
|
230
|
-
label: 'Firecrawl fallback',
|
|
231
|
-
currentValue: backends.fetch.provider === 'firecrawl' ? backends.fetch.fallback ?? 'off' : 'off',
|
|
232
|
-
values: backends.fetch.provider === 'firecrawl' ? ['off', 'http'] : ['off']
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: 'backend:secret:firecrawl',
|
|
236
|
-
label: 'Firecrawl API key',
|
|
237
|
-
currentValue: 'env var',
|
|
238
|
-
values: ['env var']
|
|
239
|
-
}
|
|
240
|
-
];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function isToolName(value: string): value is PresentationToolName {
|
|
244
|
-
return PRESENTATION_TOOL_NAMES.includes(value as PresentationToolName);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function isModeOrInherit(value: string): value is 'inherit' | 'compact' | 'preview' | 'verbose' {
|
|
248
|
-
return ['inherit', 'compact', 'preview', 'verbose'].includes(value);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function getInheritedConfigForScope(
|
|
252
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
253
|
-
scope: PresentationScope
|
|
254
|
-
): PresentationConfig {
|
|
255
|
-
if (scope === 'global') {
|
|
256
|
-
return DEFAULT_PRESENTATION_CONFIG;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function getScopeDisplayConfig(
|
|
263
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
264
|
-
scope: PresentationScope
|
|
265
|
-
): PresentationConfig {
|
|
266
|
-
if (scope === 'global') {
|
|
267
|
-
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return mergePresentationConfigLayers(
|
|
271
|
-
DEFAULT_PRESENTATION_CONFIG,
|
|
272
|
-
loaded.global.rawConfig,
|
|
273
|
-
loaded.project.rawConfig
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function getInheritedBackendsForScope(
|
|
278
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
279
|
-
scope: PresentationScope
|
|
280
|
-
): BackendConfig {
|
|
281
|
-
if (scope === 'global') {
|
|
282
|
-
return DEFAULT_BACKEND_CONFIG;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function getScopeDisplayBackends(
|
|
289
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
290
|
-
scope: PresentationScope
|
|
291
|
-
): BackendConfig {
|
|
292
|
-
if (scope === 'global') {
|
|
293
|
-
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return mergeBackendConfigLayers(
|
|
297
|
-
DEFAULT_BACKEND_CONFIG,
|
|
298
|
-
loaded.global.rawBackends,
|
|
299
|
-
loaded.project.rawBackends
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function createSettingsDraftState(
|
|
304
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
305
|
-
initialScope: PresentationScope
|
|
306
|
-
): SettingsDraftState {
|
|
307
|
-
const drafts = {
|
|
308
|
-
global: getScopeDisplayConfig(loaded, 'global'),
|
|
309
|
-
project: getScopeDisplayConfig(loaded, 'project')
|
|
310
|
-
};
|
|
311
|
-
const backendDrafts = {
|
|
312
|
-
global: getScopeDisplayBackends(loaded, 'global'),
|
|
313
|
-
project: getScopeDisplayBackends(loaded, 'project')
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
scope: initialScope,
|
|
318
|
-
drafts,
|
|
319
|
-
backendDrafts,
|
|
320
|
-
config: clonePresentationConfig(drafts[initialScope]),
|
|
321
|
-
backends: cloneBackendConfig(backendDrafts[initialScope])
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export function applySettingsValue(
|
|
326
|
-
state: SettingsDraftState,
|
|
327
|
-
id: string,
|
|
328
|
-
newValue: string
|
|
329
|
-
): SettingsDraftState {
|
|
330
|
-
const nextDrafts = {
|
|
331
|
-
global: clonePresentationConfig(state.drafts.global),
|
|
332
|
-
project: clonePresentationConfig(state.drafts.project)
|
|
333
|
-
};
|
|
334
|
-
const nextBackendDrafts = {
|
|
335
|
-
global: cloneBackendConfig(state.backendDrafts.global),
|
|
336
|
-
project: cloneBackendConfig(state.backendDrafts.project)
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
let nextScope = state.scope;
|
|
340
|
-
|
|
341
|
-
if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
|
|
342
|
-
nextScope = newValue;
|
|
343
|
-
return {
|
|
344
|
-
scope: nextScope,
|
|
345
|
-
drafts: nextDrafts,
|
|
346
|
-
backendDrafts: nextBackendDrafts,
|
|
347
|
-
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
348
|
-
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
|
|
353
|
-
const currentBackends = cloneBackendConfig(nextBackendDrafts[nextScope]);
|
|
354
|
-
|
|
355
|
-
if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
|
|
356
|
-
currentDraft.defaultMode = newValue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (id.startsWith('tool:')) {
|
|
360
|
-
const toolName = id.slice('tool:'.length) as PresentationToolName;
|
|
361
|
-
const nextTools = { ...currentDraft.tools };
|
|
362
|
-
|
|
363
|
-
if (newValue === 'inherit') {
|
|
364
|
-
delete nextTools[toolName];
|
|
365
|
-
} else if (
|
|
366
|
-
newValue === 'compact' ||
|
|
367
|
-
newValue === 'preview' ||
|
|
368
|
-
newValue === 'verbose'
|
|
369
|
-
) {
|
|
370
|
-
nextTools[toolName] = { mode: newValue };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
currentDraft.tools = nextTools;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave')) {
|
|
377
|
-
currentBackends.search.provider = newValue;
|
|
378
|
-
if (newValue !== 'searxng') {
|
|
379
|
-
delete currentBackends.search.baseUrl;
|
|
380
|
-
delete currentBackends.search.options;
|
|
381
|
-
}
|
|
382
|
-
if (newValue === 'duckduckgo') {
|
|
383
|
-
delete currentBackends.search.fallback;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (id === 'backend:search:fallback') {
|
|
388
|
-
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave')) {
|
|
389
|
-
currentBackends.search.fallback = 'duckduckgo';
|
|
390
|
-
} else {
|
|
391
|
-
delete currentBackends.search.fallback;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (id === 'backend:search:baseUrl') {
|
|
396
|
-
if (currentBackends.search.provider === 'searxng' && newValue.trim()) {
|
|
397
|
-
currentBackends.search.baseUrl = newValue.trim();
|
|
398
|
-
} else {
|
|
399
|
-
delete currentBackends.search.baseUrl;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (id === 'backend:fetch:provider' && (newValue === 'http' || newValue === 'firecrawl')) {
|
|
404
|
-
currentBackends.fetch.provider = newValue;
|
|
405
|
-
if (newValue === 'http') {
|
|
406
|
-
delete currentBackends.fetch.fallback;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (id === 'backend:fetch:fallback') {
|
|
411
|
-
if (newValue === 'http' && currentBackends.fetch.provider === 'firecrawl') {
|
|
412
|
-
currentBackends.fetch.fallback = 'http';
|
|
413
|
-
} else if (newValue === 'off' || currentBackends.fetch.provider !== 'firecrawl') {
|
|
414
|
-
delete currentBackends.fetch.fallback;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (id === 'backend:fetch:baseUrl') {
|
|
419
|
-
if (newValue.trim()) {
|
|
420
|
-
currentBackends.fetch.baseUrl = newValue.trim();
|
|
421
|
-
} else {
|
|
422
|
-
delete currentBackends.fetch.baseUrl;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
nextDrafts[nextScope] = currentDraft;
|
|
427
|
-
nextBackendDrafts[nextScope] = currentBackends;
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
scope: nextScope,
|
|
431
|
-
drafts: nextDrafts,
|
|
432
|
-
backendDrafts: nextBackendDrafts,
|
|
433
|
-
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
434
|
-
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export function collapsePresentationConfigToOverride(
|
|
439
|
-
config: PresentationConfig,
|
|
440
|
-
inheritedConfig: PresentationConfig
|
|
441
|
-
): PresentationConfigOverride {
|
|
442
|
-
const tools = Object.fromEntries(
|
|
443
|
-
PRESENTATION_TOOL_NAMES.flatMap((toolName) => {
|
|
444
|
-
const configuredMode = config.tools[toolName]?.mode;
|
|
445
|
-
if (!configuredMode) {
|
|
446
|
-
return [];
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const inheritedMode = resolvePresentationMode(toolName, inheritedConfig);
|
|
450
|
-
if (configuredMode === inheritedMode) {
|
|
451
|
-
return [];
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return [[toolName, { mode: configuredMode }]];
|
|
455
|
-
})
|
|
456
|
-
) as PresentationConfigOverride['tools'];
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
defaultMode:
|
|
460
|
-
config.defaultMode === inheritedConfig.defaultMode ? undefined : config.defaultMode,
|
|
461
|
-
tools
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function collapseBackendConfigToOverride(
|
|
466
|
-
config: BackendConfig,
|
|
467
|
-
inheritedConfig: BackendConfig
|
|
468
|
-
): BackendConfigOverride {
|
|
469
|
-
const override: BackendConfigOverride = {};
|
|
470
|
-
|
|
471
|
-
if (!sameJson(config.search, inheritedConfig.search)) {
|
|
472
|
-
override.search = config.search.provider !== inheritedConfig.search.provider
|
|
473
|
-
? { ...config.search }
|
|
474
|
-
: {
|
|
475
|
-
...(config.search.baseUrl !== inheritedConfig.search.baseUrl ? { baseUrl: config.search.baseUrl } : {}),
|
|
476
|
-
...(config.search.fallback !== inheritedConfig.search.fallback ? { fallback: config.search.fallback } : {}),
|
|
477
|
-
...(!sameJson(config.search.options, inheritedConfig.search.options) ? { options: config.search.options } : {})
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
if (config.search.provider !== inheritedConfig.search.provider) {
|
|
481
|
-
override.search.provider = config.search.provider;
|
|
482
|
-
} else if (Object.keys(override.search).length === 0) {
|
|
483
|
-
delete override.search;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (!sameJson(config.fetch, inheritedConfig.fetch)) {
|
|
488
|
-
override.fetch = config.fetch.provider !== inheritedConfig.fetch.provider
|
|
489
|
-
? { ...config.fetch, apiKey: undefined }
|
|
490
|
-
: {
|
|
491
|
-
...(config.fetch.baseUrl !== inheritedConfig.fetch.baseUrl ? { baseUrl: config.fetch.baseUrl } : {}),
|
|
492
|
-
...(config.fetch.fallback !== inheritedConfig.fetch.fallback ? { fallback: config.fetch.fallback } : {}),
|
|
493
|
-
...(!sameJson(config.fetch.options, inheritedConfig.fetch.options) ? { options: config.fetch.options } : {})
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
delete override.fetch.apiKey;
|
|
497
|
-
|
|
498
|
-
if (config.fetch.provider !== inheritedConfig.fetch.provider) {
|
|
499
|
-
override.fetch.provider = config.fetch.provider;
|
|
500
|
-
} else if (Object.keys(override.fetch).length === 0) {
|
|
501
|
-
delete override.fetch;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (!sameJson(config.headless, inheritedConfig.headless)) {
|
|
506
|
-
override.headless = { ...config.headless };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return override;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
export function handleSettingsShortcut(data: string): { action: 'cancel' | 'reset' | 'save' } | undefined {
|
|
513
|
-
if (data === '\u001b') {
|
|
514
|
-
return { action: 'cancel' };
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (data === '\u0012') {
|
|
518
|
-
return { action: 'reset' };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (data === '\u0013') {
|
|
522
|
-
return { action: 'save' };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return undefined;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async function openActionMenu(ctx: any): Promise<WebAgentAction | undefined> {
|
|
529
|
-
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: WebAgentAction | undefined) => void) => {
|
|
530
|
-
const container = new Container();
|
|
531
|
-
const items: SelectItem[] = [
|
|
532
|
-
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes and backends' },
|
|
533
|
-
{ value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
|
|
534
|
-
{ value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
|
|
535
|
-
{ value: 'changelog', label: 'Changelog', description: 'Show latest package changelog' },
|
|
536
|
-
{ value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
|
|
537
|
-
{ value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
|
|
538
|
-
];
|
|
539
|
-
|
|
540
|
-
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
541
|
-
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent')), 1, 0));
|
|
542
|
-
|
|
543
|
-
const list = new SelectList(items, Math.min(items.length, 8), {
|
|
544
|
-
selectedPrefix: (text: string) => theme.fg('accent', text),
|
|
545
|
-
selectedText: (text: string) => theme.fg('accent', text),
|
|
546
|
-
description: (text: string) => theme.fg('muted', text),
|
|
547
|
-
scrollInfo: (text: string) => theme.fg('dim', text),
|
|
548
|
-
noMatch: (text: string) => theme.fg('warning', text)
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
list.onSelect = (item) => done(item.value as WebAgentAction);
|
|
552
|
-
list.onCancel = () => done(undefined);
|
|
553
|
-
container.addChild(list);
|
|
554
|
-
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
555
|
-
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
render: (width: number) => container.render(width),
|
|
559
|
-
invalidate: () => container.invalidate(),
|
|
560
|
-
handleInput: (data: string) => {
|
|
561
|
-
list.handleInput?.(data);
|
|
562
|
-
tui.requestRender?.();
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
async function openSettingsSectionMenu(ctx: any): Promise<SettingsSection | undefined> {
|
|
569
|
-
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: SettingsSection | undefined) => void) => {
|
|
570
|
-
const container = new Container();
|
|
571
|
-
const items: SelectItem[] = [
|
|
572
|
-
{ value: 'presentation', label: 'Presentation', description: 'Compact, preview, and verbose output modes' },
|
|
573
|
-
{ value: 'backends', label: 'Backends', description: 'Search/fetch providers, URLs, and fallbacks' }
|
|
574
|
-
];
|
|
575
|
-
|
|
576
|
-
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
577
|
-
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent settings')), 1, 0));
|
|
578
|
-
|
|
579
|
-
const list = new SelectList(items, items.length, {
|
|
580
|
-
selectedPrefix: (text: string) => theme.fg('accent', text),
|
|
581
|
-
selectedText: (text: string) => theme.fg('accent', text),
|
|
582
|
-
description: (text: string) => theme.fg('muted', text),
|
|
583
|
-
scrollInfo: (text: string) => theme.fg('dim', text),
|
|
584
|
-
noMatch: (text: string) => theme.fg('warning', text)
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
list.onSelect = (item) => done(item.value as SettingsSection);
|
|
588
|
-
list.onCancel = () => done(undefined);
|
|
589
|
-
container.addChild(list);
|
|
590
|
-
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
591
|
-
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
render: (width: number) => container.render(width),
|
|
595
|
-
invalidate: () => container.invalidate(),
|
|
596
|
-
handleInput: (data: string) => {
|
|
597
|
-
list.handleInput?.(data);
|
|
598
|
-
tui.requestRender?.();
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function openPresentationSettingsUi(
|
|
605
|
-
ctx: any,
|
|
606
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
607
|
-
initialScope: PresentationScope
|
|
608
|
-
): Promise<SettingsUiResult | undefined> {
|
|
609
|
-
return ctx.ui.custom((_tui: unknown, theme: any, _kb: unknown, done: (value: SettingsUiResult) => void) => {
|
|
610
|
-
let state = createSettingsDraftState(loaded, initialScope);
|
|
611
|
-
let settingsList: SettingsList;
|
|
612
|
-
|
|
613
|
-
const container = new Container();
|
|
614
|
-
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · presentation')), 1, 1));
|
|
615
|
-
container.addChild(
|
|
616
|
-
new Text(
|
|
617
|
-
theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'),
|
|
618
|
-
1,
|
|
619
|
-
2
|
|
620
|
-
)
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
const rebuildSettingsList = () => {
|
|
624
|
-
if (settingsList) {
|
|
625
|
-
container.removeChild(settingsList);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
settingsList = new SettingsList(
|
|
629
|
-
buildPresentationSettingsItems(state.scope, state.config),
|
|
630
|
-
Math.min(PRESENTATION_TOOL_NAMES.length + 8, 18),
|
|
631
|
-
getSettingsListTheme(),
|
|
632
|
-
(id, newValue) => {
|
|
633
|
-
state = applySettingsValue(state, id, newValue);
|
|
634
|
-
rebuildSettingsList();
|
|
635
|
-
container.invalidate();
|
|
636
|
-
},
|
|
637
|
-
() => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }),
|
|
638
|
-
{ enableSearch: true }
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
container.addChild(settingsList);
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
rebuildSettingsList();
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
render: (width: number) => container.render(width),
|
|
648
|
-
invalidate: () => container.invalidate(),
|
|
649
|
-
handleInput: (data: string) => {
|
|
650
|
-
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
651
|
-
|
|
652
|
-
if (shortcut?.action === 'cancel') {
|
|
653
|
-
done({ action: 'cancel' });
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (shortcut?.action === 'reset') {
|
|
658
|
-
done({ action: 'reset', scope: state.scope });
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (shortcut?.action === 'save') {
|
|
663
|
-
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
settingsList.handleInput?.(data);
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async function openBackendSettingsUi(
|
|
674
|
-
ctx: any,
|
|
675
|
-
loaded: Awaited<LoadedPresentationConfig>,
|
|
676
|
-
initialScope: PresentationScope
|
|
677
|
-
): Promise<SettingsUiResult | undefined> {
|
|
678
|
-
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: SettingsUiResult) => void) => {
|
|
679
|
-
let state = createSettingsDraftState(loaded, initialScope);
|
|
680
|
-
let settingsList: SettingsList;
|
|
681
|
-
|
|
682
|
-
const container = new Container();
|
|
683
|
-
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
|
|
684
|
-
container.addChild(
|
|
685
|
-
new Text(
|
|
686
|
-
theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel · API keys stay in env vars'),
|
|
687
|
-
1,
|
|
688
|
-
2
|
|
689
|
-
)
|
|
690
|
-
);
|
|
691
|
-
|
|
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
|
-
const rebuildSettingsList = () => {
|
|
718
|
-
if (settingsList) {
|
|
719
|
-
container.removeChild(settingsList);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
settingsList = new SettingsList(
|
|
723
|
-
buildBackendSettingsItems(state.scope, state.backends),
|
|
724
|
-
12,
|
|
725
|
-
getSettingsListTheme(),
|
|
726
|
-
(id, newValue) => {
|
|
727
|
-
if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
|
|
728
|
-
void editUrl(id);
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
state = applySettingsValue(state, id, newValue);
|
|
733
|
-
rebuildSettingsList();
|
|
734
|
-
container.invalidate();
|
|
735
|
-
},
|
|
736
|
-
() => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }),
|
|
737
|
-
{ enableSearch: true }
|
|
738
|
-
);
|
|
739
|
-
|
|
740
|
-
container.addChild(settingsList);
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
rebuildSettingsList();
|
|
744
|
-
|
|
745
|
-
return {
|
|
746
|
-
render: (width: number) => container.render(width),
|
|
747
|
-
invalidate: () => container.invalidate(),
|
|
748
|
-
handleInput: (data: string) => {
|
|
749
|
-
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
750
|
-
|
|
751
|
-
if (shortcut?.action === 'cancel') {
|
|
752
|
-
done({ action: 'cancel' });
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (shortcut?.action === 'reset') {
|
|
757
|
-
done({ action: 'reset', scope: state.scope });
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (shortcut?.action === 'save') {
|
|
762
|
-
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
settingsList.handleInput?.(data);
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
export function registerWebAgentConfigCommands(pi: ExtensionAPI, deps: CommandDeps = {}) {
|
|
773
|
-
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
774
|
-
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
775
|
-
const saveBackends = deps.saveBackends ?? ((scope, config) => saveBackendConfigScope({}, scope, config));
|
|
776
|
-
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
777
|
-
const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
|
|
778
|
-
const runtime = deps.runtime ?? {
|
|
779
|
-
nodeVersion: process.version,
|
|
780
|
-
platform: process.platform,
|
|
781
|
-
arch: process.arch
|
|
782
|
-
};
|
|
783
|
-
const checkTypebox = deps.checkTypebox ?? defaultCheckTypebox;
|
|
784
|
-
const checkBackends = deps.checkBackends ?? ((config: BackendConfig) => checkBackendHealth(config));
|
|
785
|
-
const getChangelog = deps.getChangelog ?? (() => getLatestChangelogEntry());
|
|
786
|
-
|
|
787
|
-
pi.registerCommand('web-agent', {
|
|
788
|
-
description: 'Open settings or manage pi-web-agent presentation config',
|
|
789
|
-
handler: async (args, ctx) => {
|
|
790
|
-
let [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
791
|
-
|
|
792
|
-
if (!action) {
|
|
793
|
-
const selectedAction = await openActionMenu(ctx);
|
|
794
|
-
if (!selectedAction) return;
|
|
795
|
-
|
|
796
|
-
if (selectedAction === 'reset-project') {
|
|
797
|
-
action = 'reset';
|
|
798
|
-
maybeScope = 'project';
|
|
799
|
-
} else if (selectedAction === 'reset-global') {
|
|
800
|
-
action = 'reset';
|
|
801
|
-
maybeScope = 'global';
|
|
802
|
-
} else {
|
|
803
|
-
action = selectedAction;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (action === 'doctor') {
|
|
808
|
-
const [typeboxOk, browser, loaded] = await Promise.all([checkTypebox(), resolveBrowser(), load()]);
|
|
809
|
-
const backendConfig = loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
810
|
-
const backendIssues = validateBackendConfig(backendConfig);
|
|
811
|
-
const backendHealth = await checkBackends(backendConfig);
|
|
812
|
-
const lines = [
|
|
813
|
-
'pi-web-agent: loaded',
|
|
814
|
-
`runtime: node ${runtime.nodeVersion} ${runtime.platform} ${runtime.arch}`,
|
|
815
|
-
`typebox: ${typeboxOk ? 'ok' : 'missing'}`,
|
|
816
|
-
formatBackendSummary(backendConfig),
|
|
817
|
-
backendIssues.length > 0 ? `backend config: warning\n${backendIssues.join('\n')}` : 'backend config: ok',
|
|
818
|
-
...backendHealth
|
|
819
|
-
];
|
|
820
|
-
|
|
821
|
-
if (browser.ok) {
|
|
822
|
-
lines.push(`browser: ${browser.browser} ${browser.executablePath}`);
|
|
823
|
-
} else {
|
|
824
|
-
lines.push(`browser: missing (${browser.error.code})`);
|
|
825
|
-
lines.push(browser.error.message);
|
|
826
|
-
lines.push('Install Chrome, Chromium, Edge, or Brave and run /web-agent doctor again.');
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
ctx.ui.notify(lines.join('\n'), 'info');
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
if (action === 'show') {
|
|
834
|
-
const loaded = await load();
|
|
835
|
-
ctx.ui.notify(
|
|
836
|
-
[
|
|
837
|
-
formatConfigSummary(loaded.effectiveConfig),
|
|
838
|
-
formatBackendSummary(loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG),
|
|
839
|
-
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
840
|
-
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
841
|
-
].join('\n'),
|
|
842
|
-
'info'
|
|
843
|
-
);
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (action === 'changelog') {
|
|
848
|
-
const changelog = await getChangelog();
|
|
849
|
-
ctx.ui.notify(changelog ?? 'No pi-web-agent changelog entries found.', 'info');
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (action === 'reset') {
|
|
854
|
-
const scope = parseScopeToken(maybeScope) ?? 'project';
|
|
855
|
-
await reset(scope);
|
|
856
|
-
ctx.ui.notify(`Reset ${scope} config`, 'info');
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (action === 'mode') {
|
|
861
|
-
const [, first, second] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
862
|
-
const loaded = await load();
|
|
863
|
-
const scope: PresentationScope = 'project';
|
|
864
|
-
const baseConfig = getScopeDisplayConfig(loaded, scope);
|
|
865
|
-
const inheritedConfig = getInheritedConfigForScope(loaded, scope);
|
|
866
|
-
|
|
867
|
-
if (first && isModeOrInherit(first) && first !== 'inherit') {
|
|
868
|
-
await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, defaultMode: first }, inheritedConfig));
|
|
869
|
-
ctx.ui.notify(`Saved project default mode = ${first}`, 'info');
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
if (first && second && isToolName(first) && isModeOrInherit(second)) {
|
|
874
|
-
const nextTools = { ...baseConfig.tools };
|
|
875
|
-
|
|
876
|
-
if (second === 'inherit') {
|
|
877
|
-
delete nextTools[first];
|
|
878
|
-
} else {
|
|
879
|
-
nextTools[first] = { mode: second };
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
await save(
|
|
883
|
-
scope,
|
|
884
|
-
collapsePresentationConfigToOverride({ ...baseConfig, tools: nextTools }, inheritedConfig)
|
|
885
|
-
);
|
|
886
|
-
ctx.ui.notify(`Saved project ${first} = ${second}`, 'info');
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
ctx.ui.notify(
|
|
891
|
-
'Usage: /web-agent mode <compact|preview|verbose> or /web-agent mode <tool> <inherit|compact|preview|verbose>',
|
|
892
|
-
'info'
|
|
893
|
-
);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (!action || action === 'settings') {
|
|
898
|
-
const loaded = await load();
|
|
899
|
-
const initialScope: PresentationScope = 'project';
|
|
900
|
-
const section = await openSettingsSectionMenu(ctx);
|
|
901
|
-
if (!section) return;
|
|
902
|
-
|
|
903
|
-
const result = section === 'presentation'
|
|
904
|
-
? await openPresentationSettingsUi(ctx, loaded, initialScope)
|
|
905
|
-
: await openBackendSettingsUi(ctx, loaded, initialScope);
|
|
906
|
-
|
|
907
|
-
if (!result || result.action === 'cancel') {
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (result.action === 'reset') {
|
|
912
|
-
await reset(result.scope);
|
|
913
|
-
ctx.ui.notify(`Reset ${result.scope} config`, 'info');
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (section === 'presentation') {
|
|
918
|
-
await save(
|
|
919
|
-
result.scope,
|
|
920
|
-
collapsePresentationConfigToOverride(
|
|
921
|
-
result.config,
|
|
922
|
-
getInheritedConfigForScope(loaded, result.scope)
|
|
923
|
-
)
|
|
924
|
-
);
|
|
925
|
-
ctx.ui.notify(`Saved ${result.scope} presentation config`, 'info');
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
await saveBackends(
|
|
930
|
-
result.scope,
|
|
931
|
-
collapseBackendConfigToOverride(
|
|
932
|
-
result.backends,
|
|
933
|
-
getInheritedBackendsForScope(loaded, result.scope)
|
|
934
|
-
)
|
|
935
|
-
);
|
|
936
|
-
ctx.ui.notify(`Saved ${result.scope} backend config`, 'info');
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
ctx.ui.notify(
|
|
941
|
-
'Use /web-agent, /web-agent show, /web-agent doctor, /web-agent changelog, /web-agent reset project, or /web-agent settings',
|
|
942
|
-
'info'
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_BACKEND_CONFIG,
|
|
3
|
+
mergeBackendConfigLayers,
|
|
4
|
+
validateBackendConfig,
|
|
5
|
+
type BackendConfig,
|
|
6
|
+
type BackendConfigOverride
|
|
7
|
+
} from '../backends/config.js';
|
|
8
|
+
import { checkBackendHealth } from '../backends/doctor.js';
|
|
9
|
+
import {
|
|
10
|
+
DynamicBorder,
|
|
11
|
+
getSettingsListTheme,
|
|
12
|
+
type ExtensionAPI
|
|
13
|
+
} from '@selesai/code';
|
|
14
|
+
import { Container, SelectList, SettingsList, Text, type SelectItem, type SettingItem } from '@earendil-works/pi-tui';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_PRESENTATION_CONFIG,
|
|
17
|
+
mergePresentationConfigLayers,
|
|
18
|
+
resolvePresentationMode
|
|
19
|
+
} from '../presentation/config.js';
|
|
20
|
+
import {
|
|
21
|
+
loadPresentationConfigLayers,
|
|
22
|
+
resetPresentationConfigScope,
|
|
23
|
+
saveBackendConfigScope,
|
|
24
|
+
savePresentationConfigScope,
|
|
25
|
+
type LoadedPresentationConfig
|
|
26
|
+
} from '../presentation/config-store.js';
|
|
27
|
+
import { resolveBrowserExecutable, type BrowserResolutionResult } from '../fetch/browser-resolution.js';
|
|
28
|
+
import { getLatestChangelogEntry } from '../changelog-notice.js';
|
|
29
|
+
import type {
|
|
30
|
+
PresentationConfig,
|
|
31
|
+
PresentationConfigOverride,
|
|
32
|
+
PresentationScope,
|
|
33
|
+
PresentationToolName
|
|
34
|
+
} from '../presentation/types.js';
|
|
35
|
+
|
|
36
|
+
type CommandDeps = {
|
|
37
|
+
load?: () => ReturnType<typeof loadPresentationConfigLayers>;
|
|
38
|
+
save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
|
|
39
|
+
saveBackends?: (scope: PresentationScope, config: BackendConfigOverride) => Promise<void>;
|
|
40
|
+
reset?: (scope: PresentationScope) => Promise<void>;
|
|
41
|
+
resolveBrowser?: () => Promise<BrowserResolutionResult>;
|
|
42
|
+
runtime?: { nodeVersion: string; platform: string; arch: string };
|
|
43
|
+
checkTypebox?: () => Promise<boolean>;
|
|
44
|
+
checkBackends?: (config: BackendConfig) => Promise<string[]>;
|
|
45
|
+
getChangelog?: () => Promise<string | undefined>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type SettingsUiResult =
|
|
49
|
+
| { action: 'cancel' }
|
|
50
|
+
| { action: 'reset'; scope: PresentationScope }
|
|
51
|
+
| { action: 'save'; scope: PresentationScope; config: PresentationConfig; backends: BackendConfig };
|
|
52
|
+
|
|
53
|
+
type WebAgentAction = 'settings' | 'show' | 'doctor' | 'changelog' | 'reset-project' | 'reset-global';
|
|
54
|
+
type SettingsSection = 'presentation' | 'backends';
|
|
55
|
+
|
|
56
|
+
export type SettingsDraftState = {
|
|
57
|
+
scope: PresentationScope;
|
|
58
|
+
drafts: Record<PresentationScope, PresentationConfig>;
|
|
59
|
+
backendDrafts: Record<PresentationScope, BackendConfig>;
|
|
60
|
+
config: PresentationConfig;
|
|
61
|
+
backends: BackendConfig;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const PRESENTATION_TOOL_NAMES: PresentationToolName[] = ['web_explore'];
|
|
65
|
+
|
|
66
|
+
function parseScopeToken(token: string | undefined): PresentationScope | undefined {
|
|
67
|
+
return token === 'global' || token === 'project' ? token : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function clonePresentationConfig(config: PresentationConfig): PresentationConfig {
|
|
71
|
+
return {
|
|
72
|
+
defaultMode: config.defaultMode,
|
|
73
|
+
tools: { ...config.tools }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cloneBackendConfig(config: BackendConfig): BackendConfig {
|
|
78
|
+
return {
|
|
79
|
+
search: {
|
|
80
|
+
...config.search,
|
|
81
|
+
options: config.search.options ? { ...config.search.options } : undefined
|
|
82
|
+
},
|
|
83
|
+
fetch: {
|
|
84
|
+
...config.fetch,
|
|
85
|
+
options: config.fetch.options ? { ...config.fetch.options } : undefined
|
|
86
|
+
},
|
|
87
|
+
headless: { ...config.headless }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sameJson(left: unknown, right: unknown): boolean {
|
|
92
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateBackendUrl(value: string): { ok: true; value: string } | { ok: false; message: string } {
|
|
96
|
+
try {
|
|
97
|
+
const url = new URL(value.trim());
|
|
98
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
99
|
+
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ok: true, value: url.toString().replace(/\/$/, '') };
|
|
103
|
+
} catch {
|
|
104
|
+
return { ok: false, message: 'Invalid URL. Include http:// or https://.' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function defaultCheckTypebox(): Promise<boolean> {
|
|
109
|
+
try {
|
|
110
|
+
await import('typebox');
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatSearchOptions(config: BackendConfig['search']) {
|
|
118
|
+
return [
|
|
119
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
120
|
+
config.options?.categories?.length ? `categories ${config.options.categories.join(',')}` : undefined,
|
|
121
|
+
config.options?.language ? `language ${config.options.language}` : undefined,
|
|
122
|
+
config.options?.safesearch !== undefined ? `safesearch ${config.options.safesearch}` : undefined
|
|
123
|
+
].filter(Boolean).join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatFetchOptions(config: BackendConfig['fetch']) {
|
|
127
|
+
return [
|
|
128
|
+
config.fallback ? `fallback ${config.fallback}` : undefined,
|
|
129
|
+
config.options?.formats?.length ? `formats ${config.options.formats.join(',')}` : undefined,
|
|
130
|
+
config.options?.onlyMainContent !== undefined ? `onlyMainContent ${config.options.onlyMainContent}` : undefined
|
|
131
|
+
].filter(Boolean).join(' ');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatBackendSummary(config: BackendConfig = DEFAULT_BACKEND_CONFIG) {
|
|
135
|
+
const searchSuffix = formatSearchOptions(config.search);
|
|
136
|
+
const fetchSuffix = formatFetchOptions(config.fetch);
|
|
137
|
+
const searchBase = config.search.baseUrl
|
|
138
|
+
? `search: ${config.search.provider} (${config.search.baseUrl})`
|
|
139
|
+
: `search: ${config.search.provider}`;
|
|
140
|
+
const fetchBase = config.fetch.baseUrl
|
|
141
|
+
? `fetch: ${config.fetch.provider} (${config.fetch.baseUrl})`
|
|
142
|
+
: `fetch: ${config.fetch.provider}`;
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
searchSuffix ? `${searchBase} ${searchSuffix}` : searchBase,
|
|
146
|
+
fetchSuffix ? `${fetchBase} ${fetchSuffix}` : fetchBase,
|
|
147
|
+
`headless: ${config.headless.provider}`
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatConfigSummary(config: PresentationConfig) {
|
|
152
|
+
const lines = [`defaultMode: ${config.defaultMode}`];
|
|
153
|
+
|
|
154
|
+
for (const toolName of PRESENTATION_TOOL_NAMES) {
|
|
155
|
+
lines.push(`${toolName}: ${config.tools[toolName]?.mode ?? 'inherit'}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildPresentationSettingsItems(scope: PresentationScope, config: PresentationConfig): SettingItem[] {
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
id: 'scope',
|
|
165
|
+
label: 'Write scope',
|
|
166
|
+
currentValue: scope,
|
|
167
|
+
values: ['project', 'global']
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'defaultMode',
|
|
171
|
+
label: 'Default mode',
|
|
172
|
+
currentValue: config.defaultMode,
|
|
173
|
+
values: ['compact', 'preview', 'verbose']
|
|
174
|
+
},
|
|
175
|
+
...PRESENTATION_TOOL_NAMES.map((toolName) => ({
|
|
176
|
+
id: `tool:${toolName}`,
|
|
177
|
+
label: toolName,
|
|
178
|
+
currentValue: config.tools[toolName]?.mode ?? 'inherit',
|
|
179
|
+
values: ['inherit', 'compact', 'preview', 'verbose']
|
|
180
|
+
}))
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildBackendSettingsItems(scope: PresentationScope, backends: BackendConfig): SettingItem[] {
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
id: 'scope',
|
|
188
|
+
label: 'Write scope',
|
|
189
|
+
currentValue: scope,
|
|
190
|
+
values: ['project', 'global']
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'backend:search:provider',
|
|
194
|
+
label: 'Search backend',
|
|
195
|
+
currentValue: backends.search.provider,
|
|
196
|
+
values: ['duckduckgo', 'searxng', 'brave']
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'backend:search:baseUrl',
|
|
200
|
+
label: 'SearXNG URL',
|
|
201
|
+
currentValue: backends.search.baseUrl ?? 'not set',
|
|
202
|
+
values: ['edit']
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'backend:search:fallback',
|
|
206
|
+
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']
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'backend:secret:brave',
|
|
212
|
+
label: 'Brave API key',
|
|
213
|
+
currentValue: 'env var',
|
|
214
|
+
values: ['env var']
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'backend:fetch:provider',
|
|
218
|
+
label: 'Fetch backend',
|
|
219
|
+
currentValue: backends.fetch.provider,
|
|
220
|
+
values: ['http', 'firecrawl']
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'backend:fetch:baseUrl',
|
|
224
|
+
label: 'Firecrawl URL',
|
|
225
|
+
currentValue: backends.fetch.baseUrl ?? 'not set',
|
|
226
|
+
values: ['edit']
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'backend:fetch:fallback',
|
|
230
|
+
label: 'Firecrawl fallback',
|
|
231
|
+
currentValue: backends.fetch.provider === 'firecrawl' ? backends.fetch.fallback ?? 'off' : 'off',
|
|
232
|
+
values: backends.fetch.provider === 'firecrawl' ? ['off', 'http'] : ['off']
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'backend:secret:firecrawl',
|
|
236
|
+
label: 'Firecrawl API key',
|
|
237
|
+
currentValue: 'env var',
|
|
238
|
+
values: ['env var']
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isToolName(value: string): value is PresentationToolName {
|
|
244
|
+
return PRESENTATION_TOOL_NAMES.includes(value as PresentationToolName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isModeOrInherit(value: string): value is 'inherit' | 'compact' | 'preview' | 'verbose' {
|
|
248
|
+
return ['inherit', 'compact', 'preview', 'verbose'].includes(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function getInheritedConfigForScope(
|
|
252
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
253
|
+
scope: PresentationScope
|
|
254
|
+
): PresentationConfig {
|
|
255
|
+
if (scope === 'global') {
|
|
256
|
+
return DEFAULT_PRESENTATION_CONFIG;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getScopeDisplayConfig(
|
|
263
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
264
|
+
scope: PresentationScope
|
|
265
|
+
): PresentationConfig {
|
|
266
|
+
if (scope === 'global') {
|
|
267
|
+
return mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, loaded.global.rawConfig);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return mergePresentationConfigLayers(
|
|
271
|
+
DEFAULT_PRESENTATION_CONFIG,
|
|
272
|
+
loaded.global.rawConfig,
|
|
273
|
+
loaded.project.rawConfig
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getInheritedBackendsForScope(
|
|
278
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
279
|
+
scope: PresentationScope
|
|
280
|
+
): BackendConfig {
|
|
281
|
+
if (scope === 'global') {
|
|
282
|
+
return DEFAULT_BACKEND_CONFIG;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function getScopeDisplayBackends(
|
|
289
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
290
|
+
scope: PresentationScope
|
|
291
|
+
): BackendConfig {
|
|
292
|
+
if (scope === 'global') {
|
|
293
|
+
return mergeBackendConfigLayers(DEFAULT_BACKEND_CONFIG, loaded.global.rawBackends);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return mergeBackendConfigLayers(
|
|
297
|
+
DEFAULT_BACKEND_CONFIG,
|
|
298
|
+
loaded.global.rawBackends,
|
|
299
|
+
loaded.project.rawBackends
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function createSettingsDraftState(
|
|
304
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
305
|
+
initialScope: PresentationScope
|
|
306
|
+
): SettingsDraftState {
|
|
307
|
+
const drafts = {
|
|
308
|
+
global: getScopeDisplayConfig(loaded, 'global'),
|
|
309
|
+
project: getScopeDisplayConfig(loaded, 'project')
|
|
310
|
+
};
|
|
311
|
+
const backendDrafts = {
|
|
312
|
+
global: getScopeDisplayBackends(loaded, 'global'),
|
|
313
|
+
project: getScopeDisplayBackends(loaded, 'project')
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
scope: initialScope,
|
|
318
|
+
drafts,
|
|
319
|
+
backendDrafts,
|
|
320
|
+
config: clonePresentationConfig(drafts[initialScope]),
|
|
321
|
+
backends: cloneBackendConfig(backendDrafts[initialScope])
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function applySettingsValue(
|
|
326
|
+
state: SettingsDraftState,
|
|
327
|
+
id: string,
|
|
328
|
+
newValue: string
|
|
329
|
+
): SettingsDraftState {
|
|
330
|
+
const nextDrafts = {
|
|
331
|
+
global: clonePresentationConfig(state.drafts.global),
|
|
332
|
+
project: clonePresentationConfig(state.drafts.project)
|
|
333
|
+
};
|
|
334
|
+
const nextBackendDrafts = {
|
|
335
|
+
global: cloneBackendConfig(state.backendDrafts.global),
|
|
336
|
+
project: cloneBackendConfig(state.backendDrafts.project)
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
let nextScope = state.scope;
|
|
340
|
+
|
|
341
|
+
if (id === 'scope' && (newValue === 'project' || newValue === 'global')) {
|
|
342
|
+
nextScope = newValue;
|
|
343
|
+
return {
|
|
344
|
+
scope: nextScope,
|
|
345
|
+
drafts: nextDrafts,
|
|
346
|
+
backendDrafts: nextBackendDrafts,
|
|
347
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
348
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const currentDraft = clonePresentationConfig(nextDrafts[nextScope]);
|
|
353
|
+
const currentBackends = cloneBackendConfig(nextBackendDrafts[nextScope]);
|
|
354
|
+
|
|
355
|
+
if (id === 'defaultMode' && (newValue === 'compact' || newValue === 'preview' || newValue === 'verbose')) {
|
|
356
|
+
currentDraft.defaultMode = newValue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (id.startsWith('tool:')) {
|
|
360
|
+
const toolName = id.slice('tool:'.length) as PresentationToolName;
|
|
361
|
+
const nextTools = { ...currentDraft.tools };
|
|
362
|
+
|
|
363
|
+
if (newValue === 'inherit') {
|
|
364
|
+
delete nextTools[toolName];
|
|
365
|
+
} else if (
|
|
366
|
+
newValue === 'compact' ||
|
|
367
|
+
newValue === 'preview' ||
|
|
368
|
+
newValue === 'verbose'
|
|
369
|
+
) {
|
|
370
|
+
nextTools[toolName] = { mode: newValue };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
currentDraft.tools = nextTools;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave')) {
|
|
377
|
+
currentBackends.search.provider = newValue;
|
|
378
|
+
if (newValue !== 'searxng') {
|
|
379
|
+
delete currentBackends.search.baseUrl;
|
|
380
|
+
delete currentBackends.search.options;
|
|
381
|
+
}
|
|
382
|
+
if (newValue === 'duckduckgo') {
|
|
383
|
+
delete currentBackends.search.fallback;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (id === 'backend:search:fallback') {
|
|
388
|
+
if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave')) {
|
|
389
|
+
currentBackends.search.fallback = 'duckduckgo';
|
|
390
|
+
} else {
|
|
391
|
+
delete currentBackends.search.fallback;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (id === 'backend:search:baseUrl') {
|
|
396
|
+
if (currentBackends.search.provider === 'searxng' && newValue.trim()) {
|
|
397
|
+
currentBackends.search.baseUrl = newValue.trim();
|
|
398
|
+
} else {
|
|
399
|
+
delete currentBackends.search.baseUrl;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (id === 'backend:fetch:provider' && (newValue === 'http' || newValue === 'firecrawl')) {
|
|
404
|
+
currentBackends.fetch.provider = newValue;
|
|
405
|
+
if (newValue === 'http') {
|
|
406
|
+
delete currentBackends.fetch.fallback;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (id === 'backend:fetch:fallback') {
|
|
411
|
+
if (newValue === 'http' && currentBackends.fetch.provider === 'firecrawl') {
|
|
412
|
+
currentBackends.fetch.fallback = 'http';
|
|
413
|
+
} else if (newValue === 'off' || currentBackends.fetch.provider !== 'firecrawl') {
|
|
414
|
+
delete currentBackends.fetch.fallback;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (id === 'backend:fetch:baseUrl') {
|
|
419
|
+
if (newValue.trim()) {
|
|
420
|
+
currentBackends.fetch.baseUrl = newValue.trim();
|
|
421
|
+
} else {
|
|
422
|
+
delete currentBackends.fetch.baseUrl;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
nextDrafts[nextScope] = currentDraft;
|
|
427
|
+
nextBackendDrafts[nextScope] = currentBackends;
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
scope: nextScope,
|
|
431
|
+
drafts: nextDrafts,
|
|
432
|
+
backendDrafts: nextBackendDrafts,
|
|
433
|
+
config: clonePresentationConfig(nextDrafts[nextScope]),
|
|
434
|
+
backends: cloneBackendConfig(nextBackendDrafts[nextScope])
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function collapsePresentationConfigToOverride(
|
|
439
|
+
config: PresentationConfig,
|
|
440
|
+
inheritedConfig: PresentationConfig
|
|
441
|
+
): PresentationConfigOverride {
|
|
442
|
+
const tools = Object.fromEntries(
|
|
443
|
+
PRESENTATION_TOOL_NAMES.flatMap((toolName) => {
|
|
444
|
+
const configuredMode = config.tools[toolName]?.mode;
|
|
445
|
+
if (!configuredMode) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const inheritedMode = resolvePresentationMode(toolName, inheritedConfig);
|
|
450
|
+
if (configuredMode === inheritedMode) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return [[toolName, { mode: configuredMode }]];
|
|
455
|
+
})
|
|
456
|
+
) as PresentationConfigOverride['tools'];
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
defaultMode:
|
|
460
|
+
config.defaultMode === inheritedConfig.defaultMode ? undefined : config.defaultMode,
|
|
461
|
+
tools
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function collapseBackendConfigToOverride(
|
|
466
|
+
config: BackendConfig,
|
|
467
|
+
inheritedConfig: BackendConfig
|
|
468
|
+
): BackendConfigOverride {
|
|
469
|
+
const override: BackendConfigOverride = {};
|
|
470
|
+
|
|
471
|
+
if (!sameJson(config.search, inheritedConfig.search)) {
|
|
472
|
+
override.search = config.search.provider !== inheritedConfig.search.provider
|
|
473
|
+
? { ...config.search }
|
|
474
|
+
: {
|
|
475
|
+
...(config.search.baseUrl !== inheritedConfig.search.baseUrl ? { baseUrl: config.search.baseUrl } : {}),
|
|
476
|
+
...(config.search.fallback !== inheritedConfig.search.fallback ? { fallback: config.search.fallback } : {}),
|
|
477
|
+
...(!sameJson(config.search.options, inheritedConfig.search.options) ? { options: config.search.options } : {})
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
if (config.search.provider !== inheritedConfig.search.provider) {
|
|
481
|
+
override.search.provider = config.search.provider;
|
|
482
|
+
} else if (Object.keys(override.search).length === 0) {
|
|
483
|
+
delete override.search;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!sameJson(config.fetch, inheritedConfig.fetch)) {
|
|
488
|
+
override.fetch = config.fetch.provider !== inheritedConfig.fetch.provider
|
|
489
|
+
? { ...config.fetch, apiKey: undefined }
|
|
490
|
+
: {
|
|
491
|
+
...(config.fetch.baseUrl !== inheritedConfig.fetch.baseUrl ? { baseUrl: config.fetch.baseUrl } : {}),
|
|
492
|
+
...(config.fetch.fallback !== inheritedConfig.fetch.fallback ? { fallback: config.fetch.fallback } : {}),
|
|
493
|
+
...(!sameJson(config.fetch.options, inheritedConfig.fetch.options) ? { options: config.fetch.options } : {})
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
delete override.fetch.apiKey;
|
|
497
|
+
|
|
498
|
+
if (config.fetch.provider !== inheritedConfig.fetch.provider) {
|
|
499
|
+
override.fetch.provider = config.fetch.provider;
|
|
500
|
+
} else if (Object.keys(override.fetch).length === 0) {
|
|
501
|
+
delete override.fetch;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!sameJson(config.headless, inheritedConfig.headless)) {
|
|
506
|
+
override.headless = { ...config.headless };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return override;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function handleSettingsShortcut(data: string): { action: 'cancel' | 'reset' | 'save' } | undefined {
|
|
513
|
+
if (data === '\u001b') {
|
|
514
|
+
return { action: 'cancel' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (data === '\u0012') {
|
|
518
|
+
return { action: 'reset' };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (data === '\u0013') {
|
|
522
|
+
return { action: 'save' };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function openActionMenu(ctx: any): Promise<WebAgentAction | undefined> {
|
|
529
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: WebAgentAction | undefined) => void) => {
|
|
530
|
+
const container = new Container();
|
|
531
|
+
const items: SelectItem[] = [
|
|
532
|
+
{ value: 'settings', label: 'Settings', description: 'Edit presentation modes and backends' },
|
|
533
|
+
{ value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
|
|
534
|
+
{ value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
|
|
535
|
+
{ value: 'changelog', label: 'Changelog', description: 'Show latest package changelog' },
|
|
536
|
+
{ value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
|
|
537
|
+
{ value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
541
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent')), 1, 0));
|
|
542
|
+
|
|
543
|
+
const list = new SelectList(items, Math.min(items.length, 8), {
|
|
544
|
+
selectedPrefix: (text: string) => theme.fg('accent', text),
|
|
545
|
+
selectedText: (text: string) => theme.fg('accent', text),
|
|
546
|
+
description: (text: string) => theme.fg('muted', text),
|
|
547
|
+
scrollInfo: (text: string) => theme.fg('dim', text),
|
|
548
|
+
noMatch: (text: string) => theme.fg('warning', text)
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
list.onSelect = (item) => done(item.value as WebAgentAction);
|
|
552
|
+
list.onCancel = () => done(undefined);
|
|
553
|
+
container.addChild(list);
|
|
554
|
+
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
555
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
render: (width: number) => container.render(width),
|
|
559
|
+
invalidate: () => container.invalidate(),
|
|
560
|
+
handleInput: (data: string) => {
|
|
561
|
+
list.handleInput?.(data);
|
|
562
|
+
tui.requestRender?.();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function openSettingsSectionMenu(ctx: any): Promise<SettingsSection | undefined> {
|
|
569
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: SettingsSection | undefined) => void) => {
|
|
570
|
+
const container = new Container();
|
|
571
|
+
const items: SelectItem[] = [
|
|
572
|
+
{ value: 'presentation', label: 'Presentation', description: 'Compact, preview, and verbose output modes' },
|
|
573
|
+
{ value: 'backends', label: 'Backends', description: 'Search/fetch providers, URLs, and fallbacks' }
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
577
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent settings')), 1, 0));
|
|
578
|
+
|
|
579
|
+
const list = new SelectList(items, items.length, {
|
|
580
|
+
selectedPrefix: (text: string) => theme.fg('accent', text),
|
|
581
|
+
selectedText: (text: string) => theme.fg('accent', text),
|
|
582
|
+
description: (text: string) => theme.fg('muted', text),
|
|
583
|
+
scrollInfo: (text: string) => theme.fg('dim', text),
|
|
584
|
+
noMatch: (text: string) => theme.fg('warning', text)
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
list.onSelect = (item) => done(item.value as SettingsSection);
|
|
588
|
+
list.onCancel = () => done(undefined);
|
|
589
|
+
container.addChild(list);
|
|
590
|
+
container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
|
|
591
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg('accent', text)));
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
render: (width: number) => container.render(width),
|
|
595
|
+
invalidate: () => container.invalidate(),
|
|
596
|
+
handleInput: (data: string) => {
|
|
597
|
+
list.handleInput?.(data);
|
|
598
|
+
tui.requestRender?.();
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function openPresentationSettingsUi(
|
|
605
|
+
ctx: any,
|
|
606
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
607
|
+
initialScope: PresentationScope
|
|
608
|
+
): Promise<SettingsUiResult | undefined> {
|
|
609
|
+
return ctx.ui.custom((_tui: unknown, theme: any, _kb: unknown, done: (value: SettingsUiResult) => void) => {
|
|
610
|
+
let state = createSettingsDraftState(loaded, initialScope);
|
|
611
|
+
let settingsList: SettingsList;
|
|
612
|
+
|
|
613
|
+
const container = new Container();
|
|
614
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · presentation')), 1, 1));
|
|
615
|
+
container.addChild(
|
|
616
|
+
new Text(
|
|
617
|
+
theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel'),
|
|
618
|
+
1,
|
|
619
|
+
2
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const rebuildSettingsList = () => {
|
|
624
|
+
if (settingsList) {
|
|
625
|
+
container.removeChild(settingsList);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
settingsList = new SettingsList(
|
|
629
|
+
buildPresentationSettingsItems(state.scope, state.config),
|
|
630
|
+
Math.min(PRESENTATION_TOOL_NAMES.length + 8, 18),
|
|
631
|
+
getSettingsListTheme(),
|
|
632
|
+
(id, newValue) => {
|
|
633
|
+
state = applySettingsValue(state, id, newValue);
|
|
634
|
+
rebuildSettingsList();
|
|
635
|
+
container.invalidate();
|
|
636
|
+
},
|
|
637
|
+
() => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }),
|
|
638
|
+
{ enableSearch: true }
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
container.addChild(settingsList);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
rebuildSettingsList();
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
render: (width: number) => container.render(width),
|
|
648
|
+
invalidate: () => container.invalidate(),
|
|
649
|
+
handleInput: (data: string) => {
|
|
650
|
+
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
651
|
+
|
|
652
|
+
if (shortcut?.action === 'cancel') {
|
|
653
|
+
done({ action: 'cancel' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (shortcut?.action === 'reset') {
|
|
658
|
+
done({ action: 'reset', scope: state.scope });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (shortcut?.action === 'save') {
|
|
663
|
+
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
settingsList.handleInput?.(data);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function openBackendSettingsUi(
|
|
674
|
+
ctx: any,
|
|
675
|
+
loaded: Awaited<LoadedPresentationConfig>,
|
|
676
|
+
initialScope: PresentationScope
|
|
677
|
+
): Promise<SettingsUiResult | undefined> {
|
|
678
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: unknown, done: (value: SettingsUiResult) => void) => {
|
|
679
|
+
let state = createSettingsDraftState(loaded, initialScope);
|
|
680
|
+
let settingsList: SettingsList;
|
|
681
|
+
|
|
682
|
+
const container = new Container();
|
|
683
|
+
container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
|
|
684
|
+
container.addChild(
|
|
685
|
+
new Text(
|
|
686
|
+
theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel · API keys stay in env vars'),
|
|
687
|
+
1,
|
|
688
|
+
2
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
|
|
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
|
+
const rebuildSettingsList = () => {
|
|
718
|
+
if (settingsList) {
|
|
719
|
+
container.removeChild(settingsList);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
settingsList = new SettingsList(
|
|
723
|
+
buildBackendSettingsItems(state.scope, state.backends),
|
|
724
|
+
12,
|
|
725
|
+
getSettingsListTheme(),
|
|
726
|
+
(id, newValue) => {
|
|
727
|
+
if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
|
|
728
|
+
void editUrl(id);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
state = applySettingsValue(state, id, newValue);
|
|
733
|
+
rebuildSettingsList();
|
|
734
|
+
container.invalidate();
|
|
735
|
+
},
|
|
736
|
+
() => done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends }),
|
|
737
|
+
{ enableSearch: true }
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
container.addChild(settingsList);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
rebuildSettingsList();
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
render: (width: number) => container.render(width),
|
|
747
|
+
invalidate: () => container.invalidate(),
|
|
748
|
+
handleInput: (data: string) => {
|
|
749
|
+
const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
|
|
750
|
+
|
|
751
|
+
if (shortcut?.action === 'cancel') {
|
|
752
|
+
done({ action: 'cancel' });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (shortcut?.action === 'reset') {
|
|
757
|
+
done({ action: 'reset', scope: state.scope });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (shortcut?.action === 'save') {
|
|
762
|
+
done({ action: 'save', scope: state.scope, config: state.config, backends: state.backends });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
settingsList.handleInput?.(data);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function registerWebAgentConfigCommands(pi: ExtensionAPI, deps: CommandDeps = {}) {
|
|
773
|
+
const load = deps.load ?? (() => loadPresentationConfigLayers());
|
|
774
|
+
const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
|
|
775
|
+
const saveBackends = deps.saveBackends ?? ((scope, config) => saveBackendConfigScope({}, scope, config));
|
|
776
|
+
const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
|
|
777
|
+
const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
|
|
778
|
+
const runtime = deps.runtime ?? {
|
|
779
|
+
nodeVersion: process.version,
|
|
780
|
+
platform: process.platform,
|
|
781
|
+
arch: process.arch
|
|
782
|
+
};
|
|
783
|
+
const checkTypebox = deps.checkTypebox ?? defaultCheckTypebox;
|
|
784
|
+
const checkBackends = deps.checkBackends ?? ((config: BackendConfig) => checkBackendHealth(config));
|
|
785
|
+
const getChangelog = deps.getChangelog ?? (() => getLatestChangelogEntry());
|
|
786
|
+
|
|
787
|
+
pi.registerCommand('web-agent', {
|
|
788
|
+
description: 'Open settings or manage pi-web-agent presentation config',
|
|
789
|
+
handler: async (args, ctx) => {
|
|
790
|
+
let [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
791
|
+
|
|
792
|
+
if (!action) {
|
|
793
|
+
const selectedAction = await openActionMenu(ctx);
|
|
794
|
+
if (!selectedAction) return;
|
|
795
|
+
|
|
796
|
+
if (selectedAction === 'reset-project') {
|
|
797
|
+
action = 'reset';
|
|
798
|
+
maybeScope = 'project';
|
|
799
|
+
} else if (selectedAction === 'reset-global') {
|
|
800
|
+
action = 'reset';
|
|
801
|
+
maybeScope = 'global';
|
|
802
|
+
} else {
|
|
803
|
+
action = selectedAction;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (action === 'doctor') {
|
|
808
|
+
const [typeboxOk, browser, loaded] = await Promise.all([checkTypebox(), resolveBrowser(), load()]);
|
|
809
|
+
const backendConfig = loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG;
|
|
810
|
+
const backendIssues = validateBackendConfig(backendConfig);
|
|
811
|
+
const backendHealth = await checkBackends(backendConfig);
|
|
812
|
+
const lines = [
|
|
813
|
+
'pi-web-agent: loaded',
|
|
814
|
+
`runtime: node ${runtime.nodeVersion} ${runtime.platform} ${runtime.arch}`,
|
|
815
|
+
`typebox: ${typeboxOk ? 'ok' : 'missing'}`,
|
|
816
|
+
formatBackendSummary(backendConfig),
|
|
817
|
+
backendIssues.length > 0 ? `backend config: warning\n${backendIssues.join('\n')}` : 'backend config: ok',
|
|
818
|
+
...backendHealth
|
|
819
|
+
];
|
|
820
|
+
|
|
821
|
+
if (browser.ok) {
|
|
822
|
+
lines.push(`browser: ${browser.browser} ${browser.executablePath}`);
|
|
823
|
+
} else {
|
|
824
|
+
lines.push(`browser: missing (${browser.error.code})`);
|
|
825
|
+
lines.push(browser.error.message);
|
|
826
|
+
lines.push('Install Chrome, Chromium, Edge, or Brave and run /web-agent doctor again.');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
ctx.ui.notify(lines.join('\n'), 'info');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (action === 'show') {
|
|
834
|
+
const loaded = await load();
|
|
835
|
+
ctx.ui.notify(
|
|
836
|
+
[
|
|
837
|
+
formatConfigSummary(loaded.effectiveConfig),
|
|
838
|
+
formatBackendSummary(loaded.effectiveBackends ?? DEFAULT_BACKEND_CONFIG),
|
|
839
|
+
`global: ${loaded.global.path}${loaded.global.exists ? '' : ' (missing)'}`,
|
|
840
|
+
`project: ${loaded.project.path}${loaded.project.exists ? '' : ' (missing)'}`
|
|
841
|
+
].join('\n'),
|
|
842
|
+
'info'
|
|
843
|
+
);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (action === 'changelog') {
|
|
848
|
+
const changelog = await getChangelog();
|
|
849
|
+
ctx.ui.notify(changelog ?? 'No pi-web-agent changelog entries found.', 'info');
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (action === 'reset') {
|
|
854
|
+
const scope = parseScopeToken(maybeScope) ?? 'project';
|
|
855
|
+
await reset(scope);
|
|
856
|
+
ctx.ui.notify(`Reset ${scope} config`, 'info');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (action === 'mode') {
|
|
861
|
+
const [, first, second] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
862
|
+
const loaded = await load();
|
|
863
|
+
const scope: PresentationScope = 'project';
|
|
864
|
+
const baseConfig = getScopeDisplayConfig(loaded, scope);
|
|
865
|
+
const inheritedConfig = getInheritedConfigForScope(loaded, scope);
|
|
866
|
+
|
|
867
|
+
if (first && isModeOrInherit(first) && first !== 'inherit') {
|
|
868
|
+
await save(scope, collapsePresentationConfigToOverride({ ...baseConfig, defaultMode: first }, inheritedConfig));
|
|
869
|
+
ctx.ui.notify(`Saved project default mode = ${first}`, 'info');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (first && second && isToolName(first) && isModeOrInherit(second)) {
|
|
874
|
+
const nextTools = { ...baseConfig.tools };
|
|
875
|
+
|
|
876
|
+
if (second === 'inherit') {
|
|
877
|
+
delete nextTools[first];
|
|
878
|
+
} else {
|
|
879
|
+
nextTools[first] = { mode: second };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
await save(
|
|
883
|
+
scope,
|
|
884
|
+
collapsePresentationConfigToOverride({ ...baseConfig, tools: nextTools }, inheritedConfig)
|
|
885
|
+
);
|
|
886
|
+
ctx.ui.notify(`Saved project ${first} = ${second}`, 'info');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
ctx.ui.notify(
|
|
891
|
+
'Usage: /web-agent mode <compact|preview|verbose> or /web-agent mode <tool> <inherit|compact|preview|verbose>',
|
|
892
|
+
'info'
|
|
893
|
+
);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!action || action === 'settings') {
|
|
898
|
+
const loaded = await load();
|
|
899
|
+
const initialScope: PresentationScope = 'project';
|
|
900
|
+
const section = await openSettingsSectionMenu(ctx);
|
|
901
|
+
if (!section) return;
|
|
902
|
+
|
|
903
|
+
const result = section === 'presentation'
|
|
904
|
+
? await openPresentationSettingsUi(ctx, loaded, initialScope)
|
|
905
|
+
: await openBackendSettingsUi(ctx, loaded, initialScope);
|
|
906
|
+
|
|
907
|
+
if (!result || result.action === 'cancel') {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (result.action === 'reset') {
|
|
912
|
+
await reset(result.scope);
|
|
913
|
+
ctx.ui.notify(`Reset ${result.scope} config`, 'info');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (section === 'presentation') {
|
|
918
|
+
await save(
|
|
919
|
+
result.scope,
|
|
920
|
+
collapsePresentationConfigToOverride(
|
|
921
|
+
result.config,
|
|
922
|
+
getInheritedConfigForScope(loaded, result.scope)
|
|
923
|
+
)
|
|
924
|
+
);
|
|
925
|
+
ctx.ui.notify(`Saved ${result.scope} presentation config`, 'info');
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
await saveBackends(
|
|
930
|
+
result.scope,
|
|
931
|
+
collapseBackendConfigToOverride(
|
|
932
|
+
result.backends,
|
|
933
|
+
getInheritedBackendsForScope(loaded, result.scope)
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
ctx.ui.notify(`Saved ${result.scope} backend config`, 'info');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
ctx.ui.notify(
|
|
941
|
+
'Use /web-agent, /web-agent show, /web-agent doctor, /web-agent changelog, /web-agent reset project, or /web-agent settings',
|
|
942
|
+
'info'
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
}
|