@visual-z/xeno 0.8.4 → 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 (350) hide show
  1. package/CHANGELOG.md +8 -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 -2071
  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 -1818
  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 -3108
  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/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,278 +0,0 @@
1
- /**
2
- * Git worktree isolation for branches that write.
3
- *
4
- * A worktree is a second checkout sharing one object database, which is why
5
- * it is the isolation primitive parallel agents converged on: each branch
6
- * edits its own files, and conflicts surface at merge time through ordinary
7
- * git tooling instead of as silent overwrites during the work.
8
- *
9
- * What this does NOT isolate, stated because assuming otherwise is how these
10
- * setups break:
11
- *
12
- * ENVIRONMENT. Ports, databases, and anything else outside the working
13
- * directory are still shared. Two branches running a dev server or a
14
- * migration will collide, and a test can fail because a sibling took the
15
- * port rather than because the branch is wrong. That needs containers.
16
- *
17
- * ESCAPE. A branch can point git back at the shared checkout with `git -C`
18
- * or GIT_DIR, which has been an actual reported escape. Preventing it is a
19
- * sandbox's job, and this project has no sandbox layer; the isolation here
20
- * is for cooperative branches, not adversarial ones.
21
- */
22
-
23
- import { mkdir, mkdtemp, rm, realpath } from "node:fs/promises";
24
- import { existsSync } from "node:fs";
25
- import { createHash } from "node:crypto";
26
- import { basename, join } from "node:path";
27
- import { tmpdir } from "node:os";
28
-
29
- /** Branch prefix, so a stale worktree is identifiable months later. */
30
- export const WORKTREE_BRANCH_PREFIX = "xeno/";
31
-
32
- export interface WorktreeInfo {
33
- path: string;
34
- branch: string | null;
35
- locked: boolean;
36
- }
37
-
38
- export type CreateResult =
39
- | { ok: true; path: string; branch: string; snapshot: { head: string; tree: string } }
40
- | { ok: false; error: string };
41
-
42
- export interface CreateOptions {
43
- /**
44
- * Where the checkout goes. Default: a fresh temp directory, which suits a
45
- * writing branch that lives for one subtask. A model-entered worktree is
46
- * kept across sessions, so it names a stable parent and its own leaf.
47
- */
48
- location?: { parent: string; leaf: string };
49
- /** Carry the parent's uncommitted files (default, as a branch needs). */
50
- includeChanges?: boolean;
51
- }
52
-
53
- /**
54
- * The stable home of model-entered worktrees for one repository.
55
- *
56
- * Under xeno's config directory rather than the project: inside `.xeno` it
57
- * would collide with the rule that session state is never edited, and a
58
- * sibling directory leaves xeno's checkouts in the user's project tree (Codex
59
- * keeps its worktrees under its own home for the same reason). The digest of
60
- * the repository path keeps two clones with the same name apart.
61
- */
62
- export function persistentWorktreeParent(configDir: string, repo: string): string {
63
- const digest = createHash("sha256").update(repo).digest("hex").slice(0, 8);
64
- return join(configDir, "worktrees", `${basename(repo)}-${digest}`);
65
- }
66
-
67
- /** The repository's main checkout: `git worktree list` always lists it first. */
68
- export async function mainWorktree(dir: string): Promise<string | null> {
69
- const listed = await listWorktrees(dir).catch(() => []);
70
- return listed[0]?.path ?? null;
71
- }
72
-
73
- export type RemoveResult =
74
- | { ok: true }
75
- | { ok: false; error: string; dirty: boolean };
76
-
77
- interface GitRun {
78
- code: number;
79
- stdout: string;
80
- stderr: string;
81
- }
82
-
83
- function git(cwd: string, args: string[], env?: Record<string, string | undefined>): GitRun {
84
- const p = Bun.spawnSync(["git", ...args], { cwd, ...(env ? { env } : {}) });
85
- const dec = new TextDecoder();
86
- return {
87
- code: p.exitCode,
88
- stdout: dec.decode(p.stdout),
89
- stderr: dec.decode(p.stderr),
90
- };
91
- }
92
-
93
- export async function isGitRepo(dir: string): Promise<boolean> {
94
- return git(dir, ["rev-parse", "--git-dir"]).code === 0;
95
- }
96
-
97
- export async function createWorktree(repo: string, name: string, options: CreateOptions = {}): Promise<CreateResult> {
98
- // Refused rather than degraded. Falling back to the shared directory is the
99
- // exact failure isolation exists to prevent: parallel writing branches
100
- // would overwrite each other with nothing to indicate isolation was never
101
- // in place.
102
- if (!(await isGitRepo(repo))) {
103
- return {
104
- ok: false,
105
- error:
106
- `${repo} is not a git repository, so a writing branch cannot be isolated. ` +
107
- "Initialise one, or run the branch read-only.",
108
- };
109
- }
110
-
111
- // O.4: snapshot the working copy through a private index. Never stash, reset,
112
- // or stage the user's index merely to construct a child input.
113
- const root = git(repo, ["rev-parse", "--show-toplevel"]);
114
- if (root.code !== 0 || await realpath(repo) !== await realpath(root.stdout.trim())) {
115
- return { ok: false, error: "writing branch input must be the repository root" };
116
- }
117
- const head = git(repo, ["rev-parse", "--verify", "HEAD"]);
118
- if (head.code !== 0) return { ok: false, error: "writing branch input requires an existing HEAD commit" };
119
- const unmerged = git(repo, ["ls-files", "--unmerged"]);
120
- if (unmerged.code !== 0 || unmerged.stdout.length > 0) return { ok: false, error: "resolve index conflicts before creating a writing branch" };
121
- const staged = git(repo, ["ls-files", "--stage", "-z"]);
122
- if (staged.code !== 0 || staged.stdout.split("\0").some(entry => entry.startsWith("160000 "))) {
123
- return { ok: false, error: "writing branch snapshots do not yet support submodules" };
124
- }
125
- const scratch = await mkdtemp(join(tmpdir(), "xeno-input-index-"));
126
- let tree: string;
127
- try {
128
- const env = { ...process.env, GIT_INDEX_FILE: join(scratch, "index") };
129
- for (const args of [["read-tree", head.stdout.trim()], ["add", "--all", "--", "."]]) {
130
- const result = git(repo, args, env);
131
- if (result.code !== 0) return { ok: false, error: `cannot capture branch input: ${result.stderr.trim()}` };
132
- }
133
- const captured = git(repo, ["ls-files", "--stage", "-z"], env);
134
- if (captured.code !== 0 || captured.stdout.split("\0").some(entry => entry.startsWith("160000 "))) {
135
- return { ok: false, error: "writing branch snapshots do not yet support nested repositories or submodules" };
136
- }
137
- const written = git(repo, ["write-tree"], env);
138
- if (written.code !== 0) return { ok: false, error: `cannot capture branch input tree: ${written.stderr.trim()}` };
139
- tree = written.stdout.trim();
140
- } finally { await rm(scratch, { recursive: true, force: true }); }
141
-
142
- const branch = `${WORKTREE_BRANCH_PREFIX}${name}`;
143
- if (git(repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).code === 0) {
144
- return { ok: false, error: `branch ${branch} already exists; choose another name or work in its existing checkout` };
145
- }
146
- let path: string;
147
- if (options.location) {
148
- await mkdir(options.location.parent, { recursive: true });
149
- // Resolved for the same reason as the temp path below.
150
- path = join(await realpath(options.location.parent), options.location.leaf);
151
- if (existsSync(path)) return { ok: false, error: `${path} already exists; choose another name` };
152
- } else {
153
- const created = await mkdtemp(join(tmpdir(), `xeno-wt-${name}-`));
154
- // Resolved through symlinks, because git reports the real path and a
155
- // caller comparing the two would never match. On macOS /var is a symlink
156
- // to /private/var, so every temp path differs by default.
157
- path = await realpath(created);
158
- // mkdtemp created it; git worktree add needs the path not to exist.
159
- await rm(path, { recursive: true, force: true });
160
- }
161
-
162
- // --lock in the same call: locking afterwards leaves a window in which a
163
- // concurrent prune can remove the worktree that was just created.
164
- const add = git(repo, ["worktree", "add", "--lock", "-b", branch, path, head.stdout.trim()]);
165
- if (add.code !== 0) {
166
- return { ok: false, error: `git worktree add failed: ${add.stderr.trim()}` };
167
- }
168
- if (options.includeChanges === false) return { ok: true, path, branch, snapshot: { head: head.stdout.trim(), tree: git(path, ["rev-parse", "HEAD^{tree}"]).stdout.trim() } };
169
- // Only this freshly created disposable checkout is reset. The child index
170
- // carries the parent's uncommitted input, so normal diff/inspection still sees it.
171
- const hydrate = git(path, ["read-tree", "--reset", "-u", tree]);
172
- if (hydrate.code !== 0) {
173
- const removed = await removeWorktree(repo, path, branch, { force: true });
174
- return { ok: false, error: `cannot materialize branch input: ${hydrate.stderr.trim()}` +
175
- (removed.ok ? "" : `; cleanup required at ${path}: ${removed.error}`) };
176
- }
177
- return { ok: true, path, branch, snapshot: { head: head.stdout.trim(), tree } };
178
- }
179
-
180
- /**
181
- * Parses `--porcelain -z`, the only output format documented as stable
182
- * across git versions and user configuration. `-z` additionally survives a
183
- * path containing a newline, which a line-based parse would split into two
184
- * bogus entries.
185
- */
186
- export async function listWorktrees(repo: string): Promise<WorktreeInfo[]> {
187
- const out = git(repo, ["worktree", "list", "--porcelain", "-z"]);
188
- if (out.code !== 0) return [];
189
-
190
- const worktrees: WorktreeInfo[] = [];
191
- let current: Partial<WorktreeInfo> = {};
192
- for (const record of out.stdout.split("\0")) {
193
- if (record === "") {
194
- // A blank record terminates one worktree's attributes.
195
- if (current.path) {
196
- worktrees.push({
197
- path: current.path,
198
- branch: current.branch ?? null,
199
- locked: current.locked ?? false,
200
- });
201
- }
202
- current = {};
203
- continue;
204
- }
205
- const [key, ...rest] = record.split(" ");
206
- const value = rest.join(" ");
207
- if (key === "worktree") current.path = value;
208
- else if (key === "branch") current.branch = value.replace(/^refs\/heads\//, "");
209
- else if (key === "locked") current.locked = true;
210
- }
211
- if (current.path) {
212
- worktrees.push({
213
- path: current.path,
214
- branch: current.branch ?? null,
215
- locked: current.locked ?? false,
216
- });
217
- }
218
- return worktrees;
219
- }
220
-
221
- /**
222
- * Removes a worktree, its registration and its branch.
223
- *
224
- * A dirty worktree is reported rather than forced. The refusal is evidence
225
- * to look at: uncommitted changes there may be exactly the work the branch
226
- * was for, and discarding them silently is the destructive-restore mistake
227
- * in a different costume. `force` exists for a caller that has decided.
228
- */
229
- export async function removeWorktree(
230
- repo: string,
231
- path: string,
232
- branch: string | null,
233
- opts: { force?: boolean } = {},
234
- ): Promise<RemoveResult> {
235
- // Created with --lock, so unlock first; a locked worktree otherwise needs
236
- // --force twice, which would conflate "locked" with "dirty".
237
- git(repo, ["worktree", "unlock", path]);
238
-
239
- const args = ["worktree", "remove", path];
240
- if (opts.force) args.push("--force");
241
- const removed = git(repo, args);
242
-
243
- if (removed.code !== 0) {
244
- const dirty = /contains modified or untracked files|is dirty/i.test(removed.stderr);
245
- return {
246
- ok: false,
247
- error: dirty
248
- ? `${path} has uncommitted changes. Inspect them before removing, ` +
249
- "or pass force to discard them."
250
- : `git worktree remove failed: ${removed.stderr.trim()}`,
251
- dirty,
252
- };
253
- }
254
-
255
- // Removing a worktree never deletes its branch, so a run of branches would
256
- // otherwise leave a branch behind for every one of them.
257
- if (branch) git(repo, ["branch", "-D", branch]);
258
- return { ok: true };
259
- }
260
-
261
- /**
262
- * Clears registrations left behind by hand-deleted directories.
263
- *
264
- * Only for that case. A worktree that was MOVED must be repaired instead --
265
- * pruning it discards a working directory that still exists.
266
- */
267
- export async function pruneWorktrees(repo: string): Promise<void> {
268
- // A lock exists to stop pruning, and createWorktree takes one -- so a
269
- // hand-deleted worktree of ours would stay registered forever without
270
- // unlocking first. Only ours: the prefix check keeps this from unlocking
271
- // a worktree someone locked deliberately.
272
- for (const wt of await listWorktrees(repo)) {
273
- if (wt.locked && wt.branch?.startsWith(WORKTREE_BRANCH_PREFIX)) {
274
- git(repo, ["worktree", "unlock", wt.path]);
275
- }
276
- }
277
- git(repo, ["worktree", "prune"]);
278
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * ANSI → styled spans. Pure; no renderer import.
3
- *
4
- * Message content (Markdown with highlighting, Mermaid, diffs, tool views)
5
- * and every extension renderer produce `string[]` lines with SGR escapes —
6
- * that is the Pi-compatible component contract extensions already rely on.
7
- * The OpenTUI view draws cells from styled chunks, not escapes, and ships no
8
- * ANSI parser, so this module is the one bridge: it turns those lines into
9
- * spans the view converts to TextChunks. Keeping the bridge here (instead of
10
- * re-implementing every renderer natively) keeps the extension API and the
11
- * tested renderers intact across the UI rewrite (D20).
12
- *
13
- * Supported: SGR 0/1/2/3/4/7/9/22/23/24/27/29, 30–37/90–97/39, 40–47/100–107/49,
14
- * 38;5;n / 48;5;n, 38;2;r;g;b / 48;2;r;g;b, OSC 8 hyperlinks. Every other
15
- * CSI/OSC/APC (cursor moves, Pi's cursor marker, kitty graphics) is dropped:
16
- * a renderable lays out its own cells, so movement escapes have no meaning.
17
- */
18
-
19
- export type SpanColor = { kind: "index"; index: number } | { kind: "rgb"; r: number; g: number; b: number };
20
-
21
- export interface SpanStyle {
22
- readonly fg?: SpanColor;
23
- readonly bg?: SpanColor;
24
- readonly bold?: boolean;
25
- readonly dim?: boolean;
26
- readonly italic?: boolean;
27
- readonly underline?: boolean;
28
- readonly reverse?: boolean;
29
- readonly strikethrough?: boolean;
30
- readonly link?: string;
31
- }
32
-
33
- export interface Span { readonly text: string; readonly style: SpanStyle }
34
-
35
- type Mutable = { -readonly [K in keyof SpanStyle]: SpanStyle[K] };
36
-
37
- function applySgr(style: Mutable, params: number[]): void {
38
- if (params.length === 0) params = [0];
39
- for (let i = 0; i < params.length; i++) {
40
- const p = params[i]!;
41
- if (p === 0) { for (const key of Object.keys(style) as (keyof Mutable)[]) if (key !== "link") delete style[key]; }
42
- else if (p === 1) style.bold = true;
43
- else if (p === 2) style.dim = true;
44
- else if (p === 3) style.italic = true;
45
- else if (p === 4) style.underline = true;
46
- else if (p === 7) style.reverse = true;
47
- else if (p === 9) style.strikethrough = true;
48
- else if (p === 22) { delete style.bold; delete style.dim; }
49
- else if (p === 23) delete style.italic;
50
- else if (p === 24) delete style.underline;
51
- else if (p === 27) delete style.reverse;
52
- else if (p === 29) delete style.strikethrough;
53
- else if (p >= 30 && p <= 37) style.fg = { kind: "index", index: p - 30 };
54
- else if (p >= 90 && p <= 97) style.fg = { kind: "index", index: p - 90 + 8 };
55
- else if (p === 39) delete style.fg;
56
- else if (p >= 40 && p <= 47) style.bg = { kind: "index", index: p - 40 };
57
- else if (p >= 100 && p <= 107) style.bg = { kind: "index", index: p - 100 + 8 };
58
- else if (p === 49) delete style.bg;
59
- else if (p === 38 || p === 48) {
60
- const mode = params[i + 1];
61
- let color: SpanColor | undefined;
62
- if (mode === 5 && params[i + 2] !== undefined) { color = { kind: "index", index: clamp(params[i + 2]!) }; i += 2; }
63
- else if (mode === 2 && params[i + 4] !== undefined) { color = { kind: "rgb", r: clamp(params[i + 2]!), g: clamp(params[i + 3]!), b: clamp(params[i + 4]!) }; i += 4; }
64
- else break; // malformed extended colour: ignore the rest of this sequence
65
- if (p === 38) style.fg = color; else style.bg = color;
66
- }
67
- }
68
- }
69
-
70
- function clamp(n: number): number { return Math.max(0, Math.min(255, Number.isFinite(n) ? n : 0)); }
71
-
72
- function freeze(style: Mutable): SpanStyle { return { ...style }; }
73
-
74
- function sameStyle(a: SpanStyle, b: SpanStyle): boolean { return JSON.stringify(a) === JSON.stringify(b); }
75
-
76
- /** Parse one line. Adjacent text with equal style merges into one span. */
77
- export function ansiSpans(line: string): Span[] {
78
- const spans: Span[] = [];
79
- const style: Mutable = {};
80
- let text = "";
81
- const flush = () => {
82
- if (!text) return;
83
- const frozen = freeze(style);
84
- const last = spans[spans.length - 1];
85
- if (last && sameStyle(last.style, frozen)) spans[spans.length - 1] = { text: last.text + text, style: frozen };
86
- else spans.push({ text, style: frozen });
87
- text = "";
88
- };
89
- let i = 0;
90
- while (i < line.length) {
91
- const ch = line[i]!;
92
- if (ch !== "\x1b") { text += ch; i++; continue; }
93
- const kind = line[i + 1];
94
- if (kind === "[") {
95
- let j = i + 2;
96
- while (j < line.length && !/[@-~]/.test(line[j]!)) j++;
97
- const body = line.slice(i + 2, j);
98
- if (line[j] === "m" && /^[0-9;]*$/.test(body)) {
99
- flush();
100
- applySgr(style, body === "" ? [] : body.split(";").map(part => (part === "" ? 0 : Number(part))));
101
- }
102
- i = j + 1;
103
- continue;
104
- }
105
- if (kind === "]" || kind === "_" || kind === "P") {
106
- let j = i + 2;
107
- while (j < line.length && line[j] !== "\x07" && !(line[j] === "\x1b" && line[j + 1] === "\\")) j++;
108
- const body = line.slice(i + 2, j);
109
- if (kind === "]" && body.startsWith("8;")) {
110
- flush();
111
- const url = body.slice(body.indexOf(";", 2) + 1);
112
- if (url) style.link = url; else delete style.link;
113
- }
114
- i = line[j] === "\x07" ? j + 1 : j + 2;
115
- continue;
116
- }
117
- i += 2;
118
- }
119
- flush();
120
- return spans;
121
- }
122
-
123
- /** Visible text of a line: what a terminal would print, escapes removed. */
124
- export function plainText(line: string): string {
125
- return ansiSpans(line).map(span => span.text).join("");
126
- }
package/src/tui/diff.ts DELETED
@@ -1,160 +0,0 @@
1
- /**
2
- * Line diff for an edit's old/new snippets.
3
- *
4
- * Ported from pi (earendil-works/pi, MIT) — packages/coding-agent/src/core/tools/edit-diff.ts
5
- * `generateDiffString`: context around changed lines, skip the rest. We cannot
6
- * take the `diff` package (tui may not import npm), so the hunks are an LCS
7
- * of lines rather than Myers. Edits carry snippets, not whole files, so O(n·m)
8
- * stays small.
9
- *
10
- * Why not the whole file: the information in an edit is what changed. Dumping
11
- * the destination file fills the live region and makes the user hunt.
12
- */
13
-
14
- import { PLAIN_ATTRS, paint, type Attrs, type Style } from "./style.ts";
15
- import { displayWidth } from "./width.ts";
16
-
17
- /**
18
- * Unchanged lines kept on each side of a hunk. From pi's generateDiffString
19
- * default (4). Assumed, not measured: more context drowns the `+`/`-` lines
20
- * this view exists to show.
21
- */
22
- const CONTEXT_LINES = 4;
23
-
24
- export type DiffOp = { kind: "eq" | "del" | "add"; line: string };
25
-
26
- export function diffLines(oldText: string, newText: string): DiffOp[] {
27
- const a = oldText.split("\n");
28
- const b = newText.split("\n");
29
- const n = a.length;
30
- const m = b.length;
31
- const dp: number[][] = Array.from({ length: n + 1 }, () => Array<number>(m + 1).fill(0));
32
- for (let i = n - 1; i >= 0; i--) {
33
- for (let j = m - 1; j >= 0; j--) {
34
- dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!);
35
- }
36
- }
37
- const out: DiffOp[] = [];
38
- let i = 0;
39
- let j = 0;
40
- while (i < n && j < m) {
41
- if (a[i] === b[j]) {
42
- out.push({ kind: "eq", line: a[i]! });
43
- i++;
44
- j++;
45
- } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {
46
- out.push({ kind: "del", line: a[i]! });
47
- i++;
48
- } else {
49
- out.push({ kind: "add", line: b[j]! });
50
- j++;
51
- }
52
- }
53
- while (i < n) {
54
- out.push({ kind: "del", line: a[i]! });
55
- i++;
56
- }
57
- while (j < m) {
58
- out.push({ kind: "add", line: b[j]! });
59
- j++;
60
- }
61
- return out;
62
- }
63
-
64
- /**
65
- * Keep only ops within CONTEXT_LINES of a change. Skipped equal runs become
66
- * a dim `... (N lines)` so the omission is counted, not silent.
67
- */
68
- export function withContext(ops: readonly DiffOp[]): DiffOp[] {
69
- const keep = new Array<boolean>(ops.length).fill(false);
70
- for (let i = 0; i < ops.length; i++) {
71
- if (ops[i]!.kind === "eq") continue;
72
- const from = Math.max(0, i - CONTEXT_LINES);
73
- const to = Math.min(ops.length - 1, i + CONTEXT_LINES);
74
- for (let k = from; k <= to; k++) keep[k] = true;
75
- }
76
- const out: DiffOp[] = [];
77
- let i = 0;
78
- while (i < ops.length) {
79
- if (keep[i]) {
80
- out.push(ops[i]!);
81
- i++;
82
- continue;
83
- }
84
- let j = i;
85
- while (j < ops.length && !keep[j]) j++;
86
- const skipped = j - i;
87
- if (skipped > 0) out.push({ kind: "eq", line: `... (${skipped} lines)` });
88
- i = j;
89
- }
90
- return out;
91
- }
92
-
93
- const DEL: Attrs = { ...PLAIN_ATTRS, red: true };
94
- const ADD: Attrs = { ...PLAIN_ATTRS, green: true };
95
- const EQ: Attrs = { ...PLAIN_ATTRS, dim: true };
96
-
97
- export function renderDiff(oldText: string, newText: string, width: number, style: Style): string[] {
98
- const ops = withContext(diffLines(oldText, newText));
99
- const out: string[] = [];
100
- for (const op of ops) {
101
- const mark = op.kind === "del" ? "-" : op.kind === "add" ? "+" : " ";
102
- const attrs = op.kind === "del" ? DEL : op.kind === "add" ? ADD : EQ;
103
- out.push(...emitPrefixed(mark, op.line, width, style, attrs));
104
- }
105
- return out;
106
- }
107
-
108
- /**
109
- * Columns left for the body after the prefix. A prefix that does not
110
- * leave at least one column is dropped: wrapPlain floors a zero budget
111
- * at 1, so keeping the prefix would paint prefixW+1 columns and the
112
- * terminal would wrap. The line-diff renderer then counts one row where
113
- * the screen used two — the same trap as a 0-column pty (`usableColumns`
114
- * in terminal.ts).
115
- */
116
- export function innerWidth(prefix: string, width: number): number {
117
- const w = Math.max(0, Math.floor(width));
118
- if (w <= 0) return 0;
119
- const prefixW = displayWidth(prefix);
120
- return prefixW > 0 && prefixW < w ? w - prefixW : w;
121
- }
122
-
123
- export function emitPrefixed(
124
- prefix: string,
125
- body: string,
126
- width: number,
127
- style: Style,
128
- attrs: Attrs,
129
- ): string[] {
130
- const w = Math.max(0, Math.floor(width));
131
- if (w <= 0) return [""];
132
- const prefixW = displayWidth(prefix);
133
- const inner = innerWidth(prefix, w);
134
- const rows = wrapPlain(body, inner);
135
- if (inner === w) {
136
- return rows.map((row) => paint(row, attrs, style));
137
- }
138
- const pad = " ".repeat(prefixW);
139
- return rows.map((row, i) => paint((i === 0 ? prefix : pad) + row, attrs, style));
140
- }
141
-
142
- export function wrapPlain(text: string, width: number): string[] {
143
- if (width <= 0) return [""];
144
- const lines: string[] = [];
145
- let current = "";
146
- let currentW = 0;
147
- for (const ch of text) {
148
- const cw = displayWidth(ch);
149
- if (cw > 0 && currentW + cw > width && current.length > 0) {
150
- lines.push(current);
151
- current = ch;
152
- currentW = cw;
153
- } else {
154
- current += ch;
155
- currentW += cw;
156
- }
157
- }
158
- lines.push(current);
159
- return lines;
160
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * Exhaustiveness for discriminated unions.
3
- *
4
- * Copied rather than imported from core: tui may not import src/ outside
5
- * itself, and the boundary checker exists to keep that true. The two copies
6
- * must stay one function; if they drift, the newer union is the one that
7
- * will fail to compile first.
8
- */
9
- export function assertNever(value: never, context: string): never {
10
- throw new Error(
11
- `${context}: unhandled variant ${JSON.stringify(value)} -- ` +
12
- "a union gained a member without every switch over it being updated",
13
- );
14
- }
package/src/tui/fuzzy.ts DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * Subsequence fuzzy matching with a stable rank.
3
- *
4
- * Consecutive hits and word-start hits score better (pi-tui's scheme,
5
- * lower is better). Ties keep the original index: an unstable sort would
6
- * reshuffle the select list between two renders, and Enter would confirm
7
- * a different item than the one under the cursor.
8
- */
9
-
10
- export interface FuzzyMatch {
11
- readonly text: string;
12
- readonly score: number;
13
- readonly index: number;
14
- }
15
-
16
- /**
17
- * Lower is better. `null` if `query` is not a subsequence of `text`.
18
- * Case-insensitive. Empty query matches with score 0.
19
- */
20
- export function fuzzyMatch(text: string, query: string): number | null {
21
- if (query.length === 0) return 0;
22
- return score(text, query);
23
- }
24
-
25
- export function fuzzyFilter(candidates: readonly string[], query: string): string[] {
26
- if (query.length === 0) return [...candidates];
27
- const ranked: FuzzyMatch[] = [];
28
- for (let index = 0; index < candidates.length; index++) {
29
- const text = candidates[index]!;
30
- const s = score(text, query);
31
- if (s === null) continue;
32
- ranked.push({ text, score: s, index });
33
- }
34
- ranked.sort((a, b) => a.score - b.score || a.index - b.index);
35
- return ranked.map((r) => r.text);
36
- }
37
-
38
- /**
39
- * Leftmost greedy subsequence, scored like pi-tui:
40
- * consecutive run bonus, word-boundary bonus, later-match penalty.
41
- * Walks code points so a CJK query cannot land on a UTF-16 half-unit.
42
- */
43
- function score(text: string, query: string): number | null {
44
- const t = [...text.toLowerCase()];
45
- const q = [...query.toLowerCase()];
46
- if (q.length > t.length) return null;
47
-
48
- let qi = 0;
49
- let total = 0;
50
- let last = -1;
51
- let consecutive = 0;
52
-
53
- for (let i = 0; i < t.length && qi < q.length; i++) {
54
- if (t[i] !== q[qi]) continue;
55
- const prev = i === 0 ? "" : t[i - 1]!;
56
- const boundary = i === 0 || /[\s\-_./:]/.test(prev);
57
-
58
- if (last === i - 1) {
59
- consecutive++;
60
- total -= consecutive * 5;
61
- } else {
62
- consecutive = 0;
63
- if (last >= 0) total += (i - last - 1) * 2;
64
- }
65
- if (boundary) total -= 10;
66
- total += i * 0.1;
67
-
68
- last = i;
69
- qi++;
70
- }
71
-
72
- if (qi < q.length) return null;
73
- if (q.join("") === t.join("")) total -= 100;
74
- return total;
75
- }