@visual-z/xeno 0.2.1 → 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 (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  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/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. 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
+ }