@selesai/code 0.9.14 → 0.10.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 (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -1,586 +0,0 @@
1
- import { readdirSync } from "node:fs";
2
- import { execFile } from "node:child_process";
3
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
- import type { AutocompleteItem, AutocompleteProvider, AutocompleteSuggestions } from "@earendil-works/pi-tui";
5
- import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory } from "./history.ts";
6
- import type { ExtendedCompletionItem, GhostSuggestion } from "./types.ts";
7
-
8
- interface TokenContext {
9
- line: string;
10
- cursorCol: number;
11
- beforeCursor: string;
12
- afterCursor: string;
13
- token: string;
14
- tokenStart: number;
15
- tokenEnd: number;
16
- tokenIndex: number;
17
- previousTokens: string[];
18
- }
19
-
20
- export interface OneOffBashCommandContext {
21
- prefix: string;
22
- command: string;
23
- offset: number;
24
- }
25
-
26
- const GIT_SUBCOMMANDS = [
27
- "add", "bisect", "branch", "checkout", "cherry-pick", "clean", "clone", "commit", "diff", "fetch",
28
- "grep", "init", "log", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert", "rm",
29
- "show", "stash", "status", "switch", "tag", "worktree",
30
- ];
31
-
32
- function tokenizeBeforeCursor(text: string): string[] {
33
- const tokens: string[] = [];
34
- let current = "";
35
- let quote: "'" | '"' | null = null;
36
- let escaped = false;
37
-
38
- for (const char of text) {
39
- if (escaped) {
40
- current += char;
41
- escaped = false;
42
- continue;
43
- }
44
-
45
- if (char === "\\") {
46
- escaped = true;
47
- current += char;
48
- continue;
49
- }
50
-
51
- if (quote) {
52
- current += char;
53
- if (char === quote) quote = null;
54
- continue;
55
- }
56
-
57
- if (char === "'" || char === '"') {
58
- quote = char;
59
- current += char;
60
- continue;
61
- }
62
-
63
- if (/\s/.test(char)) {
64
- if (current) {
65
- tokens.push(current);
66
- current = "";
67
- }
68
- continue;
69
- }
70
-
71
- current += char;
72
- }
73
-
74
- if (current) tokens.push(current);
75
- return tokens;
76
- }
77
-
78
- function getTokenContext(line: string, cursorCol: number): TokenContext {
79
- const beforeCursor = line.slice(0, cursorCol);
80
- const afterCursor = line.slice(cursorCol);
81
- const tokens = tokenizeBeforeCursor(beforeCursor);
82
-
83
- let tokenStart = 0;
84
- for (let i = beforeCursor.length - 1; i >= 0; i -= 1) {
85
- const char = beforeCursor[i];
86
- if (char && /\s/.test(char)) {
87
- tokenStart = i + 1;
88
- break;
89
- }
90
- }
91
-
92
- let tokenEnd = cursorCol;
93
- for (let i = cursorCol; i < line.length; i += 1) {
94
- const char = line[i];
95
- if (char && /\s/.test(char)) break;
96
- tokenEnd = i + 1;
97
- }
98
-
99
- return {
100
- line,
101
- cursorCol,
102
- beforeCursor,
103
- afterCursor,
104
- token: line.slice(tokenStart, tokenEnd),
105
- tokenStart,
106
- tokenEnd,
107
- tokenIndex: Math.max(0, tokens.length - 1),
108
- previousTokens: tokens,
109
- };
110
- }
111
-
112
- export function getOneOffBashCommandContext(line: string): OneOffBashCommandContext | null {
113
- if (line.startsWith("!!")) {
114
- return {
115
- prefix: "!!",
116
- command: line.slice(2),
117
- offset: 2,
118
- };
119
- }
120
-
121
- if (line.startsWith("!")) {
122
- return {
123
- prefix: "!",
124
- command: line.slice(1),
125
- offset: 1,
126
- };
127
- }
128
-
129
- return null;
130
- }
131
-
132
- function supportsShouldTriggerFileCompletion(
133
- provider: AutocompleteProvider,
134
- ): provider is AutocompleteProvider & {
135
- shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
136
- } {
137
- return "shouldTriggerFileCompletion" in provider && typeof provider.shouldTriggerFileCompletion === "function";
138
- }
139
-
140
- function isExtendedCompletionItem(item: AutocompleteItem): item is ExtendedCompletionItem {
141
- return "replacement" in item
142
- && typeof item.replacement === "string"
143
- && "startCol" in item
144
- && typeof item.startCol === "number"
145
- && "endCol" in item
146
- && typeof item.endCol === "number";
147
- }
148
-
149
- function uniqueByReplacement(items: ExtendedCompletionItem[]): ExtendedCompletionItem[] {
150
- const best = new Map<string, ExtendedCompletionItem>();
151
- for (const item of items) {
152
- const key = `${item.startCol}:${item.endCol}:${item.replacement}`;
153
- const existing = best.get(key);
154
- if (!existing || item.score > existing.score) {
155
- best.set(key, item);
156
- }
157
- }
158
- return [...best.values()].sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
159
- }
160
-
161
- function pathBase(token: string, cwd: string): { dir: string; prefix: string; displayPrefix: string } {
162
- const expanded = token.startsWith("~/")
163
- ? join(process.env.HOME || "", token.slice(2))
164
- : token;
165
-
166
- const hasSlash = expanded.includes("/");
167
- if (!hasSlash) {
168
- return { dir: cwd, prefix: expanded, displayPrefix: "" };
169
- }
170
-
171
- const baseDir = expanded.endsWith("/") ? expanded.slice(0, -1) : dirname(expanded);
172
- const resolvedDir = isAbsolute(baseDir) ? baseDir : resolve(cwd, baseDir);
173
- const prefix = expanded.endsWith("/") ? "" : basename(expanded);
174
- const displayPrefix = token.endsWith("/") ? token : token.slice(0, Math.max(0, token.length - prefix.length));
175
- return { dir: resolvedDir, prefix, displayPrefix };
176
- }
177
-
178
- function escapeShellPath(value: string): string {
179
- return value.replace(/([\\\s"'`$&|;<>()[\]{}?!*])/g, "\\$1");
180
- }
181
-
182
- function getPathSuggestions(token: string, cwd: string): ExtendedCompletionItem[] {
183
- if (!token) return [];
184
- const { dir, prefix, displayPrefix } = pathBase(token, cwd);
185
-
186
- try {
187
- const entries = readdirSync(dir, { withFileTypes: true })
188
- .filter((entry) => prefix.length === 0 || entry.name.startsWith(prefix))
189
- .slice(0, 100);
190
-
191
- return entries.map((entry) => {
192
- const suffix = entry.isDirectory() ? "/" : "";
193
- const label = `${displayPrefix}${entry.name}${suffix}`;
194
- const replacement = `${displayPrefix}${escapeShellPath(entry.name)}${suffix}`;
195
- return {
196
- value: replacement,
197
- label,
198
- replacement,
199
- startCol: 0,
200
- endCol: 0,
201
- source: "path",
202
- score: 40 + (entry.isDirectory() ? 4 : 0),
203
- } satisfies ExtendedCompletionItem;
204
- });
205
- } catch {
206
- // Missing or unreadable directories should only remove this completion source.
207
- return [];
208
- }
209
- }
210
-
211
- function runGit(args: string[], cwd: string, signal: AbortSignal): Promise<string[]> {
212
- return new Promise((resolve) => {
213
- try {
214
- execFile("git", args, { cwd, encoding: "utf8", signal }, (error, stdout) => {
215
- if (error || !stdout) {
216
- resolve([]);
217
- return;
218
- }
219
- resolve(stdout.split("\n").map((line) => line.trim()).filter(Boolean));
220
- });
221
- } catch {
222
- // Git-aware completions are optional and should not break the main completion flow.
223
- resolve([]);
224
- }
225
- });
226
- }
227
-
228
- async function getGitSuggestions(
229
- ctx: TokenContext,
230
- cwd: string,
231
- signal: AbortSignal,
232
- ): Promise<ExtendedCompletionItem[]> {
233
- const tokens = ctx.previousTokens;
234
- if (tokens[0] !== "git") return [];
235
-
236
- if (ctx.tokenIndex <= 1) {
237
- return GIT_SUBCOMMANDS
238
- .filter((command) => command.startsWith(ctx.token))
239
- .map((command) => ({
240
- value: command,
241
- label: command,
242
- replacement: command,
243
- startCol: 0,
244
- endCol: 0,
245
- source: "git",
246
- score: 52,
247
- }));
248
- }
249
-
250
- const subcommand = tokens[1] ?? "";
251
- if (!["checkout", "switch", "merge", "rebase", "branch", "show", "diff"].includes(subcommand)) {
252
- return [];
253
- }
254
-
255
- const [branches, tags] = await Promise.all([
256
- runGit(["branch", "--format=%(refname:short)"], cwd, signal),
257
- runGit(["tag", "--list"], cwd, signal),
258
- ]);
259
- if (signal.aborted) return [];
260
-
261
- return [...new Set([...branches, ...tags])]
262
- .filter((ref) => ref.startsWith(ctx.token))
263
- .slice(0, 100)
264
- .map((ref) => ({
265
- value: ref,
266
- label: ref,
267
- replacement: ref,
268
- startCol: 0,
269
- endCol: 0,
270
- source: "git",
271
- score: 50,
272
- }));
273
- }
274
-
275
- function canUseHistorySuggestion(ctx: TokenContext): boolean {
276
- return ctx.cursorCol === ctx.line.length && ctx.line.trim().length > 0;
277
- }
278
-
279
- function withRange(items: ExtendedCompletionItem[], startCol: number, endCol: number): ExtendedCompletionItem[] {
280
- return items.map((item) => ({ ...item, startCol, endCol }));
281
- }
282
-
283
- function applyCompletionToLine(line: string, item: ExtendedCompletionItem): string {
284
- return line.slice(0, item.startCol) + item.replacement + line.slice(item.endCol);
285
- }
286
-
287
- function commandHead(value: string): string {
288
- return tokenizeBeforeCursor(value.trim())[0] ?? "";
289
- }
290
-
291
- function findNewestHistoryMatchForHead(entries: string[], prefix: string, head: string): string | null {
292
- for (const rawEntry of entries) {
293
- const entry = rawEntry.trim();
294
- if (!entry || !entry.startsWith(prefix)) continue;
295
- if (commandHead(entry) !== head) continue;
296
- return entry;
297
- }
298
- return null;
299
- }
300
-
301
- function getCuratedCommandFallback(prefix: string): GhostSuggestion | null {
302
- const trimmed = prefix.trim();
303
- if (!trimmed) return null;
304
-
305
- if ("cd".startsWith(trimmed)) {
306
- return { value: "cd ..", source: "path" };
307
- }
308
-
309
- if ("git".startsWith(trimmed)) {
310
- return { value: "git status", source: "git" };
311
- }
312
-
313
- return null;
314
- }
315
-
316
- function isLikelyGitCommandHead(prefix: string): boolean {
317
- const trimmed = prefix.trim();
318
- return trimmed.length >= 2 && "git".startsWith(trimmed);
319
- }
320
-
321
- function boostValidatedItemsFromGlobalHistory(
322
- line: string,
323
- items: ExtendedCompletionItem[],
324
- globalHistoryMatches: string[],
325
- ): ExtendedCompletionItem[] {
326
- if (items.length === 0 || globalHistoryMatches.length === 0) {
327
- return items;
328
- }
329
-
330
- const boosts = new Map<string, number>();
331
- for (const [index, value] of globalHistoryMatches.entries()) {
332
- boosts.set(value, Math.max(1, 4 - index));
333
- }
334
-
335
- return items.map((item) => {
336
- const boost = boosts.get(applyCompletionToLine(line, item));
337
- return boost ? { ...item, score: item.score + boost } : item;
338
- });
339
- }
340
-
341
- export class BashCompletionEngine {
342
- async getGhostSuggestion(line: string, cwd: string, shellPath: string, signal: AbortSignal): Promise<GhostSuggestion | null> {
343
- const projectHistoryEntries = readProjectHistory(cwd);
344
-
345
- if (line.trim().length === 0) {
346
- const prefix = line;
347
- const projectHistory = matchHistoryEntries(
348
- projectHistoryEntries.map((entry) => entry.command),
349
- line,
350
- 1,
351
- );
352
- if (projectHistory.length > 0) {
353
- return { value: `${prefix}${projectHistory[0]!}`, source: "project-history" };
354
- }
355
-
356
- return null;
357
- }
358
-
359
- const ctx = getTokenContext(line, line.length);
360
- if (!canUseHistorySuggestion(ctx)) return null;
361
-
362
- const projectHistory = matchHistoryEntries(
363
- projectHistoryEntries.map((entry) => entry.command),
364
- line,
365
- 10,
366
- );
367
- if (projectHistory.length > 0) {
368
- return { value: projectHistory[0]!, source: "project-history" };
369
- }
370
-
371
- if (ctx.tokenIndex === 0) {
372
- return this.getCommandPositionGhostSuggestion(line, readGlobalShellHistory(shellPath));
373
- }
374
-
375
- const deterministic = await this.getDeterministicInlineSuggestions(ctx, cwd, signal);
376
- if (signal.aborted) return null;
377
- const globalHistory = matchHistoryEntries(readGlobalShellHistory(shellPath), line, 5);
378
- const ranked = boostValidatedItemsFromGlobalHistory(
379
- line,
380
- uniqueByReplacement(deterministic),
381
- globalHistory,
382
- );
383
-
384
- for (const item of ranked) {
385
- const value = this.buildInlineSuggestionValue(line, item);
386
- if (value) {
387
- return { value, source: item.source };
388
- }
389
- }
390
-
391
- return null;
392
- }
393
-
394
- private getCommandPositionGhostSuggestion(
395
- line: string,
396
- globalHistoryEntries: string[],
397
- ): GhostSuggestion | null {
398
- if (isLikelyGitCommandHead(line)) {
399
- const globalMatch = findNewestHistoryMatchForHead(globalHistoryEntries, line, "git");
400
- if (globalMatch) {
401
- return { value: globalMatch, source: "global-history" };
402
- }
403
- }
404
-
405
- return getCuratedCommandFallback(line);
406
- }
407
-
408
- private async getDeterministicInlineSuggestions(
409
- ctx: TokenContext,
410
- cwd: string,
411
- signal: AbortSignal,
412
- ): Promise<ExtendedCompletionItem[]> {
413
- const gitSuggestions = getGitSuggestions(ctx, cwd, signal);
414
- const pathSuggestions = getPathSuggestions(ctx.token, cwd);
415
- const items = [
416
- ...withRange(await gitSuggestions, ctx.tokenStart, ctx.tokenEnd),
417
- ...withRange(pathSuggestions, ctx.tokenStart, ctx.tokenEnd),
418
- ];
419
-
420
- return uniqueByReplacement(items);
421
- }
422
-
423
- private buildInlineSuggestionValue(line: string, item: ExtendedCompletionItem): string | null {
424
- const value = applyCompletionToLine(line, item);
425
- if (!value.startsWith(line) || value === line) {
426
- return null;
427
- }
428
- return value;
429
- }
430
-
431
- }
432
-
433
- export class BashAutocompleteProvider implements AutocompleteProvider {
434
- async getSuggestions(): Promise<AutocompleteSuggestions | null> {
435
- return null;
436
- }
437
-
438
- applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem): {
439
- lines: string[];
440
- cursorLine: number;
441
- cursorCol: number;
442
- } {
443
- if (!isExtendedCompletionItem(item)) {
444
- throw new Error("Expected an extended completion item for bash autocomplete");
445
- }
446
-
447
- return applyExtendedCompletion(lines, cursorLine, item);
448
- }
449
-
450
- shouldTriggerFileCompletion(): boolean {
451
- return false;
452
- }
453
- }
454
-
455
- function applyExtendedCompletion(lines: string[], cursorLine: number, item: ExtendedCompletionItem): {
456
- lines: string[];
457
- cursorLine: number;
458
- cursorCol: number;
459
- } {
460
- const currentLine = lines[cursorLine] || "";
461
- const startCol = Math.max(0, Math.min(item.startCol, currentLine.length));
462
- const endCol = Math.max(startCol, Math.min(item.endCol, currentLine.length));
463
- const nextLine = currentLine.slice(0, startCol) + item.replacement + currentLine.slice(endCol);
464
- const nextLines = [...lines];
465
- nextLines[cursorLine] = nextLine;
466
- return {
467
- lines: nextLines,
468
- cursorLine,
469
- cursorCol: startCol + item.replacement.length,
470
- };
471
- }
472
-
473
- export class OneOffBashAutocompleteProvider implements AutocompleteProvider {
474
- async getSuggestions(): Promise<AutocompleteSuggestions | null> {
475
- return null;
476
- }
477
-
478
- applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem): {
479
- lines: string[];
480
- cursorLine: number;
481
- cursorCol: number;
482
- } {
483
- if (!isExtendedCompletionItem(item)) {
484
- throw new Error("Expected an extended completion item for one-off bash autocomplete");
485
- }
486
-
487
- return applyExtendedCompletion(lines, cursorLine, item);
488
- }
489
-
490
- shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {
491
- const bang = cursorLine === 0 ? getOneOffBashCommandContext(lines[0] || "") : null;
492
- return bang !== null && cursorCol >= bang.offset;
493
- }
494
- }
495
-
496
- export class ModeAwareAutocompleteProvider implements AutocompleteProvider {
497
- private readonly defaultProvider: AutocompleteProvider | undefined;
498
- private readonly bashProvider: AutocompleteProvider;
499
- private readonly oneOffBashProvider: AutocompleteProvider;
500
- private readonly isBashModeActive: () => boolean;
501
-
502
- constructor(
503
- defaultProvider: AutocompleteProvider | undefined,
504
- bashProvider: AutocompleteProvider,
505
- oneOffBashProvider: AutocompleteProvider,
506
- isBashModeActive: () => boolean,
507
- ) {
508
- this.defaultProvider = defaultProvider;
509
- this.bashProvider = bashProvider;
510
- this.oneOffBashProvider = oneOffBashProvider;
511
- this.isBashModeActive = isBashModeActive;
512
- }
513
-
514
- /**
515
- * Forward trigger characters from the underlying providers. Without this the
516
- * editor resets its trigger set to the defaults when this wrapper is installed
517
- * (e.g. "$" is dropped), so inline completions like `$skill` never open a popup.
518
- */
519
- get triggerCharacters(): string[] | undefined {
520
- const chars = new Set<string>();
521
- for (const provider of [this.defaultProvider, this.bashProvider, this.oneOffBashProvider]) {
522
- for (const character of provider?.triggerCharacters ?? []) {
523
- chars.add(character);
524
- }
525
- }
526
- return chars.size > 0 ? [...chars] : undefined;
527
- }
528
-
529
- async getSuggestions(
530
- lines: string[],
531
- cursorLine: number,
532
- cursorCol: number,
533
- options: { signal: AbortSignal; force?: boolean },
534
- ): Promise<AutocompleteSuggestions | null> {
535
- if (this.isBashModeActive()) {
536
- return this.bashProvider.getSuggestions(lines, cursorLine, cursorCol, options);
537
- }
538
-
539
- const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
540
- && this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
541
- if (shouldUseOneOffBash) {
542
- return this.oneOffBashProvider.getSuggestions(lines, cursorLine, cursorCol, options);
543
- }
544
-
545
- return this.defaultProvider?.getSuggestions(lines, cursorLine, cursorCol, options) ?? null;
546
- }
547
-
548
- applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string) {
549
- if (this.isBashModeActive()) {
550
- return this.bashProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
551
- }
552
-
553
- const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
554
- && this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
555
- if (shouldUseOneOffBash) {
556
- return this.oneOffBashProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
557
- }
558
-
559
- if (!this.defaultProvider) {
560
- return { lines, cursorLine, cursorCol };
561
- }
562
- return this.defaultProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
563
- }
564
-
565
- shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {
566
- if (this.isBashModeActive()) {
567
- if (!supportsShouldTriggerFileCompletion(this.bashProvider)) {
568
- return true;
569
- }
570
-
571
- return this.bashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
572
- }
573
-
574
- const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
575
- && this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
576
- if (shouldUseOneOffBash) {
577
- return true;
578
- }
579
-
580
- if (!this.defaultProvider || !supportsShouldTriggerFileCompletion(this.defaultProvider)) {
581
- return false;
582
- }
583
-
584
- return this.defaultProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
585
- }
586
- }