@selesai/code 0.9.15 → 0.10.1

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 (269) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -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/modes/interactive/components/settings-selector.d.ts +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +1 -1
  119. package/dist/skills/design-references/SKILL.md +31 -0
  120. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  121. package/dist/themes/adventure.json +84 -0
  122. package/dist/themes/adwaita-dark.json +84 -0
  123. package/dist/themes/arcoiris.json +84 -0
  124. package/dist/themes/arthur.json +84 -0
  125. package/dist/themes/atom.json +84 -0
  126. package/dist/themes/aura.json +84 -0
  127. package/dist/themes/black-metal-bathory.json +84 -0
  128. package/dist/themes/black-metal-burzum.json +84 -0
  129. package/dist/themes/black-metal-khold.json +84 -0
  130. package/dist/themes/box.json +84 -0
  131. package/dist/themes/brogrammer.json +84 -0
  132. package/dist/themes/carbonfox.json +84 -0
  133. package/dist/themes/catppuccin-mocha.json +84 -0
  134. package/dist/themes/citruszest.json +84 -0
  135. package/dist/themes/cursor-dark.json +84 -0
  136. package/dist/themes/cutie-pro.json +84 -0
  137. package/dist/themes/dark-modern.json +84 -0
  138. package/dist/themes/dark-pastel.json +84 -0
  139. package/dist/themes/dimmed-monokai.json +84 -0
  140. package/dist/themes/doom-peacock.json +84 -0
  141. package/dist/themes/dracula-plus.json +84 -0
  142. package/dist/themes/dracula.json +80 -0
  143. package/dist/themes/earthsong.json +84 -0
  144. package/dist/themes/everforest-dark-hard.json +84 -0
  145. package/dist/themes/everforest-dark.json +80 -0
  146. package/dist/themes/fahrenheit.json +84 -0
  147. package/dist/themes/flatland.json +84 -0
  148. package/dist/themes/flexoki-dark.json +84 -0
  149. package/dist/themes/front-end-delight.json +84 -0
  150. package/dist/themes/fun-forrest.json +84 -0
  151. package/dist/themes/galizur.json +84 -0
  152. package/dist/themes/github-dark-colorblind.json +84 -0
  153. package/dist/themes/github-dark-high-contrast.json +84 -0
  154. package/dist/themes/glacier.json +84 -0
  155. package/dist/themes/gruber-darker.json +84 -0
  156. package/dist/themes/gruvbox-dark-hard.json +84 -0
  157. package/dist/themes/gruvbox-dark.json +84 -0
  158. package/dist/themes/gruvbox-material.json +84 -0
  159. package/dist/themes/guezwhoz.json +84 -0
  160. package/dist/themes/hacktober.json +84 -0
  161. package/dist/themes/hardcore.json +84 -0
  162. package/dist/themes/havn-skumring.json +84 -0
  163. package/dist/themes/ic-orange-ppl.json +84 -0
  164. package/dist/themes/iterm2-smoooooth.json +84 -0
  165. package/dist/themes/iterm2-tango-dark.json +84 -0
  166. package/dist/themes/japanesque.json +84 -0
  167. package/dist/themes/jellybeans.json +84 -0
  168. package/dist/themes/kanagawa-wave.json +84 -0
  169. package/dist/themes/kurokula.json +84 -0
  170. package/dist/themes/later-this-evening.json +84 -0
  171. package/dist/themes/lovelace.json +84 -0
  172. package/dist/themes/material-darker.json +84 -0
  173. package/dist/themes/matte-black.json +84 -0
  174. package/dist/themes/mellow.json +84 -0
  175. package/dist/themes/miasma.json +84 -0
  176. package/dist/themes/nord.json +80 -0
  177. package/dist/themes/nvim-dark.json +84 -0
  178. package/dist/themes/one-dark.json +80 -0
  179. package/dist/themes/popping-and-locking.json +84 -0
  180. package/dist/themes/rose-pine.json +80 -0
  181. package/dist/themes/sea-shells.json +84 -0
  182. package/dist/themes/sleepy-hollow.json +84 -0
  183. package/dist/themes/smyck.json +84 -0
  184. package/dist/themes/solarized-dark.json +80 -0
  185. package/dist/themes/tokyo-night.json +80 -0
  186. package/dist/themes/tomorrow-night-bright.json +84 -0
  187. package/dist/themes/tomorrow-night-burns.json +84 -0
  188. package/dist/themes/tomorrow-night.json +84 -0
  189. package/dist/themes/twilight.json +84 -0
  190. package/dist/themes/vague.json +84 -0
  191. package/dist/themes/vesper.json +84 -0
  192. package/dist/themes/xcode-dark-hc.json +84 -0
  193. package/docs/custom-provider.md +0 -1
  194. package/package.json +3 -3
  195. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  196. package/dist/extensions/auto-model/classifier.ts +0 -23
  197. package/dist/extensions/auto-model/config.ts +0 -51
  198. package/dist/extensions/auto-model/index.ts +0 -22
  199. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  200. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  201. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  202. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  203. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  204. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  205. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  206. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  207. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  208. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  209. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  210. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  211. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  212. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  213. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  214. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  215. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  216. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  217. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  218. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  219. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  220. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  221. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  222. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  223. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  224. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  225. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  226. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  227. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  228. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  229. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  230. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  231. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  232. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  233. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  234. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  235. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  236. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  237. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  238. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  239. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  240. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  241. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  242. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  243. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  244. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  245. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  246. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  247. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  248. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  249. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  250. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  251. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  252. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  253. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  254. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  255. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  256. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  257. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  258. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  259. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  260. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  261. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  262. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  263. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  264. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  265. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  266. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  267. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  268. package/dist/extensions/preview-tools-disabled.ts +0 -11
  269. 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
- }