@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,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S.3: a session directory holds exactly one on-disk format, and this build
|
|
3
|
-
* reads only that one. A directory written in any other format is refused
|
|
4
|
-
* with its location -- never reinterpreted under today's defaults. Reading old
|
|
5
|
-
* logs leniently is how a resumed session once ran without its stall watchdog:
|
|
6
|
-
* new sessions wrote stallLimit 12 while the resume path restored an absent
|
|
7
|
-
* field as 0.
|
|
8
|
-
*
|
|
9
|
-
* The format covers the metadata journal (whose first record declares it),
|
|
10
|
-
* the frozen session context parsed below, events and handoffs. Changing any
|
|
11
|
-
* of them incompatibly means bumping SESSION_FORMAT, not adding a fallback.
|
|
12
|
-
*/
|
|
13
|
-
import { parseAcceptanceContract, type AcceptanceContract } from "./acceptance.ts";
|
|
14
|
-
import type { BoundaryConfig } from "./boundary.ts";
|
|
15
|
-
import { isContextPolicy, type ContextPolicy } from "./context-policy.ts";
|
|
16
|
-
import type { FrozenPrefix } from "./project.ts";
|
|
17
|
-
import { validateOutputRecoveryLimit } from "./response-recovery.ts";
|
|
18
|
-
import { schedulingWindow } from "./scheduling.ts";
|
|
19
|
-
import type { Skill } from "./skills.ts";
|
|
20
|
-
import { isBlindChecks, type BlindChecks } from "./blind-checks.ts";
|
|
21
|
-
import { emptyStallState, stallDecision } from "./stall.ts";
|
|
22
|
-
|
|
23
|
-
export const SESSION_FORMAT = 1;
|
|
24
|
-
|
|
25
|
-
export type FoundFormat = number | "undeclared";
|
|
26
|
-
|
|
27
|
-
export function sessionFormatMessage(dir: string, found: FoundFormat): string {
|
|
28
|
-
const written = found === "undeclared" ? "a layout from before session formats were declared" : `session format ${found}`;
|
|
29
|
-
return `${dir} holds ${written}; this xeno reads session format ${SESSION_FORMAT} only. ` +
|
|
30
|
-
"The log is intact and was not modified. Start a new session with --fresh.";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Raised before any read or write of a directory in another format. */
|
|
34
|
-
export class SessionFormatError extends Error {
|
|
35
|
-
readonly dir: string;
|
|
36
|
-
readonly found: FoundFormat;
|
|
37
|
-
constructor(dir: string, found: FoundFormat) {
|
|
38
|
-
super(sessionFormatMessage(dir, found));
|
|
39
|
-
this.name = "SessionFormatError";
|
|
40
|
-
this.dir = dir;
|
|
41
|
-
this.found = found;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* What a session freezes at start and restores on resume. Required fields
|
|
47
|
-
* are written by every session of this format, so resume restores them as
|
|
48
|
-
* recorded; there is no default to drift from what was written.
|
|
49
|
-
*/
|
|
50
|
-
export interface SessionContext {
|
|
51
|
-
frozen: FrozenPrefix;
|
|
52
|
-
gates: Record<string, string>;
|
|
53
|
-
shellCheckpoints: boolean;
|
|
54
|
-
skills: readonly Skill[];
|
|
55
|
-
strictVerification: boolean;
|
|
56
|
-
workingSet: boolean;
|
|
57
|
-
workNamespace: string;
|
|
58
|
-
maxParallelTools: number;
|
|
59
|
-
contextPolicy: ContextPolicy;
|
|
60
|
-
stallLimit: number;
|
|
61
|
-
outputRecoveryLimit: number;
|
|
62
|
-
acceptanceContract?: AcceptanceContract;
|
|
63
|
-
workflowMode?: "build" | "plan";
|
|
64
|
-
agentProfile?: { name: string; workflowExplicit: boolean };
|
|
65
|
-
workflowToolProfile?: "foundation";
|
|
66
|
-
boundary?: Partial<BoundaryConfig>;
|
|
67
|
-
guidance?: boolean;
|
|
68
|
-
blindChecks?: BlindChecks;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const REQUIRED = ["frozen", "gates", "shellCheckpoints", "skills", "strictVerification", "workingSet",
|
|
72
|
-
"workNamespace", "maxParallelTools", "contextPolicy", "stallLimit", "outputRecoveryLimit"] as const;
|
|
73
|
-
const OPTIONAL = ["acceptanceContract", "workflowMode", "agentProfile", "workflowToolProfile", "boundary", "guidance", "blindChecks"] as const;
|
|
74
|
-
const BOUNDARY_KEYS = ["triggerTokens", "hardTokens", "minNodeWorkTokens"] as const;
|
|
75
|
-
|
|
76
|
-
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
77
|
-
const strings = (value: unknown): value is string[] => Array.isArray(value) && value.every(item => typeof item === "string");
|
|
78
|
-
|
|
79
|
-
function invalid(field: string, why: string): never {
|
|
80
|
-
throw new Error(`invalid session context: ${field} ${why}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function skill(value: unknown, index: number): Skill {
|
|
84
|
-
const field = `skills[${index}]`;
|
|
85
|
-
if (!record(value)) invalid(field, "must be an object");
|
|
86
|
-
const text = (key: string): string => { const v = value[key]; if (typeof v !== "string") invalid(`${field}.${key}`, "must be a string"); return v; };
|
|
87
|
-
const disable = value["disableModelInvocation"];
|
|
88
|
-
if (typeof disable !== "boolean") invalid(`${field}.disableModelInvocation`, "must be a boolean");
|
|
89
|
-
const out: { -readonly [K in keyof Skill]: Skill[K] } = { name: text("name"), description: text("description"), filePath: text("filePath"), body: text("body"), disableModelInvocation: disable };
|
|
90
|
-
const invocable = value["userInvocable"];
|
|
91
|
-
if (invocable !== undefined) { if (typeof invocable !== "boolean") invalid(`${field}.userInvocable`, "must be a boolean"); out.userInvocable = invocable; }
|
|
92
|
-
if (value["context"] !== undefined) { if (value["context"] !== "fork") invalid(`${field}.context`, "must be fork"); out.context = "fork"; }
|
|
93
|
-
for (const key of ["allowedTools", "disallowedTools"] as const) {
|
|
94
|
-
const names = value[key];
|
|
95
|
-
if (names !== undefined) { if (!strings(names)) invalid(`${field}.${key}`, "must be strings"); out[key] = [...names]; }
|
|
96
|
-
}
|
|
97
|
-
if (value["sourceText"] !== undefined) out.sourceText = text("sourceText");
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Strict: every required field present and typed, no field this format does not define. */
|
|
102
|
-
export function parseSessionContext(value: unknown): SessionContext {
|
|
103
|
-
if (!record(value)) invalid("context", "must be an object");
|
|
104
|
-
for (const key of Object.keys(value)) {
|
|
105
|
-
if (!(REQUIRED as readonly string[]).includes(key) && !(OPTIONAL as readonly string[]).includes(key)) invalid(key, "is not part of this session format");
|
|
106
|
-
}
|
|
107
|
-
for (const key of REQUIRED) if (value[key] === undefined) invalid(key, "is missing");
|
|
108
|
-
|
|
109
|
-
const frozen = value["frozen"];
|
|
110
|
-
const system = record(frozen) ? frozen["system"] : undefined, tools = record(frozen) ? frozen["tools"] : undefined;
|
|
111
|
-
if (typeof system !== "string" || !Array.isArray(tools)) invalid("frozen", "must hold a system string and a tools array");
|
|
112
|
-
const gates = value["gates"], gateMap: Record<string, string> = {};
|
|
113
|
-
if (!record(gates)) invalid("gates", "must map names to commands");
|
|
114
|
-
for (const [name, command] of Object.entries(gates)) { if (typeof command !== "string") invalid(`gates.${name}`, "must be a command string"); gateMap[name] = command; }
|
|
115
|
-
const flag = (key: "shellCheckpoints" | "strictVerification" | "workingSet"): boolean => { const v = value[key]; if (typeof v !== "boolean") invalid(key, "must be a boolean"); return v; };
|
|
116
|
-
if (!Array.isArray(value["skills"])) invalid("skills", "must be an array");
|
|
117
|
-
const skills = value["skills"].map(skill);
|
|
118
|
-
const namespace = value["workNamespace"];
|
|
119
|
-
if (typeof namespace !== "string" || namespace.length === 0) invalid("workNamespace", "must be a non-empty string");
|
|
120
|
-
const limit = (key: "maxParallelTools" | "stallLimit" | "outputRecoveryLimit", check: (n: number) => void): number => {
|
|
121
|
-
const n = value[key];
|
|
122
|
-
if (typeof n !== "number") invalid(key, "must be a number");
|
|
123
|
-
try { check(n); } catch (error) { invalid(key, error instanceof Error ? `is out of range: ${error.message}` : "is out of range"); }
|
|
124
|
-
return n;
|
|
125
|
-
};
|
|
126
|
-
const maxParallelTools = limit("maxParallelTools", n => schedulingWindow([], 0, n));
|
|
127
|
-
const stallLimit = limit("stallLimit", n => stallDecision(emptyStallState(), n));
|
|
128
|
-
const outputRecoveryLimit = limit("outputRecoveryLimit", validateOutputRecoveryLimit);
|
|
129
|
-
const policy = value["contextPolicy"];
|
|
130
|
-
if (!isContextPolicy(policy)) invalid("contextPolicy", "is not a known policy");
|
|
131
|
-
|
|
132
|
-
const context: SessionContext = {
|
|
133
|
-
frozen: { system, tools }, gates: gateMap, shellCheckpoints: flag("shellCheckpoints"),
|
|
134
|
-
skills, strictVerification: flag("strictVerification"), workingSet: flag("workingSet"),
|
|
135
|
-
workNamespace: namespace, maxParallelTools, contextPolicy: policy, stallLimit, outputRecoveryLimit,
|
|
136
|
-
};
|
|
137
|
-
if (value["acceptanceContract"] !== undefined) {
|
|
138
|
-
try { context.acceptanceContract = parseAcceptanceContract(value["acceptanceContract"]); }
|
|
139
|
-
catch (error) { invalid("acceptanceContract", error instanceof Error ? `is invalid: ${error.message}` : "is invalid"); }
|
|
140
|
-
}
|
|
141
|
-
const mode = value["workflowMode"];
|
|
142
|
-
if (mode !== undefined) { if (mode !== "build" && mode !== "plan") invalid("workflowMode", "must be build or plan"); context.workflowMode = mode; }
|
|
143
|
-
const profile = value["agentProfile"];
|
|
144
|
-
if (profile !== undefined) {
|
|
145
|
-
const name = record(profile) ? profile["name"] : undefined, explicit = record(profile) ? profile["workflowExplicit"] : undefined;
|
|
146
|
-
if (!record(profile) || Object.keys(profile).length !== 2 || typeof name !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(name) || typeof explicit !== "boolean")
|
|
147
|
-
invalid("agentProfile", "must name a profile and whether its workflow was explicit");
|
|
148
|
-
context.agentProfile = { name, workflowExplicit: explicit };
|
|
149
|
-
}
|
|
150
|
-
if (value["workflowToolProfile"] !== undefined) {
|
|
151
|
-
if (value["workflowToolProfile"] !== "foundation") invalid("workflowToolProfile", "must be foundation");
|
|
152
|
-
context.workflowToolProfile = "foundation";
|
|
153
|
-
}
|
|
154
|
-
const boundary = value["boundary"];
|
|
155
|
-
if (boundary !== undefined) {
|
|
156
|
-
if (!record(boundary)) invalid("boundary", "must be an object");
|
|
157
|
-
const thresholds: Partial<BoundaryConfig> = {};
|
|
158
|
-
for (const [key, n] of Object.entries(boundary)) {
|
|
159
|
-
if (!(BOUNDARY_KEYS as readonly string[]).includes(key)) invalid(`boundary.${key}`, "is not a boundary threshold");
|
|
160
|
-
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0) invalid(`boundary.${key}`, "must be a non-negative integer");
|
|
161
|
-
thresholds[key as keyof BoundaryConfig] = n;
|
|
162
|
-
}
|
|
163
|
-
context.boundary = thresholds;
|
|
164
|
-
}
|
|
165
|
-
const guidance = value["guidance"], blind = value["blindChecks"];
|
|
166
|
-
if (guidance !== undefined) { if (typeof guidance !== "boolean") invalid("guidance", "must be a boolean"); context.guidance = guidance; }
|
|
167
|
-
if (blind !== undefined) { if (!isBlindChecks(blind)) invalid("blindChecks", "is not a frozen blind check set"); context.blindChecks = blind; }
|
|
168
|
-
return context;
|
|
169
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** D8: session configuration is projected from the append-only metadata journal. */
|
|
2
|
-
export interface PositionedState { node: number; at_event?: string }
|
|
3
|
-
|
|
4
|
-
export function stateAtPosition<T extends PositionedState>(
|
|
5
|
-
states: readonly T[], node: number, eventIds: readonly string[], atEvent?: string,
|
|
6
|
-
): T | null {
|
|
7
|
-
const positions = new Map(eventIds.map((id, index) => [id, index]));
|
|
8
|
-
const target = atEvent === undefined ? eventIds.length : positions.get(atEvent);
|
|
9
|
-
if (target === undefined) throw new Error(`NO_SUCH_EVENT: ${atEvent}`);
|
|
10
|
-
let result: T | null = null;
|
|
11
|
-
for (const state of states) {
|
|
12
|
-
if (state.node !== node) continue;
|
|
13
|
-
const position = state.at_event === undefined ? -1 : positions.get(state.at_event);
|
|
14
|
-
if (position !== undefined && position <= target) result = state;
|
|
15
|
-
}
|
|
16
|
-
return result;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function sameModel(a: { model: string; provider?: string }, b: { model: string; provider?: string }): boolean {
|
|
20
|
-
return a.model === b.model && a.provider !== undefined && a.provider === b.provider;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Selector values must remain unambiguous when two providers serve the same ID. */
|
|
24
|
-
export function modelKey(model: { provider: string; id: string }): string {
|
|
25
|
-
return `${model.provider}/${model.id}`;
|
|
26
|
-
}
|
package/src/core/session-tree.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session tree as data. Pure: restore points + parent pointers in,
|
|
3
|
-
* a forest out; plus a flattening that the UI and the line-mode printer
|
|
4
|
-
* share. Moved from src/tui/tree.ts when the view was rewritten (D20) so the
|
|
5
|
-
* shape of history does not depend on any renderer.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Same mark /back uses for a branch the session rewound away from. */
|
|
9
|
-
export const LEFT_BRANCH_MARK = "↩";
|
|
10
|
-
export const EMPTY_TREE_MESSAGE = "nothing to go back to -- no exchange has completed yet";
|
|
11
|
-
|
|
12
|
-
export type TreeKind = "user" | "other";
|
|
13
|
-
export type TreeFilter = "user" | "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
14
|
-
|
|
15
|
-
export interface TreeNode {
|
|
16
|
-
readonly id: string;
|
|
17
|
-
readonly prompt: string;
|
|
18
|
-
readonly onPath: boolean;
|
|
19
|
-
readonly current: boolean;
|
|
20
|
-
readonly kind: TreeKind;
|
|
21
|
-
readonly children: readonly TreeNode[];
|
|
22
|
-
readonly category?: "tool" | "message" | "metadata";
|
|
23
|
-
readonly label?: string;
|
|
24
|
-
readonly labelAt?: string;
|
|
25
|
-
readonly at?: string;
|
|
26
|
-
readonly content?: string;
|
|
27
|
-
/** Exact event position; user messages navigate before themselves. */
|
|
28
|
-
readonly target?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A restore point as the log already knows it. parent pointers are a
|
|
33
|
-
* separate map so this file never imports store.
|
|
34
|
-
*/
|
|
35
|
-
export interface TreePoint {
|
|
36
|
-
readonly node: number;
|
|
37
|
-
readonly atEvent: string;
|
|
38
|
-
readonly prompt: string;
|
|
39
|
-
readonly onPath: boolean;
|
|
40
|
-
readonly kind?: TreeKind;
|
|
41
|
-
readonly category?: "tool" | "message" | "metadata";
|
|
42
|
-
readonly label?: string;
|
|
43
|
-
readonly labelAt?: string;
|
|
44
|
-
readonly at?: string;
|
|
45
|
-
readonly content?: string;
|
|
46
|
-
readonly target?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type NodeParent = { readonly node: number; readonly at_event?: string } | null;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Restore points plus parent pointers → the tree the view paints.
|
|
53
|
-
*
|
|
54
|
-
* Same-node exchanges stay a chain. A child node whose parent.at_event
|
|
55
|
-
* matches a point hangs off that point as a sibling of the continuation,
|
|
56
|
-
* which is how a rewind becomes a visible fork instead of a missing row.
|
|
57
|
-
*/
|
|
58
|
-
export function treeFromPoints(
|
|
59
|
-
points: readonly TreePoint[],
|
|
60
|
-
parents: ReadonlyMap<number, NodeParent>,
|
|
61
|
-
currentNode: number,
|
|
62
|
-
): TreeNode[] {
|
|
63
|
-
const byNode = new Map<number, TreePoint[]>();
|
|
64
|
-
for (const p of points) {
|
|
65
|
-
const list = byNode.get(p.node) ?? [];
|
|
66
|
-
list.push(p);
|
|
67
|
-
byNode.set(p.node, list);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const lastHere = [...points].reverse().find((p) => p.onPath && p.node === currentNode);
|
|
71
|
-
const lastOnPath = lastHere ?? [...points].reverse().find((p) => p.onPath);
|
|
72
|
-
|
|
73
|
-
function toNode(p: TreePoint, children: readonly TreeNode[]): TreeNode {
|
|
74
|
-
return {
|
|
75
|
-
...p,
|
|
76
|
-
id: `${p.node}:${p.atEvent}`,
|
|
77
|
-
prompt: p.prompt,
|
|
78
|
-
onPath: p.onPath,
|
|
79
|
-
current: lastOnPath !== undefined && lastOnPath.node === p.node && lastOnPath.atEvent === p.atEvent,
|
|
80
|
-
kind: p.kind ?? "user",
|
|
81
|
-
children,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Wire event chains and branch edges separately. Recursing once per event
|
|
86
|
-
// overflows the JS stack on otherwise valid long sessions.
|
|
87
|
-
type MutableNode = TreeNode & { children: TreeNode[] };
|
|
88
|
-
const chains = new Map<number, MutableNode[]>();
|
|
89
|
-
const events = new Map<string, MutableNode>();
|
|
90
|
-
for (const [id, pts] of byNode) {
|
|
91
|
-
const nodes = pts.map(p => toNode(p, []) as MutableNode);
|
|
92
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
93
|
-
events.set(nodes[i]!.id, nodes[i]!);
|
|
94
|
-
if (nodes[i + 1]) nodes[i]!.children.push(nodes[i + 1]!);
|
|
95
|
-
}
|
|
96
|
-
chains.set(id, nodes);
|
|
97
|
-
}
|
|
98
|
-
const forest: TreeNode[] = [];
|
|
99
|
-
for (const [id, nodes] of [...chains].sort(([a], [b]) => a - b)) {
|
|
100
|
-
const first = nodes[0];
|
|
101
|
-
if (!first) continue;
|
|
102
|
-
let parent = parents.get(id);
|
|
103
|
-
const visited = new Set<number>([id]);
|
|
104
|
-
while (parent && !chains.get(parent.node)?.length && !visited.has(parent.node)) {
|
|
105
|
-
visited.add(parent.node); parent = parents.get(parent.node);
|
|
106
|
-
}
|
|
107
|
-
const anchor = parent && !visited.has(parent.node) ? events.get(`${parent.node}:${parent.at_event}`) ?? chains.get(parent.node)?.at(-1) : undefined;
|
|
108
|
-
if (anchor) anchor.children.push(first); else forest.push(first);
|
|
109
|
-
}
|
|
110
|
-
return forest;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
export interface TreeRow { readonly node: TreeNode; readonly depth: number; readonly branch: string }
|
|
115
|
-
|
|
116
|
-
/** Depth-first rows with connector prefixes; iterative so a 10k-event session cannot overflow the stack. */
|
|
117
|
-
export function flattenTree(nodes: readonly TreeNode[], filter: TreeFilter = "all"): TreeRow[] {
|
|
118
|
-
const rows: TreeRow[] = [];
|
|
119
|
-
const stack = visibleTree(nodes, filter).map((node, i, all) => ({ node, prefix: "", root: true, last: i === all.length - 1 })).reverse();
|
|
120
|
-
while (stack.length) {
|
|
121
|
-
const { node, prefix, root, last } = stack.pop()!;
|
|
122
|
-
rows.push({ node, depth: prefix.length / 3, branch: root ? "" : prefix + (last ? "└─ " : "├─ ") });
|
|
123
|
-
const next = root ? "" : prefix + (last ? " " : "│ ");
|
|
124
|
-
const children = node.children;
|
|
125
|
-
for (let i = children.length - 1; i >= 0; i--) stack.push({ node: children[i]!, prefix: next.length > 30 ? next.slice(-30) : next, root: false, last: i === children.length - 1 });
|
|
126
|
-
}
|
|
127
|
-
return rows;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Filtering hides a node but keeps its descendants: they re-attach to the
|
|
132
|
-
* nearest visible ancestor, so a "user only" view is still a tree.
|
|
133
|
-
*/
|
|
134
|
-
export function visibleTree(nodes: readonly TreeNode[], filter: TreeFilter): TreeNode[] {
|
|
135
|
-
if (filter === "all") return [...nodes];
|
|
136
|
-
const out: TreeNode[] = [];
|
|
137
|
-
const stack = [...nodes].reverse().map(node => ({ node, into: out }));
|
|
138
|
-
while (stack.length) {
|
|
139
|
-
const { node: n, into } = stack.pop()!;
|
|
140
|
-
const include = filter === "user" || filter === "user-only" ? n.kind === "user" :
|
|
141
|
-
filter === "labeled-only" ? Boolean(n.label) : filter === "no-tools" ? n.category !== "tool" && n.category !== "metadata" : n.category !== "metadata";
|
|
142
|
-
const children: TreeNode[] = [];
|
|
143
|
-
if (include) into.push({ ...n, children });
|
|
144
|
-
for (let i = n.children.length - 1; i >= 0; i--) stack.push({ node: n.children[i]!, into: include ? children : into });
|
|
145
|
-
}
|
|
146
|
-
return out;
|
|
147
|
-
}
|
package/src/core/settings.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { THINKING_LEVELS, asThinkingLevel, type ThinkingLevel } from "./thinking.ts";
|
|
2
|
-
import { packageDeclaration, type PackageDeclaration } from "./packages.ts";
|
|
3
|
-
|
|
4
|
-
/** Validated configuration. D19: display policy never mutates the log. */
|
|
5
|
-
export interface Settings {
|
|
6
|
-
readonly theme?: string;
|
|
7
|
-
/** Presentation only: no animation is allowed to advance a log or block input. */
|
|
8
|
-
readonly animations?: boolean;
|
|
9
|
-
readonly thinking?: string;
|
|
10
|
-
/** Look for a newer published xeno once a day and say so (docs/tasks/feat-update-check.md). Default on. */
|
|
11
|
-
readonly updateCheck?: boolean;
|
|
12
|
-
readonly hideThinkingBlock?: boolean;
|
|
13
|
-
readonly showCacheMissNotices?: boolean;
|
|
14
|
-
readonly defaultProvider?: string;
|
|
15
|
-
readonly defaultModel?: string;
|
|
16
|
-
readonly defaultThinking?: string;
|
|
17
|
-
readonly modelThinkingLevels?: Readonly<Record<string, ThinkingLevel>>;
|
|
18
|
-
readonly scopedModels?: readonly string[];
|
|
19
|
-
/** provider/model keys, most recent first; picker ordering and startup fallback for older settings. */
|
|
20
|
-
readonly recentModels?: readonly string[];
|
|
21
|
-
readonly steeringMode?: "all" | "one-at-a-time";
|
|
22
|
-
readonly followUpMode?: "all" | "one-at-a-time";
|
|
23
|
-
readonly tuiMode?: "auto" | "regular" | "fullscreen";
|
|
24
|
-
readonly fullscreenExitOutput?: "transcript" | "resume-hint";
|
|
25
|
-
readonly fullscreenScrollbar?: "auto" | "always" | "hidden";
|
|
26
|
-
readonly fullscreenCopyOnSelect?: boolean;
|
|
27
|
-
readonly quietStartup?: boolean;
|
|
28
|
-
readonly doubleEscapeAction?: "tree" | "fork" | "none";
|
|
29
|
-
readonly treeFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
30
|
-
readonly externalEditor?: string;
|
|
31
|
-
readonly skills?: readonly string[];
|
|
32
|
-
readonly prompts?: readonly string[];
|
|
33
|
-
readonly themes?: readonly string[];
|
|
34
|
-
readonly extensions?: readonly string[];
|
|
35
|
-
readonly packages?: readonly PackageDeclaration[];
|
|
36
|
-
readonly enableSkillCommands?: boolean;
|
|
37
|
-
readonly defaultProjectTrust?: "ask" | "always" | "never";
|
|
38
|
-
readonly terminal?: { readonly showImages?: boolean; readonly imageWidthCells?: number;
|
|
39
|
-
/** How pictures are drawn; `auto` names them instead when the terminal can only draw block characters. */
|
|
40
|
-
readonly imageProtocol?: "auto" | "kitty" | "sixel" | "blocks" };
|
|
41
|
-
readonly images?: { readonly autoResize?: boolean; readonly blockImages?: boolean };
|
|
42
|
-
readonly markdown?: { readonly mermaid?: "off" | "final" | "streaming" };
|
|
43
|
-
}
|
|
44
|
-
export const SETTINGS_KEYS = ["theme", "animations", "thinking", "updateCheck", "hideThinkingBlock", "showCacheMissNotices", "defaultProvider", "defaultModel", "defaultThinking",
|
|
45
|
-
"modelThinkingLevels", "scopedModels", "recentModels", "steeringMode", "followUpMode", "tuiMode", "fullscreenExitOutput", "fullscreenScrollbar",
|
|
46
|
-
"fullscreenCopyOnSelect", "quietStartup", "doubleEscapeAction", "treeFilterMode", "externalEditor", "terminal", "images", "markdown",
|
|
47
|
-
"skills", "prompts", "themes", "extensions", "packages", "enableSkillCommands", "defaultProjectTrust"] as const;
|
|
48
|
-
export type SettingsKey = (typeof SETTINGS_KEYS)[number];
|
|
49
|
-
export interface SettingsLayers { readonly builtin: Settings; readonly user?: Settings; readonly project?: Settings; readonly cli?: Settings }
|
|
50
|
-
export interface SettingsParse { readonly settings: Settings; readonly diagnostics: readonly string[] }
|
|
51
|
-
type Rule = { readonly kind: "boolean" | "string" | "strings" } | { readonly kind: "enum"; readonly values: readonly string[] } |
|
|
52
|
-
{ readonly kind: "integer"; readonly min: number; readonly max: number; readonly auto?: boolean };
|
|
53
|
-
const levels: Rule = { kind: "enum", values: THINKING_LEVELS };
|
|
54
|
-
const delivery: Rule = { kind: "enum", values: ["all", "one-at-a-time"] };
|
|
55
|
-
const rules: Readonly<Record<string, Rule>> = {
|
|
56
|
-
theme: { kind: "string" }, animations: { kind: "boolean" }, thinking: levels, updateCheck: { kind: "boolean" },
|
|
57
|
-
hideThinkingBlock: { kind: "boolean" }, showCacheMissNotices: { kind: "boolean" },
|
|
58
|
-
defaultProvider: { kind: "string" }, defaultModel: { kind: "string" }, defaultThinking: levels,
|
|
59
|
-
scopedModels: { kind: "strings" }, recentModels: { kind: "strings" }, steeringMode: delivery, followUpMode: delivery,
|
|
60
|
-
tuiMode: { kind: "enum", values: ["auto", "regular", "fullscreen"] },
|
|
61
|
-
fullscreenExitOutput: { kind: "enum", values: ["transcript", "resume-hint"] },
|
|
62
|
-
fullscreenScrollbar: { kind: "enum", values: ["auto", "always", "hidden"] },
|
|
63
|
-
fullscreenCopyOnSelect: { kind: "boolean" }, quietStartup: { kind: "boolean" },
|
|
64
|
-
doubleEscapeAction: { kind: "enum", values: ["tree", "fork", "none"] },
|
|
65
|
-
treeFilterMode: { kind: "enum", values: ["default", "no-tools", "user-only", "labeled-only", "all"] },
|
|
66
|
-
externalEditor: { kind: "string" },
|
|
67
|
-
"terminal.showImages": { kind: "boolean" }, "terminal.imageWidthCells": { kind: "integer", min: 1, max: 240 },
|
|
68
|
-
"terminal.imageProtocol": { kind: "enum", values: ["auto", "kitty", "sixel", "blocks"] },
|
|
69
|
-
"images.autoResize": { kind: "boolean" }, "images.blockImages": { kind: "boolean" },
|
|
70
|
-
"markdown.mermaid": { kind: "enum", values: ["off", "final", "streaming"] },
|
|
71
|
-
skills: { kind: "strings" }, prompts: { kind: "strings" }, themes: { kind: "strings" }, extensions: { kind: "strings" },
|
|
72
|
-
enableSkillCommands: { kind: "boolean" }, defaultProjectTrust: { kind: "enum", values: ["ask", "always", "never"] },
|
|
73
|
-
};
|
|
74
|
-
/** One catalog supplies menu values and command validation. */
|
|
75
|
-
export const DISPLAY_SETTINGS = [
|
|
76
|
-
{ id: "animations", label: "Animations", fallback: "on" },
|
|
77
|
-
{ id: "hideThinkingBlock", label: "Hide thinking blocks", fallback: "on" },
|
|
78
|
-
{ id: "showCacheMissNotices", label: "Cache miss notices", fallback: "off" },
|
|
79
|
-
{ id: "enableSkillCommands", label: "Skill commands", fallback: "on" },
|
|
80
|
-
{ id: "defaultProjectTrust", label: "Default project trust", fallback: "ask" },
|
|
81
|
-
{ id: "updateCheck", label: "Check for updates", fallback: "on" },
|
|
82
|
-
{ id: "terminal.showImages", label: "Show images", fallback: "on" },
|
|
83
|
-
{ id: "terminal.imageProtocol", label: "Image protocol", fallback: "auto" },
|
|
84
|
-
{ id: "terminal.imageWidthCells", label: "Image width", fallback: "60" },
|
|
85
|
-
{ id: "images.autoResize", label: "Resize images", fallback: "on" },
|
|
86
|
-
{ id: "images.blockImages", label: "Block image transmission", fallback: "off" },
|
|
87
|
-
{ id: "markdown.mermaid", label: "Mermaid diagrams", fallback: "streaming" },
|
|
88
|
-
{ id: "tuiMode", label: "TUI mode", fallback: "fullscreen" },
|
|
89
|
-
{ id: "fullscreenExitOutput", label: "Fullscreen exit output", fallback: "resume-hint" },
|
|
90
|
-
{ id: "fullscreenScrollbar", label: "Fullscreen scrollbar", fallback: "hidden" },
|
|
91
|
-
{ id: "fullscreenCopyOnSelect", label: "Copy on select", fallback: "on" },
|
|
92
|
-
{ id: "quietStartup", label: "Quiet startup", fallback: "off" },
|
|
93
|
-
{ id: "doubleEscapeAction", label: "Double Escape", fallback: "tree" },
|
|
94
|
-
{ id: "treeFilterMode", label: "Tree filter", fallback: "default" },
|
|
95
|
-
] as const;
|
|
96
|
-
/** Only fields with a real picker may be suggested without a value. Raw config keys remain writable explicitly. */
|
|
97
|
-
export function interactiveSettingKeys(): string[] {
|
|
98
|
-
const pickers = ["theme", "thinking", "defaultModel", "modelThinkingLevels"];
|
|
99
|
-
return [...new Set([...pickers, ...SETTINGS_KEYS, ...DISPLAY_SETTINGS.map(item => item.id)])]
|
|
100
|
-
.filter(key => pickers.includes(key) || settingValues(key).length > 0);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function defaultSettings(): Settings { return { theme: "dark", thinking: "high" }; }
|
|
104
|
-
export function mergeSettings(layers: SettingsLayers): Settings {
|
|
105
|
-
const { defaultProjectTrust: _projectCannotGrantTrust, ...project } = layers.project ?? {};
|
|
106
|
-
const all = [layers.builtin, layers.user ?? {}, project, layers.cli ?? {}];
|
|
107
|
-
const merged: Settings = Object.assign({}, ...all);
|
|
108
|
-
return { ...merged,
|
|
109
|
-
...(all.some(s => s.terminal !== undefined) ? { terminal: Object.assign({}, ...all.map(s => s.terminal ?? {})) } : {}),
|
|
110
|
-
...(all.some(s => s.images !== undefined) ? { images: Object.assign({}, ...all.map(s => s.images ?? {})) } : {}),
|
|
111
|
-
...(all.some(s => s.markdown !== undefined) ? { markdown: Object.assign({}, ...all.map(s => s.markdown ?? {})) } : {}),
|
|
112
|
-
...(all.some(s => s.modelThinkingLevels !== undefined) ? { modelThinkingLevels: Object.assign({}, ...all.map(s => s.modelThinkingLevels ?? {})) } : {}),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export function settingsToWrite(explicit: Partial<Settings>): Record<string, unknown> {
|
|
116
|
-
return Object.fromEntries(SETTINGS_KEYS.filter(key => explicit[key] !== undefined).map(key => [key, explicit[key]]));
|
|
117
|
-
}
|
|
118
|
-
export function explicitSettings(raw: unknown, parsed: Settings): Partial<Settings> {
|
|
119
|
-
if (!isRecord(raw)) return {};
|
|
120
|
-
return Object.fromEntries(SETTINGS_KEYS.filter(key => (key in raw || Object.entries(aliases).some(([alias, canonical]) => canonical === key && alias in raw)) && parsed[key] !== undefined).map(key => [key, parsed[key]]));
|
|
121
|
-
}
|
|
122
|
-
const aliases: Readonly<Record<string, string>> = { defaultThinkingLevel: "defaultThinking", enabledModels: "scopedModels" };
|
|
123
|
-
export function parseSettings(json: unknown): SettingsParse {
|
|
124
|
-
if (json === null || json === undefined) return { settings: {}, diagnostics: [] };
|
|
125
|
-
if (!isRecord(json)) return { settings: {}, diagnostics: ["settings is not an object"] };
|
|
126
|
-
const out: Record<string, unknown> = {}, diagnostics: string[] = [];
|
|
127
|
-
for (const [rawKey, value] of Object.entries(json)) {
|
|
128
|
-
const key = aliases[rawKey] ?? rawKey;
|
|
129
|
-
if (rawKey !== key && key in json) continue;
|
|
130
|
-
if (value === undefined) continue;
|
|
131
|
-
if (key === "packages") {
|
|
132
|
-
if (Array.isArray(value) && value.every(packageDeclaration)) out[key] = value;
|
|
133
|
-
else diagnostics.push("settings.packages must contain source strings or package resource filters");
|
|
134
|
-
} else if (key === "modelThinkingLevels") {
|
|
135
|
-
if (!isRecord(value)) { diagnostics.push("settings.modelThinkingLevels must be an object"); continue; }
|
|
136
|
-
const mapped: Record<string, ThinkingLevel> = {};
|
|
137
|
-
for (const [id, level] of Object.entries(value)) {
|
|
138
|
-
const parsed = asThinkingLevel(level);
|
|
139
|
-
if (id.includes("/") && !id.startsWith("/") && !id.endsWith("/") && parsed) mapped[id] = parsed;
|
|
140
|
-
else diagnostics.push("settings.modelThinkingLevels requires provider/model identities and supported thinking names");
|
|
141
|
-
}
|
|
142
|
-
out[key] = mapped;
|
|
143
|
-
} else if (key === "terminal" || key === "images" || key === "markdown") {
|
|
144
|
-
if (!isRecord(value)) { diagnostics.push("settings." + key + " must be an object"); continue; }
|
|
145
|
-
const nested: Record<string, unknown> = {};
|
|
146
|
-
for (const [subkey, subvalue] of Object.entries(value)) validate(key + "." + subkey, subvalue, nested, diagnostics, subkey);
|
|
147
|
-
out[key] = nested;
|
|
148
|
-
} else validate(key, value, out, diagnostics, key);
|
|
149
|
-
}
|
|
150
|
-
return { settings: out as Settings, diagnostics };
|
|
151
|
-
}
|
|
152
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
153
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
154
|
-
}
|
|
155
|
-
function validate(key: string, raw: unknown, out: Record<string, unknown>, diagnostics: string[], field: string): void {
|
|
156
|
-
const rule = rules[key];
|
|
157
|
-
if (!rule) { diagnostics.push("unknown setting: " + key); return; }
|
|
158
|
-
const value = rule.kind === "strings" && typeof raw === "string" ? raw.split(",").map(v => v.trim()).filter(Boolean) : raw;
|
|
159
|
-
const valid = rule.kind === "string" ? typeof value === "string" && value.length > 0 :
|
|
160
|
-
rule.kind === "boolean" ? typeof value === "boolean" :
|
|
161
|
-
rule.kind === "strings" ? Array.isArray(value) && value.every(v => typeof v === "string") :
|
|
162
|
-
rule.kind === "enum" ? typeof value === "string" && rule.values.includes(value) :
|
|
163
|
-
rule.kind === "integer" && ((rule.auto && value === "auto") || (typeof value === "number" && Number.isSafeInteger(value) && value >= rule.min && value <= rule.max));
|
|
164
|
-
if (valid) out[field] = value;
|
|
165
|
-
else diagnostics.push("settings." + key + " must be " + (rule.kind === "enum" ? rule.values.join("|") : rule.kind === "integer" ? rule.min + ".." + rule.max + (rule.auto ? " or auto" : "") : rule.kind));
|
|
166
|
-
}
|
|
167
|
-
export function settingValues(key: string): string[] {
|
|
168
|
-
// `auto` is the unset default and stays valid in settings.json; cycling it in
|
|
169
|
-
// the menu would be a press that visibly does nothing on a terminal where
|
|
170
|
-
// auto already means fullscreen, so the menu toggles the two real screens.
|
|
171
|
-
if (key === "tuiMode") return ["regular", "fullscreen"];
|
|
172
|
-
const rule = rules[key];
|
|
173
|
-
if (rule?.kind === "enum") return [...rule.values];
|
|
174
|
-
if (rule?.kind === "boolean") return ["on", "off"];
|
|
175
|
-
if (rule?.kind === "integer") return [...(rule.auto ? ["auto"] : []), ...Array.from({ length: rule.max - rule.min + 1 }, (_, i) => String(i + rule.min))];
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
export function settingValue(settings: Settings, path: string, fallback: string): string {
|
|
179
|
-
const [key, subkey] = path.split(".");
|
|
180
|
-
const object = settings[key as SettingsKey];
|
|
181
|
-
const raw = subkey && isRecord(object) ? (object as Record<string, unknown>)[subkey] : object;
|
|
182
|
-
return typeof raw === "boolean" ? raw ? "on" : "off" : raw === undefined ? fallback : String(raw);
|
|
183
|
-
}
|
|
184
|
-
export function settingPatch(field: string, text: string): SettingsParse {
|
|
185
|
-
const key = Object.keys(rules).find(k => k.toLowerCase() === field.toLowerCase());
|
|
186
|
-
if (!key) return { settings: {}, diagnostics: ["unknown setting: " + field] };
|
|
187
|
-
const rule = rules[key]!;
|
|
188
|
-
const value = rule.kind === "boolean" ? ["on", "true", "1"].includes(text) ? true : ["off", "false", "0"].includes(text) ? false : text :
|
|
189
|
-
rule.kind === "integer" && text !== "auto" ? Number(text) : text;
|
|
190
|
-
const [first, second] = key.split(".");
|
|
191
|
-
return parseSettings(second ? { [first!]: { [second]: value } } : { [key]: value });
|
|
192
|
-
}
|