@visual-z/xeno 0.2.0 → 0.3.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 (259) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +52 -9
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/cli.ts +10 -11
  8. package/src/apps/commands.ts +38 -5
  9. package/src/apps/completions.ts +73 -19
  10. package/src/apps/image-draft.ts +46 -0
  11. package/src/apps/project-trust.ts +2 -42
  12. package/src/apps/provider-connect.ts +346 -0
  13. package/src/apps/render.ts +22 -47
  14. package/src/apps/resources.ts +5 -2
  15. package/src/apps/shell.ts +298 -1526
  16. package/src/apps/startup-input.ts +130 -0
  17. package/src/apps/startup.ts +143 -115
  18. package/src/apps/ui/app.ts +1383 -0
  19. package/src/apps/ui/extension-host.ts +194 -0
  20. package/src/apps/ui/image-cache.ts +78 -0
  21. package/src/apps/ui/palette.ts +88 -0
  22. package/src/apps/ui/project.ts +127 -0
  23. package/src/apps/ui/standalone.ts +146 -0
  24. package/src/apps/ui/status-model.ts +68 -0
  25. package/src/apps/ui/surface.ts +84 -0
  26. package/src/apps/ui/theme.ts +86 -0
  27. package/src/core/acceptance.ts +107 -0
  28. package/src/core/action-defaults.ts +47 -295
  29. package/src/core/benchmark.ts +2 -2
  30. package/src/core/branch-receipt.ts +24 -0
  31. package/src/core/checkpoints.ts +64 -0
  32. package/src/core/churn.ts +2 -0
  33. package/src/core/command-artifacts.ts +94 -0
  34. package/src/core/command-contract.ts +35 -0
  35. package/src/core/completion-review.ts +21 -5
  36. package/src/core/context-detail.ts +37 -0
  37. package/src/core/context-policy.ts +74 -1
  38. package/src/core/continuation.ts +17 -2
  39. package/src/core/derive.ts +29 -3
  40. package/src/core/draft-images.ts +56 -0
  41. package/src/core/environment.ts +145 -0
  42. package/src/core/evaluation-coverage.ts +74 -0
  43. package/src/core/event-validation.ts +80 -5
  44. package/src/core/events.ts +97 -3
  45. package/src/core/evidence-links.ts +7 -0
  46. package/src/core/evidence.ts +13 -3
  47. package/src/core/execution.ts +8 -2
  48. package/src/core/experiment.ts +133 -0
  49. package/src/core/export.ts +9 -0
  50. package/src/core/failure-recovery.ts +173 -0
  51. package/src/core/file-precondition.ts +72 -0
  52. package/src/core/handoff.ts +65 -4
  53. package/src/core/history-query.ts +38 -2
  54. package/src/core/history.ts +38 -1
  55. package/src/core/integration-health.ts +34 -0
  56. package/src/core/keymap.ts +8 -20
  57. package/src/core/model-picker.ts +64 -0
  58. package/src/core/native-eval.ts +25 -0
  59. package/src/core/note-guidance.ts +34 -0
  60. package/src/core/observation-view.ts +49 -8
  61. package/src/core/permissions.ts +24 -2
  62. package/src/core/process-receipt.ts +105 -0
  63. package/src/core/process-state.ts +37 -0
  64. package/src/core/project.ts +45 -18
  65. package/src/core/prompt-policy.ts +37 -0
  66. package/src/core/query-eval-contract.ts +14 -0
  67. package/src/core/read-observation.ts +82 -0
  68. package/src/core/recovery-corpus.ts +17 -0
  69. package/src/core/recovery-evaluation.ts +60 -0
  70. package/src/core/recovery-sources.ts +74 -0
  71. package/src/core/runtime-target.ts +94 -0
  72. package/src/core/search-observation.ts +97 -0
  73. package/src/core/session-tree.ts +147 -0
  74. package/src/core/settings.ts +26 -10
  75. package/src/core/skills.ts +44 -0
  76. package/src/core/sql-query.ts +211 -0
  77. package/src/core/theme.ts +36 -58
  78. package/src/core/tool-contract.ts +51 -2
  79. package/src/core/tool-discovery.ts +87 -0
  80. package/src/core/tool-failure.ts +60 -0
  81. package/src/core/tool-mode-protocol.ts +25 -0
  82. package/src/core/transcript.ts +19 -4
  83. package/src/core/trial-lifecycle.ts +14 -0
  84. package/src/core/tui-mode.ts +29 -0
  85. package/src/core/update-check.ts +97 -0
  86. package/src/core/version.ts +55 -0
  87. package/src/core/work-state.ts +49 -3
  88. package/src/core/working-set.ts +91 -0
  89. package/src/runtime/auth.ts +2 -2
  90. package/src/runtime/branch-jobs.ts +36 -0
  91. package/src/runtime/clipboard.ts +15 -4
  92. package/src/runtime/command-artifacts.ts +64 -0
  93. package/src/runtime/environment-tools.ts +60 -0
  94. package/src/runtime/evidence-notes.ts +4 -2
  95. package/src/runtime/exec-sessions.ts +206 -61
  96. package/src/runtime/execution-control.ts +1 -0
  97. package/src/runtime/extension-ui.ts +22 -30
  98. package/src/runtime/extensions.ts +95 -16
  99. package/src/runtime/harness-comparison.ts +27 -7
  100. package/src/runtime/harness-eval.ts +460 -0
  101. package/src/runtime/history-cache.ts +57 -0
  102. package/src/runtime/history-tools.ts +62 -12
  103. package/src/runtime/integrations.ts +210 -0
  104. package/src/runtime/loop.ts +1021 -268
  105. package/src/runtime/model-discovery.ts +208 -0
  106. package/src/runtime/native-eval-fixtures.ts +33 -0
  107. package/src/runtime/native-eval.ts +143 -0
  108. package/src/runtime/oauth.ts +1 -1
  109. package/src/runtime/provider-connect.ts +109 -0
  110. package/src/runtime/provider-presets.ts +87 -0
  111. package/src/runtime/query-eval-verifier.ts +28 -0
  112. package/src/runtime/recovery-coverage.ts +71 -0
  113. package/src/runtime/recovery-evidence.ts +76 -0
  114. package/src/runtime/recovery-fixtures.ts +116 -0
  115. package/src/runtime/runtime-target.ts +149 -0
  116. package/src/runtime/tool-broker.ts +137 -0
  117. package/src/runtime/tool-discovery.ts +57 -0
  118. package/src/runtime/tool-mode-eval.ts +146 -0
  119. package/src/runtime/tool-queue.ts +77 -0
  120. package/src/runtime/tools.ts +628 -183
  121. package/src/runtime/trial-control.ts +45 -0
  122. package/src/runtime/update-check.ts +47 -0
  123. package/src/runtime/window-trial.ts +2 -1
  124. package/src/runtime/working-notes.ts +33 -3
  125. package/src/runtime/workspace-lifecycle.ts +97 -0
  126. package/src/store/acceptance.ts +15 -0
  127. package/src/store/checkpoint-history.ts +43 -0
  128. package/src/store/command-artifacts.ts +96 -0
  129. package/src/store/command.ts +65 -37
  130. package/src/store/file-checkpoints.ts +104 -0
  131. package/src/store/file-index.ts +13 -1
  132. package/src/store/git-checkpoints.ts +67 -0
  133. package/src/store/observations.ts +61 -20
  134. package/src/store/process-receipts.ts +34 -0
  135. package/src/store/query-source.ts +13 -8
  136. package/src/store/recovery.ts +2 -3
  137. package/src/store/replay.ts +16 -4
  138. package/src/store/runtime-target.ts +64 -0
  139. package/src/store/search.ts +171 -132
  140. package/src/store/session-archive.ts +4 -0
  141. package/src/store/source-cancellation.ts +6 -0
  142. package/src/store/update-cache.ts +36 -0
  143. package/src/store/workspace-observation.ts +113 -0
  144. package/src/tui/ansi-spans.ts +126 -0
  145. package/src/tui/style.ts +19 -16
  146. package/src/tui/ui/atomic-draft.ts +121 -0
  147. package/src/tui/ui/canvas.ts +96 -0
  148. package/src/tui/ui/colors.ts +70 -0
  149. package/src/tui/ui/composer.ts +199 -0
  150. package/src/tui/ui/dialog.ts +379 -0
  151. package/src/tui/ui/draft.ts +23 -0
  152. package/src/tui/ui/edge.ts +186 -0
  153. package/src/tui/ui/image-size.ts +40 -0
  154. package/src/tui/ui/ink.ts +210 -0
  155. package/src/tui/ui/items.ts +40 -0
  156. package/src/tui/ui/keys.ts +40 -0
  157. package/src/tui/ui/layout.ts +74 -0
  158. package/src/tui/ui/list-model.ts +86 -0
  159. package/src/tui/ui/list-view.ts +172 -0
  160. package/src/tui/ui/mascot.ts +67 -0
  161. package/src/tui/ui/messages.ts +518 -0
  162. package/src/tui/ui/motion.ts +28 -0
  163. package/src/tui/ui/paths.ts +10 -0
  164. package/src/tui/ui/pictures.ts +78 -0
  165. package/src/tui/ui/screen.ts +421 -0
  166. package/src/tui/ui/status.ts +271 -0
  167. package/src/tui/ui/styled.ts +85 -0
  168. package/src/tui/ui/testing.ts +5 -0
  169. package/src/tui/ui/tool-card.ts +150 -0
  170. package/src/apps/extension-ui.ts +0 -330
  171. package/src/apps/resource-browser.ts +0 -75
  172. package/src/apps/session-browser.ts +0 -113
  173. package/src/apps/session-picker.ts +0 -55
  174. package/src/tui/ansi.ts +0 -67
  175. package/src/tui/autocomplete.ts +0 -29
  176. package/src/tui/chat-viewport.ts +0 -47
  177. package/src/tui/complete.ts +0 -87
  178. package/src/tui/editor.ts +0 -940
  179. package/src/tui/host.ts +0 -264
  180. package/src/tui/input.ts +0 -75
  181. package/src/tui/keys.ts +0 -378
  182. package/src/tui/markdown.ts +0 -17
  183. package/src/tui/render.ts +0 -153
  184. package/src/tui/select.ts +0 -258
  185. package/src/tui/settings.ts +0 -44
  186. package/src/tui/statusline.ts +0 -162
  187. package/src/tui/system-theme.ts +0 -700
  188. package/src/tui/terminal.ts +0 -30
  189. package/src/tui/text.ts +0 -52
  190. package/src/tui/toolview.ts +0 -386
  191. package/src/tui/transcript.ts +0 -123
  192. package/src/tui/tree.ts +0 -471
  193. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  194. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  195. package/src/vendor/pi-tui/VENDOR.md +0 -25
  196. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  197. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  198. package/src/vendor/pi-tui/colors.ts +0 -367
  199. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  200. package/src/vendor/pi-tui/components/box.ts +0 -166
  201. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  202. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  203. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  204. package/src/vendor/pi-tui/components/image.ts +0 -127
  205. package/src/vendor/pi-tui/components/input.ts +0 -494
  206. package/src/vendor/pi-tui/components/loader.ts +0 -101
  207. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  208. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  209. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  210. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  211. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  212. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  213. package/src/vendor/pi-tui/components/stack.ts +0 -154
  214. package/src/vendor/pi-tui/components/text.ts +0 -107
  215. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  216. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  217. package/src/vendor/pi-tui/editor-component.ts +0 -74
  218. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  219. package/src/vendor/pi-tui/index.ts +0 -186
  220. package/src/vendor/pi-tui/keybindings.ts +0 -320
  221. package/src/vendor/pi-tui/keys.ts +0 -1401
  222. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  223. package/src/vendor/pi-tui/latex.ts +0 -1506
  224. package/src/vendor/pi-tui/layout-node.ts +0 -51
  225. package/src/vendor/pi-tui/layout.ts +0 -449
  226. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  227. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  228. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  229. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  232. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  233. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  234. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  237. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  238. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  239. package/src/vendor/pi-tui/native/napi.h +0 -91
  240. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  241. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  244. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  245. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  246. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  247. package/src/vendor/pi-tui/native-platform.ts +0 -65
  248. package/src/vendor/pi-tui/oklab.ts +0 -233
  249. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  250. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  251. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  252. package/src/vendor/pi-tui/terminal.ts +0 -554
  253. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  254. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  255. package/src/vendor/pi-tui/tui.ts +0 -1493
  256. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  257. package/src/vendor/pi-tui/utils.ts +0 -1385
  258. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  259. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Log → status facts for the sidebar and top bar (docs/tasks/refactor-tui-dock-layout.md). Pure.
