@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
@@ -0,0 +1,128 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { renderAccentRailEditorFrame } from "./accent-rail-editor";
4
+ import type { PolishedTuiConfig } from "./config";
5
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format";
6
+ import { renderMinimalistFrame } from "./minimalist-editor";
7
+ import { safeThemeFg } from "./style";
8
+ import { renderPolishedEditorFrame } from "./ui";
9
+ import { renderUserMessageStyle } from "./user-message-styles";
10
+
11
+ export const SETTINGS_PREVIEW_MAX_WIDTH = 72;
12
+ export const SETTINGS_PREVIEW_MAX_ROWS = 10;
13
+ export const EDITOR_PREVIEW_INPUT = "Explain this change safely.";
14
+ export const USER_MESSAGE_PREVIEW_MARKDOWN = "Please review **this change** safely.";
15
+
16
+ function boundedRows(rows: string[], width: number): string[] {
17
+ const safeWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
18
+ if (safeWidth <= 0) return [];
19
+ const bounded = rows
20
+ .slice(0, SETTINGS_PREVIEW_MAX_ROWS)
21
+ .map((line) => truncateToWidth(line, safeWidth, ""));
22
+ while (bounded.length > 0 && visibleWidth(bounded.at(-1) ?? "") === 0) bounded.pop();
23
+ return bounded;
24
+ }
25
+
26
+ function previewConfig(config: PolishedTuiConfig): PolishedTuiConfig {
27
+ const derived = structuredClone(config);
28
+ derived.icons = Object.fromEntries(
29
+ Object.entries(derived.icons).map(([key, value]) => [
30
+ key,
31
+ typeof value === "string" ? sanitizeEditorMetadataText(value) : value,
32
+ ]),
33
+ ) as typeof derived.icons;
34
+ return derived;
35
+ }
36
+
37
+ function adaptiveBorder(theme: Theme): (text: string) => string {
38
+ return (text) => safeThemeFg(theme, "thinkingHigh", text);
39
+ }
40
+
41
+ export function renderEditorSettingsPreview(
42
+ config: PolishedTuiConfig,
43
+ theme: Theme,
44
+ width: number,
45
+ ): string[] {
46
+ const previewWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
47
+ if (previewWidth <= 0) return [];
48
+ const safeConfig = previewConfig(config);
49
+ const editor = safeConfig.components.editor;
50
+ const borderColor = adaptiveBorder(theme);
51
+ const modelLabel = editor.modelLabel === "name" ? "Sonnet 4" : "sonnet-4";
52
+ const editorLines = [EDITOR_PREVIEW_INPUT];
53
+ const autocompleteLines = [
54
+ safeThemeFg(theme, "accent", "→ settings Open settings"),
55
+ " files Search files",
56
+ safeThemeFg(theme, "muted", " (1/47)"),
57
+ ];
58
+ const viewport = editor.viewportIndicators ? { above: "2", below: "3" } : undefined;
59
+ let frame: string[];
60
+ if (editor.style === "accent-rail") {
61
+ frame = renderAccentRailEditorFrame({
62
+ width: previewWidth,
63
+ editorLines,
64
+ autocompleteLines,
65
+ viewport,
66
+ uiTheme: theme,
67
+ config: safeConfig,
68
+ });
69
+ } else if (editor.style === "minimalist") {
70
+ frame = renderMinimalistFrame({
71
+ width: previewWidth,
72
+ editorLines,
73
+ viewport,
74
+ inputText: EDITOR_PREVIEW_INPUT,
75
+ metadata: {
76
+ cwd: "/workspace/zentui/src",
77
+ projectRoot: "/workspace/zentui",
78
+ branch: "feat/settings-previews",
79
+ dirty: true,
80
+ ahead: 2,
81
+ behind: 1,
82
+ costLabel: "$.12",
83
+ modelLabel,
84
+ thinkingLevel: "high",
85
+ contextPercent: 75,
86
+ contextWindow: 372_000,
87
+ sessionName: "Preview",
88
+ agentDurationMs: 12_000,
89
+ agentActive: true,
90
+ },
91
+ uiTheme: theme,
92
+ config: safeConfig,
93
+ borderColor,
94
+ });
95
+ } else {
96
+ frame = renderPolishedEditorFrame({
97
+ width: previewWidth,
98
+ editorLines,
99
+ autocompleteLines,
100
+ viewport,
101
+ uiTheme: theme,
102
+ config: safeConfig,
103
+ modelMeta: {
104
+ modelLabel,
105
+ modelId: "sonnet-4",
106
+ modelName: "Sonnet 4",
107
+ providerLabel: "Anthropic",
108
+ sessionName: "Preview",
109
+ },
110
+ thinkingLevel: "high",
111
+ borderColor,
112
+ });
113
+ }
114
+ return boundedRows(frame, previewWidth);
115
+ }
116
+
117
+ export function renderUserMessageSettingsPreview(
118
+ config: PolishedTuiConfig,
119
+ theme: Theme,
120
+ width: number,
121
+ text = USER_MESSAGE_PREVIEW_MARKDOWN,
122
+ ): string[] {
123
+ const previewWidth = Math.max(0, Math.min(SETTINGS_PREVIEW_MAX_WIDTH, width));
124
+ if (previewWidth <= 0) return [];
125
+ const safeConfig = previewConfig(config);
126
+ const frame = renderUserMessageStyle({ text, width: previewWidth, theme, config: safeConfig });
127
+ return boundedRows(frame, previewWidth);
128
+ }
@@ -0,0 +1,94 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ModelLabelSource } from "./config";
3
+ import {
4
+ buildCacheReadLabel,
5
+ buildCacheWriteLabel,
6
+ buildContextLabel,
7
+ buildCostLabel,
8
+ buildTokenLabel,
9
+ formatProviderLabel,
10
+ getUsageTotals,
11
+ type UsageTotals,
12
+ } from "./format";
13
+ import type { GitStatusSummary } from "./git";
14
+ import type { PackageVersionResult } from "./package-version";
15
+ import type { RuntimeInfo } from "./runtime";
16
+ import type { FooterTelemetry } from "./telemetry";
17
+
18
+ export type FooterState = GitStatusSummary & {
19
+ modelLabel: string;
20
+ modelId: string;
21
+ modelName: string;
22
+ providerLabel: string;
23
+ contextLabel: string;
24
+ tokenLabel: string;
25
+ cacheReadLabel: string;
26
+ cacheWriteLabel: string;
27
+ costLabel: string;
28
+ usageTotals: UsageTotals;
29
+ subscription: boolean;
30
+ autoCompaction: boolean;
31
+ runtime?: RuntimeInfo;
32
+ packageVersion?: PackageVersionResult;
33
+ sessionStartEpoch?: number;
34
+ };
35
+
36
+ export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
37
+ return {
38
+ modelLabel: "no-model",
39
+ modelId: "",
40
+ modelName: "",
41
+ providerLabel: "Unknown",
42
+ contextLabel: "--",
43
+ tokenLabel: "↑0 ↓0",
44
+ cacheReadLabel: "",
45
+ cacheWriteLabel: "",
46
+ costLabel: "$0.000",
47
+ usageTotals: {
48
+ input: 0,
49
+ output: 0,
50
+ cacheRead: 0,
51
+ cacheWrite: 0,
52
+ latestCacheHitRate: undefined,
53
+ cost: 0,
54
+ },
55
+ subscription: false,
56
+ autoCompaction: false,
57
+ runtime: undefined,
58
+ packageVersion: undefined,
59
+ sessionStartEpoch: Date.now(),
60
+ ...gitDefaults,
61
+ };
62
+ }
63
+
64
+ export function modelLabelFor(
65
+ state: Pick<FooterState, "modelId" | "modelName">,
66
+ source: ModelLabelSource,
67
+ ): string {
68
+ return source === "name"
69
+ ? state.modelName || state.modelId || "no-model"
70
+ : state.modelId || "no-model";
71
+ }
72
+
73
+ export function syncState(
74
+ state: FooterState,
75
+ ctx: ExtensionContext,
76
+ cacheHitIcon: string,
77
+ telemetry: FooterTelemetry = {},
78
+ ): void {
79
+ const totals = getUsageTotals(ctx);
80
+ const m = ctx.model;
81
+ state.modelId = m?.id ?? "";
82
+ state.modelName = m?.name ?? "";
83
+ // Retained as a compatibility snapshot only; production surfaces format from raw fields.
84
+ state.modelLabel = modelLabelFor(state, "id");
85
+ state.providerLabel = formatProviderLabel(ctx.model?.provider);
86
+ state.contextLabel = buildContextLabel(ctx);
87
+ state.tokenLabel = buildTokenLabel(totals, cacheHitIcon);
88
+ state.cacheReadLabel = buildCacheReadLabel(totals.cacheRead);
89
+ state.cacheWriteLabel = buildCacheWriteLabel(totals.cacheWrite);
90
+ state.costLabel = buildCostLabel(totals);
91
+ state.usageTotals = totals;
92
+ state.subscription = telemetry.subscription === true;
93
+ state.autoCompaction = telemetry.autoCompaction === true;
94
+ }
@@ -0,0 +1,424 @@
1
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import type { ColorSource, ColorSpec } from "./config";
3
+
4
+ type ThemeLike = {
5
+ fg(color: string, text: string): string;
6
+ bold?: (text: string) => string;
7
+ italic?: (text: string) => string;
8
+ underline?: (text: string) => string;
9
+ };
10
+
11
+ export type { ThemeLike };
12
+
13
+ export const EDITOR_ACCENT_STYLE = "blue";
14
+ export const EDITOR_BORDER_STYLE = "bright-black";
15
+ export const MAX_SAFE_SGR_PREFIX_CODE_UNITS = 96;
16
+ export const MAX_SAFE_SGR_PREFIX_SEQUENCES = 4;
17
+
18
+ export type SourceStyleFallback = {
19
+ theme: ColorSpec;
20
+ terminal: ColorSpec;
21
+ };
22
+
23
+ export const EDITOR_ACCENT_FALLBACK: SourceStyleFallback = {
24
+ theme: "accent",
25
+ terminal: EDITOR_ACCENT_STYLE,
26
+ };
27
+
28
+ export const EDITOR_BORDER_FALLBACK: SourceStyleFallback = {
29
+ theme: "borderMuted",
30
+ terminal: EDITOR_BORDER_STYLE,
31
+ };
32
+
33
+ function isHexColor(value: string): boolean {
34
+ return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
35
+ }
36
+
37
+ function expandHexColor(hex: string): string {
38
+ const body = hex.slice(1);
39
+ if (body.length === 3) {
40
+ return body
41
+ .split("")
42
+ .map((ch) => ch + ch)
43
+ .join("");
44
+ }
45
+ return body;
46
+ }
47
+
48
+ function hexToAnsi(hex: string, isBackground = false): string {
49
+ const normalized = expandHexColor(hex);
50
+ const r = Number.parseInt(normalized.slice(0, 2), 16);
51
+ const g = Number.parseInt(normalized.slice(2, 4), 16);
52
+ const b = Number.parseInt(normalized.slice(4, 6), 16);
53
+ return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
54
+ }
55
+
56
+ const terminalColorCodes = new Map([
57
+ ["black", 30],
58
+ ["red", 31],
59
+ ["green", 32],
60
+ ["yellow", 33],
61
+ ["blue", 34],
62
+ ["purple", 35],
63
+ ["cyan", 36],
64
+ ["white", 37],
65
+ ["bright-black", 90],
66
+ ["bright-red", 91],
67
+ ["bright-green", 92],
68
+ ["bright-yellow", 93],
69
+ ["bright-blue", 94],
70
+ ["bright-purple", 95],
71
+ ["bright-cyan", 96],
72
+ ["bright-white", 97],
73
+ ]);
74
+
75
+ const terminalStyleModifiers = new Map([
76
+ ["bold", 1],
77
+ ["dim", 2],
78
+ ["dimmed", 2],
79
+ ["italic", 3],
80
+ ["underline", 4],
81
+ ]);
82
+
83
+ const themeColorNameMap = new Map([
84
+ ["red", "error"],
85
+ ["bright-red", "error"],
86
+ ["green", "success"],
87
+ ["bright-green", "success"],
88
+ ["yellow", "warning"],
89
+ ["bright-yellow", "warning"],
90
+ ["blue", "syntaxFunction"],
91
+ ["bright-blue", "syntaxFunction"],
92
+ ["cyan", "syntaxFunction"],
93
+ ["bright-cyan", "syntaxFunction"],
94
+ ["purple", "syntaxKeyword"],
95
+ ["bright-purple", "syntaxKeyword"],
96
+ ["black", "muted"],
97
+ ["bright-black", "muted"],
98
+ ["white", "text"],
99
+ ["bright-white", "text"],
100
+ ]);
101
+
102
+ const themeStyleModifiers = new Set(["bold", "italic", "underline"]);
103
+
104
+ const themeColorTokens = new Set<ThemeColor>([
105
+ "accent",
106
+ "border",
107
+ "borderAccent",
108
+ "borderMuted",
109
+ "success",
110
+ "error",
111
+ "warning",
112
+ "muted",
113
+ "dim",
114
+ "text",
115
+ "thinkingText",
116
+ "userMessageText",
117
+ "customMessageText",
118
+ "customMessageLabel",
119
+ "toolTitle",
120
+ "toolOutput",
121
+ "mdHeading",
122
+ "mdLink",
123
+ "mdLinkUrl",
124
+ "mdCode",
125
+ "mdCodeBlock",
126
+ "mdCodeBlockBorder",
127
+ "mdQuote",
128
+ "mdQuoteBorder",
129
+ "mdHr",
130
+ "mdListBullet",
131
+ "toolDiffAdded",
132
+ "toolDiffRemoved",
133
+ "toolDiffContext",
134
+ "syntaxComment",
135
+ "syntaxKeyword",
136
+ "syntaxFunction",
137
+ "syntaxVariable",
138
+ "syntaxString",
139
+ "syntaxNumber",
140
+ "syntaxType",
141
+ "syntaxOperator",
142
+ "syntaxPunctuation",
143
+ "thinkingOff",
144
+ "thinkingMinimal",
145
+ "thinkingLow",
146
+ "thinkingMedium",
147
+ "thinkingHigh",
148
+ "thinkingXhigh",
149
+ "thinkingMax",
150
+ "bashMode",
151
+ ]);
152
+
153
+ function terminalColorToAnsi(color: string, isBackground = false): string | undefined {
154
+ const normalized = color.toLowerCase();
155
+ const colorCode = terminalColorCodes.get(normalized);
156
+ if (colorCode !== undefined) return `${isBackground ? colorCode + 10 : colorCode}`;
157
+
158
+ if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
159
+ return `${isBackground ? 48 : 38};5;${normalized}`;
160
+ }
161
+
162
+ if (isHexColor(normalized)) return hexToAnsi(normalized, isBackground).slice(2, -1);
163
+ return undefined;
164
+ }
165
+
166
+ function isExplicitTerminalColorToken(token: string): boolean {
167
+ const normalized = token.toLowerCase();
168
+ if (normalized.startsWith("fg:") || normalized.startsWith("bg:")) return true;
169
+ if (isHexColor(normalized)) return true;
170
+ return /^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized);
171
+ }
172
+
173
+ function isSupportedStyleToken(token: string): boolean {
174
+ const normalized = token.toLowerCase();
175
+ if (terminalStyleModifiers.has(normalized)) return true;
176
+ if (terminalColorToAnsi(normalized) !== undefined) return true;
177
+
178
+ const isForeground = normalized.startsWith("fg:");
179
+ const isBackground = normalized.startsWith("bg:");
180
+ if (isForeground || isBackground) {
181
+ return terminalColorToAnsi(normalized.slice(3), isBackground) !== undefined;
182
+ }
183
+
184
+ return themeColorTokens.has(token as ThemeColor);
185
+ }
186
+
187
+ export function isSupportedColorSpec(style: ColorSpec): boolean {
188
+ const trimmed = style.trim();
189
+ if (trimmed === "") return true;
190
+ return trimmed.split(/\s+/).every(isSupportedStyleToken);
191
+ }
192
+
193
+ function applyThemeModifiers(theme: ThemeLike, styleTokens: string[], text: string): string {
194
+ let rendered = text;
195
+ for (const token of styleTokens) {
196
+ const normalized = token.toLowerCase();
197
+ if (normalized === "bold") rendered = theme.bold?.(rendered) ?? rendered;
198
+ if (normalized === "italic") rendered = theme.italic?.(rendered) ?? rendered;
199
+ if (normalized === "underline") rendered = theme.underline?.(rendered) ?? rendered;
200
+ }
201
+ return rendered;
202
+ }
203
+
204
+ export function safeThemeFg(theme: ThemeLike, color: string, text: string): string {
205
+ try {
206
+ return theme.fg(color, text);
207
+ } catch {
208
+ return text;
209
+ }
210
+ }
211
+
212
+ function mapThemeColor(styleTokens: string[]): string | undefined {
213
+ let fallback: string | undefined;
214
+ for (const token of styleTokens) {
215
+ const normalized = token.toLowerCase();
216
+ if (themeStyleModifiers.has(normalized)) continue;
217
+ if (normalized === "dim" || normalized === "dimmed") {
218
+ fallback = "muted";
219
+ continue;
220
+ }
221
+
222
+ const mapped = themeColorNameMap.get(normalized);
223
+ if (mapped) return mapped;
224
+ return token;
225
+ }
226
+ return fallback;
227
+ }
228
+
229
+ /**
230
+ * Colorize text using a theme color token or hex color.
231
+ * Non-hex values are passed directly to `theme.fg()`; invalid tokens fall back
232
+ * to unstyled text so a config typo does not break rendering.
233
+ */
234
+ export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
235
+ if (isHexColor(color)) {
236
+ return `${hexToAnsi(color)}${text}\x1b[39m`;
237
+ }
238
+ return safeThemeFg(theme, color, text);
239
+ }
240
+
241
+ /**
242
+ * Render text with Starship-style terminal styling strings (e.g. "bold red", "fg:202",
243
+ * "bg:blue", "underline bg:#bf5700").
244
+ */
245
+ function isSafeSgrParameters(parameters: string): boolean {
246
+ const tokens = parameters.split(";");
247
+ if (tokens.some((token) => !/^\d+$/.test(token))) return false;
248
+ const values = tokens.map(Number);
249
+ for (let index = 0; index < values.length; index += 1) {
250
+ const value = values[index];
251
+ if (
252
+ value === 1 ||
253
+ value === 2 ||
254
+ value === 3 ||
255
+ value === 4 ||
256
+ (value !== undefined && value >= 30 && value <= 37) ||
257
+ (value !== undefined && value >= 40 && value <= 47) ||
258
+ (value !== undefined && value >= 90 && value <= 97) ||
259
+ (value !== undefined && value >= 100 && value <= 107)
260
+ ) {
261
+ continue;
262
+ }
263
+ if (value !== 38 && value !== 48) return false;
264
+ const mode = values[index + 1];
265
+ if (mode === 5) {
266
+ const color = values[index + 2];
267
+ if (color === undefined || color < 0 || color > 255) return false;
268
+ index += 2;
269
+ continue;
270
+ }
271
+ if (mode === 2) {
272
+ const channels = values.slice(index + 2, index + 5);
273
+ if (channels.length !== 3 || channels.some((channel) => channel < 0 || channel > 255)) {
274
+ return false;
275
+ }
276
+ index += 4;
277
+ continue;
278
+ }
279
+ return false;
280
+ }
281
+ return true;
282
+ }
283
+
284
+ /** Accept only the bounded SGR subset emitted by Zentui's supported style tokens. */
285
+ export function isSafeSgrStylePrefix(value: unknown): value is string {
286
+ if (
287
+ typeof value !== "string" ||
288
+ value.length === 0 ||
289
+ value.length > MAX_SAFE_SGR_PREFIX_CODE_UNITS
290
+ ) {
291
+ return false;
292
+ }
293
+ const sequence = /\x1b\[([0-9]+(?:;[0-9]+)*)m/g;
294
+ let offset = 0;
295
+ let count = 0;
296
+ for (const match of value.matchAll(sequence)) {
297
+ if (match.index !== offset || !isSafeSgrParameters(match[1] ?? "")) return false;
298
+ offset = match.index + match[0].length;
299
+ count += 1;
300
+ if (count > MAX_SAFE_SGR_PREFIX_SEQUENCES) return false;
301
+ }
302
+ return count > 0 && offset === value.length;
303
+ }
304
+
305
+ export function renderTerminalStyle(style: string, text: string): string {
306
+ const codes: string[] = [];
307
+ for (const token of style.trim().split(/\s+/)) {
308
+ if (!token) continue;
309
+
310
+ const normalized = token.toLowerCase();
311
+ const modifier = terminalStyleModifiers.get(normalized);
312
+ if (modifier !== undefined) {
313
+ codes.push(`${modifier}`);
314
+ continue;
315
+ }
316
+
317
+ const isForeground = normalized.startsWith("fg:");
318
+ const isBackground = normalized.startsWith("bg:");
319
+ const colorName = isForeground || isBackground ? normalized.slice(3) : normalized;
320
+ const color = terminalColorToAnsi(colorName, isBackground);
321
+ if (color) codes.push(color);
322
+ }
323
+
324
+ return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
325
+ }
326
+
327
+ /**
328
+ * Apply Starship-style terminal styling first, falling back to Pi theme tokens for
329
+ * legacy config values such as "accent" or "syntaxKeyword".
330
+ */
331
+ export function renderStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
332
+ if (style.trim() === "") return text;
333
+ const styled = renderTerminalStyle(style, text);
334
+ return styled === text ? colorize(theme, style, text) : styled;
335
+ }
336
+
337
+ export function renderThemeStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
338
+ const trimmed = style.trim();
339
+ if (trimmed === "") return text;
340
+
341
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
342
+ if (tokens.some(isExplicitTerminalColorToken)) return renderTerminalStyle(style, text);
343
+
344
+ const color = mapThemeColor(tokens) ?? "text";
345
+ return safeThemeFg(theme, color, applyThemeModifiers(theme, tokens, text));
346
+ }
347
+
348
+ function renderStyleStrict(theme: ThemeLike, style: ColorSpec, text: string): string {
349
+ if (style.trim() === "") return text;
350
+ const styled = renderTerminalStyle(style, text);
351
+ return styled === text ? theme.fg(style, text) : styled;
352
+ }
353
+
354
+ function renderThemeStyleStrict(theme: ThemeLike, style: ColorSpec, text: string): string {
355
+ const trimmed = style.trim();
356
+ if (trimmed === "") return text;
357
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
358
+ if (tokens.some(isExplicitTerminalColorToken)) return renderTerminalStyle(style, text);
359
+ const color = mapThemeColor(tokens) ?? "text";
360
+ return theme.fg(color, applyThemeModifiers(theme, tokens, text));
361
+ }
362
+
363
+ export function renderStyleForSource(
364
+ theme: ThemeLike,
365
+ source: ColorSource,
366
+ style: ColorSpec,
367
+ text: string,
368
+ ): string {
369
+ return source === "terminal"
370
+ ? renderStyle(theme, style, text)
371
+ : renderThemeStyle(theme, style, text);
372
+ }
373
+
374
+ export function renderStyleForSourceOrFallback(
375
+ theme: ThemeLike,
376
+ source: ColorSource,
377
+ style: ColorSpec | undefined,
378
+ fallback: ColorSpec | SourceStyleFallback,
379
+ text: string,
380
+ ): string {
381
+ const fallbackStyle = typeof fallback === "string" ? fallback : fallback[source];
382
+ return renderStyleForSource(theme, source, style ?? fallbackStyle, text);
383
+ }
384
+
385
+ export function renderStyleForSourceOrFallbackStrict(
386
+ theme: ThemeLike,
387
+ source: ColorSource,
388
+ style: ColorSpec | undefined,
389
+ fallback: ColorSpec | SourceStyleFallback,
390
+ text: string,
391
+ ): string {
392
+ const fallbackStyle = typeof fallback === "string" ? fallback : fallback[source];
393
+ const resolvedStyle = style ?? fallbackStyle;
394
+ return source === "terminal"
395
+ ? renderStyleStrict(theme, resolvedStyle, text)
396
+ : renderThemeStyleStrict(theme, resolvedStyle, text);
397
+ }
398
+
399
+ export function renderEditorAccent(text: string): string {
400
+ return renderTerminalStyle(EDITOR_ACCENT_STYLE, text);
401
+ }
402
+
403
+ export function renderEditorBorder(text: string): string {
404
+ return renderTerminalStyle(EDITOR_BORDER_STYLE, text);
405
+ }
406
+
407
+ export function renderAccentLine(theme: ThemeLike, source: ColorSource, text: string): string {
408
+ return renderStyleForSourceOrFallback(theme, source, undefined, EDITOR_ACCENT_FALLBACK, text);
409
+ }
410
+
411
+ export function renderChromeBorder(
412
+ theme: ThemeLike,
413
+ source: ColorSource,
414
+ terminalFallbackStyle: ColorSpec,
415
+ text: string,
416
+ ): string {
417
+ return renderStyleForSourceOrFallback(
418
+ theme,
419
+ source,
420
+ undefined,
421
+ { theme: "borderMuted", terminal: terminalFallbackStyle },
422
+ text,
423
+ );
424
+ }