@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,306 +1,58 @@
1
- /** Adapted from Pi keybindings at 02eed88 (MIT). Platform-neutral defaults; overrides share scoped ownership. */
1
+ /**
2
+ * xeno's key bindings (D20). One flat namespace:
3
+ * app.* global, handled before the focused component
4
+ * editor.* the composer (submit / newline / history / completion)
5
+ * list.* every list dialog (palette, model, login, resume, tree, …)
6
+ * Shortcuts that only exist inside one dialog are shown in that dialog's
7
+ * hint row instead of being configurable globals; the palette (Ctrl+P)
8
+ * reaches every command, so rarely used actions need no chord at all.
9
+ */
2
10
  export const ACTION_DEFAULTS = {
3
- "editor.cursorUp": [
4
- "up"
5
- ],
6
- "editor.cursorDown": [
7
- "down"
8
- ],
9
- "tui.editor.historyPrevious": [],
10
- "tui.editor.historyNext": [],
11
- "editor.cursorLeft": [
12
- "left",
13
- "ctrl+b"
14
- ],
15
- "editor.cursorRight": [
16
- "right",
17
- "ctrl+f"
18
- ],
19
- "editor.cursorWordLeft": [
20
- "alt+left",
21
- "ctrl+left",
22
- "alt+b"
23
- ],
24
- "editor.cursorWordRight": [
25
- "alt+right",
26
- "ctrl+right",
27
- "alt+f"
28
- ],
29
- "editor.cursorLineStart": [
30
- "home",
31
- "ctrl+home",
32
- "ctrl+a"
33
- ],
34
- "editor.cursorLineEnd": [
35
- "end",
36
- "ctrl+end",
37
- "ctrl+e"
38
- ],
39
- "tui.editor.jumpForward": [
40
- "ctrl+]"
41
- ],
42
- "tui.editor.jumpBackward": [
43
- "ctrl+alt+]"
44
- ],
45
- "tui.editor.pageUp": [
46
- "pageUp",
47
- "ctrl+pageUp"
48
- ],
49
- "tui.editor.pageDown": [
50
- "pageDown",
51
- "ctrl+pageDown"
52
- ],
53
- "editor.deleteCharBackward": [
54
- "backspace"
55
- ],
56
- "editor.deleteCharForward": [
57
- "delete",
58
- "ctrl+d"
59
- ],
60
- "editor.deleteWordBackward": [
61
- "ctrl+w",
62
- "alt+backspace"
63
- ],
64
- "editor.deleteWordForward": [
65
- "alt+d",
66
- "alt+delete"
67
- ],
68
- "editor.deleteToLineStart": [
69
- "ctrl+u"
70
- ],
71
- "editor.deleteToLineEnd": [
72
- "ctrl+k"
73
- ],
74
- "editor.yank": [
75
- "ctrl+y"
76
- ],
77
- "tui.editor.yankPop": [
78
- "alt+y"
79
- ],
80
- "editor.undo": [
81
- "ctrl+-",
82
- "ctrl+_"
83
- ],
84
- "editor.newLine": [
85
- "shift+enter",
86
- "ctrl+j"
87
- ],
88
- "editor.submit": [
89
- "enter"
90
- ],
91
- "editor.complete": [
92
- "tab"
93
- ],
94
- "tui.input.copy": [
95
- "ctrl+c"
96
- ],
97
- "tui.select.up": [
98
- "up"
99
- ],
100
- "tui.select.down": [
101
- "down"
102
- ],
103
- "tui.select.pageUp": [
104
- "pageUp"
105
- ],
106
- "tui.select.pageDown": [
107
- "pageDown"
108
- ],
109
- "tui.select.confirm": [
110
- "enter"
111
- ],
112
- "tui.select.cancel": [
113
- "escape",
114
- "ctrl+c"
115
- ],
116
- "tui.altScreen.pageUp": [
117
- "pageUp"
118
- ],
119
- "tui.altScreen.pageDown": [
120
- "pageDown"
121
- ],
122
- "tui.altScreen.halfPageUp": [],
123
- "tui.altScreen.halfPageDown": [],
124
- "tui.altScreen.lineUp": [],
125
- "tui.altScreen.lineDown": [],
126
- "tui.altScreen.previousPrompt": [
127
- "ctrl+shift+up",
128
- "ctrl+up"
129
- ],
130
- "tui.altScreen.nextPrompt": [
131
- "ctrl+shift+down",
132
- "ctrl+down"
133
- ],
134
- "tui.altScreen.search": [
135
- "ctrl+shift+f"
136
- ],
137
- "tui.altScreen.searchNext": [
138
- "enter",
139
- "ctrl+g"
140
- ],
141
- "tui.altScreen.searchPrevious": [
142
- "shift+enter",
143
- "ctrl+shift+g"
144
- ],
145
- "tui.altScreen.searchClose": [
146
- "escape"
147
- ],
148
- "tui.altScreen.top": [
149
- "home"
150
- ],
151
- "tui.altScreen.bottom": [
152
- "end"
153
- ],
154
- "app.interrupt": [
155
- "escape"
156
- ],
157
- "app.clear": [
158
- "ctrl+c"
159
- ],
160
- "app.exit": [
161
- "ctrl+d"
162
- ],
163
- "app.suspend": [
164
- "ctrl+z"
165
- ],
166
- "app.cycleThinking": [
167
- "shift+tab"
168
- ],
169
- "app.saveDefault": [
170
- "ctrl+s"
171
- ],
172
- "app.cycleModel": [
173
- "ctrl+p"
174
- ],
175
- "app.cycleModelReverse": [
176
- "shift+ctrl+p"
177
- ],
178
- "app.model": [
179
- "ctrl+l"
180
- ],
181
- "app.toggleTools": [
182
- "ctrl+o"
183
- ],
184
- "app.toggleThinking": [
185
- "ctrl+t"
186
- ],
187
- "app.session.toggleNamedFilter": [
188
- "ctrl+n"
189
- ],
190
- "app.externalEditor": [
191
- "ctrl+g"
192
- ],
193
- "app.message.copy": [
194
- "ctrl+x"
195
- ],
196
- "app.message.followUp": [
197
- "alt+enter"
198
- ],
199
- "app.message.dequeue": [
200
- "alt+up"
201
- ],
202
- "app.clipboard.pasteImage": [
203
- "ctrl+v"
204
- ],
11
+ "app.palette": ["ctrl+p"],
12
+ "app.model": ["ctrl+l"],
13
+ "app.cycleModel": ["alt+m"],
14
+ "app.cycleModelReverse": ["alt+shift+m"],
15
+ "app.cycleThinking": ["shift+tab"],
16
+ "app.toggleTools": ["ctrl+o"],
17
+ "app.toggleThinking": ["ctrl+t"],
18
+ "app.toggleSidebar": ["ctrl+b"],
19
+ "app.interrupt": ["escape"],
20
+ "app.clear": ["ctrl+c"],
21
+ "app.exit": ["ctrl+d"],
22
+ "app.suspend": ["ctrl+z"],
23
+ "app.externalEditor": ["ctrl+g"],
24
+ "app.message.copy": ["ctrl+x"],
25
+ "app.message.followUp": ["alt+enter"],
26
+ "app.message.dequeue": ["alt+up"],
27
+ "app.clipboard.pasteImage": ["ctrl+v"],
28
+ "app.scroll.pageUp": ["pageup"],
29
+ "app.scroll.pageDown": ["pagedown"],
30
+ "app.scroll.top": ["ctrl+home"],
31
+ "app.scroll.bottom": ["ctrl+end"],
205
32
  "app.session.new": [],
206
33
  "app.session.tree": [],
207
34
  "app.session.fork": [],
208
35
  "app.session.resume": [],
209
- "app.tree.foldOrUp": [
210
- "alt+left",
211
- "ctrl+left"
212
- ],
213
- "app.tree.unfoldOrDown": [
214
- "alt+right",
215
- "ctrl+right"
216
- ],
217
- "app.tree.editLabel": [
218
- "shift+l"
219
- ],
220
- "app.tree.toggleLabelTimestamp": [
221
- "shift+t"
222
- ],
223
- "app.session.togglePath": [
224
- "ctrl+p"
225
- ],
226
- "app.session.toggleSort": [
227
- "ctrl+s"
228
- ],
229
- "app.session.rename": [
230
- "ctrl+r"
231
- ],
232
- "app.session.delete": [
233
- "ctrl+d"
234
- ],
235
- "app.session.deleteNoninvasive": [
236
- "ctrl+backspace"
237
- ],
238
- "app.models.save": [
239
- "ctrl+s"
240
- ],
241
- "app.models.enableAll": [
242
- "ctrl+a"
243
- ],
244
- "app.models.clearAll": [
245
- "ctrl+x"
246
- ],
247
- "app.models.toggleProvider": [
248
- "ctrl+p"
249
- ],
250
- "app.models.reorderUp": [
251
- "alt+up"
252
- ],
253
- "app.models.reorderDown": [
254
- "alt+down"
255
- ],
256
- "app.tree.filter.default": [
257
- "ctrl+d"
258
- ],
259
- "app.tree.filter.noTools": [
260
- "ctrl+t"
261
- ],
262
- "app.tree.filter.userOnly": [
263
- "ctrl+u"
264
- ],
265
- "app.tree.filter.labeledOnly": [
266
- "ctrl+l"
267
- ],
268
- "app.tree.filter.all": [
269
- "ctrl+a"
270
- ],
271
- "app.tree.filter.cycleForward": [
272
- "ctrl+o"
273
- ],
274
- "app.tree.filter.cycleBackward": [
275
- "shift+ctrl+o"
276
- ]
277
- } as const;
278
- export const ACTION_ALIASES: Readonly<Record<string,string>> = {
279
- "tui.input.submit": "editor.submit",
280
- "tui.input.newLine": "editor.newLine",
281
- "tui.input.tab": "editor.complete",
36
+ "app.saveDefault": ["ctrl+s"],
37
+ "editor.submit": ["enter"],
38
+ "editor.newLine": ["shift+enter", "ctrl+j"],
39
+ "editor.complete": ["tab"],
40
+ "editor.historyPrevious": ["up"],
41
+ "editor.historyNext": ["down"],
42
+ "list.up": ["up", "ctrl+k"],
43
+ "list.down": ["down", "ctrl+j"],
44
+ "list.pageUp": ["pageup"],
45
+ "list.pageDown": ["pagedown"],
46
+ "list.confirm": ["enter"],
47
+ "list.cancel": ["escape"],
48
+ "list.toggle": ["space"],
49
+ } as const satisfies Record<string, readonly string[]>;
50
+
51
+ /** Renamed ids still read from a user's keybindings file. */
52
+ export const ACTION_ALIASES: Readonly<Record<string, string>> = {
282
53
  "app.model.select": "app.model",
283
- "app.model.cycleForward": "app.cycleModel",
284
- "app.model.cycleBackward": "app.cycleModelReverse",
285
54
  "app.thinking.cycle": "app.cycleThinking",
286
55
  "app.thinking.toggle": "app.toggleThinking",
287
56
  "app.tools.expand": "app.toggleTools",
288
57
  "app.editor.external": "app.externalEditor",
289
- "app.thinking.save": "app.saveDefault",
290
- "tui.editor.cursorUp": "editor.cursorUp",
291
- "tui.editor.cursorDown": "editor.cursorDown",
292
- "tui.editor.cursorLeft": "editor.cursorLeft",
293
- "tui.editor.cursorRight": "editor.cursorRight",
294
- "tui.editor.cursorWordLeft": "editor.cursorWordLeft",
295
- "tui.editor.cursorWordRight": "editor.cursorWordRight",
296
- "tui.editor.cursorLineStart": "editor.cursorLineStart",
297
- "tui.editor.cursorLineEnd": "editor.cursorLineEnd",
298
- "tui.editor.deleteCharBackward": "editor.deleteCharBackward",
299
- "tui.editor.deleteCharForward": "editor.deleteCharForward",
300
- "tui.editor.deleteWordBackward": "editor.deleteWordBackward",
301
- "tui.editor.deleteWordForward": "editor.deleteWordForward",
302
- "tui.editor.deleteToLineStart": "editor.deleteToLineStart",
303
- "tui.editor.deleteToLineEnd": "editor.deleteToLineEnd",
304
- "tui.editor.yank": "editor.yank",
305
- "tui.editor.undo": "editor.undo"
306
58
  };
