@visual-z/xeno 0.8.3 → 0.9.0

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.
Files changed (348) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2066
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1713
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3001
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. 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
- }
@@ -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
- }
@@ -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
- }