@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,721 +0,0 @@
1
- // working-vibes.ts
2
- // AI-generated contextual working messages that match a user's preferred theme/vibe.
3
- // Uses module-level state (matching powerline-footer pattern).
4
-
5
- import type { AssistantMessage, Context, Model, ProviderStreamOptions } from "@earendil-works/pi-ai";
6
- import { getSettingsPath, type ExtensionContext } from "@selesai/code";
7
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
- import { join, dirname } from "node:path";
9
- import { getAgentPath } from "./paths.ts";
10
-
11
- type VibeMode = "generate" | "file";
12
-
13
- // Extension-registered providers live in the model registry only: their custom `api` values
14
- // are absent from pi-ai's global api table, so streaming has to go through the provider.
15
- // Credential-derived base URLs are resolved per request, mirroring ModelRuntime.prepareRequest;
16
- // getApiKeyAndHeaders() covers the rest of the request auth but never reports a base URL.
17
- async function completeVibe(
18
- providerId: string,
19
- model: Model<string>,
20
- context: Context,
21
- options: ProviderStreamOptions,
22
- ): Promise<AssistantMessage> {
23
- const registry = extensionCtx?.modelRegistry;
24
- const provider = registry?.getProvider(providerId);
25
- if (!registry || !provider) {
26
- throw new Error(`Provider not registered: ${providerId}`);
27
- }
28
-
29
- const baseUrl = (await registry.getProviderAuth(providerId))?.auth.baseUrl;
30
- const requestModel = baseUrl ? { ...model, baseUrl } : model;
31
- return provider.stream(requestModel, context, options).result();
32
- }
33
-
34
- // ═══════════════════════════════════════════════════════════════════════════
35
- // Constants
36
- // ═══════════════════════════════════════════════════════════════════════════
37
-
38
- const DEFAULT_MODEL = "openai-codex/gpt-5.4-mini";
39
-
40
- const DEFAULT_PROMPT = `Generate a 2-4 word "{theme}" themed loading message ending in "...".
41
-
42
- Task: {task}
43
-
44
- Be creative and unexpected. Avoid obvious/clichéd phrases for this theme.
45
- The message should hint at the task using theme vocabulary.
46
- {exclude}
47
- Output only the message, nothing else.`;
48
-
49
- const BATCH_PROMPT = `Generate {count} unique 2-4 word loading messages for a "{theme}" theme.
50
- Each message should end with "..."
51
- Be creative, varied, and thematic. No duplicates.
52
- Output one message per line, nothing else. No numbering, no bullets.`;
53
-
54
- const VIBE_SYSTEM_PROMPT = "You generate short themed loading messages and reply with the requested text only.";
55
-
56
- // ═══════════════════════════════════════════════════════════════════════════
57
- // Types
58
- // ═══════════════════════════════════════════════════════════════════════════
59
-
60
- interface VibeConfig {
61
- theme: string | null; // null = disabled
62
- mode: VibeMode; // "generate" (on-demand) or "file" (pre-generated)
63
- modelSpec: string; // default: "openai-codex/gpt-5.4-mini"
64
- fallback: string; // default: "Working"
65
- timeout: number; // default: 3000ms
66
- refreshInterval: number; // default: 30000ms (30s)
67
- promptTemplate: string; // template with {theme}, {task}, {exclude} placeholders
68
- maxLength: number; // default: 65 chars
69
- }
70
-
71
- interface VibeGenContext {
72
- theme: string;
73
- userPrompt: string; // from event.prompt in before_agent_start
74
- }
75
-
76
- // ═══════════════════════════════════════════════════════════════════════════
77
- // Module-level State
78
- // ═══════════════════════════════════════════════════════════════════════════
79
-
80
- let config: VibeConfig = loadConfig();
81
- let extensionCtx: ExtensionContext | null = null;
82
- let currentGeneration: AbortController | null = null;
83
- let isStreaming = false;
84
- let lastVibeTime = 0;
85
-
86
- // File-based mode state
87
- let vibeCache: string[] = []; // Cached vibes from file
88
- let vibeCacheTheme: string | null = null; // Theme the cache is for
89
- let vibeSeed = Date.now(); // Seed for deterministic shuffle
90
- let vibeIndex = 0; // Current position in shuffled list
91
-
92
- // Recent vibes tracking (to avoid repetition in generate mode)
93
- const MAX_RECENT_VIBES = 5;
94
- let recentVibes: string[] = [];
95
-
96
- // ═══════════════════════════════════════════════════════════════════════════
97
- // Configuration Management
98
- // ═══════════════════════════════════════════════════════════════════════════
99
-
100
- function isRecord(value: unknown): value is Record<string, unknown> {
101
- return typeof value === "object" && value !== null && !Array.isArray(value);
102
- }
103
-
104
- function readSettingsForLoad(): Record<string, unknown> {
105
- const settingsPath = getSettingsPath();
106
-
107
- try {
108
- if (!existsSync(settingsPath)) {
109
- return {};
110
- }
111
-
112
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
113
- if (!isRecord(parsed)) {
114
- console.debug(`[working-vibes] Ignoring non-object settings at ${settingsPath}`);
115
- return {};
116
- }
117
-
118
- return parsed;
119
- } catch (error) {
120
- console.debug(`[working-vibes] Failed to load settings from ${settingsPath}:`, error);
121
- return {};
122
- }
123
- }
124
-
125
- function readSettingsForWrite(scope: string): Record<string, unknown> | null {
126
- const settingsPath = getSettingsPath();
127
-
128
- if (!existsSync(settingsPath)) {
129
- return {};
130
- }
131
-
132
- try {
133
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
134
- if (!isRecord(parsed)) {
135
- console.debug(`[working-vibes] Refusing to write ${scope}: settings at ${settingsPath} is not an object`);
136
- return null;
137
- }
138
-
139
- return parsed;
140
- } catch (error) {
141
- console.debug(`[working-vibes] Failed to parse settings while writing ${scope} at ${settingsPath}:`, error);
142
- return null;
143
- }
144
- }
145
-
146
- function persistSettings(settings: Record<string, unknown>, scope: string): boolean {
147
- const settingsPath = getSettingsPath();
148
-
149
- try {
150
- mkdirSync(dirname(settingsPath), { recursive: true });
151
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
152
- return true;
153
- } catch (error) {
154
- console.debug(`[working-vibes] Failed to persist ${scope} to ${settingsPath}:`, error);
155
- return false;
156
- }
157
- }
158
-
159
- function loadConfig(): VibeConfig {
160
- const settings = readSettingsForLoad();
161
-
162
- // Handle "off" in settings.json (same as null/disabled)
163
- const rawTheme = typeof settings.workingVibe === "string" ? settings.workingVibe : null;
164
- const theme = rawTheme?.toLowerCase() === "off" ? null : rawTheme;
165
-
166
- // Validate mode setting
167
- const rawMode = settings.workingVibeMode;
168
- const mode: VibeMode = rawMode === "file" || rawMode === "generate" ? rawMode : "generate";
169
-
170
- const refreshSeconds =
171
- typeof settings.workingVibeRefreshInterval === "number" && Number.isFinite(settings.workingVibeRefreshInterval)
172
- ? Math.max(0, settings.workingVibeRefreshInterval)
173
- : 30;
174
-
175
- const maxLength =
176
- typeof settings.workingVibeMaxLength === "number" && Number.isFinite(settings.workingVibeMaxLength)
177
- ? Math.max(4, Math.floor(settings.workingVibeMaxLength))
178
- : 65;
179
-
180
- return {
181
- theme,
182
- mode,
183
- modelSpec: typeof settings.workingVibeModel === "string" ? settings.workingVibeModel : DEFAULT_MODEL,
184
- fallback: typeof settings.workingVibeFallback === "string" ? settings.workingVibeFallback : "Working",
185
- timeout: 3000,
186
- refreshInterval: refreshSeconds * 1000,
187
- promptTemplate: typeof settings.workingVibePrompt === "string" ? settings.workingVibePrompt : DEFAULT_PROMPT,
188
- maxLength,
189
- };
190
- }
191
-
192
- function saveConfig(): boolean {
193
- const settings = readSettingsForWrite("workingVibe");
194
- if (!settings) {
195
- return false;
196
- }
197
-
198
- if (config.theme === null) {
199
- delete settings.workingVibe;
200
- } else {
201
- settings.workingVibe = config.theme;
202
- }
203
-
204
- return persistSettings(settings, "workingVibe");
205
- }
206
-
207
- function saveModelConfig(): boolean {
208
- const settings = readSettingsForWrite("workingVibeModel");
209
- if (!settings) {
210
- return false;
211
- }
212
-
213
- if (config.modelSpec === DEFAULT_MODEL) {
214
- delete settings.workingVibeModel;
215
- } else {
216
- settings.workingVibeModel = config.modelSpec;
217
- }
218
-
219
- return persistSettings(settings, "workingVibeModel");
220
- }
221
-
222
- // ═══════════════════════════════════════════════════════════════════════════
223
- // File-Based Vibe Management
224
- // ═══════════════════════════════════════════════════════════════════════════
225
-
226
- function getVibesDir(): string {
227
- return getAgentPath("vibes");
228
- }
229
-
230
- function toVibeFileSlug(theme: string): string {
231
- const slug = theme
232
- .trim()
233
- .toLowerCase()
234
- .replace(/[^a-z0-9_-]+/g, "-")
235
- .replace(/-+/g, "-")
236
- .replace(/^[-_]+|[-_]+$/g, "");
237
-
238
- return slug || "theme";
239
- }
240
-
241
- function getVibeFilePath(theme: string): string {
242
- const filename = `${toVibeFileSlug(theme)}.txt`;
243
- return join(getVibesDir(), filename);
244
- }
245
-
246
- function loadVibesFromFile(theme: string): string[] {
247
- const filePath = getVibeFilePath(theme);
248
- if (!existsSync(filePath)) return [];
249
-
250
- try {
251
- const content = readFileSync(filePath, "utf-8");
252
- return content
253
- .split("\n")
254
- .map(line => line.trim())
255
- .filter(line => line.length > 0 && line.endsWith("..."));
256
- } catch (error) {
257
- console.debug(`[working-vibes] Failed to load vibe file ${filePath}:`, error);
258
- return [];
259
- }
260
- }
261
-
262
- function saveVibesToFile(theme: string, vibes: string[]): void {
263
- const vibesDir = getVibesDir();
264
- const filePath = getVibeFilePath(theme);
265
-
266
- // Ensure directory exists
267
- if (!existsSync(vibesDir)) {
268
- mkdirSync(vibesDir, { recursive: true });
269
- }
270
-
271
- writeFileSync(filePath, vibes.join("\n"));
272
- }
273
-
274
- // Mulberry32 PRNG - fast, deterministic, good distribution
275
- function mulberry32(seed: number): () => number {
276
- return function() {
277
- let t = seed += 0x6D2B79F5;
278
- t = Math.imul(t ^ t >>> 15, t | 1);
279
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
280
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
281
- };
282
- }
283
-
284
- // Get vibe at index using seeded shuffle (no-repeat until all used)
285
- function getVibeAtIndex(vibes: string[], index: number, seed: number): string {
286
- if (vibes.length === 0) return `${config.fallback}...`;
287
-
288
- // For small lists or when we've cycled through, just use modulo
289
- const effectiveIndex = index % vibes.length;
290
-
291
- // Create deterministic shuffle using seed
292
- const rng = mulberry32(seed);
293
- const indices = Array.from({ length: vibes.length }, (_, i) => i);
294
-
295
- // Fisher-Yates shuffle with seeded RNG
296
- for (let i = indices.length - 1; i > 0; i--) {
297
- const j = Math.floor(rng() * (i + 1));
298
- [indices[i], indices[j]] = [indices[j], indices[i]];
299
- }
300
-
301
- return vibes[indices[effectiveIndex]];
302
- }
303
-
304
- function getNextVibeFromFile(): string {
305
- if (!config.theme) return `${config.fallback}...`;
306
-
307
- // Load/reload cache if theme changed
308
- if (vibeCacheTheme !== config.theme) {
309
- vibeCache = loadVibesFromFile(config.theme);
310
- vibeCacheTheme = config.theme;
311
- vibeSeed = Date.now(); // New seed for new theme
312
- vibeIndex = 0;
313
- }
314
-
315
- if (vibeCache.length === 0) {
316
- return `${config.fallback}...`;
317
- }
318
-
319
- const vibe = getVibeAtIndex(vibeCache, vibeIndex, vibeSeed);
320
- vibeIndex++;
321
- return vibe;
322
- }
323
-
324
- // ═══════════════════════════════════════════════════════════════════════════
325
- // Prompt Building & Response Parsing (Pure Functions)
326
- // ═══════════════════════════════════════════════════════════════════════════
327
-
328
- function buildVibePrompt(ctx: VibeGenContext): string {
329
- // Truncate user prompt to save tokens (most context in first 100 chars)
330
- const task = ctx.userPrompt.slice(0, 100);
331
-
332
- // Build exclusion list from recent vibes
333
- const exclude = recentVibes.length > 0
334
- ? `Don't use: ${recentVibes.join(", ")}`
335
- : "";
336
-
337
- // Use configured template with variable substitution
338
- return config.promptTemplate
339
- .replace(/\{theme\}/g, ctx.theme)
340
- .replace(/\{task\}/g, task)
341
- .replace(/\{exclude\}/g, exclude);
342
- }
343
-
344
- function parseVibeResponse(response: string, fallback: string): string {
345
- if (!response) return `${fallback}...`;
346
-
347
- // Take only the first line (AI sometimes adds explanations)
348
- let vibe = response.trim().split('\n')[0].trim();
349
-
350
- // Remove quotes if model wrapped the response
351
- vibe = vibe.replace(/^["']|["']$/g, "");
352
-
353
- // Ensure ellipsis
354
- if (!vibe.endsWith("...")) {
355
- vibe = vibe.replace(/\.+$/, "") + "...";
356
- }
357
-
358
- // Enforce length limit (configurable, default 65 chars)
359
- if (vibe.length > config.maxLength) {
360
- vibe = vibe.slice(0, config.maxLength - 3) + "...";
361
- }
362
-
363
- // Final validation
364
- if (!vibe || vibe === "...") {
365
- return `${fallback}...`;
366
- }
367
-
368
- return vibe;
369
- }
370
-
371
- function buildAiContext(prompt: string): Context {
372
- return {
373
- systemPrompt: VIBE_SYSTEM_PROMPT,
374
- messages: [{
375
- role: "user",
376
- content: [{ type: "text", text: prompt }],
377
- timestamp: Date.now(),
378
- }],
379
- };
380
- }
381
-
382
- // ═══════════════════════════════════════════════════════════════════════════
383
- // AI Generation
384
- // ═══════════════════════════════════════════════════════════════════════════
385
-
386
- async function generateVibe(
387
- ctx: VibeGenContext,
388
- signal: AbortSignal,
389
- ): Promise<string> {
390
- if (!extensionCtx) {
391
- return `${config.fallback}...`;
392
- }
393
-
394
- // Parse model spec (provider/modelId format, where modelId may contain slashes)
395
- const slashIndex = config.modelSpec.indexOf("/");
396
- if (slashIndex === -1) {
397
- return `${config.fallback}...`;
398
- }
399
- const provider = config.modelSpec.slice(0, slashIndex);
400
- const modelId = config.modelSpec.slice(slashIndex + 1);
401
- if (!provider || !modelId) {
402
- return `${config.fallback}...`;
403
- }
404
-
405
- // Resolve model from registry
406
- const model = extensionCtx.modelRegistry.find(provider, modelId);
407
- if (!model) {
408
- console.debug(`[working-vibes] Model not found: ${config.modelSpec}`);
409
- return `${config.fallback}...`;
410
- }
411
-
412
- // Get auth
413
- const auth = await extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
414
- if (!auth.ok) {
415
- console.debug(`[working-vibes] Auth failed for ${provider}: ${auth.error}`);
416
- return `${config.fallback}...`;
417
- }
418
-
419
- const aiContext = buildAiContext(buildVibePrompt(ctx));
420
- const response = await completeVibe(provider, model, aiContext, { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, signal });
421
-
422
- const textContent = response.content.find(c => c.type === "text");
423
- if (!textContent?.text && response.stopReason === "error" && response.errorMessage) {
424
- console.debug(`[working-vibes] Vibe generation failed for ${config.modelSpec}: ${response.errorMessage}`);
425
- }
426
- return parseVibeResponse(textContent?.text || "", config.fallback);
427
- }
428
-
429
- function trackRecentVibe(vibe: string): void {
430
- // Don't track fallback messages
431
- if (vibe === `${config.fallback}...`) return;
432
-
433
- // Add to front, remove duplicates
434
- recentVibes = [vibe, ...recentVibes.filter(v => v !== vibe)].slice(0, MAX_RECENT_VIBES);
435
- }
436
-
437
- function updateVibeFromFile(setWorkingMessage: (msg?: string) => void): void {
438
- setWorkingMessage(getNextVibeFromFile());
439
- }
440
-
441
- async function generateAndUpdate(
442
- prompt: string,
443
- setWorkingMessage: (msg?: string) => void,
444
- ): Promise<void> {
445
- // File mode: instant, no API call
446
- if (config.mode === "file") {
447
- updateVibeFromFile(setWorkingMessage);
448
- return;
449
- }
450
-
451
- // Generate mode: API call with abort handling
452
- // Cancel any in-flight generation and create new controller
453
- // Capture in local variable to avoid race condition with subsequent calls
454
- const controller = new AbortController();
455
- currentGeneration?.abort();
456
- currentGeneration = controller;
457
-
458
- // Create timeout signal (3 seconds)
459
- const timeoutSignal = AbortSignal.timeout(config.timeout);
460
- const combinedSignal = AbortSignal.any([
461
- controller.signal,
462
- timeoutSignal,
463
- ]);
464
-
465
- try {
466
- const vibe = await generateVibe(
467
- { theme: config.theme!, userPrompt: prompt },
468
- combinedSignal,
469
- );
470
-
471
- // Only update if still streaming and THIS generation wasn't aborted
472
- if (isStreaming && !controller.signal.aborted) {
473
- trackRecentVibe(vibe);
474
- setWorkingMessage(vibe);
475
- }
476
- } catch (error) {
477
- // AbortError is expected on timeout/cancel - don't log as error
478
- if (error instanceof Error && error.name === "AbortError") {
479
- console.debug("[working-vibes] Generation aborted");
480
- } else {
481
- console.debug("[working-vibes] Generation failed:", error);
482
- }
483
- // Fallback already showing, no action needed
484
- }
485
- }
486
-
487
- // ═══════════════════════════════════════════════════════════════════════════
488
- // Exported Functions (called from index.ts)
489
- // ═══════════════════════════════════════════════════════════════════════════
490
-
491
- export function initVibeManager(ctx: ExtensionContext): void {
492
- extensionCtx = ctx;
493
- config = loadConfig(); // Refresh config in case settings changed
494
- }
495
-
496
- export function getVibeTheme(): string | null {
497
- return config.theme;
498
- }
499
-
500
- export function setVibeTheme(theme: string | null): boolean {
501
- config = { ...config, theme };
502
- recentVibes = []; // Clear recent vibes on theme change
503
- return saveConfig();
504
- }
505
-
506
- export function getVibeModel(): string {
507
- return config.modelSpec;
508
- }
509
-
510
- export function setVibeModel(modelSpec: string): boolean {
511
- config = { ...config, modelSpec };
512
- return saveModelConfig();
513
- }
514
-
515
- export function onVibeBeforeAgentStart(
516
- prompt: string,
517
- setWorkingMessage: (msg?: string) => void,
518
- ): void {
519
- // Skip if no theme configured or no extensionCtx
520
- if (!config.theme || !extensionCtx) return;
521
-
522
- // Queue themed placeholder BEFORE agent_start creates the loader
523
- // This sets pendingWorkingMessage which is applied when loader is created
524
- setWorkingMessage(`Channeling ${config.theme}...`);
525
-
526
- // Mark vibe generation time for rate limiting
527
- lastVibeTime = Date.now();
528
-
529
- // Async: generate and update (fire-and-forget, don't await)
530
- generateAndUpdate(prompt, setWorkingMessage);
531
- }
532
-
533
- export function onVibeAgentStart(): void {
534
- isStreaming = true;
535
- }
536
-
537
- export function onVibeToolCall(
538
- toolName: string,
539
- toolInput: Record<string, unknown>,
540
- setWorkingMessage: (msg?: string) => void,
541
- agentContext?: string, // Optional: recent agent response text for richer context
542
- ): void {
543
- // Skip if no theme, not streaming, or no extensionCtx
544
- if (!config.theme || !extensionCtx || !isStreaming) return;
545
-
546
- // Rate limit: skip if not enough time has passed
547
- const now = Date.now();
548
- if (now - lastVibeTime < config.refreshInterval) return;
549
-
550
- // Prefer agent context if provided (richer, more contextual)
551
- // Fall back to tool-based hint
552
- let hint: string;
553
- if (agentContext && agentContext.length > 10) {
554
- // Use first ~150 chars of agent context
555
- hint = agentContext.slice(0, 150);
556
- } else {
557
- // Build hint from tool name and input
558
- hint = `using ${toolName} tool`;
559
- if (toolName === "read" && toolInput.path) {
560
- hint = `reading file: ${toolInput.path}`;
561
- } else if (toolName === "write" && toolInput.path) {
562
- hint = `writing file: ${toolInput.path}`;
563
- } else if (toolName === "edit" && toolInput.path) {
564
- hint = `editing file: ${toolInput.path}`;
565
- } else if (toolName === "bash" && toolInput.command) {
566
- const cmd = String(toolInput.command).slice(0, 40);
567
- hint = `running command: ${cmd}`;
568
- }
569
- }
570
-
571
- // Update time and generate new vibe
572
- lastVibeTime = now;
573
- generateAndUpdate(hint, setWorkingMessage);
574
- }
575
-
576
- export function onVibeAgentEnd(setWorkingMessage: (msg?: string) => void): void {
577
- isStreaming = false;
578
- // Cancel any in-flight generation
579
- currentGeneration?.abort();
580
- // Reset to pi's default working message
581
- setWorkingMessage(undefined);
582
- }
583
-
584
- export function getVibeMode(): VibeMode {
585
- return config.mode;
586
- }
587
-
588
- export function setVibeMode(mode: VibeMode): boolean {
589
- config = { ...config, mode };
590
- return saveModeConfig();
591
- }
592
-
593
- function saveModeConfig(): boolean {
594
- const settings = readSettingsForWrite("workingVibeMode");
595
- if (!settings) {
596
- return false;
597
- }
598
-
599
- if (config.mode === "generate") {
600
- delete settings.workingVibeMode;
601
- } else {
602
- settings.workingVibeMode = config.mode;
603
- }
604
-
605
- return persistSettings(settings, "workingVibeMode");
606
- }
607
-
608
- export function hasVibeFile(theme: string): boolean {
609
- return existsSync(getVibeFilePath(theme));
610
- }
611
-
612
- export function getVibeFileCount(theme: string): number {
613
- const vibes = loadVibesFromFile(theme);
614
- return vibes.length;
615
- }
616
-
617
- export type GenerateVibesResult =
618
- | { success: true; count: number; filePath: string }
619
- | { success: false; count: 0; filePath: string; error: string };
620
-
621
- export function parseVibeGenerateArgs(args: readonly string[]): { theme: string; count: number } | null {
622
- if (args.length === 0) return null;
623
-
624
- const last = args.at(-1);
625
- const parsedCount = last && /^\d+$/.test(last) ? Number.parseInt(last, 10) : Number.NaN;
626
- const hasCount = Number.isFinite(parsedCount) && args.length > 1;
627
- const theme = hasCount ? args.slice(0, -1).join(" ") : args.join(" ");
628
- if (!theme) return null;
629
-
630
- return {
631
- theme,
632
- count: hasCount ? Math.min(Math.max(Math.floor(parsedCount), 1), 500) : 100,
633
- };
634
- }
635
-
636
- export async function generateVibesBatch(
637
- theme: string,
638
- count: number = 100,
639
- ): Promise<GenerateVibesResult> {
640
- const filePath = getVibeFilePath(theme);
641
- const safeCount = Number.isFinite(count) ? Math.min(Math.max(Math.floor(count), 1), 500) : 100;
642
-
643
- if (!extensionCtx) {
644
- return { success: false, count: 0, filePath, error: "Extension not initialized" };
645
- }
646
-
647
- // Parse model spec
648
- const slashIndex = config.modelSpec.indexOf("/");
649
- if (slashIndex === -1) {
650
- return { success: false, count: 0, filePath, error: "Invalid model spec" };
651
- }
652
- const provider = config.modelSpec.slice(0, slashIndex);
653
- const modelId = config.modelSpec.slice(slashIndex + 1);
654
-
655
- // Resolve model
656
- const model = extensionCtx.modelRegistry.find(provider, modelId);
657
- if (!model) {
658
- return { success: false, count: 0, filePath, error: `Model not found: ${config.modelSpec}` };
659
- }
660
-
661
- // Get auth
662
- const auth = await extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
663
- if (!auth.ok) {
664
- return { success: false, count: 0, filePath, error: auth.error };
665
- }
666
-
667
- // Build batch prompt
668
- const prompt = BATCH_PROMPT
669
- .replace(/\{theme\}/g, theme)
670
- .replace(/\{count\}/g, String(safeCount));
671
-
672
- const aiContext = buildAiContext(prompt);
673
-
674
- try {
675
- // Use longer timeout for batch generation (30 seconds)
676
- const signal = AbortSignal.timeout(30000);
677
- const response = await completeVibe(provider, model, aiContext, { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, signal });
678
-
679
- const textContent = response.content.find(c => c.type === "text");
680
- if (!textContent?.text) {
681
- const error = response.stopReason === "error" && response.errorMessage
682
- ? response.errorMessage
683
- : "Empty response from model";
684
- return { success: false, count: 0, filePath, error };
685
- }
686
-
687
- // Parse response: one vibe per line
688
- const vibes = textContent.text
689
- .split("\n")
690
- .map(line => line.trim())
691
- .filter(line => line.length > 0)
692
- .map(line => {
693
- // Clean up each line
694
- let vibe = line.replace(/^["'\d.\-)\s]+/, "").trim(); // Remove leading quotes, numbers, bullets
695
- vibe = vibe.replace(/["']$/g, ""); // Remove trailing quotes
696
- if (!vibe.endsWith("...")) {
697
- vibe = vibe.replace(/\.+$/, "") + "...";
698
- }
699
- return vibe;
700
- })
701
- .filter(vibe => vibe.length > 3 && vibe !== "..."); // Filter invalid
702
-
703
- if (vibes.length === 0) {
704
- return { success: false, count: 0, filePath, error: "No valid vibes generated" };
705
- }
706
-
707
- // Save to file
708
- saveVibesToFile(theme, vibes);
709
-
710
- // Clear cache so next use loads fresh
711
- if (vibeCacheTheme === theme) {
712
- vibeCache = [];
713
- vibeCacheTheme = null;
714
- }
715
-
716
- return { success: true, count: vibes.length, filePath };
717
- } catch (error) {
718
- const message = error instanceof Error ? error.message : "Unknown error";
719
- return { success: false, count: 0, filePath, error: message };
720
- }
721
- }