@@ -126,7 +126,7 @@ export function benchmarkPrompt(item: BenchmarkItem, chunks: number): string {
126
126
  'Format your final response as follows: "The correct answer is (insert answer here)".';
127
127
  }
128
128
 
129
- export interface BenchmarkPrice { input: number; output: number; cacheRead: number }
129
+ export interface BenchmarkPrice { input: number; output: number; cacheRead: number; cacheWrite?: number }
130
130
  export interface BenchmarkUsage { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }
131
131
 
132
132
  /** A conservative allowance, not a tokenizer: wire bytes plus framing, at uncached peak rates. */
@@ -139,7 +139,7 @@ export function measuredCharge(usage: BenchmarkUsage, price: BenchmarkPrice): nu
139
139
  const counts = [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens];
140
140
  if (counts.some((value) => !Number.isSafeInteger(value) || value < 0) ||
141
141
  usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens === 0 || usage.output_tokens === 0) return null;
142
- return ((usage.input_tokens + usage.cache_creation_input_tokens) * price.input +
142
+ return (usage.input_tokens * price.input + usage.cache_creation_input_tokens * (price.cacheWrite ?? price.input) +
143
143
  usage.cache_read_input_tokens * price.cacheRead + usage.output_tokens * price.output) / 1_000_000;
144
144
  }