3
+ *
4
+ * Everything here is a projection of what the log already records: file
5
+ * counts come from the logged input of edit calls that succeeded, handoff
6
+ * entries from the derived preview (D8), background work from the processes
7
+ * and branches the log observed. Nothing reads model narration (D11).
8
+ */
9
+ import type { Handoff } from "../../core/handoff.ts";
10
+ import type { TranscriptItem } from "../../core/transcript.ts";
11
+ import { diffCounts } from "../../tui/ui/tool-card.ts";
12
+ import type { FileStatus, StatusModel } from "../../tui/ui/status.ts";
13
+
14
+ /** Files the session touched, most recent first. Counts only for files changed solely by edits (a write's prior content is unknown). */
15
+ export function fileChanges(items: readonly TranscriptItem[]): FileStatus[] {
16
+ const files = new Map<string, { edited: boolean; added: number; removed: number; rewritten: boolean; at: number }>();
17
+ items.forEach((item, at) => {
18
+ if (item.kind !== "tool" || !item.result?.ok) return;
19
+ const input = item.input !== null && typeof item.input === "object" ? item.input as Record<string, unknown> : {};
20
+ const path = typeof input["path"] === "string" ? input["path"] : undefined;
21
+ if (!path || !["read", "edit", "write"].includes(item.name)) return;
22
+ const entry = files.get(path) ?? { edited: false, added: 0, removed: 0, rewritten: false, at };
23
+ entry.at = at;
24
+ if (item.name === "edit") { const counts = diffCounts(item.input); entry.edited = true; entry.added += counts.added; entry.removed += counts.removed; }
25
+ if (item.name === "write") { entry.edited = true; entry.rewritten = true; }
26
+ files.set(path, entry);
27
+ });
28
+ return [...files.entries()].sort((a, b) => b[1].at - a[1].at)
29
+ .map(([path, entry]) => ({ path, edited: entry.edited, added: entry.rewritten ? 0 : entry.added, removed: entry.rewritten ? 0 : entry.removed }));
30
+ }
31
+
32
+ export function firstPrompt(items: readonly TranscriptItem[]): string | null {
33
+ const user = items.find(item => item.kind === "user" && item.text.trim());
34
+ return user?.kind === "user" ? user.text.trim() : null;
35
+ }
36
+
37
+ type Derived = Pick<StatusModel, "handoff" | "tasks" | "background">;
38
+
39
+ /** The parts of a handoff preview the status surfaces show. Each list keeps its most recent entries last. */
40
+ export function handoffStatus(handoff: Handoff): Derived {
41
+ const background = [
42
+ ...handoff.branches.filter(branch => branch.background && branch.resolution === null && !branch.handoff_ref).map(branch => `branch ${branch.node} · ${branch.purpose}`),
43
+ ...(handoff.processes ?? []).filter(process => process.observed === "running").map(process => process.command ?? `process ${process.session}`),
44
+ ];
45
+ const tasks = (handoff.state.work_items ?? []).filter(item => item.status === "active" || item.status === "blocked")
46
+ .map(item => ({ what: item.what, status: item.status === "blocked" ? "blocked" as const : "active" as const }));
47
+ return {
48
+ handoff: {
49
+ verified: handoff.state.completed.map(item => item.what),
50
+ inFlight: handoff.state.in_flight.map(item => item.what),
51
+ unverified: handoff.state.unverified_claims.map(item => item.what),
52
+ },
53
+ tasks, background,
54
+ };
55
+ }
56
+
57
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
58
+
59
+ /** "20:13" today, "Oct 5 20:13" on another day, in the given local offset. */
60
+ export function sessionClock(iso: string, now: Date, offsetMinutes = -now.getTimezoneOffset()): string | undefined {
61
+ const at = Date.parse(iso);
62
+ if (!Number.isFinite(at)) return undefined;
63
+ const local = (ms: number) => new Date(ms + offsetMinutes * 60_000);
64
+ const t = local(at), n = local(now.getTime());
65
+ const hhmm = `${String(t.getUTCHours()).padStart(2, "0")}:${String(t.getUTCMinutes()).padStart(2, "0")}`;
66
+ const sameDay = t.getUTCFullYear() === n.getUTCFullYear() && t.getUTCMonth() === n.getUTCMonth() && t.getUTCDate() === n.getUTCDate();
67
+ return sameDay ? hhmm : `${MONTHS[t.getUTCMonth()]} ${t.getUTCDate()} ${hhmm}`;
68
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * What command and turn code may ask of the interactive UI. shell.ts
3
+ * depends on this interface only; apps/ui/app.ts implements it. A null Ui
4
+ * means print/json/line mode, where every branch has a text fallback.
5
+ */
6
+ import type { ImageRef, UserInput } from "../../core/images.ts";
7
+ import type { Phase, QueueKind } from "../../core/queue.ts";
8
+ import type { PermissionRequest } from "../../core/permissions.ts";
9
+ import type { ApprovalChoice } from "../../runtime/tools.ts";
10
+ import type { LoopEvent } from "../../runtime/loop.ts";
11
+ import type { ExtensionUI } from "../../runtime/extension-ui.ts";
12
+ import type { ClipboardContent } from "../../runtime/clipboard.ts";
13
+ import type { CallbackServer, OAuthCredential, OAuthInteraction } from "../../runtime/oauth.ts";
14
+
15
+ export interface TurnSurface {
16
+ pushDelta(kind: "text" | "thinking", text: string): void;
17
+ discardStream(): void;
18
+ commitStream(): void;
19
+ syncTranscript(): Promise<void>;
20
+ pushProgress?(progress: NonNullable<LoopEvent["progress"]>): void;
21
+ pullQueued?(phase: Phase): Promise<UserInput | null>;
22
+ }
23
+
24
+ export interface Ui extends TurnSurface {
25
+ readonly extensionUi: ExtensionUI;
26
+ readonly signal: AbortSignal | undefined;
27
+ readonly queueLength: number;
28
+ sessionName: string | null;
29
+ oauthEffects: OAuthEffects;
30
+ clipboardWriter: (text: string) => boolean | Promise<boolean>;
31
+ clipboardReader: () => Promise<ClipboardContent>;
32
+
33
+ /** Notices and documents in the message stream. */
34
+ notice(text: string, tone?: "info" | "success" | "warning" | "error"): void;
35
+ showDocument(title: string, text: string): void;
36
+ toast(text: string, tone?: "info" | "success" | "warning" | "error"): void;
37
+ redraw(): void;
38
+
39
+ /** A masked single-line form; resolves undefined when cancelled. The value never reaches a renderable. */
40
+ askSecret(title: string, label: string, hint: string): Promise<string | undefined>;
41
+ confirm(title: string, message: string): Promise<boolean>;
42
+ /** A cancellable "waiting" dialog (OAuth browser / device code). Returns a closer. */
43
+ waiting(title: string, lines: readonly string[], onCancel: () => void, pasteLabel?: string, onPaste?: (value: string) => void): () => void;
44
+
45
+ openLogin(method?: "oauth" | "api_key"): void;
46
+ openLogout(): void;
47
+ openModel(): void;
48
+ openScopedModels(): void;
49
+ openThinking(): void;
50
+ openSettings(field?: string): void;
51
+ openResume(): Promise<void>;
52
+ openFork(): Promise<void>;
53
+ openTree(): Promise<void>;
54
+ openResources(): Promise<void>;
55
+ openTrust(): Promise<void>;
56
+ openAttachments(): void;
57
+
58
+ restoreDraft(text: string, images?: readonly ImageRef[]): void;
59
+ refreshResources(): Promise<void>;
60
+ applyConfiguration(): void;
61
+ followTheme(): void;
62
+ beginBash(command: string): void;
63
+ pushBash(chunk: Uint8Array): void;
64
+ ask(request: PermissionRequest): Promise<ApprovalChoice>;
65
+
66
+ runExtensionAction(action: () => void | Promise<void>): void;
67
+ runExtensionCommand(name: string, args: string): void;
68
+ runSessionTransition(action: () => Promise<void>): void;
69
+ deliverExtension(text: string, kind: QueueKind): void;
70
+ bindExtensionRegistry(): void;
71
+ requestShutdown(): void;
72
+ /** Abort the running turn (Esc semantics) without quitting. */
73
+ interrupt(): void;
74
+ /** Plain-text snapshot of the current frame (/debug). */
75
+ snapshot(): { width: number; height: number; lines: string[] };
76
+ }
77
+
78
+ /** Injectable OAuth side effects (browser, callback server, device flow, fetch). */
79
+ export interface OAuthEffects {
80
+ fetch: typeof globalThis.fetch;
81
+ openBrowser(url: string): void;
82
+ callback(options: { expectedState: string; host: string; port: number; path: string }): Promise<CallbackServer>;
83
+ device(interaction: OAuthInteraction): Promise<OAuthCredential>;
84
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * core Theme + the terminal's reported palette → the view's colours.
3
+ *
4
+ * Built-in appearances (dark, light, system/auto) take every colour from the
5
+ * terminal (tui/ui/ink.ts); their name only says which way to guess when the
6
+ * terminal does not answer the palette query. A custom theme file is an
7
+ * explicit choice and is drawn with its own colours; it paints a background
8
+ * only when it sets one (refactor-tui-terminal-colors).
9
+ */
10
+ import type { TerminalColors } from "@opentui/core";
11
+ import { isBuiltinTheme, type Theme } from "../../core/theme.ts";
12
+ import { colorsFromInks, rgba, type UiColors } from "../../tui/ui/colors.ts";
13
+ import { isHex, terminalInks, type InkSet, type InkSpec, type TerminalPalette } from "../../tui/ui/ink.ts";
14
+
15
+ /** OpenTUI's detection result → the 16-slot palette ink.ts expects, or null when incomplete. */
16
+ export function terminalPalette(colors: TerminalColors | null | undefined): TerminalPalette | null {
17
+ if (!colors) return null;
18
+ const slots = colors.palette.slice(0, 16).map(hex => normalizeHex(hex));
19
+ const foreground = normalizeHex(colors.defaultForeground), background = normalizeHex(colors.defaultBackground);
20
+ if (!foreground || !background || slots.length < 16 || slots.some(slot => !slot)) return null;
21
+ const hb = normalizeHex(colors.highlightBackground), hf = normalizeHex(colors.highlightForeground);
22
+ return { foreground, background, slots: slots as string[], ...(hb && hf ? { highlight: { background: hb, foreground: hf } } : {}) };
23
+ }
24
+
25
+ function normalizeHex(value: string | null | undefined): string | undefined {
26
+ if (!value) return undefined;
27
+ const hex = value.startsWith("#") ? value : `#${value}`;
28
+ return isHex(hex) ? hex.toLowerCase() : undefined;
29
+ }
30
+
31
+ function hint(theme: Theme): "dark" | "light" { return theme.name === "light" ? "light" : "dark"; }
32
+
33
+ /** "38;2;r;g;b" / "48;2;r;g;b" → "#rrggbb"; anything else is undefined. */
34
+ export function sgrHex(value: string | undefined): string | undefined {
35
+ const match = value ? /(?:^|;)[34]8;2;(\d+);(\d+);(\d+)/.exec(value) : null;
36
+ if (!match) return undefined;
37
+ return "#" + [match[1], match[2], match[3]].map(n => Math.max(0, Math.min(255, Number(n))).toString(16).padStart(2, "0")).join("");
38
+ }
39
+
40
+ export function uiColors(theme: Theme, palette: TerminalPalette | null = null): UiColors {
41
+ const terminal = colorsFromInks(terminalInks(palette, hint(theme)));
42
+ if (isBuiltinTheme(theme)) return terminal;
43
+ // A theme file's colours are the user's explicit choice; tokens it leaves
44
+ // out (or sets to the terminal default) still come from the terminal.
45
+ const pick = (...tokens: string[]) => { const hex = tokens.map(token => sgrHex(theme.colors[token])).find(Boolean); return hex ? rgba(hex) : undefined; };
46
+ return {
47
+ background: pick("background") ?? terminal.background,
48
+ text: pick("text") ?? terminal.text,
49
+ muted: pick("muted", "toolOutput") ?? terminal.muted,
50
+ edge: pick("border", "borderMuted") ?? terminal.edge,
51
+ line: pick("borderMuted", "border") ?? terminal.line,
52
+ accent: pick("accent", "mdLink") ?? terminal.accent,
53
+ success: pick("success", "toolDiffAdded") ?? terminal.success,
54
+ error: pick("error", "toolDiffRemoved") ?? terminal.error,
55
+ warning: pick("warning") ?? terminal.warning,
56
+ reasoning: pick("thinkingText") ?? terminal.reasoning,
57
+ code: pick("mdCode") ?? terminal.code,
58
+ selectionBg: pick("selectedBg") ?? terminal.selectionBg,
59
+ selectionFg: terminal.selectionFg,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * SGR tokens for the ANSI renderers (tool details, diffs, code highlighting)
65
+ * from the same terminal-derived inks, so text drawn through ANSI strings
66
+ * matches the view. Custom themes keep their own tokens.
67
+ */
68
+ export function styleTokens(theme: Theme, palette: TerminalPalette | null): Readonly<Record<string, string>> {
69
+ if (!isBuiltinTheme(theme)) return theme.colors;
70
+ const inks: InkSet = terminalInks(palette, hint(theme));
71
+ const sgr = (spec: InkSpec): string => spec.kind === "default" ? "" : spec.kind === "index"
72
+ ? String(spec.index < 8 ? 30 + spec.index : 90 + spec.index - 8)
73
+ : `38;2;${[1, 3, 5].map(i => Number.parseInt(spec.hex.slice(i, i + 2), 16)).join(";")}`;
74
+ const muted = sgr(inks.muted);
75
+ return {
76
+ ...theme.colors,
77
+ text: "", prompt: "", toolTitle: "",
78
+ muted, dim: muted, status: muted, toolOutput: muted, thinkingOff: muted, syntaxComment: muted,
79
+ error: sgr(inks.error), toolDiffRemoved: sgr(inks.error), toolDiffAdded: sgr(inks.success), success: sgr(inks.success),
80
+ warning: sgr(inks.warning), syntaxNumber: sgr(inks.warning),
81
+ accent: sgr(inks.accent), mdHeading: sgr(inks.accent), mdLink: sgr(inks.accent), syntaxFunction: sgr(inks.accent),
82
+ mdCode: sgr(inks.code), syntaxString: sgr(inks.success),
83
+ thinkingText: sgr(inks.reasoning), syntaxKeyword: sgr(inks.reasoning),
84
+ border: sgr(inks.line), borderMuted: sgr(inks.line),
85
+ };
86
+ }
@@ -0,0 +1,107 @@
1
+ /** O.11: declared criteria, command evidence and human acceptance are separate. */
2
+ import type { LogEvent } from "./events.ts";
3
+ import type { Evidence } from "./handoff.ts";
4
+ import { evidenceStale } from "./evidence.ts";
5
+ import { changedPaths } from "./checkpoints.ts";
6
+ import { parseRuntimeTarget, type RuntimeTarget, type TargetAcceptance } from "./runtime-target.ts";
7
+
8
+ export type AcceptanceCriterion = { id: string; description: string } &
9
+ ({ kind: "command"; command: string } | { kind: "human"; target?: string });
10
+ export type AcceptanceContract = { version: 1; id: string; scope?: "request" | "changes"; criteria: AcceptanceCriterion[];
11
+ source?: { path: string; sha256: string }; targets?: RuntimeTarget[] };
12
+ export type AcceptanceState = {
13
+ contract_id: string; contract_event: string; request: string; scope: "request" | "changes"; changed: boolean;
14
+ criteria: (AcceptanceCriterion & { status: "not_run" | "pass" | "fail" | "stale" | "awaiting_human" | "accepted"; evidence?: Evidence; accepted_event?: string; accepted_binding?: string })[];
15
+ targets?: TargetAcceptance[];
16
+ status: "needs_work" | "awaiting_human" | "verified" | "not_applicable";
17
+ };
18
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
19
+ const label = (value: unknown, max: number) => typeof value === "string" && value.trim().length > 0 && value.length <= max;
20
+
21
+ export function parseAcceptanceContract(value: unknown): AcceptanceContract {
22
+ if (!record(value) || value["version"] !== 1 || !label(value["id"], 80) ||
23
+ (value["scope"] !== undefined && value["scope"] !== "request" && value["scope"] !== "changes") ||
24
+ !Array.isArray(value["criteria"]) || value["criteria"].length < 1 || value["criteria"].length > 20) throw new Error("invalid acceptance contract v1");
25
+ if (Object.keys(value).some(key => !["version", "id", "scope", "criteria", "source", "targets"].includes(key))) throw new Error("unsupported acceptance contract field");
26
+ const source = value["source"];
27
+ if (source !== undefined && (!record(source) || !label(source["path"], 4096) || typeof source["sha256"] !== "string" || !/^[a-f0-9]{64}$/.test(source["sha256"]))) throw new Error("invalid acceptance source");
28
+ const criteria: AcceptanceCriterion[] = value["criteria"].map((item: unknown) => {
29
+ if (!record(item) || !label(item["id"], 80) || !label(item["description"], 300)) throw new Error("invalid acceptance criterion");
30
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(String(item["id"])) || Object.keys(item).some(key => !["id", "description", "kind", "command", "target"].includes(key))) throw new Error("unsupported acceptance criterion field or ID");
31
+ const fields = { id: String(item["id"]), description: String(item["description"]) };
32
+ if (item["kind"] === "human" && item["command"] === undefined && (item["target"] === undefined || label(item["target"], 80))) return { ...fields, kind: "human", ...(item["target"] ? { target: String(item["target"]) } : {}) };
33
+ if (item["kind"] === "command" && item["target"] === undefined && label(item["command"], 500)) return { ...fields, kind: "command", command: String(item["command"]) };
34
+ throw new Error("acceptance criterion must declare a command or human decision");
35
+ });
36
+ if (new Set(criteria.map(item => item.id)).size !== criteria.length) throw new Error("duplicate acceptance criterion IDs");
37
+ const declarations = value["targets"];
38
+ if (declarations !== undefined && (!Array.isArray(declarations) || declarations.length < 1 || declarations.length > 8)) throw Error("targets requires 1..8 declared runtimes");
39
+ const targets = Array.isArray(declarations) ? declarations.map(parseRuntimeTarget) : undefined;
40
+ if (targets && (new Set(targets.map(target => target.id)).size !== targets.length || targets.some(target =>
41
+ !criteria.some(item => item.id === target.build && item.kind === "command") || !criteria.some(item => item.kind === "human" && item.target === target.id)))) throw Error("targets require a build condition and a bound human condition");
42
+ if (criteria.some(item => item.kind === "human" && item.target && !targets?.some(target => target.id === item.target))) throw Error("unknown human acceptance target");
43
+ return { version: 1, id: String(value["id"]), criteria,
44
+ ...(targets ? { targets } : {}),
45
+ ...(record(source) ? { source: { path: String(source["path"]), sha256: String(source["sha256"]) } } : {}),
46
+ ...(value["scope"] ? { scope: value["scope"] as "request" | "changes" } : {}) };
47
+ }
48
+
49
+ export function deriveAcceptance(previous: AcceptanceState | undefined, events: readonly LogEvent[]): AcceptanceState | undefined {
50
+ let state = previous ? { ...previous, criteria: previous.criteria.map(item => ({ ...item })), ...(previous.targets ? { targets: previous.targets.map(target => ({ ...target })) } : {}) } : undefined;
51
+ for (const event of events) {
52
+ if (event.t === "acceptance_contract") {
53
+ state = { contract_id: event.contract.id, contract_event: event.id, request: event.request, scope: event.contract.scope ?? "request", changed: false,
54
+ criteria: event.contract.criteria.map(item => ({ ...item, status: item.kind === "human" ? "awaiting_human" : "not_run" })), status: "needs_work" };
55
+ if (event.contract.targets) state.targets = event.contract.targets.map(target => ({ id: target.id, build: target.build, status: "not_run" }));
56
+ continue;
57
+ }
58
+ if (!state) continue;
59
+ const mutation = event.t === "tool_result" && event.effect?.kind === "edit" && event.effect.sha256_before !== event.effect.sha256_after ||
60
+ event.t === "snapshot" && Object.keys(event.added).length + Object.keys(event.changed).length + event.removed.length > 0 ||
61
+ event.t === "checkpoint" && event.phase === "applied" && changedPaths(event.changes).length > 0 ||
62
+ event.t === "workspace_observation" && event.changed;
63
+ if (mutation) { state.changed = true; for (const target of state.targets ?? []) target.status = "stale"; }
64
+ if (event.t === "target_observation" && event.observation.request === state.request && event.observation.contract_event === state.contract_event) {
65
+ const target = state.targets?.find(item => item.id === event.observation.target), receipt = event.observation;
66
+ if (target) {
67
+ const build = state.criteria.find(item => item.id === target.build);
68
+ target.status = receipt.state === "ready" && (build?.status !== "pass" || build.evidence?.event_id !== receipt.build?.event) ? "stale" : receipt.state;
69
+ target.observation = event.id; target.reason = receipt.reason;
70
+ if (receipt.build) target.build_event = receipt.build.event;
71
+ if (receipt.instance) { target.binding = receipt.instance.id; target.session = receipt.instance.session; target.pid = receipt.instance.pid; target.entry = receipt.instance.entry.path; }
72
+ else { delete target.binding; delete target.session; delete target.pid; delete target.entry; }
73
+ if (receipt.port !== undefined) target.port = receipt.port;
74
+ }
75
+ }
76
+ for (const target of state.targets ?? []) if (event.t === "process_exit" && event.session === target.session ||
77
+ event.t === "tool_result" && record(event.data) && event.data["session_id"] === target.session && event.data["running"] === false) target.status = "stale";
78
+ for (const item of state.criteria) {
79
+ if (item.evidence && evidenceStale([event], item.evidence, item.status === "stale")) item.status = "stale";
80
+ if (mutation && item.kind === "human" && item.status === "accepted") { item.status = "awaiting_human"; delete item.accepted_event; delete item.accepted_binding; }
81
+ if (item.kind === "command" && event.t === "tool_result" && event.effect?.kind === "command" && event.effect.cmd === item.command) {
82
+ item.status = event.effect.exit === 0 && event.ok ? event.effect.workspace_observed === false ? "stale" : "pass" : "fail";
83
+ item.evidence = { command: event.effect.cmd, exit: event.effect.exit, event_id: event.id, at: event.at,
84
+ ...(event.effect.workspace_scope ? { workspace_scope: event.effect.workspace_scope } : {}),
85
+ ...(event.effect.workspace_sha256 ? { workspace_sha256: event.effect.workspace_sha256 } : {}) };
86
+ for (const target of state.targets ?? []) if (target.build === item.id && target.build_event !== event.id) target.status = "stale";
87
+ }
88
+ if (item.kind === "human" && event.t === "acceptance_decision" && event.request === state.request &&
89
+ event.contract_id === state.contract_id && event.criterion === item.id && event.actor === "user") {
90
+ const target = item.target ? state.targets?.find(target => target.id === item.target) : undefined;
91
+ if (!item.target || target?.status === "ready" && target.binding === event.target_binding && target.observation === event.target_observation) {
92
+ item.status = "accepted"; item.accepted_event = event.id;
93
+ if (event.target_binding) item.accepted_binding = event.target_binding;
94
+ }
95
+ }
96
+ }
97
+ for (const target of state.targets ?? []) if (target.status === "ready" && !state.criteria.some(item => item.id === target.build && item.status === "pass" && item.evidence?.event_id === target.build_event)) target.status = "stale";
98
+ for (const item of state.criteria) if (item.kind === "human" && item.target && item.status === "accepted") {
99
+ const target = state.targets?.find(target => target.id === item.target);
100
+ if (target?.status !== "ready" || target.binding !== item.accepted_binding) { item.status = "awaiting_human"; delete item.accepted_event; delete item.accepted_binding; }
101
+ }
102
+ }
103
+ if (state) state.status = state.scope === "changes" && !state.changed ? "not_applicable" :
104
+ state.criteria.some(item => item.kind === "command" && item.status !== "pass") || state.targets?.some(target => target.status !== "ready") ? "needs_work" :
105
+ state.criteria.some(item => item.kind === "human" && item.status !== "accepted") ? "awaiting_human" : "verified";
106
+ return state;
107
+ }