@visual-z/xeno 0.8.4 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom endpoints and connected-provider management, reached from `/provider`.
|
|
3
|
-
*
|
|
4
|
-
* Flow (magpie's add form + OpenCode's /connect, see
|
|
5
|
-
* docs/tasks/feat-provider-connect.md): choose a preset or a custom endpoint
|
|
6
|
-
* → base URL and id (custom only) → API key (masked; `$VAR` keeps it in the
|
|
7
|
-
* environment) → list the provider's own models with that key → check the
|
|
8
|
-
* ones to enable → save → offer to switch.
|
|
9
|
-
*
|
|
10
|
-
* Every write goes through the files the rest of xeno already reads:
|
|
11
|
-
* `auth.json` for keys and `models.json` `providers.<id>` for endpoints and
|
|
12
|
-
* enabled models. After saving, the catalog is reloaded through the same
|
|
13
|
-
* entry `/reload` uses, so a connected provider behaves exactly like a
|
|
14
|
-
* hand-configured one.
|
|
15
|
-
*/
|
|
16
|
-
import { modelKey } from "../core/session-position.ts";
|
|
17
|
-
import { loadCatalog } from "../runtime/catalog.ts";
|
|
18
|
-
import type { ModelSpec } from "../runtime/model.ts";
|
|
19
|
-
import { discoverModels, discoveryFamily, likelyChatModel, normalizeBaseUrl, type DiscoveredModel } from "../runtime/model-discovery.ts";
|
|
20
|
-
import { SOURCE_PROVIDERS } from "../runtime/model-data.ts";
|
|
21
|
-
import { applyConnect, applyDisconnect, catalogTwin } from "../runtime/provider-connect.ts";
|
|
22
|
-
import { CUSTOM_PRESETS, presetById, slugProviderId, suggestProviderId, validProviderId, type ProviderPreset } from "../runtime/provider-presets.ts";
|
|
23
|
-
import { providerAccess } from "../runtime/provider-access.ts";
|
|
24
|
-
import { writeAuthFile } from "../store/auth-store.ts";
|
|
25
|
-
import { authPath } from "../store/config.ts";
|
|
26
|
-
import { modelsPath, readModelSources, readModelsText, writeModelsFile } from "../store/models.ts";
|
|
27
|
-
import { publishModelCatalog } from "./model-catalog.ts";
|
|
28
|
-
import type { App } from "./startup.ts";
|
|
29
|
-
import type { DialogHost } from "../tui/ui/dialog.ts";
|
|
30
|
-
import type { ListItem } from "../tui/ui/list-model.ts";
|
|
31
|
-
type SelectItem = ListItem;
|
|
32
|
-
|
|
33
|
-
/** Lists at or below this size are preselected whole; larger ones keep only known models. magpie uses 24. */
|
|
34
|
-
const PRESELECT_ALL = 24;
|
|
35
|
-
|
|
36
|
-
export interface ConnectHost {
|
|
37
|
-
app: App;
|
|
38
|
-
wizard: Pick<DialogHost, "push" | "reset" | "pop" | "top" | "depth">;
|
|
39
|
-
/** Unmount the wizard. */
|
|
40
|
-
close(): void;
|
|
41
|
-
switchTo(spec: ModelSpec): Promise<void>;
|
|
42
|
-
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface Draft {
|
|
46
|
-
preset: ProviderPreset;
|
|
47
|
-
id: string;
|
|
48
|
-
name: string;
|
|
49
|
-
baseUrl: string;
|
|
50
|
-
key: string;
|
|
51
|
-
/** `$VAR` / `{env:VAR}` / `!cmd`: stays in config, never stored. */
|
|
52
|
-
reference?: string;
|
|
53
|
-
previous?: readonly string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function providerReady(app: App, provider: string): { ready: number; source?: string } {
|
|
57
|
-
const models = app.catalog.filter(model => model.provider === provider);
|
|
58
|
-
const resolved = models.map(model => app.credentialOf(model)).find(got => got.ok);
|
|
59
|
-
return { ready: resolved ? models.filter(model => app.credentialOf(model).ok).length : 0, ...(resolved?.ok ? { source: resolved.source } : {}) };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function providerName(app: App, id: string): string {
|
|
63
|
-
return SOURCE_PROVIDERS[id]?.name ?? (app.catalog.find(model => model.provider === id) ? connectedName(id) : id);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function connectedName(id: string): string {
|
|
67
|
-
return CUSTOM_PRESETS.find(preset => preset.id === id)?.label ?? id;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Entry points from `/provider`. API-key and subscription sign-in stay on the
|
|
72
|
-
* login picker itself (their masked editor and OAuth flows are fixed by
|
|
73
|
-
* tests); the wizard owns what login never did: custom endpoints and
|
|
74
|
-
* per-provider model choice.
|
|
75
|
-
*/
|
|
76
|
-
export function startConnect(host: ConnectHost, entry: "custom" | "manage"): void {
|
|
77
|
-
const { app, wizard } = host;
|
|
78
|
-
if (entry === "custom") {
|
|
79
|
-
wizard.push({
|
|
80
|
-
kind: "list", keep: true,
|
|
81
|
-
title: "Which protocol does the endpoint speak?",
|
|
82
|
-
items: CUSTOM_PRESETS.map(preset => ({ value: preset.id, label: preset.label, description: preset.hint ?? "" })),
|
|
83
|
-
onPick: item => { const preset = presetById(item.value); if (preset) beginPreset(host, preset); },
|
|
84
|
-
});
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const connected = [...new Set(app.catalog.map(model => model.provider))].filter(id => providerReady(app, id).ready > 0).sort();
|
|
88
|
-
if (connected.length === 0) {
|
|
89
|
-
wizard.push({ kind: "message", title: "No provider is connected yet", lines: ["Choose a subscription, an API-key provider or a custom endpoint from /provider first."], onDone: () => host.close() });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
wizard.push({
|
|
93
|
-
kind: "list", keep: true,
|
|
94
|
-
title: `${connected.length} connected · choose one to manage`,
|
|
95
|
-
items: connected.map(id => {
|
|
96
|
-
const state = providerReady(app, id);
|
|
97
|
-
return { value: id, label: providerName(app, id), description: `${state.ready} model${state.ready === 1 ? "" : "s"} · ${state.source ?? "configured"}` };
|
|
98
|
-
}),
|
|
99
|
-
onPick: item => manage(host, item.value),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** A provider configured by hand (or by an earlier /provider) that is not a preset. */
|
|
104
|
-
function customFromCatalog(app: App, id: string): ProviderPreset | undefined {
|
|
105
|
-
const model = app.catalog.find(candidate => candidate.provider === id);
|
|
106
|
-
if (!model) return undefined;
|
|
107
|
-
return { id, label: id, api: model.api, baseUrl: model.baseUrl ?? "", catalog: false, custom: true };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function manage(host: ConnectHost, id: string): void {
|
|
111
|
-
const { app, wizard } = host;
|
|
112
|
-
const preset = presetById(id) ?? customFromCatalog(app, id);
|
|
113
|
-
if (!preset) return;
|
|
114
|
-
const enabled = app.catalog.filter(model => model.provider === id).map(model => model.id);
|
|
115
|
-
wizard.push({
|
|
116
|
-
kind: "list", keep: true,
|
|
117
|
-
title: providerName(app, id),
|
|
118
|
-
items: [
|
|
119
|
-
{ value: "models", label: "Choose models", description: "list the provider's models again and pick which appear in /model" },
|
|
120
|
-
{ value: "key", label: "Replace API key", description: app.auth[id] ? `stored in ${authPath()}` : "currently from the environment or config" },
|
|
121
|
-
{ value: "remove", label: "Disconnect", description: "remove the stored key and this provider's endpoint config" },
|
|
122
|
-
],
|
|
123
|
-
onPick: async item => {
|
|
124
|
-
const sample = app.catalog.find(model => model.provider === id);
|
|
125
|
-
const baseUrl = sample?.baseUrl ?? preset.baseUrl;
|
|
126
|
-
const got = sample ? app.credentialOf(sample) : undefined;
|
|
127
|
-
const draft: Draft = { preset, id, name: preset.custom ? id : preset.label, baseUrl, key: got?.ok ? got.key : "", previous: enabled };
|
|
128
|
-
if (item.value === "models") { await fetchModels(host, draft); return; }
|
|
129
|
-
if (item.value === "key") { askKey(host, draft); return; }
|
|
130
|
-
await disconnect(host, id);
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function beginPreset(host: ConnectHost, preset: ProviderPreset): void {
|
|
136
|
-
const { app, wizard } = host;
|
|
137
|
-
const draft: Draft = { preset, id: preset.id, name: preset.label, baseUrl: preset.baseUrl, key: "" };
|
|
138
|
-
if (!preset.custom) { askKey(host, draft); return; }
|
|
139
|
-
wizard.push({
|
|
140
|
-
kind: "form", keep: true,
|
|
141
|
-
title: `${preset.label} · base URL`,
|
|
142
|
-
label: "URL",
|
|
143
|
-
value: preset.baseUrl,
|
|
144
|
-
hint: preset.api === "anthropic-messages" ? "the root, e.g. https://gateway.example.com/anthropic · Enter continue · Esc back"
|
|
145
|
-
: preset.api === "google-generative-ai" ? "e.g. https://generativelanguage.googleapis.com/v1beta · Enter continue · Esc back"
|
|
146
|
-
: "usually ends in /v1 · a pasted …/chat/completions is trimmed · Enter continue · Esc back",
|
|
147
|
-
validate: value => /^https?:\/\/[^\s/]+/.test(value) ? undefined : "Enter an http(s) URL",
|
|
148
|
-
onSubmit: value => {
|
|
149
|
-
draft.baseUrl = normalizeBaseUrl(value);
|
|
150
|
-
const taken = new Set(app.catalog.map(model => model.provider).filter(id => !(id in app.auth) && SOURCE_PROVIDERS[id] !== undefined));
|
|
151
|
-
const suggested = preset.keyless ? preset.id : suggestProviderId(draft.baseUrl, taken);
|
|
152
|
-
wizard.push({
|
|
153
|
-
kind: "form", keep: true,
|
|
154
|
-
title: `${preset.label} · name`,
|
|
155
|
-
label: "Provider ID",
|
|
156
|
-
value: suggested,
|
|
157
|
-
hint: "shown in /model as <id>/<model>; lowercase a-z, digits, - and _",
|
|
158
|
-
validate: id => !validProviderId(id) ? `Use lowercase a-z, digits, - and _ (no spaces, capitals or non-Latin letters); try "${slugProviderId(id) || suggested}"`
|
|
159
|
-
: SOURCE_PROVIDERS[id] !== undefined ? `"${id}" is a built-in provider; pick another id or choose it from the list` : undefined,
|
|
160
|
-
onSubmit: id => {
|
|
161
|
-
draft.id = id;
|
|
162
|
-
draft.name = id;
|
|
163
|
-
if (preset.keyless) { void fetchModels(host, draft); return; }
|
|
164
|
-
askKey(host, draft);
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function askKey(host: ConnectHost, draft: Draft): void {
|
|
172
|
-
const { app, wizard } = host;
|
|
173
|
-
const sample = app.catalog.find(model => model.provider === draft.id);
|
|
174
|
-
const existing = sample ? app.credentialOf(sample) : undefined;
|
|
175
|
-
const reuse = existing?.ok ? existing.source : undefined;
|
|
176
|
-
wizard.push({
|
|
177
|
-
kind: "form", keep: true,
|
|
178
|
-
title: `${draft.name} · API key`,
|
|
179
|
-
label: "Key",
|
|
180
|
-
secret: true,
|
|
181
|
-
hint: [reuse ? `empty keeps the current key (${reuse})` : draft.preset.keyUrl ? `get one at ${draft.preset.keyUrl}` : "paste the key",
|
|
182
|
-
"$VAR reads the environment", "Enter continue · Esc back"].join(" · "),
|
|
183
|
-
validate: value => value.length === 0 && !reuse ? "Paste a key, or $VAR to read one from the environment" : undefined,
|
|
184
|
-
onSubmit: async value => {
|
|
185
|
-
if (value.length === 0 && existing?.ok) draft.key = existing.key;
|
|
186
|
-
else if (/^(\$|\{env:|!)/.test(value)) {
|
|
187
|
-
draft.reference = value;
|
|
188
|
-
const name = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(value)?.[1] ?? /^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(value)?.[1];
|
|
189
|
-
draft.key = name ? process.env[name] ?? "" : "";
|
|
190
|
-
} else draft.key = value;
|
|
191
|
-
await fetchModels(host, draft);
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function fetchModels(host: ConnectHost, draft: Draft): Promise<void> {
|
|
197
|
-
const { app, wizard } = host;
|
|
198
|
-
const family = discoveryFamily(draft.preset.api);
|
|
199
|
-
const known = app.catalog.filter(model => model.provider === draft.id);
|
|
200
|
-
const builtin = loadCatalog({ userText: null }).models.filter(model => model.provider === draft.id);
|
|
201
|
-
const catalogModels = [...new Map([...builtin, ...known].map(model => [model.id, model])).values()];
|
|
202
|
-
if (!family) { chooseModels(host, draft, catalogModels.map(toDiscovered), undefined); return; }
|
|
203
|
-
const abort = new AbortController();
|
|
204
|
-
wizard.push({ kind: "busy", title: "models", message: `Listing models from ${draft.baseUrl} …`, onCancel: () => abort.abort() });
|
|
205
|
-
const headers = Object.fromEntries(Object.entries(providerAccess(known[0] ?? ({} as ModelSpec)).headers ?? {}).filter((entry): entry is [string, string] => typeof entry[1] === "string" && !entry[1].includes("$")));
|
|
206
|
-
const result = await discoverModels({ family, baseUrl: draft.baseUrl, key: draft.key, headers, fetch: host.fetch ?? ((input, init) => globalThis.fetch(input, init)), signal: abort.signal });
|
|
207
|
-
if (abort.signal.aborted || wizard.top?.kind !== "busy") return;
|
|
208
|
-
if (result.ok) {
|
|
209
|
-
const live = result.models;
|
|
210
|
-
// Catalog metadata wins for display; the live list decides availability.
|
|
211
|
-
const merged = live.map(model => {
|
|
212
|
-
const twin = catalogModels.find(candidate => candidate.id === model.id);
|
|
213
|
-
return twin ? { ...toDiscovered(twin), ...model, name: model.name ?? twin.name } : model;
|
|
214
|
-
});
|
|
215
|
-
chooseModels(host, draft, merged, `${live.length} models from ${result.url}`, true);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const options: SelectItem[] = [
|
|
219
|
-
{ value: "retry", label: "Retry", description: result.reason },
|
|
220
|
-
{ value: "key", label: "Change the API key" },
|
|
221
|
-
...(draft.preset.custom ? [{ value: "url", label: "Change the base URL", description: draft.baseUrl }] : []),
|
|
222
|
-
...(catalogModels.length ? [{ value: "catalog", label: `Use the ${catalogModels.length} built-in models`, description: "skip the live list" }] : []),
|
|
223
|
-
{ value: "manual", label: "Type model IDs", description: "for endpoints without a /models listing" },
|
|
224
|
-
];
|
|
225
|
-
wizard.push({
|
|
226
|
-
kind: "list", keep: true,
|
|
227
|
-
title: `Could not list models: ${result.reason}`,
|
|
228
|
-
items: options,
|
|
229
|
-
onPick: async item => {
|
|
230
|
-
if (item.value === "retry") { wizard.pop(); await fetchModels(host, draft); }
|
|
231
|
-
else if (item.value === "key") { wizard.pop(); wizard.pop(); askKey(host, draft); }
|
|
232
|
-
else if (item.value === "url") { while (wizard.depth > 1 && wizard.top?.title !== `${draft.preset.label} · base URL`) wizard.pop(); }
|
|
233
|
-
else if (item.value === "catalog") chooseModels(host, draft, catalogModels.map(toDiscovered), "built-in catalog");
|
|
234
|
-
else wizard.push({
|
|
235
|
-
kind: "form", keep: true, title: "Model IDs", label: "IDs", hint: "comma or space separated, exactly as the provider names them · Enter save",
|
|
236
|
-
validate: value => value.split(/[\s,]+/).filter(Boolean).length ? undefined : "Enter at least one model id",
|
|
237
|
-
onSubmit: async value => { await save(host, draft, value.split(/[\s,]+/).filter(Boolean).map(id => ({ id }))); },
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
}, true);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function toDiscovered(model: ModelSpec): DiscoveredModel {
|
|
244
|
-
return { id: model.id, ...(model.name !== model.id ? { name: model.name } : {}),
|
|
245
|
-
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}), ...(model.maxTokens ? { maxTokens: model.maxTokens } : {}) };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function chooseModels(host: ConnectHost, draft: Draft, models: readonly DiscoveredModel[], source: string | undefined, replace = false): void {
|
|
249
|
-
const { app, wizard } = host;
|
|
250
|
-
const chat = models.filter(model => likelyChatModel(model.id));
|
|
251
|
-
const known = new Set(app.catalog.filter(model => model.provider === draft.id).map(model => model.id));
|
|
252
|
-
const preselect = (model: DiscoveredModel): boolean => {
|
|
253
|
-
if (draft.previous) return draft.previous.includes(model.id);
|
|
254
|
-
if (!likelyChatModel(model.id)) return false;
|
|
255
|
-
if (chat.length <= PRESELECT_ALL) return true;
|
|
256
|
-
return known.has(model.id) || catalogTwin(model.id, app.catalog) !== undefined && draft.preset.custom === false;
|
|
257
|
-
};
|
|
258
|
-
const ordered = [...chat, ...models.filter(model => !likelyChatModel(model.id))];
|
|
259
|
-
const items: ListItem[] = ordered.map(model => ({
|
|
260
|
-
value: model.id,
|
|
261
|
-
label: model.id,
|
|
262
|
-
description: [model.name && model.name !== model.id ? model.name : "", model.contextWindow ? `${Math.round(model.contextWindow / 1000)}k ctx` : "", likelyChatModel(model.id) ? "" : "not a chat model?"].filter(Boolean).join(" · "),
|
|
263
|
-
}));
|
|
264
|
-
wizard.push({
|
|
265
|
-
kind: "list", multi: true, keep: true,
|
|
266
|
-
title: `${draft.name} · models to enable${source ? ` (${source})` : ""}`,
|
|
267
|
-
items, checked: ordered.filter(preselect).map(model => model.id),
|
|
268
|
-
hint: "space toggle · ctrl+a all · enter save · esc back",
|
|
269
|
-
onSubmitChecked: async values => {
|
|
270
|
-
if (!values.length) { fail(host, "Select at least one model (space toggles, ctrl+a selects all)"); return; }
|
|
271
|
-
await save(host, draft, ordered.filter(model => values.includes(model.id)));
|
|
272
|
-
},
|
|
273
|
-
}, replace);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function save(host: ConnectHost, draft: Draft, chosen: readonly DiscoveredModel[]): Promise<void> {
|
|
277
|
-
const { app, wizard } = host;
|
|
278
|
-
const read = await readModelsText();
|
|
279
|
-
let json: unknown = {};
|
|
280
|
-
if (read.error) { fail(host, `${read.error}; nothing was changed`); return; }
|
|
281
|
-
if (read.text !== null && read.text.trim().length) {
|
|
282
|
-
try { json = JSON.parse(read.text); }
|
|
283
|
-
catch { fail(host, `${modelsPath()} is not valid JSON; fix it first — nothing was changed`); return; }
|
|
284
|
-
}
|
|
285
|
-
const catalogDefault = SOURCE_PROVIDERS[draft.id]?.baseUrl;
|
|
286
|
-
const keyless = draft.preset.keyless === true;
|
|
287
|
-
const next = applyConnect(json, {
|
|
288
|
-
id: draft.id, name: draft.name, api: draft.preset.api, baseUrl: draft.baseUrl, catalog: !draft.preset.custom,
|
|
289
|
-
...(catalogDefault ? { catalogBaseUrl: catalogDefault } : {}), keyless,
|
|
290
|
-
...(draft.reference ? { keyReference: draft.reference } : {}), chosen,
|
|
291
|
-
}, loadCatalog({ userText: null }).models);
|
|
292
|
-
if (!keyless && !draft.reference && draft.key && (app.auth[draft.id]?.type !== "api_key" || app.auth[draft.id]?.type === "api_key" && (app.auth[draft.id] as { key: string }).key !== draft.key)) {
|
|
293
|
-
const auth = { ...app.auth, [draft.id]: { type: "api_key" as const, key: draft.key } };
|
|
294
|
-
await writeAuthFile(auth);
|
|
295
|
-
app.auth[draft.id] = auth[draft.id];
|
|
296
|
-
}
|
|
297
|
-
await writeModelsFile(next);
|
|
298
|
-
const loaded = loadCatalog(await readModelSources());
|
|
299
|
-
await publishModelCatalog(app, loaded.models);
|
|
300
|
-
const ready = app.catalog.filter(model => model.provider === draft.id && app.credentialOf(model).ok);
|
|
301
|
-
const warnings = loaded.warnings.filter(warning => warning.includes(draft.id));
|
|
302
|
-
if (ready.length === 0) {
|
|
303
|
-
fail(host, warnings[0] ?? "saved, but no model of this provider resolves a credential yet — check the key");
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
app.io.say(app.io.palette.green(` connected ${draft.name}`) + app.io.palette.dim(` ${ready.length} model${ready.length === 1 ? "" : "s"} · ${modelsPath()}`));
|
|
307
|
-
wizard.reset({
|
|
308
|
-
kind: "list",
|
|
309
|
-
title: `Connected ${draft.name} · ${ready.length} model${ready.length === 1 ? "" : "s"} ready. Switch now?`,
|
|
310
|
-
items: [
|
|
311
|
-
...ready.map(model => ({ value: modelKey(model), label: model.id, description: model.name !== model.id ? model.name : "" })),
|
|
312
|
-
{ value: "stay", label: "Keep the current model", description: modelKey(app.providerConfig.spec) },
|
|
313
|
-
],
|
|
314
|
-
onPick: async item => {
|
|
315
|
-
host.close();
|
|
316
|
-
const spec = app.catalog.find(model => modelKey(model) === item.value);
|
|
317
|
-
if (spec) await host.switchTo(spec);
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function disconnect(host: ConnectHost, id: string): Promise<void> {
|
|
323
|
-
const { app } = host;
|
|
324
|
-
const read = await readModelsText();
|
|
325
|
-
if (read.text !== null && read.text.trim().length) {
|
|
326
|
-
let json: unknown;
|
|
327
|
-
try { json = JSON.parse(read.text); } catch { fail(host, `${modelsPath()} is not valid JSON; nothing was changed`); return; }
|
|
328
|
-
await writeModelsFile(applyDisconnect(json, id));
|
|
329
|
-
}
|
|
330
|
-
if (app.auth[id] !== undefined) {
|
|
331
|
-
const auth = { ...app.auth };
|
|
332
|
-
delete auth[id];
|
|
333
|
-
await writeAuthFile(auth);
|
|
334
|
-
delete app.auth[id];
|
|
335
|
-
}
|
|
336
|
-
const loaded = loadCatalog(await readModelSources());
|
|
337
|
-
if (app.providerConfig.spec.provider === id) app.io.say(app.io.palette.yellow(" the current model belonged to this provider; choose another with /model"));
|
|
338
|
-
await publishModelCatalog(app, loaded.models);
|
|
339
|
-
const left = providerReady(app, id).ready;
|
|
340
|
-
app.io.say(app.io.palette.green(` disconnected ${id}`) + (left ? app.io.palette.dim(` (${left} models still resolve a key from the environment)`) : ""));
|
|
341
|
-
host.close();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function fail(host: ConnectHost, reason: string): void {
|
|
345
|
-
host.wizard.push({ kind: "message", title: "Not saved", lines: [reason] });
|
|
346
|
-
}
|
package/src/apps/question-ui.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/** Existing app dialogs own focus and cancellation; question tools receive only validated values. */
|
|
2
|
-
import type { QuestionRequest, QuestionReply, QuestionAnswer } from "../core/question.ts";
|
|
3
|
-
import type { ExtensionUI } from "../runtime/extension-ui.ts";
|
|
4
|
-
|
|
5
|
-
export async function presentQuestions(ui: ExtensionUI, request: QuestionRequest, signal: AbortSignal): Promise<QuestionReply | undefined> {
|
|
6
|
-
const answers: QuestionAnswer[] = [];
|
|
7
|
-
for (const question of request.questions) {
|
|
8
|
-
const selected = new Set<string>(); let text: string | undefined;
|
|
9
|
-
if (!question.options.length) {
|
|
10
|
-
text = await ui.input(`${question.header}: ${question.question}`, "Your answer", { signal });
|
|
11
|
-
if (!text?.trim() || signal.aborted) return undefined;
|
|
12
|
-
} else for (;;) {
|
|
13
|
-
const labels = question.options.map(option => `${selected.has(option.id) ? "✓ " : ""}[${option.id}] ${option.label}${option.description ? " — " + option.description : ""}`);
|
|
14
|
-
const custom = "Enter a custom answer", submit = "Submit selected answers";
|
|
15
|
-
const choice = await ui.select(`${question.header}: ${question.question}`, [...labels, ...(question.custom ? [custom] : []), ...(question.multiple ? [submit] : [])], { signal });
|
|
16
|
-
if (choice === undefined || signal.aborted) return undefined;
|
|
17
|
-
if (choice === custom && question.custom) { text = await ui.input(question.question, "Your answer", { signal }); if (!text?.trim() || signal.aborted) return undefined; break; }
|
|
18
|
-
if (choice === submit && question.multiple) { if (selected.size) break; ui.notify("Select at least one answer.", "warning"); continue; }
|
|
19
|
-
const index = labels.indexOf(choice); if (index < 0) return undefined;
|
|
20
|
-
const id = question.options[index]!.id;
|
|
21
|
-
if (!question.multiple) { selected.add(id); break; }
|
|
22
|
-
if (selected.has(id)) selected.delete(id); else selected.add(id);
|
|
23
|
-
}
|
|
24
|
-
answers.push({ id: question.id, choices: [...selected], ...(text === undefined ? {} : { text }) });
|
|
25
|
-
}
|
|
26
|
-
return { request_id: request.request_id, turn_id: request.turn_id, answers };
|
|
27
|
-
}
|
package/src/apps/render.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loop events become lines. The shell prints them; this file does not
|
|
3
|
-
* touch a terminal, so "what a tool call looks like" can be asserted
|
|
4
|
-
* without starting one.
|
|
5
|
-
*
|
|
6
|
-
* Colour is a parameter, not a global. Piped and CI runs have no tty,
|
|
7
|
-
* and a CSI 2026 (or even a dim code) in `-p` output is a parse error
|
|
8
|
-
* waiting for a script.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
12
|
-
import type { LoopEvent } from "../runtime/loop.ts";
|
|
13
|
-
import { ANSI as ANSI_STYLE, IDENTITY, type Style } from "../tui/style.ts";
|
|
14
|
-
import { toolCard, toolDetail } from "../tui/ui/tool-card.ts";
|
|
15
|
-
|
|
16
|
-
export interface Palette {
|
|
17
|
-
dim: (s: string) => string;
|
|
18
|
-
bold: (s: string) => string;
|
|
19
|
-
red: (s: string) => string;
|
|
20
|
-
green: (s: string) => string;
|
|
21
|
-
yellow: (s: string) => string;
|
|
22
|
-
cyan: (s: string) => string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const identity = (s: string): string => s;
|
|
26
|
-
|
|
27
|
-
const ANSI: Palette = {
|
|
28
|
-
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
29
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
30
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
31
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
32
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
33
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const PLAIN: Palette = {
|
|
37
|
-
dim: identity,
|
|
38
|
-
bold: identity,
|
|
39
|
-
red: identity,
|
|
40
|
-
green: identity,
|
|
41
|
-
yellow: identity,
|
|
42
|
-
cyan: identity,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export function createPalette(color: boolean): Palette {
|
|
46
|
-
return color ? ANSI : PLAIN;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Colour is a tty AND not NO_COLOR. A theme that still emits ESC in a pipe
|
|
51
|
-
* or under NO_COLOR is the same class of bug as createPalette(true) on a
|
|
52
|
-
* redirected stdout.
|
|
53
|
-
*/
|
|
54
|
-
export function colorEnabled(): boolean {
|
|
55
|
-
const noColor = process.env["NO_COLOR"];
|
|
56
|
-
if (noColor !== undefined && noColor !== "") return false;
|
|
57
|
-
return stdoutIsTty();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** True only for a real tty. `undefined` (piped, CI) must not count. */
|
|
61
|
-
export function stdoutIsTty(): boolean {
|
|
62
|
-
return process.stdout.isTTY === true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function stdinIsTty(): boolean {
|
|
66
|
-
return process.stdin.isTTY === true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* One readable line from whatever was thrown.
|
|
71
|
-
*
|
|
72
|
-
* Never a stack. A stack is for whoever wrote the harness; the person running
|
|
73
|
-
* it needs the status and the provider's own sentence, which is the part that
|
|
74
|
-
* says whether this is their key, their quota, or the provider being down.
|
|
75
|
-
*/
|
|
76
|
-
export function describeError(error: unknown): string {
|
|
77
|
-
if (error instanceof Error) {
|
|
78
|
-
const status = (error as { status?: unknown }).status;
|
|
79
|
-
const prefix = typeof status === "number" ? `HTTP ${status}: ` : "";
|
|
80
|
-
return error.message.startsWith("HTTP ") ? error.message : prefix + error.message;
|
|
81
|
-
}
|
|
82
|
-
return String(error);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function indent(text: string, pad: string): string {
|
|
86
|
-
return text.split("\n").map((l) => pad + l).join("\n");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function wrap(text: string, width: number): string {
|
|
90
|
-
const out: string[] = [];
|
|
91
|
-
let line = "";
|
|
92
|
-
for (const word of text.split(/\s+/)) {
|
|
93
|
-
if (line && line.length + word.length + 1 > width) { out.push(line); line = word; }
|
|
94
|
-
else line = line ? `${line} ${word}` : word;
|
|
95
|
-
}
|
|
96
|
-
if (line) out.push(line);
|
|
97
|
-
return out.join("\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function firstLine(text: string, width: number): string {
|
|
101
|
-
const line = text.split("\n")[0] ?? "";
|
|
102
|
-
return line.length > width ? line.slice(0, width - 1) + "…" : line;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Zero or more lines that the shell will print, matching the previous
|
|
107
|
-
* inline switch one-for-one -- including the leading newline on
|
|
108
|
-
* stop_and_ask, which is a blank line in the transcript.
|
|
109
|
-
*
|
|
110
|
-
* Kinds the old CLI ignored stay silent so this extraction cannot change
|
|
111
|
-
* what a piped run prints.
|
|
112
|
-
*/
|
|
113
|
-
function markdownStyle(C: Palette, style?: Style): Style {
|
|
114
|
-
if (style !== undefined) return style;
|
|
115
|
-
// Palette has no colour flag; identity wrappers are how -p and pipes
|
|
116
|
-
// already opt out of CSI. A sentinel that round-trips is that same gate.
|
|
117
|
-
return C.bold("\0") === "\0" ? IDENTITY : ANSI_STYLE;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface FormatFlags {
|
|
121
|
-
readonly toolsExpanded?: boolean;
|
|
122
|
-
readonly thinkingExpanded?: boolean;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function formatEvent(
|
|
126
|
-
e: LoopEvent,
|
|
127
|
-
C: Palette,
|
|
128
|
-
width = 80,
|
|
129
|
-
flags: FormatFlags = {},
|
|
130
|
-
style?: Style,
|
|
131
|
-
): string[] {
|
|
132
|
-
const md = markdownStyle(C, style);
|
|
133
|
-
switch (e.kind) {
|
|
134
|
-
case "thinking": {
|
|
135
|
-
// Print mode sends this to stderr, so 2>&1 can still read the reasoning in full.
|
|
136
|
-
const text = (e.text ?? "").trim();
|
|
137
|
-
void flags;
|
|
138
|
-
return text ? text.split("\n").map(line => C.dim(` ${line}`)) : [];
|
|
139
|
-
}
|
|
140
|
-
case "assistant_text":
|
|
141
|
-
// Markdown stays source text in a pipe: scripts read it, and terminals render the TUI.
|
|
142
|
-
return (e.text ?? "").split("\n");
|
|
143
|
-
case "tool_call": {
|
|
144
|
-
const tool = e.tool ?? { name: "", input: {} };
|
|
145
|
-
const card = toolCard({ name: tool.name, input: tool.input, result: null });
|
|
146
|
-
return [C.cyan(` → ${tool.name}${card.summary ? ` ${card.summary}` : ""}`)];
|
|
147
|
-
}
|
|
148
|
-
case "tool_result": {
|
|
149
|
-
const tool = e.tool ?? { name: "", input: {} };
|
|
150
|
-
const call = { name: tool.name, input: tool.input, result: { text: e.text ?? "", ok: e.ok !== false } };
|
|
151
|
-
const card = toolCard(call);
|
|
152
|
-
const head = ` ${e.ok === false ? "err" : "ok "}${card.evidence ? ` ${card.evidence}` : ""}`;
|
|
153
|
-
const body = flags.toolsExpanded === true ? toolDetail({ ...call, input: tool.name === "bash" ? {} : tool.input }, width - 6, md).map(line => ` ${line}`) : [];
|
|
154
|
-
return [e.ok === false ? C.red(head) : C.dim(head), ...body];
|
|
155
|
-
}
|
|
156
|
-
case "usage": {
|
|
157
|
-
const u = e.usage!;
|
|
158
|
-
const line =
|
|
159
|
-
` ${u.input_tokens} new ${u.cache_read_input_tokens} cached ` +
|
|
160
|
-
`${u.cache_creation_input_tokens} written ${u.output_tokens} out ${u.ms}ms`;
|
|
161
|
-
return [e.ok ? C.dim(line) : C.yellow(line + ` ⚠ ${e.prefix}`)];
|
|
162
|
-
}
|
|
163
|
-
case "cut":
|
|
164
|
-
return [C.yellow(` ── node ${e.node} closed (${e.reason}); handoff written`)];
|
|
165
|
-
case "stop_and_ask":
|
|
166
|
-
return [C.red(`\n stopping: ${e.reason}`)];
|
|
167
|
-
case "done":
|
|
168
|
-
if (e.reason && e.reason !== "end_turn") return [C.dim(` [${e.reason}]`)];
|
|
169
|
-
return [];
|
|
170
|
-
case "turn_start":
|
|
171
|
-
case "branch_start":
|
|
172
|
-
case "branch_done":
|
|
173
|
-
case "text_delta":
|
|
174
|
-
case "thinking_delta":
|
|
175
|
-
case "discard":
|
|
176
|
-
case "progress":
|
|
177
|
-
return [];
|
|
178
|
-
default:
|
|
179
|
-
return assertNever(e.kind, "formatEvent");
|
|
180
|
-
}
|
|
181
|
-
}
|
package/src/apps/resources.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { loadSessionResources, sessionToolCatalog, processPermissionRules, type App, type Args } from "./startup.ts";
|
|
2
|
-
import { loadCatalog, type createProvider } from "../runtime/catalog.ts";
|
|
3
|
-
import { readModelSources } from "../store/models.ts";
|
|
4
|
-
import { stageModelCatalog, applyModelCatalog } from "./model-catalog.ts";
|
|
5
|
-
import { sessionControl } from "../runtime/session-control.ts";
|
|
6
|
-
|
|
7
|
-
export interface ResourceReloadOptions {
|
|
8
|
-
readonly ownerLocked?: boolean;
|
|
9
|
-
readonly args?: Args;
|
|
10
|
-
readonly providerFactory?: typeof createProvider;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Parsing and discovery never retire the currently published runtime. */
|
|
14
|
-
export async function reloadAppResources(app: App, options: ResourceReloadOptions = {}): Promise<boolean> {
|
|
15
|
-
if (!options.ownerLocked) return sessionControl(app.session).withIdleChange(() => reloadAppResources(app, { ...options, ownerLocked: true }));
|
|
16
|
-
const args = structuredClone(options.args ?? app.args);
|
|
17
|
-
const resources = await loadSessionResources(args, app.workspace, app.io, app.projectTrusted, app.foundation?.questionSlot);
|
|
18
|
-
let published = false;
|
|
19
|
-
try {
|
|
20
|
-
const catalog = loadCatalog(await readModelSources());
|
|
21
|
-
for (const warning of catalog.warnings) app.io.say(app.io.palette.yellow(warning));
|
|
22
|
-
const profileModel = args.role?.profile.model;
|
|
23
|
-
const stage = stageModelCatalog(app, catalog.models, {
|
|
24
|
-
...(profileModel ? { model: args.model, provider: args.provider, ...(args.thinking ? { thinking: args.thinking } : {}), requireCredential: true } : {}),
|
|
25
|
-
...(options.providerFactory ? { providerFactory: options.providerFactory } : {}),
|
|
26
|
-
});
|
|
27
|
-
const tools = sessionToolCatalog(args, resources.extensions, app.session.config.frozen.tools);
|
|
28
|
-
const hostRules = app.session.config.tools.appOwnsPermissions === true ? undefined : app.session.config.tools.hostPermissionRules ?? app.session.config.tools.rules ?? [];
|
|
29
|
-
const rules = processPermissionRules(args, resources.foundation, hostRules);
|
|
30
|
-
const previousMode = app.session.config.tools.workflowMode, previousProfile = app.session.config.agentProfile, previousScope = app.session.config.workflowToolProfile;
|
|
31
|
-
const nextProfile = args.agent ? { name: args.agent, workflowExplicit: args.cliFlags?.includes("--workflow") === true } : undefined;
|
|
32
|
-
const nextScope = args.toolProfile === "foundation" ? "foundation" as const : undefined;
|
|
33
|
-
const metadataChanged = previousMode !== (args.workflow ?? "build") || JSON.stringify(previousProfile) !== JSON.stringify(nextProfile) || previousScope !== nextScope;
|
|
34
|
-
// Pairing is checked before retiring any existing integration generation.
|
|
35
|
-
app.session.assertResourceIdle();
|
|
36
|
-
if (app.extensions && !await app.extensions.releaseIntegrations()) { app.extensions.restoreIntegrations(); throw Error("An integration still owns unfinished work or unconfirmed cleanup. Wait for it to settle before reloading; no replacement was activated."); }
|
|
37
|
-
app.session.config.tools.workflowMode = args.workflow ?? "build";
|
|
38
|
-
if (nextProfile) app.session.config.agentProfile = nextProfile; else delete app.session.config.agentProfile;
|
|
39
|
-
if (nextScope) app.session.config.workflowToolProfile = nextScope; else delete app.session.config.workflowToolProfile;
|
|
40
|
-
let cut: boolean;
|
|
41
|
-
try { cut = await app.session.reloadResources({ system: resources.system, tools: tools.schemas }, [...resources.slash.skills.values()], stage.next, metadataChanged); }
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (previousMode === undefined) delete app.session.config.tools.workflowMode; else app.session.config.tools.workflowMode = previousMode;
|
|
44
|
-
if (previousProfile === undefined) delete app.session.config.agentProfile; else app.session.config.agentProfile = previousProfile;
|
|
45
|
-
if (previousScope === undefined) delete app.session.config.workflowToolProfile; else app.session.config.workflowToolProfile = previousScope;
|
|
46
|
-
app.extensions?.restoreIntegrations();
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
const oldExtensions = app.extensions, oldFoundation = app.foundation;
|
|
50
|
-
app.args = args; app.extensions = resources.extensions; app.foundation = resources.foundation;
|
|
51
|
-
app.session.config.tools.questionSlot = resources.foundation.questionSlot;
|
|
52
|
-
app.session.config.tools.instructionCheck = (ctx, name, input) => resources.foundation.instructions(ctx, name, input, app.args.noContextFiles);
|
|
53
|
-
app.session.config.tools.extensions = resources.extensions.definitions(); app.session.config.tools.deferredTools = tools.deferred;
|
|
54
|
-
app.session.config.tools.extensionHooks = resources.extensions.toolHooks(); app.session.config.tools.rules = rules;
|
|
55
|
-
if (hostRules) app.session.config.tools.hostPermissionRules = structuredClone([...hostRules]);
|
|
56
|
-
app.session.config.maxTurns = args.maxTurns;
|
|
57
|
-
if (args.timeBudgetMs === undefined) delete app.session.config.timeBudgetMs; else app.session.config.timeBudgetMs = args.timeBudgetMs;
|
|
58
|
-
app.loadedContext = resources.loadedContext; app.slash = resources.slash; app.ui = resources.ui;
|
|
59
|
-
app.resources = resources.index; app.projectTrusted = resources.trusted; app.system = app.session.config.frozen.system;
|
|
60
|
-
published = true; applyModelCatalog(app, stage);
|
|
61
|
-
// The new owner is live. Cleanup errors cannot roll it back to an already retired generation.
|
|
62
|
-
try { if (oldExtensions) await oldExtensions.dispose(); else await oldFoundation?.dispose(); }
|
|
63
|
-
catch { app.io.err("Previous resources could not finish cleanup; the new checkpoint remains active."); }
|
|
64
|
-
return cut;
|
|
65
|
-
} finally { if (!published) await resources.extensions.dispose().catch(() => {}); }
|
|
66
|
-
}
|