145
145
 
@@ -0,0 +1,24 @@
1
+ /** O.9: references to facts, never a child's self-assessed completion. */
2
+ import type { LogEvent, NodeRef } from "./events.ts";
3
+ import type { Handoff } from "./handoff.ts";
4
+ import type { ExecutionOutcome } from "./outcome.ts";
5
+
6
+ export function deriveBranchReceipt(input: {
7
+ source: NodeRef; outcome: ExecutionOutcome; handoff: Handoff;
8
+ nodes: readonly { node: number; events: readonly LogEvent[] }[];
9
+ }) {
10
+ const entries = input.nodes.flatMap(part => part.events.map(event => ({ node: part.node, event })));
11
+ const commands = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "command"
12
+ ? [{ node, event: event.id, exit: event.effect.exit }] : []);
13
+ const changes = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "edit"
14
+ ? [{ node, event: event.id, path: event.effect.path }] : []);
15
+ const observations = entries.flatMap(({ node, event }) => event.t === "tool_result" ? [{ node, event: event.id }] : []);
16
+ const unfinished = (input.handoff.state.work_items ?? []).filter(item => item.status === "active" || item.status === "blocked")
17
+ .map(item => ({ work_id: item.id, status: item.status, next_action: item.next_action ?? null }));
18
+ return { version: 1, source: input.source, execution: input.outcome,
19
+ verification: commands.length === 0 ? "not_recorded" : commands.some(command => command.exit === 0) ? "command_succeeded" : "commands_failed",
20
+ task_acceptance: "not_assessed", commands, changes, observations, unfinished,
21
+ ...(input.handoff.processes?.length ? { processes: input.handoff.processes } : {}),
22
+ unresolved_branches: input.handoff.branches.filter(b => b.resolution === null).map(b => b.node),
23
+ };
24
+ }
@@ -0,0 +1,64 @@
1
+ /** P.3: recovery describes observed file transitions, never a complete disk image. */
2
+ export type FileVersion = { sha256: string; mode: number } | null;
3
+ export type FileChange = { before: FileVersion; after: FileVersion };
4
+ export type FileChanges = Record<string, FileChange>;
5
+ export interface CheckpointEvent {
6
+ t: "checkpoint";
7
+ id: string;
8
+ at: string;
9
+ operation: string;
10
+ source: "edit" | "git" | "restore";
11
+ phase: "prepared" | "applied" | "unavailable";
12
+ changes: FileChanges;
13
+ tree?: string;
14
+ reason?: string;
15
+ }
16
+ export interface WorkspaceObservationEvent {
17
+ t: "workspace_observation";
18
+ id: string;
19
+ at: string;
20
+ /** P.3: this scope excludes ignored/build output and is not a backup. */
21
+ fingerprint?: string;
22
+ scope: "source-v1";
23
+ changed: boolean;
24
+ reason?: string;
25
+ }
26
+
27
+ export function sameVersion(a: FileVersion, b: FileVersion): boolean {
28
+ return a === null || b === null ? a === b : a.sha256 === b.sha256 && a.mode === b.mode;
29
+ }
30
+ export function changedPaths(changes: FileChanges): string[] {
31
+ return Object.keys(changes).filter(path => !sameVersion(changes[path]!.before, changes[path]!.after)).sort();
32
+ }
33
+
34
+ /** Undo the abandoned suffix then replay the selected suffix, touching only their paths. */
35
+ export function planFileTransition(from: readonly CheckpointEvent[], to: readonly CheckpointEvent[]): FileChanges {
36
+ const left = from.filter(event => event.phase === "applied");
37
+ const right = to.filter(event => event.phase === "applied");
38
+ let common = 0;
39
+ while (common < left.length && common < right.length && left[common]!.id === right[common]!.id) common++;
40
+ const changes: FileChanges = Object.create(null) as FileChanges;
41
+ const apply = (path: string, before: FileVersion, after: FileVersion) => {
42
+ const existing = changes[path];
43
+ if (existing && !sameVersion(existing.after, before)) throw new Error(`discontinuous file history: ${path}; inspect intervening external edits`);
44
+ changes[path] = { before: existing ? existing.before : before, after };
45
+ };
46
+ for (const event of left.slice(common).reverse()) for (const path of changedPaths(event.changes)) {
47
+ const change = event.changes[path]!;
48
+ apply(path, change.after, change.before);
49
+ }
50
+ for (const event of right.slice(common)) for (const path of changedPaths(event.changes)) {
51
+ const change = event.changes[path]!;
52
+ apply(path, change.before, change.after);
53
+ }
54
+ return Object.fromEntries(changedPaths(changes).map(path => [path, changes[path]!])) as FileChanges;
55
+ }
56
+
57
+ export function validFileChanges(value: unknown): value is FileChanges {
58
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
59
+ const version = (v: unknown): boolean => v === null || typeof v === "object" && !Array.isArray(v) &&
60
+ typeof (v as Record<string, unknown>)["sha256"] === "string" && /^[a-f0-9]{64}$/.test(String((v as Record<string, unknown>)["sha256"])) &&
61
+ Number.isInteger((v as Record<string, unknown>)["mode"]) && Number((v as Record<string, unknown>)["mode"]) >= 0 && Number((v as Record<string, unknown>)["mode"]) <= 0o777;
62
+ return Object.entries(value).every(([path, change]: [string, unknown]) => path.length > 0 && typeof change === "object" && change !== null &&
63
+ version((change as Record<string, unknown>)["before"]) && version((change as Record<string, unknown>)["after"]));
64
+ }
package/src/core/churn.ts CHANGED
@@ -34,6 +34,8 @@ export function churnReview(events: readonly LogEvent[]): Pick<GuidanceEvent, "r
34
34
  if (event.t !== "tool_result" || event.effect === undefined) continue;
