@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,709 +0,0 @@
1
- import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
2
- import { join, basename } from "node:path";
3
- import { homedir as osHomedir } from "node:os";
4
- import { CONFIG_DIR_NAME, getAgentDir, getSettingsPath } from "@selesai/code";
5
- import type { Component } from "@earendil-works/pi-tui";
6
- import { truncateToWidth as tuiTruncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
7
- import { ansi, fgOnly, getFgAnsiCode } from "./colors.ts";
8
- import { GUIDE_COMPACT_LINES, GUIDE_DISMISS_HINT, getGuideFeatureLines, type GuideFeature, type GuideMode } from "./guide.ts";
9
-
10
- export interface RecentSession {
11
- name: string;
12
- timeAgo: string;
13
- }
14
-
15
- export interface LoadedCounts {
16
- contextFiles: number;
17
- extensions: number;
18
- skills: number;
19
- promptTemplates: number;
20
- }
21
-
22
- function formatTokens(tokens: number): string {
23
- if (tokens < 1000) return tokens.toString();
24
- if (tokens < 10000) return `${(tokens / 1000).toFixed(1)}k`;
25
- if (tokens < 1000000) return `${Math.round(tokens / 1000)}k`;
26
- return `${(tokens / 1000000).toFixed(tokens < 10000000 ? 1 : 0)}M`;
27
- }
28
-
29
- // ═══════════════════════════════════════════════════════════════════════════
30
- // Shared rendering utilities
31
- // ═══════════════════════════════════════════════════════════════════════════
32
-
33
- const PI_LOGO = [
34
- "███ ███ █ ███ ███ ███ █",
35
- "█ █ █ █ █ █ █ █",
36
- "█ █ █ █ █ █ █ █",
37
- "███ ███ █ ███ ███ ███ █",
38
- " █ █ █ █ █ █ █ █",
39
- " █ █ █ █ █ █ █ █",
40
- "███ ███ ███ ███ ███ █ █ █",
41
- ];
42
-
43
- const GRADIENT_COLORS = [
44
- "\x1b[38;5;199m",
45
- "\x1b[38;5;171m",
46
- "\x1b[38;5;135m",
47
- "\x1b[38;5;99m",
48
- "\x1b[38;5;75m",
49
- "\x1b[38;5;51m",
50
- ];
51
- const SESSION_HEADER_READ_BYTES = 8192;
52
-
53
- function bold(text: string): string {
54
- return `\x1b[1m${text}\x1b[22m`;
55
- }
56
-
57
- function dim(text: string): string {
58
- return getFgAnsiCode("sep") + text + ansi.reset;
59
- }
60
-
61
- function gradientLine(line: string): string {
62
- const reset = ansi.reset;
63
- let result = "";
64
- let colorIdx = 0;
65
- const step = Math.max(1, Math.floor(line.length / GRADIENT_COLORS.length));
66
-
67
- for (let i = 0; i < line.length; i++) {
68
- if (i > 0 && i % step === 0 && colorIdx < GRADIENT_COLORS.length - 1) colorIdx++;
69
- const char = line[i];
70
- if (char !== " ") {
71
- result += GRADIENT_COLORS[colorIdx] + char + reset;
72
- } else {
73
- result += char;
74
- }
75
- }
76
- return result;
77
- }
78
-
79
- function centerText(text: string, width: number): string {
80
- const visLen = visibleWidth(text);
81
- if (visLen > width) return tuiTruncateToWidth(text, width, "…");
82
- if (visLen === width) return text;
83
- const leftPad = Math.floor((width - visLen) / 2);
84
- const rightPad = width - visLen - leftPad;
85
- return " ".repeat(leftPad) + text + " ".repeat(rightPad);
86
- }
87
-
88
- function fitToWidth(str: string, width: number): string {
89
- const visLen = visibleWidth(str);
90
- if (visLen > width) return tuiTruncateToWidth(str, width, "…");
91
- return str + " ".repeat(width - visLen);
92
- }
93
-
94
- function wrapLines(lines: string[], width: number): string[] {
95
- const continuation = " ";
96
- const contentWidth = Math.max(1, width - continuation.length);
97
- return lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth)
98
- .map((wrapped, index) => index === 0 ? wrapped : continuation + wrapped));
99
- }
100
-
101
- interface WelcomeData {
102
- modelName: string;
103
- providerName: string;
104
- recentSessions: RecentSession[];
105
- loadedCounts: LoadedCounts;
106
- guideMode: GuideMode;
107
- newFeatures: GuideFeature[];
108
- initialContextTokens: number | null;
109
- }
110
-
111
- function buildLeftColumn(data: WelcomeData, colWidth: number): string[] {
112
- const logoColored = PI_LOGO.map((line) => gradientLine(line));
113
-
114
- return [
115
- "",
116
- centerText(bold("Welcome back!"), colWidth),
117
- "",
118
- ...logoColored.map((l) => centerText(l, colWidth)),
119
- "",
120
- centerText(fgOnly("model", data.modelName), colWidth),
121
- centerText(dim(data.providerName), colWidth),
122
- ];
123
- }
124
-
125
- function buildRightColumn(data: WelcomeData, colWidth: number): string[] {
126
- const hChar = "─";
127
- const separator = ` ${dim(hChar.repeat(colWidth - 2))}`;
128
-
129
- const guideLines = data.guideMode === "compact"
130
- ? [
131
- ` ${bold(fgOnly("accent", "Guide"))}`,
132
- ...(data.newFeatures.length > 0
133
- ? [` ${bold(fgOnly("model", `New: ${data.newFeatures.slice(0, 2).map((feature) => feature.title).join(" · ")}`))}`]
134
- : []),
135
- ...GUIDE_COMPACT_LINES.map((line) => ` ${dim(line)}`),
136
- ]
137
- : [
138
- ` ${bold(fgOnly("accent", "Selesai guide"))}`,
139
- ...(data.newFeatures.length > 0
140
- ? [
141
- ` ${bold(fgOnly("model", "New since your last guide"))}`,
142
- ...data.newFeatures.slice(0, 3).map((feature) => ` ${dim("• ")}${feature.title}`),
143
- ]
144
- : []),
145
- ...getGuideFeatureLines().map((line) => line.kind === "section"
146
- ? ` ${bold(fgOnly("accent", line.text))}`
147
- : ` ${dim("• ")}${line.feature.title}: ${dim(line.feature.example)}`),
148
- ];
149
-
150
- // Session lines
151
- const sessionLines: string[] = [];
152
- if (data.recentSessions.length === 0) {
153
- sessionLines.push(` ${dim("No recent sessions")}`);
154
- } else {
155
- for (const session of data.recentSessions.slice(0, 3)) {
156
- sessionLines.push(
157
- ` ${dim("• ")}${fgOnly("path", session.name)}${dim(` (${session.timeAgo})`)}`,
158
- );
159
- }
160
- }
161
-
162
- // Loaded counts lines
163
- const countLines: string[] = [];
164
- const { contextFiles, extensions, skills, promptTemplates } = data.loadedCounts;
165
- const itemPrefix = dim("- ");
166
-
167
- if (contextFiles > 0 || extensions > 0 || skills > 0 || promptTemplates > 0) {
168
- if (contextFiles > 0) {
169
- countLines.push(` ${itemPrefix}${fgOnly("gitClean", `${contextFiles}`)} context file${contextFiles !== 1 ? "s" : ""}`);
170
- }
171
- if (extensions > 0) {
172
- countLines.push(` ${itemPrefix}${fgOnly("gitClean", `${extensions}`)} extension${extensions !== 1 ? "s" : ""}`);
173
- }
174
- if (skills > 0) {
175
- countLines.push(` ${itemPrefix}${fgOnly("gitClean", `${skills}`)} skill${skills !== 1 ? "s" : ""}`);
176
- }
177
- if (promptTemplates > 0) {
178
- countLines.push(` ${itemPrefix}${fgOnly("gitClean", `${promptTemplates}`)} prompt template${promptTemplates !== 1 ? "s" : ""}`);
179
- }
180
- } else {
181
- countLines.push(` ${dim("No extensions loaded")}`);
182
- }
183
-
184
- if (
185
- data.initialContextTokens !== null
186
- && Number.isFinite(data.initialContextTokens)
187
- && data.initialContextTokens > 0
188
- ) {
189
- countLines.push(` ${itemPrefix}${fgOnly("gitClean", `≈ ${formatTokens(data.initialContextTokens)}`)} initial prompt tokens`);
190
- }
191
-
192
- const dashboardLines = data.guideMode === "full"
193
- ? []
194
- : [
195
- separator,
196
- ` ${bold(fgOnly("accent", "Loaded"))}`,
197
- ...countLines,
198
- separator,
199
- ` ${bold(fgOnly("accent", "Recent sessions"))}`,
200
- ...sessionLines,
201
- ];
202
- return [
203
- ` ${bold(fgOnly("accent", "Tips"))}: ${dim("/")} commands · ${dim("/settings")} configure`,
204
- ` ${dim("Shift+Tab")} cycle thinking`,
205
- separator,
206
- ...guideLines,
207
- ...dashboardLines,
208
- "",
209
- ];
210
- }
211
-
212
- function renderWelcomeBox(
213
- data: WelcomeData,
214
- termWidth: number,
215
- bottomLine: string,
216
- ): string[] {
217
- // Minimum width for two-column layout: leftCol(26) + separator(3) + minRightCol(15) = 44
218
- const minLayoutWidth = 44;
219
-
220
- // If terminal is too narrow for the layout, return empty (skip welcome box)
221
- if (termWidth < minLayoutWidth) {
222
- return [];
223
- }
224
-
225
- const minWidth = 76;
226
- const maxWidth = data.guideMode === "full" ? 132 : 96;
227
- // Clamp to termWidth to prevent crash on narrow terminals.
228
- const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
229
- const leftCol = 26;
230
- const rightCol = Math.max(1, boxWidth - leftCol - 3); // Ensure rightCol is at least 1
231
-
232
- const hChar = "─";
233
- const v = dim("│");
234
- const tl = dim("╭");
235
- const tr = dim("╮");
236
- const bl = dim("╰");
237
- const br = dim("╯");
238
-
239
- const leftLines = buildLeftColumn(data, leftCol);
240
- const rightLines = wrapLines(buildRightColumn(data, rightCol), rightCol);
241
-
242
- const lines: string[] = [];
243
-
244
- // Top border with title
245
- const title = "SelesaiCode";
246
- const titlePrefix = dim(hChar.repeat(3));
247
- const titleStyled = titlePrefix + fgOnly("model", title);
248
- const titleVisLen = 3 + visibleWidth(title);
249
- const afterTitle = boxWidth - 2 - titleVisLen;
250
- const afterTitleText = afterTitle > 0 ? dim(hChar.repeat(afterTitle)) : "";
251
- lines.push(tl + titleStyled + afterTitleText + tr);
252
-
253
- // Content rows
254
- const maxRows = Math.max(leftLines.length, rightLines.length);
255
- for (let i = 0; i < maxRows; i++) {
256
- const left = fitToWidth(leftLines[i] ?? "", leftCol);
257
- const right = fitToWidth(rightLines[i] ?? "", rightCol);
258
- lines.push(v + left + v + right + v);
259
- }
260
-
261
- // Bottom border
262
- lines.push(bl + bottomLine + br);
263
-
264
- return lines;
265
- }
266
-
267
- // ═══════════════════════════════════════════════════════════════════════════
268
- // Welcome Components
269
- // ═══════════════════════════════════════════════════════════════════════════
270
-
271
- /**
272
- * Welcome overlay component for SelesaiCode.
273
- * Displays a branded splash screen with logo, tips, and loaded counts.
274
- */
275
- export class WelcomeComponent implements Component {
276
- private data: WelcomeData;
277
-
278
- constructor(
279
- modelName: string,
280
- providerName: string,
281
- recentSessions: RecentSession[] = [],
282
- loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
283
- guideMode: GuideMode = "full",
284
- newFeatures: GuideFeature[] = [],
285
- initialContextTokens: number | null = null,
286
- ) {
287
- this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures, initialContextTokens };
288
- }
289
-
290
- invalidate(): void {}
291
-
292
- render(termWidth: number): string[] {
293
- // Minimum width for two-column layout (must match renderWelcomeBox)
294
- const minLayoutWidth = 44;
295
- if (termWidth < minLayoutWidth) {
296
- return [];
297
- }
298
-
299
- const minWidth = 76;
300
- const maxWidth = 132;
301
- // Clamp to termWidth to prevent crash on narrow terminals.
302
- const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
303
-
304
- // The guide is intentionally blocking: no timer or implicit dismissal.
305
- const promptText = ` ${GUIDE_DISMISS_HINT} `;
306
- const promptStyled = dim(promptText);
307
- const bottomContentWidth = boxWidth - 2;
308
- const promptVisLen = visibleWidth(promptText);
309
- const leftPad = Math.floor((bottomContentWidth - promptVisLen) / 2);
310
- const rightPad = bottomContentWidth - promptVisLen - leftPad;
311
- const hChar = "─";
312
- const bottomLine = dim(hChar.repeat(Math.max(0, leftPad))) +
313
- promptStyled +
314
- dim(hChar.repeat(Math.max(0, rightPad)));
315
-
316
- return renderWelcomeBox(this.data, termWidth, bottomLine);
317
- }
318
- }
319
-
320
- /**
321
- * Compact guide header used when the user selects `/guide compact`.
322
- */
323
- export class WelcomeHeader implements Component {
324
- private data: WelcomeData;
325
-
326
- constructor(
327
- modelName: string,
328
- providerName: string,
329
- recentSessions: RecentSession[] = [],
330
- loadedCounts: LoadedCounts = { contextFiles: 0, extensions: 0, skills: 0, promptTemplates: 0 },
331
- guideMode: GuideMode = "compact",
332
- newFeatures: GuideFeature[] = [],
333
- initialContextTokens: number | null = null,
334
- ) {
335
- this.data = { modelName, providerName, recentSessions, loadedCounts, guideMode, newFeatures, initialContextTokens };
336
- }
337
-
338
- invalidate(): void {}
339
-
340
- render(termWidth: number): string[] {
341
- // Minimum width for two-column layout (must match renderWelcomeBox)
342
- const minLayoutWidth = 44;
343
- if (termWidth < minLayoutWidth) {
344
- return [];
345
- }
346
-
347
- const minWidth = 76;
348
- const maxWidth = 96;
349
- // Clamp to termWidth to prevent crash on narrow terminals
350
- const boxWidth = Math.min(termWidth, Math.max(minWidth, Math.min(termWidth - 2, maxWidth)));
351
- const hChar = "─";
352
-
353
- // Bottom line with column separator (leftCol=26, rightCol=boxWidth-29)
354
- const leftCol = 26;
355
- const rightCol = Math.max(1, boxWidth - leftCol - 3);
356
- const bottomLine = dim(hChar.repeat(leftCol)) + dim("┴") + dim(hChar.repeat(rightCol));
357
-
358
- const lines = renderWelcomeBox(this.data, termWidth, bottomLine);
359
- if (lines.length > 0) {
360
- lines.push(""); // Add empty line for spacing only if we rendered content
361
- }
362
- return lines;
363
- }
364
- }
365
-
366
- // ═══════════════════════════════════════════════════════════════════════════
367
- // Discovery functions
368
- // ═══════════════════════════════════════════════════════════════════════════
369
-
370
- const loggedDiscoveryErrors = new Set<string>();
371
-
372
- function logDiscoveryError(scope: string, error: unknown): void {
373
- if (
374
- typeof error === "object" &&
375
- error !== null &&
376
- "code" in error &&
377
- (error as { code?: unknown }).code === "ENOENT"
378
- ) {
379
- return;
380
- }
381
-
382
- const message = error instanceof Error ? error.message : String(error);
383
- const key = `${scope}:${message}`;
384
- if (loggedDiscoveryErrors.has(key)) {
385
- return;
386
- }
387
-
388
- loggedDiscoveryErrors.add(key);
389
- if (loggedDiscoveryErrors.size > 500) {
390
- loggedDiscoveryErrors.clear();
391
- }
392
-
393
- console.debug(`[powerline-welcome] ${scope}:`, error);
394
- }
395
-
396
- // ponytail: only .claude paths still need raw home; everything else routes via getAgentDir.
397
- function homedirSafe(): string {
398
- return process.env.HOME || process.env.USERPROFILE || osHomedir();
399
- }
400
-
401
- /**
402
- * Discover loaded counts by scanning filesystem.
403
- */
404
- export function discoverLoadedCounts(): LoadedCounts {
405
- const homeDir = getAgentDir();
406
- const cwd = process.cwd();
407
-
408
- let contextFiles = 0;
409
- let extensions = 0;
410
- let skills = 0;
411
- let promptTemplates = 0;
412
-
413
- const agentsMdPaths = [
414
- join(homeDir, "AGENTS.md"),
415
- join(homedirSafe(), ".claude", "AGENTS.md"),
416
- join(cwd, "AGENTS.md"),
417
- join(cwd, CONFIG_DIR_NAME, "AGENTS.md"),
418
- join(cwd, ".claude", "AGENTS.md"),
419
- ];
420
-
421
- for (const path of agentsMdPaths) {
422
- if (existsSync(path)) contextFiles++;
423
- }
424
-
425
- const extensionDirs = [
426
- join(homeDir, "extensions"),
427
- join(cwd, "extensions"),
428
- join(cwd, CONFIG_DIR_NAME, "extensions"),
429
- ];
430
-
431
- const countedExtensions = new Set<string>();
432
-
433
- const settingsPaths = [
434
- getSettingsPath(),
435
- join(cwd, CONFIG_DIR_NAME, "settings.json"),
436
- ];
437
-
438
- for (const settingsPath of settingsPaths) {
439
- if (!existsSync(settingsPath)) {
440
- continue;
441
- }
442
-
443
- try {
444
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
445
- let packages: unknown = null;
446
- if (typeof settings === "object" && settings !== null && !Array.isArray(settings)) {
447
- packages = (settings as { packages?: unknown }).packages;
448
- }
449
-
450
- if (Array.isArray(packages)) {
451
- for (const pkg of packages) {
452
- let source: unknown = null;
453
- let extensionsFilter: unknown = null;
454
-
455
- if (typeof pkg === "string") {
456
- source = pkg;
457
- } else if (typeof pkg === "object" && pkg !== null && !Array.isArray(pkg)) {
458
- source = (pkg as { source?: unknown }).source;
459
- extensionsFilter = (pkg as { extensions?: unknown }).extensions;
460
- }
461
-
462
- if (typeof source !== "string") {
463
- continue;
464
- }
465
-
466
- const normalizedSource = source.trim();
467
- if (!normalizedSource.startsWith("npm:")) {
468
- continue;
469
- }
470
-
471
- if (Array.isArray(extensionsFilter) && extensionsFilter.length === 0) {
472
- continue;
473
- }
474
-
475
- const body = normalizedSource.slice(4);
476
- const versionIndex = body.lastIndexOf("@");
477
- const name = versionIndex > 0 ? body.slice(0, versionIndex) : body;
478
- if (!name || countedExtensions.has(name)) {
479
- continue;
480
- }
481
-
482
- countedExtensions.add(name);
483
- extensions++;
484
- }
485
- }
486
- } catch (error) {
487
- logDiscoveryError(`Failed to read settings at ${settingsPath}`, error);
488
- }
489
- }
490
-
491
- for (const dir of extensionDirs) {
492
- if (existsSync(dir)) {
493
- try {
494
- const entries = readdirSync(dir);
495
- for (const entry of entries) {
496
- const entryPath = join(dir, entry);
497
-
498
- try {
499
- const stats = statSync(entryPath);
500
-
501
- if (stats.isDirectory()) {
502
- if (
503
- existsSync(join(entryPath, "index.ts")) ||
504
- existsSync(join(entryPath, "index.js")) ||
505
- existsSync(join(entryPath, "package.json"))
506
- ) {
507
- if (!countedExtensions.has(entry)) {
508
- countedExtensions.add(entry);
509
- extensions++;
510
- }
511
- }
512
- } else if ((entry.endsWith(".ts") || entry.endsWith(".js")) && !entry.startsWith(".")) {
513
- const ext = entry.endsWith(".ts") ? ".ts" : ".js";
514
- const name = basename(entry, ext);
515
- if (!countedExtensions.has(name)) {
516
- countedExtensions.add(name);
517
- extensions++;
518
- }
519
- }
520
- } catch (error) {
521
- logDiscoveryError(`Failed to inspect extension entry ${entryPath}`, error);
522
- }
523
- }
524
- } catch (error) {
525
- logDiscoveryError(`Failed to scan extensions dir ${dir}`, error);
526
- }
527
- }
528
- }
529
-
530
- const skillDirs = [
531
- join(homeDir, "skills"),
532
- join(cwd, CONFIG_DIR_NAME, "skills"),
533
- join(cwd, "skills"),
534
- ];
535
-
536
- const countedSkills = new Set<string>();
537
-
538
- for (const dir of skillDirs) {
539
- if (existsSync(dir)) {
540
- try {
541
- const entries = readdirSync(dir);
542
- for (const entry of entries) {
543
- const entryPath = join(dir, entry);
544
- try {
545
- if (statSync(entryPath).isDirectory()) {
546
- if (existsSync(join(entryPath, "SKILL.md"))) {
547
- if (!countedSkills.has(entry)) {
548
- countedSkills.add(entry);
549
- skills++;
550
- }
551
- }
552
- }
553
- } catch (error) {
554
- logDiscoveryError(`Failed to inspect skill entry ${entryPath}`, error);
555
- }
556
- }
557
- } catch (error) {
558
- logDiscoveryError(`Failed to scan skills dir ${dir}`, error);
559
- }
560
- }
561
- }
562
-
563
- const templateDirs = [
564
- join(homeDir, "commands"),
565
- join(homedirSafe(), ".claude", "commands"),
566
- join(cwd, CONFIG_DIR_NAME, "commands"),
567
- join(cwd, ".claude", "commands"),
568
- ];
569
-
570
- const countedTemplates = new Set<string>();
571
-
572
- function countTemplatesInDir(dir: string) {
573
- if (!existsSync(dir)) return;
574
- try {
575
- const entries = readdirSync(dir);
576
- for (const entry of entries) {
577
- const entryPath = join(dir, entry);
578
- try {
579
- const stats = statSync(entryPath);
580
- if (stats.isDirectory()) {
581
- countTemplatesInDir(entryPath);
582
- } else if (entry.endsWith(".md")) {
583
- const name = basename(entry, ".md");
584
- if (!countedTemplates.has(name)) {
585
- countedTemplates.add(name);
586
- promptTemplates++;
587
- }
588
- }
589
- } catch (error) {
590
- logDiscoveryError(`Failed to inspect prompt template entry ${entryPath}`, error);
591
- }
592
- }
593
- } catch (error) {
594
- logDiscoveryError(`Failed to scan prompt template dir ${dir}`, error);
595
- }
596
- }
597
-
598
- for (const dir of templateDirs) {
599
- countTemplatesInDir(dir);
600
- }
601
-
602
- return { contextFiles, extensions, skills, promptTemplates };
603
- }
604
-
605
- /**
606
- * Get recent sessions from the sessions directory.
607
- */
608
- function readSessionHeaderProjectName(filePath: string): string | null {
609
- let fd: number | null = null;
610
- try {
611
- fd = openSync(filePath, "r");
612
- const buffer = Buffer.alloc(SESSION_HEADER_READ_BYTES);
613
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
614
- const firstLine = buffer.toString("utf8", 0, bytesRead).split(/\r?\n/, 1)[0]?.trim();
615
- if (!firstLine) return null;
616
-
617
- const header: unknown = JSON.parse(firstLine);
618
- if (typeof header !== "object" || header === null || Array.isArray(header)) return null;
619
-
620
- const cwd = Reflect.get(header, "cwd");
621
- if (typeof cwd !== "string" || cwd.trim().length === 0) return null;
622
-
623
- return basename(cwd) || cwd;
624
- } catch {
625
- return null;
626
- } finally {
627
- if (fd !== null) closeSync(fd);
628
- }
629
- }
630
-
631
- function sessionProjectNameFromDirectory(dir: string): string {
632
- const parentName = basename(dir);
633
- if (!parentName.startsWith("--")) {
634
- return parentName;
635
- }
636
-
637
- const parts = parentName.split("-").filter(p => p);
638
- return parts[parts.length - 1] || parentName;
639
- }
640
-
641
- export function getRecentSessions(maxCount: number = 3): RecentSession[] {
642
- const homeDir = getAgentDir();
643
- const home = homedirSafe();
644
-
645
- const sessionsDirs = [
646
- join(homeDir, "sessions"),
647
- join(home, CONFIG_DIR_NAME, "sessions"),
648
- ];
649
-
650
- const sessions: { name: string; mtime: number }[] = [];
651
-
652
- function scanDir(dir: string) {
653
- if (!existsSync(dir)) return;
654
- try {
655
- const entries = readdirSync(dir);
656
- for (const entry of entries) {
657
- const entryPath = join(dir, entry);
658
- try {
659
- const stats = statSync(entryPath);
660
- if (stats.isDirectory()) {
661
- scanDir(entryPath);
662
- } else if (entry.endsWith(".jsonl")) {
663
- const projectName = readSessionHeaderProjectName(entryPath) ?? sessionProjectNameFromDirectory(dir);
664
- sessions.push({ name: projectName, mtime: stats.mtimeMs });
665
- }
666
- } catch (error) {
667
- logDiscoveryError(`Failed to inspect session entry ${entryPath}`, error);
668
- }
669
- }
670
- } catch (error) {
671
- logDiscoveryError(`Failed to scan sessions dir ${dir}`, error);
672
- }
673
- }
674
-
675
- for (const sessionsDir of sessionsDirs) {
676
- scanDir(sessionsDir);
677
- }
678
-
679
- if (sessions.length === 0) return [];
680
-
681
- sessions.sort((a, b) => b.mtime - a.mtime);
682
-
683
- const seen = new Set<string>();
684
- const uniqueSessions: typeof sessions = [];
685
- for (const s of sessions) {
686
- if (!seen.has(s.name)) {
687
- seen.add(s.name);
688
- uniqueSessions.push(s);
689
- }
690
- }
691
-
692
- const now = Date.now();
693
- return uniqueSessions.slice(0, maxCount).map(s => ({
694
- name: s.name.length > 20 ? s.name.slice(0, 17) + "…" : s.name,
695
- timeAgo: formatTimeAgo(now - s.mtime),
696
- }));
697
- }
698
-
699
- function formatTimeAgo(ms: number): string {
700
- const seconds = Math.floor(ms / 1000);
701
- const minutes = Math.floor(seconds / 60);
702
- const hours = Math.floor(minutes / 60);
703
- const days = Math.floor(hours / 24);
704
-
705
- if (days > 0) return `${days}d ago`;
706
- if (hours > 0) return `${hours}h ago`;
707
- if (minutes > 0) return `${minutes}m ago`;
708
- return "just now";
709
- }