35
35
  const effect = event.effect;
36
36
  if (effect.kind === "command") {
37
+ // R5 handles structured failures with version/change premises; keep this path for legacy logs.
38
+ if (event.diagnostic?.execution !== undefined) continue;
37
39
  const key = effect.cmd.trim();
38
40
  if (effect.exit === 0) { failures.delete(key); continue; }
39
41
  const seen = { count: (failures.get(key)?.count ?? 0) + 1, last: event.id };
@@ -0,0 +1,94 @@
1
+ /** R8 / D8 / D11: an explicit observation scope is not a claim of build causality. */
2
+ import type { ArtifactRef } from "./events.ts";
3
+ import { isObservationArtifact } from "./observation-view.ts";
4
+
5
+ // Assumed observation budgets keep a model-supplied plan bounded before execution.
6
+ export const MAX_COMMAND_ARTIFACTS = 16;
7
+ export const MAX_ARTIFACT_PLAN_BYTES = 16 * 1024;
8
+ export const MAX_COMMAND_ARTIFACT_BYTES = 512 * 1024 * 1024;
9
+ export const COMMAND_ARTIFACT_SCOPE = "source-v1-minus-artifacts";
10
+ export type CommandArtifactPlan = { paths: string[]; output_roots: string[] };
11
+ export type ArtifactFileIdentity = { device: string; inode: string; mtime_ns: string; ctime_ns: string; mode: number };
12
+ export type ArtifactFile = { state: "observed"; sha256: string; bytes: number; identity: ArtifactFileIdentity } |
13
+ { state: "missing" | "unsafe" | "changed" | "unknown"; reason: string };
14
+ export type ArtifactChange = "created" | "modified" | "unchanged" | "unknown";
15
+ export type CommandArtifactOutput = { path: string; before: ArtifactFile; after: ArtifactFile; change: ArtifactChange };
16
+ export type CommandArtifactReceipt = {
17
+ kind: "command_artifacts"; version: 1;
18
+ state: "pending" | "observed" | "changed" | "unknown";
19
+ plan: CommandArtifactPlan;
20
+ source: { host: string; workspace: string; cwd: string; runtime: string };
21
+ command: string; exit: number | null;
22
+ started_at: string; observed_at: string | null;
23
+ inputs: { scope: typeof COMMAND_ARTIFACT_SCOPE; exclusions: string[]; before: string | null; after: string | null };
24
+ outputs: CommandArtifactOutput[];
25
+ reasons: string[];
26
+ manifest?: ArtifactRef;
27
+ };
28
+
29
+ const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
30
+ const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
31
+ const covers = (root: string, path: string) => root === path || path.startsWith(root + "/");
32
+ export function validArtifactPath(value: unknown): value is string {
33
+ return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).length <= 4096 &&
34
+ !/[\x00-\x1f\x7f\\:]/.test(value) && value.split("/").every(part => part !== "" && part !== "." && part !== ".." && part !== ".git" && part !== ".xeno");
35
+ }
36
+ export function parseCommandArtifactPlan(value: unknown): CommandArtifactPlan {
37
+ if (!object(value) || Object.keys(value).some(key => !["paths", "output_roots"].includes(key))) throw new Error("artifacts needs paths and optional output_roots only");
38
+ const paths = value["paths"], roots = value["output_roots"] ?? paths;
39
+ for (const list of [paths, roots]) {
40
+ if (!Array.isArray(list) || list.length < 1 || list.length > MAX_COMMAND_ARTIFACTS || !list.every(validArtifactPath) || new Set(list).size !== list.length)
41
+ throw new Error("artifacts needs 1..16 unique workspace-relative file paths; no traversal or protected paths");
42
+ }
43
+ const plan = { paths: [...paths as string[]].sort(), output_roots: [...roots as string[]].sort() };
44
+ if (new TextEncoder().encode(JSON.stringify(plan)).length > MAX_ARTIFACT_PLAN_BYTES) throw new Error("artifact plan exceeds 16 KiB");
45
+ if (plan.paths.some((path, i) => plan.paths.some((other, j) => i !== j && covers(path, other)))) throw new Error("artifact file paths cannot contain one another");
46
+ if (plan.output_roots.some((root, i) => !plan.paths.some(path => covers(root, path)) || plan.output_roots.some((other, j) => i !== j && covers(root, other))) ||
47
+ plan.paths.some(path => !plan.output_roots.some(root => covers(root, path)))) throw new Error("output_roots must be nonoverlapping and cover exactly the requested artifact paths");
48
+ return plan;
49
+ }
50
+ export function artifactChange(before: ArtifactFile, after: ArtifactFile): ArtifactChange {
51
+ if (after.state !== "observed") return "unknown";
52
+ if (before.state === "missing") return "created";
53
+ if (before.state !== "observed") return "unknown";
54
+ return before.sha256 === after.sha256 && before.bytes === after.bytes && sameArtifactIdentity(before.identity, after.identity) ? "unchanged" : "modified";
55
+ }
56
+ export function sameArtifactIdentity(a: ArtifactFileIdentity, b: ArtifactFileIdentity): boolean {
57
+ return a.device === b.device && a.inode === b.inode && a.mtime_ns === b.mtime_ns && a.ctime_ns === b.ctime_ns && a.mode === b.mode;
58
+ }
59
+ export function commandArtifactState(receipt: Pick<CommandArtifactReceipt, "exit" | "inputs" | "outputs" | "reasons">): CommandArtifactReceipt["state"] {
60
+ if (receipt.exit === null) return "pending";
61
+ if (receipt.inputs.before !== null && receipt.inputs.after !== null && receipt.inputs.before !== receipt.inputs.after ||
62
+ receipt.outputs.some(output => output.after.state === "changed")) return "changed";
63
+ if (receipt.exit !== 0 || receipt.reasons.length > 0 || receipt.inputs.before === null || receipt.inputs.after === null ||
64
+ receipt.outputs.length === 0 || receipt.outputs.some(output => output.after.state !== "observed")) return "unknown";
65
+ return "observed";
66
+ }
67
+ export function isArtifactFile(value: unknown): value is ArtifactFile {
68
+ if (!object(value)) return false;
69
+ if (value["state"] !== "observed") return ["missing", "unsafe", "changed", "unknown"].includes(String(value["state"])) && typeof value["reason"] === "string" && value["reason"].length > 0;
70
+ const id = value["identity"];
71
+ return digest(value["sha256"]) && Number.isSafeInteger(value["bytes"]) && Number(value["bytes"]) >= 0 && Number(value["bytes"]) <= MAX_COMMAND_ARTIFACT_BYTES &&
72
+ object(id) && ["device", "inode", "mtime_ns", "ctime_ns"].every(key => typeof id[key] === "string" && /^-?\d+$/.test(id[key])) &&
73
+ Number.isInteger(id["mode"]) && Number(id["mode"]) >= 0 && Number(id["mode"]) <= 0o777;
74
+ }
75
+ export function isCommandArtifactReceipt(value: unknown): value is CommandArtifactReceipt {
76
+ if (!object(value) || value["kind"] !== "command_artifacts" || value["version"] !== 1 || typeof value["command"] !== "string" || !value["command"].trim() ||
77
+ typeof value["started_at"] !== "string" || (value["exit"] !== null && !Number.isInteger(value["exit"]))) return false;
78
+ let plan: CommandArtifactPlan;
79
+ try { plan = parseCommandArtifactPlan(value["plan"]); } catch { return false; }
80
+ if (!object(value["plan"]) || JSON.stringify(value["plan"]["paths"]) !== JSON.stringify(plan.paths) ||
81
+ JSON.stringify(value["plan"]["output_roots"]) !== JSON.stringify(plan.output_roots)) return false;
82
+ const source = value["source"], inputs = value["inputs"], outputs = value["outputs"], reasons = value["reasons"];
83
+ if (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== "") ||
84
+ !object(inputs) || inputs["scope"] !== COMMAND_ARTIFACT_SCOPE || !Array.isArray(inputs["exclusions"]) ||
85
+ !inputs["exclusions"].every(path => typeof path === "string" && path.length > 0) || new Set(inputs["exclusions"]).size !== inputs["exclusions"].length ||
86
+ !plan.output_roots.every(root => (inputs["exclusions"] as unknown[]).includes(root)) ||
87
+ !["before", "after"].every(key => inputs[key] === null || digest(inputs[key])) || !Array.isArray(outputs) ||
88
+ !Array.isArray(reasons) || !reasons.every(reason => typeof reason === "string" && reason.length > 0)) return false;
89
+ if (value["exit"] === null) return value["state"] === "pending" && value["observed_at"] === null && inputs["after"] === null && outputs.length === 0 && value["manifest"] === undefined;
90
+ if (typeof value["observed_at"] !== "string" || !value["observed_at"] || outputs.length !== plan.paths.length || !outputs.every((item, index) =>
91
+ object(item) && item["path"] === plan.paths[index] && isArtifactFile(item["before"]) && isArtifactFile(item["after"]) && item["change"] === artifactChange(item["before"], item["after"]))) return false;
92
+ if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
93
+ return value["state"] === commandArtifactState(value as CommandArtifactReceipt) && (value["state"] !== "observed" || isObservationArtifact(value["manifest"]));
94
+ }
@@ -0,0 +1,35 @@
1
+ /** R1 / D11: all command entrances agree on invocation and observed termination. */
2
+ import { executionOutcome } from "./outcome.ts";
3
+ import type { ToolDiagnostic } from "./tool-contract.ts";
4
+
5
+ /** Operational grace, not a measured optimum: ignored SIGTERM cannot defeat cancellation. */
6
+ export const COMMAND_TERMINATION_GRACE_MS = 200;
7
+
8
+ /** A fresh login Bash per command; interactive stdin does not change shell success semantics. */
9
+ export function commandArgv(command: string): string[] {
10
+ return ["bash", "-o", "pipefail", "-lc", command];
11
+ }
12
+
13
+ /** Preserve inherited values except documented noninteractive defaults; never mutate the host. */
14
+ export function commandEnvironment(base: Readonly<Record<string, string | undefined>>): Record<string, string> {
15
+ const env: Record<string, string> = {};
16
+ for (const [key, value] of Object.entries(base)) if (value !== undefined) env[key] = value;
17
+ Object.assign(env, {
18
+ TERM: "dumb", PAGER: "cat", GIT_PAGER: "cat", NO_COLOR: "1",
19
+ GIT_TERMINAL_PROMPT: "0", DEBIAN_FRONTEND: "noninteractive",
20
+ });
21
+ if (env["LANG"] === undefined || env["LANG"] === "") env["LANG"] = "C.UTF-8";
22
+ return env;
23
+ }
24
+
25
+ /** Model seconds and host milliseconds meet once; a yield is never a command deadline. */
26
+ export function commandTimeout(requestedSeconds: number | undefined, defaultMs: number, maximumMs = defaultMs): number {
27
+ return Math.min(requestedSeconds === undefined ? defaultMs : requestedSeconds * 1000, maximumMs);
28
+ }
29
+
30
+ /** A trap can exit zero after cancellation: successful exit alone does not erase the stop. */
31
+ export function commandCompletion(exit: number, stopped?: "cancelled" | "timeout", cleanup?: ToolDiagnostic) {
32
+ return { ok: stopped === undefined && exit === 0 && cleanup === undefined,
33
+ outcome: executionOutcome(stopped ?? (exit === 0 && cleanup === undefined ? "tool_completed" : "tool_failed")),
34
+ ...(cleanup ? { warnings: [cleanup] } : {}) };
35
+ }
@@ -0,0 +1,20 @@
1
+ import { assertNever } from "./exhaustive.ts";
2
+
3
+ /** D19: confirmation follows the selected completion, never the unfinished draft. */
4
+ export function completionAction(kind: "command" | "argument" | "path", key: "enter" | "tab", requiresArgument: boolean): "insert" | "submit" {
5
+ if (key === "tab") return "insert";
6
+ switch (kind) {
7
+ case "command": return requiresArgument ? "insert" : "submit";
8
+ case "argument": return "submit";
9
+ case "path": return "insert";
10
+ default: return assertNever(kind, "completion kind");
11
+ }
12
+ }
13
+
14
+ /** Match the parser's exact identity before discoverable prefixes and substrings. */
15
+ export function commandCompletionRank(name: string, query: string): number {
16
+ const canonical = name.toLowerCase(), typed = query.toLowerCase();
17
+ if (canonical === typed) return 0;
18
+ if (canonical.startsWith(typed)) return 1;
19
+ return typed.length > 1 && canonical.includes(typed) ? 2 : Number.POSITIVE_INFINITY;
20
+ }