@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,3428 +0,0 @@
1
- import {
2
- copyToClipboard,
3
- CONFIG_DIR_NAME,
4
- getAgentDir,
5
- getSettingsPath,
6
- VERSION,
7
- type ExtensionAPI,
8
- type ReadonlyFooterDataProvider,
9
- type Theme,
10
- } from "@selesai/code";
11
- import type { AssistantMessage } from "@earendil-works/pi-ai";
12
- import { CURSOR_MARKER, isKeyRelease, type AutocompleteProvider, type SelectItem, SelectList, truncateToWidth, TUI_KEYBINDINGS, visibleWidth } from "@earendil-works/pi-tui";
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
14
- import { join, dirname } from "node:path";
15
-
16
- import type { ColorScheme, SegmentContext, StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "./types.ts";
17
- import type { PowerlineConfig } from "./powerline-config.ts";
18
- import { BashTranscriptStore } from "./bash-mode/transcript.ts";
19
- import {
20
- BashCompletionEngine,
21
- BashAutocompleteProvider,
22
- getOneOffBashCommandContext,
23
- ModeAwareAutocompleteProvider,
24
- OneOffBashAutocompleteProvider,
25
- } from "./bash-mode/completion.ts";
26
- import { BashModeEditor, isPrintableInput } from "./bash-mode/editor.ts";
27
- import { ManagedShellSession } from "./bash-mode/shell-session.ts";
28
- import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory, appendProjectHistory } from "./bash-mode/history.ts";
29
- import type { BashModeSettings } from "./bash-mode/types.ts";
30
- import { getPreset, PRESETS } from "./presets.ts";
31
- import { getAgentPath } from "./paths.ts";
32
- import { collectHiddenExtensionStatusKeys, getNotificationExtensionStatuses, mergeSegmentOptions, mergeSegmentsWithCustomItems, nextPowerlineSettingWithOptions, nextPowerlineSettingWithPreset, parsePowerlineConfig } from "./powerline-config.ts";
33
- import { getSeparator } from "./separators.ts";
34
- import { renderSegment } from "./segments.ts";
35
- import { getGitStatus, invalidateGitStatus, invalidateGitBranch, subscribeGitUpdates } from "./git-status.ts";
36
- import { SessionBranchCache, SessionTokenStatsCache } from "./token-stats.ts";
37
- import { ansi, getFgAnsiCode } from "./colors.ts";
38
- import { WelcomeComponent, WelcomeHeader, discoverLoadedCounts, getRecentSessions } from "./welcome.ts";
39
- import { getGuidePreferences, getNewGuideFeatures, markGuideVersionSeen, resolveGuideDisplayMode, saveGuidePreferences, type GuideFeature } from "./guide.ts";
40
- import { createRenderScheduler } from "./render-scheduler.ts";
41
- import { createWelcomeDismissScheduler } from "./welcome-dismiss.ts";
42
- import { getEditorAutocompleteProvider, passAutocompleteProviderThroughPreviousEditor } from "./editor-composition.ts";
43
- import { EditorPerfProfiler, readEditorPerfOptions } from "./editor-performance.ts";
44
- import { CoreContextUsageCache, estimateInitialContextTokens, estimateUnknownContextUsage, resolveDisplayContextUsage, type CoreContextUsage } from "./context-usage.ts";
45
- import { isStaleExtensionContextError, shouldShowStartupWelcome } from "./lifecycle.ts";
46
- import { getDefaultColors } from "./theme.ts";
47
- import { registerCdCommand } from "./cd-command.ts";
48
- import {
49
- isSupportedSuperShortcut,
50
- matchesConfiguredShortcut,
51
- matchesStashShortcutInput,
52
- shortcutConflictKey,
53
- shortcutUsesSuper,
54
- } from "./shortcuts.ts";
55
- import {
56
- initVibeManager,
57
- onVibeBeforeAgentStart,
58
- onVibeAgentStart,
59
- onVibeAgentEnd,
60
- onVibeToolCall,
61
- getVibeTheme,
62
- setVibeTheme,
63
- getVibeModel,
64
- setVibeModel,
65
- getVibeMode,
66
- setVibeMode,
67
- hasVibeFile,
68
- getVibeFileCount,
69
- generateVibesBatch,
70
- parseVibeGenerateArgs,
71
- } from "./working-vibes.ts";
72
- import { setupTpsTracker } from "./tps.ts";
73
- import { readAsyncSubagentUsage } from "./session-usage.ts";
74
- import { PowerlineQueueStore, currentQueueContext, formatQueueDeliveryText, parseCompactQueuedPrompt } from "./queue/store.ts";
75
- import type { PowerlineQueueItem, QueueContext, QueueIntent, QueueSummary, QueueTarget } from "./queue/types.ts";
76
-
77
- // ═══════════════════════════════════════════════════════════════════════════
78
- // Configuration
79
- // ═══════════════════════════════════════════════════════════════════════════
80
-
81
- let config: PowerlineConfig = {
82
- preset: "default",
83
- customItems: [],
84
- fixedEditorPromptGlyph: "",
85
- disabledSegments: [],
86
- invalidDisabledSegments: [],
87
- layout: null,
88
- invalidLayoutSegments: [],
89
- separator: null,
90
- segmentOptions: {},
91
- placement: "above",
92
- invalidPlacement: null,
93
- welcome: true,
94
- stashSharpSShortcut: false,
95
- };
96
-
97
- const CUSTOM_COMPACTION_STATUS_KEY = "compact-policy";
98
- let customCompactionEnabled = false;
99
-
100
- type ShortcutBinding = string | null;
101
-
102
- export interface PowerlineShortcuts {
103
- stashHistory: ShortcutBinding;
104
- copyEditor: ShortcutBinding;
105
- cutEditor: ShortcutBinding;
106
- queueOpen: ShortcutBinding;
107
- editorStart: ShortcutBinding;
108
- editorEnd: ShortcutBinding;
109
- }
110
-
111
- type PowerlineShortcutKey = keyof PowerlineShortcuts;
112
- type PowerlineShortcutAction =
113
- | { kind: "stashHistory" }
114
- | { kind: "copyEditor" }
115
- | { kind: "cutEditor" }
116
- | { kind: "queueOpen" }
117
- | { kind: "bashMode" };
118
- const STASH_HISTORY_LIMIT = 12;
119
- const PROJECT_PROMPT_HISTORY_LIMIT = 50;
120
- const STASH_PREVIEW_WIDTH = 72;
121
- const DEFAULT_SHORTCUTS: PowerlineShortcuts = {
122
- stashHistory: "ctrl+alt+h",
123
- copyEditor: "ctrl+alt+c",
124
- cutEditor: "ctrl+alt+x",
125
- queueOpen: "ctrl+alt+q",
126
- editorStart: "super+shift+up",
127
- editorEnd: "super+shift+down",
128
- };
129
- const DEFAULT_BASH_MODE_SETTINGS = {
130
- toggleShortcut: "ctrl+shift+b",
131
- completions: false,
132
- transcriptMaxLines: 2000,
133
- transcriptMaxBytes: 512 * 1024,
134
- } as const satisfies BashModeSettings;
135
- const SHORTCUT_KEYS: PowerlineShortcutKey[] = ["stashHistory", "copyEditor", "cutEditor", "queueOpen", "editorStart", "editorEnd"];
136
- const APP_RESERVED_SHORTCUTS = [
137
- "escape",
138
- "ctrl+c",
139
- "ctrl+d",
140
- "ctrl+z",
141
- "shift+tab",
142
- "ctrl+p",
143
- "shift+ctrl+p",
144
- "ctrl+l",
145
- "ctrl+o",
146
- "shift+ctrl+o",
147
- "ctrl+t",
148
- "ctrl+n",
149
- "ctrl+g",
150
- "alt+enter",
151
- "alt+up",
152
- "alt+down",
153
- "ctrl+v",
154
- "alt+v",
155
- "shift+l",
156
- "shift+t",
157
- "ctrl+s",
158
- "ctrl+r",
159
- "ctrl+backspace",
160
- "ctrl+a",
161
- "ctrl+x",
162
- "ctrl+u",
163
- ] as const;
164
- const EXTRA_RESERVED_SHORTCUTS = ["alt+s"] as const;
165
- const SHORTCUT_MODIFIER_ORDER = ["ctrl", "alt", "super", "shift"] as const;
166
- const SHORTCUT_MODIFIERS = new Set<string>(SHORTCUT_MODIFIER_ORDER);
167
- const SHORTCUT_NAMED_KEYS = new Set([
168
- "escape", "esc", "enter", "return", "tab", "space", "backspace", "delete", "insert", "clear",
169
- "home", "end", "pageup", "pagedown", "up", "down", "left", "right",
170
- ]);
171
- const SHORTCUT_SYMBOL_KEYS = new Set([
172
- "`", "-", "=", "[", "]", "\\", ";", "'", ",", ".", "/",
173
- "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "|", "~", "{", "}", ":", "<", ">", "?",
174
- ]);
175
- const PROMPT_HISTORY_LIMIT = 100;
176
- const LAYOUT_CACHE_TTL_MS = 250;
177
- const STREAMING_LAYOUT_CACHE_TTL_MS = 1000;
178
- const STATUS_RENDER_DEBOUNCE_MS = 33;
179
- const CONTEXT_STATUS_RENDER_MS = 250;
180
- const EDITOR_STATUS_DEFER_MS = 150;
181
- const QUEUE_SUMMARY_CACHE_TTL_MS = 250;
182
- const PROMPT_HISTORY_TRACKED = Symbol.for("powerlinePromptHistoryTracked");
183
- const PROMPT_HISTORY_STATE_KEY = Symbol.for("powerlinePromptHistoryState");
184
-
185
- interface PromptHistoryEditor {
186
- addToHistory?: (text: string) => void;
187
- }
188
-
189
- type PromptHistoryState = { savedPromptHistory: string[] };
190
- type SessionAssistantUsage = AssistantMessage["usage"];
191
-
192
- function getUsageTokenTotal(usage: SessionAssistantUsage): number {
193
- const totalTokens = "totalTokens" in usage && typeof usage.totalTokens === "number" ? usage.totalTokens : 0;
194
- return totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
195
- }
196
-
197
- function hasSessionAssistantUsage(value: unknown): value is SessionAssistantUsage {
198
- if (!isRecord(value)) {
199
- return false;
200
- }
201
-
202
- if (
203
- typeof value.input !== "number" ||
204
- typeof value.output !== "number" ||
205
- typeof value.cacheRead !== "number" ||
206
- typeof value.cacheWrite !== "number"
207
- ) {
208
- return false;
209
- }
210
-
211
- return isRecord(value.cost) && typeof value.cost.total === "number";
212
- }
213
-
214
- function isSessionAssistantMessage(value: unknown): value is AssistantMessage {
215
- return isRecord(value)
216
- && value.role === "assistant"
217
- && hasSessionAssistantUsage(value.usage)
218
- && (value.stopReason === undefined || typeof value.stopReason === "string");
219
- }
220
-
221
- function isPromptHistoryState(value: unknown): value is PromptHistoryState {
222
- return isRecord(value)
223
- && Array.isArray(value.savedPromptHistory)
224
- && value.savedPromptHistory.every((entry) => typeof entry === "string");
225
- }
226
-
227
- function getPromptHistoryState(): PromptHistoryState {
228
- const existing = Reflect.get(globalThis, PROMPT_HISTORY_STATE_KEY);
229
- if (isPromptHistoryState(existing)) {
230
- return existing;
231
- }
232
-
233
- const state: PromptHistoryState = { savedPromptHistory: [] };
234
- Reflect.set(globalThis, PROMPT_HISTORY_STATE_KEY, state);
235
- return state;
236
- }
237
-
238
- function readPromptHistory(editor: PromptHistoryEditor | null | undefined): string[] {
239
- if (!editor) return [];
240
- const history = Reflect.get(editor, "history");
241
- if (!Array.isArray(history)) return [];
242
-
243
- const normalized: string[] = [];
244
- for (const entry of history) {
245
- if (typeof entry !== "string") continue;
246
- const trimmed = entry.trim();
247
- if (!trimmed) continue;
248
- if (normalized.length > 0 && normalized[normalized.length - 1] === trimmed) continue;
249
- normalized.push(trimmed);
250
- if (normalized.length >= PROMPT_HISTORY_LIMIT) break;
251
- }
252
-
253
- return normalized;
254
- }
255
-
256
- function snapshotPromptHistory(editor: PromptHistoryEditor | null | undefined): void {
257
- const history = readPromptHistory(editor);
258
- if (history.length > 0) {
259
- getPromptHistoryState().savedPromptHistory = [...history];
260
- }
261
- }
262
-
263
- function restorePromptHistory(editor: PromptHistoryEditor | null | undefined): void {
264
- const { savedPromptHistory } = getPromptHistoryState();
265
- if (!savedPromptHistory.length || typeof editor?.addToHistory !== "function") return;
266
-
267
- for (let i = savedPromptHistory.length - 1; i >= 0; i--) {
268
- editor.addToHistory(savedPromptHistory[i]);
269
- }
270
- }
271
-
272
- function trackPromptHistory(editor: PromptHistoryEditor | null | undefined): void {
273
- if (!editor || typeof editor.addToHistory !== "function") return;
274
- if (Reflect.get(editor, PROMPT_HISTORY_TRACKED)) {
275
- snapshotPromptHistory(editor);
276
- return;
277
- }
278
-
279
- const originalAddToHistory = editor.addToHistory.bind(editor);
280
- editor.addToHistory = (text: string) => {
281
- originalAddToHistory(text);
282
- snapshotPromptHistory(editor);
283
- };
284
- Reflect.set(editor, PROMPT_HISTORY_TRACKED, true);
285
- snapshotPromptHistory(editor);
286
- }
287
-
288
- function getProjectSettingsPath(cwd: string): string {
289
- return join(cwd, CONFIG_DIR_NAME, "settings.json");
290
- }
291
-
292
- function getGlobalCompactionPolicyPath(): string {
293
- return join(getAgentDir(), "compaction-policy.json");
294
- }
295
-
296
- function getCustomCompactionExtensionPath(): string {
297
- return join(getAgentDir(), "extensions", "pi-custom-compaction");
298
- }
299
-
300
- function mergeSettings(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
301
- const merged: Record<string, unknown> = { ...base };
302
-
303
- for (const [key, overrideValue] of Object.entries(override)) {
304
- const baseValue = merged[key];
305
- merged[key] = isRecord(baseValue) && isRecord(overrideValue)
306
- ? mergeSettings(baseValue, overrideValue)
307
- : overrideValue;
308
- }
309
-
310
- return merged;
311
- }
312
-
313
- function readSettingsFile(settingsPath: string): Record<string, unknown> {
314
- try {
315
- if (!existsSync(settingsPath)) {
316
- return {};
317
- }
318
-
319
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
320
- if (!isRecord(parsed)) {
321
- console.debug(`[powerline-footer] Ignoring non-object settings at ${settingsPath}`);
322
- return {};
323
- }
324
-
325
- return parsed;
326
- } catch (error) {
327
- // Settings are user-edited input. Log and keep the extension running with defaults
328
- // instead of crashing the UI during startup.
329
- console.debug(`[powerline-footer] Failed to read settings from ${settingsPath}:`, error);
330
- return {};
331
- }
332
- }
333
-
334
- function readWritableSettingsFile(settingsPath: string): Record<string, unknown> | null {
335
- if (!existsSync(settingsPath)) {
336
- return {};
337
- }
338
-
339
- try {
340
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
341
- if (!isRecord(parsed)) {
342
- console.debug(`[powerline-footer] Refusing to write settings to non-object file at ${settingsPath}`);
343
- return null;
344
- }
345
-
346
- return parsed;
347
- } catch (error) {
348
- // Do not overwrite malformed user settings with partial data. Surface the failure
349
- // through the command handler so the user can fix the file intentionally.
350
- console.debug(`[powerline-footer] Failed to parse settings at ${settingsPath}:`, error);
351
- return null;
352
- }
353
- }
354
-
355
- function readCompactionPolicyEnabled(configPath: string): boolean | undefined {
356
- if (!existsSync(configPath)) return undefined;
357
- try {
358
- const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
359
- if (!isRecord(parsed) || typeof parsed.enabled !== "boolean") return false;
360
- return parsed.enabled;
361
- } catch (error) {
362
- console.debug(`[powerline-footer] Failed to read compaction policy from ${configPath}:`, error);
363
- return false;
364
- }
365
- }
366
-
367
- function detectCustomCompactionEnabled(cwd: string): boolean {
368
- if (!existsSync(getCustomCompactionExtensionPath())) return false;
369
-
370
- const projectSetting = readCompactionPolicyEnabled(join(cwd, CONFIG_DIR_NAME, "compaction-policy.json"));
371
- if (projectSetting !== undefined) return projectSetting;
372
-
373
- return readCompactionPolicyEnabled(getGlobalCompactionPolicyPath()) ?? false;
374
- }
375
-
376
- function isRecord(value: unknown): value is Record<string, unknown> {
377
- return typeof value === "object" && value !== null && !Array.isArray(value);
378
- }
379
-
380
- function getStashHistoryPath(): string {
381
- return join(getAgentDir(), "powerline-footer", "stash-history.json");
382
- }
383
-
384
- function getSessionsPath(): string {
385
- return join(getAgentDir(), "sessions");
386
- }
387
-
388
- function getProjectSessionsPath(cwd: string): string {
389
- const projectKey = cwd
390
- .replace(/^[/\\]+|[/\\]+$/g, "")
391
- .replace(/[\\/]+/g, "-");
392
-
393
- return join(getSessionsPath(), `--${projectKey}--`);
394
- }
395
-
396
- function getPromptHistoryText(content: unknown): string {
397
- if (typeof content === "string") {
398
- return content.replace(/\s+/g, " ").trim();
399
- }
400
-
401
- if (!Array.isArray(content)) {
402
- return "";
403
- }
404
-
405
- const parts: string[] = [];
406
- for (const block of content) {
407
- if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") {
408
- continue;
409
- }
410
- parts.push(block.text);
411
- }
412
-
413
- return parts.join("\n").replace(/\s+/g, " ").trim();
414
- }
415
-
416
- function readRecentProjectPrompts(cwd: string, limit: number): string[] {
417
- const sessionsPath = getProjectSessionsPath(cwd);
418
- if (!existsSync(sessionsPath)) {
419
- return [];
420
- }
421
-
422
- const promptEntries: { text: string; timestamp: number }[] = [];
423
- const fileNames = readdirSync(sessionsPath)
424
- .filter((fileName) => fileName.endsWith(".jsonl"));
425
-
426
- for (const fileName of fileNames) {
427
- const filePath = join(sessionsPath, fileName);
428
- const lines = readFileSync(filePath, "utf-8").split("\n");
429
-
430
- for (let i = lines.length - 1; i >= 0; i--) {
431
- const line = lines[i];
432
- if (!line || !line.includes('"type":"message"') || !line.includes('"role":"user"')) {
433
- continue;
434
- }
435
-
436
- let entry: unknown;
437
- try {
438
- entry = JSON.parse(line);
439
- } catch (error) {
440
- const message = error instanceof Error ? error.message : String(error);
441
- throw new Error(`Failed to parse session file ${filePath}: ${message}`, { cause: error });
442
- }
443
-
444
- if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") {
445
- continue;
446
- }
447
-
448
- const text = getPromptHistoryText(entry.message.content);
449
- if (!hasNonWhitespaceText(text)) {
450
- continue;
451
- }
452
-
453
- const timestamp = typeof entry.message.timestamp === "number"
454
- ? entry.message.timestamp
455
- : typeof entry.timestamp === "string"
456
- ? Date.parse(entry.timestamp)
457
- : 0;
458
-
459
- promptEntries.push({ text, timestamp: Number.isFinite(timestamp) ? timestamp : 0 });
460
- }
461
- }
462
-
463
- promptEntries.sort((a, b) => b.timestamp - a.timestamp);
464
-
465
- const prompts: string[] = [];
466
- const seen = new Set<string>();
467
- for (const entry of promptEntries) {
468
- if (seen.has(entry.text)) {
469
- continue;
470
- }
471
-
472
- seen.add(entry.text);
473
- prompts.push(entry.text);
474
- if (prompts.length >= limit) {
475
- return prompts;
476
- }
477
- }
478
-
479
- return prompts;
480
- }
481
-
482
- function normalizeStashHistoryEntries(value: unknown): string[] {
483
- if (!Array.isArray(value)) {
484
- return [];
485
- }
486
-
487
- const history: string[] = [];
488
- for (const entry of value) {
489
- if (typeof entry !== "string") {
490
- continue;
491
- }
492
-
493
- if (!hasNonWhitespaceText(entry)) {
494
- continue;
495
- }
496
-
497
- if (history[history.length - 1] === entry) {
498
- continue;
499
- }
500
-
501
- history.push(entry);
502
- if (history.length >= STASH_HISTORY_LIMIT) {
503
- break;
504
- }
505
- }
506
-
507
- return history;
508
- }
509
-
510
- function readPersistedStashHistory(): string[] {
511
- const stashHistoryPath = getStashHistoryPath();
512
-
513
- try {
514
- if (!existsSync(stashHistoryPath)) {
515
- return [];
516
- }
517
-
518
- const parsed = JSON.parse(readFileSync(stashHistoryPath, "utf-8"));
519
- if (!isRecord(parsed)) {
520
- console.debug(`[powerline-footer] Ignoring invalid stash history at ${stashHistoryPath}`);
521
- return [];
522
- }
523
-
524
- return normalizeStashHistoryEntries(parsed.history);
525
- } catch (error) {
526
- console.debug(`[powerline-footer] Failed to read stash history from ${stashHistoryPath}:`, error);
527
- return [];
528
- }
529
- }
530
-
531
- function persistStashHistory(history: string[]): void {
532
- const stashHistoryPath = getStashHistoryPath();
533
- const payload = {
534
- version: 1,
535
- history: history.slice(0, STASH_HISTORY_LIMIT),
536
- };
537
-
538
- try {
539
- mkdirSync(dirname(stashHistoryPath), { recursive: true });
540
- writeFileSync(stashHistoryPath, JSON.stringify(payload, null, 2) + "\n");
541
- } catch (error) {
542
- console.debug(`[powerline-footer] Failed to persist stash history to ${stashHistoryPath}:`, error);
543
- }
544
- }
545
-
546
- function readSettings(cwd: string = process.cwd()): Record<string, unknown> {
547
- return mergeSettings(readSettingsFile(getSettingsPath()), readSettingsFile(getProjectSettingsPath(cwd)));
548
- }
549
-
550
- function writePowerlineSetting(cwd: string, update: (existingPowerlineSetting: unknown) => unknown): boolean {
551
- const globalSettingsPath = getSettingsPath();
552
- const projectSettingsPath = getProjectSettingsPath(cwd);
553
- const globalSettings = readWritableSettingsFile(globalSettingsPath);
554
- const projectSettings = readWritableSettingsFile(projectSettingsPath);
555
-
556
- if (globalSettings === null || projectSettings === null) {
557
- return false;
558
- }
559
-
560
- const writeToProject = Object.prototype.hasOwnProperty.call(projectSettings, "powerline");
561
- const settingsPath = writeToProject ? projectSettingsPath : globalSettingsPath;
562
- const settings = writeToProject ? projectSettings : globalSettings;
563
-
564
- settings.powerline = update(settings.powerline);
565
-
566
- try {
567
- mkdirSync(dirname(settingsPath), { recursive: true });
568
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
569
- return true;
570
- } catch (error) {
571
- console.debug(`[powerline-footer] Failed to persist powerline setting to ${settingsPath}:`, error);
572
- return false;
573
- }
574
- }
575
-
576
- function writePowerlinePresetSetting(preset: StatusLinePreset, cwd: string = process.cwd()): boolean {
577
- return writePowerlineSetting(cwd, (existingPowerlineSetting) => (
578
- nextPowerlineSettingWithPreset(existingPowerlineSetting, preset)
579
- ));
580
- }
581
-
582
- function writePowerlineOptionSetting(
583
- cwd: string,
584
- updates: Partial<Pick<PowerlineConfig, "welcome" | "stashSharpSShortcut" | "placement">>,
585
- currentPreset: StatusLinePreset,
586
- ): boolean {
587
- return writePowerlineSetting(cwd, (existingPowerlineSetting) => (
588
- nextPowerlineSettingWithOptions(existingPowerlineSetting, updates, currentPreset)
589
- ));
590
- }
591
-
592
- const PRESET_NAMES = Object.keys(PRESETS) as StatusLinePreset[];
593
-
594
- function isValidPreset(value: unknown): value is StatusLinePreset {
595
- return typeof value === "string" && Object.prototype.hasOwnProperty.call(PRESETS, value);
596
- }
597
-
598
- function normalizePreset(value: unknown): StatusLinePreset | null {
599
- if (typeof value !== "string") {
600
- return null;
601
- }
602
-
603
- const preset = value.trim().toLowerCase();
604
- return isValidPreset(preset) ? preset : null;
605
- }
606
-
607
- function hasNonWhitespaceText(text: string): boolean {
608
- return text.trim().length > 0;
609
- }
610
-
611
- function getCurrentEditorText(ctx: any, editor: any): string {
612
- const editorText = editor?.getExpandedText?.();
613
- if (typeof editorText === "string" && editorText.length > 0) return editorText;
614
- return ctx.ui.getEditorText?.() ?? editorText ?? "";
615
- }
616
-
617
- function buildStashPreview(text: string, maxWidth: number): string {
618
- const compact = text.replace(/\s+/g, " ").trim();
619
- if (!compact) return "(empty)";
620
- return truncateToWidth(compact, maxWidth, "…");
621
- }
622
-
623
- function pushStashHistory(history: string[], text: string): boolean {
624
- if (!hasNonWhitespaceText(text)) return false;
625
- if (history[0] === text) return false;
626
-
627
- history.unshift(text);
628
- if (history.length > STASH_HISTORY_LIMIT) {
629
- history.length = STASH_HISTORY_LIMIT;
630
- }
631
-
632
- return true;
633
- }
634
-
635
- function normalizeShortcut(value: string): string {
636
- const parts = value.trim().toLowerCase().split("+");
637
- if (parts.length <= 1) return parts[0] ?? "";
638
-
639
- const modifierRank = new Map<string, number>(SHORTCUT_MODIFIER_ORDER.map((modifier, index) => [modifier, index]));
640
- const modifiers = parts.slice(0, -1).sort((a, b) => (modifierRank.get(a) ?? 99) - (modifierRank.get(b) ?? 99));
641
- return [...modifiers, parts[parts.length - 1]].join("+");
642
- }
643
-
644
- function reservedShortcuts(): Set<string> {
645
- const shortcuts = new Set<string>([
646
- ...EXTRA_RESERVED_SHORTCUTS,
647
- ...APP_RESERVED_SHORTCUTS,
648
- ].map(normalizeShortcut));
649
-
650
- for (const definition of Object.values(TUI_KEYBINDINGS)) {
651
- const defaultKeys = definition.defaultKeys;
652
- const keys = defaultKeys === undefined ? [] : Array.isArray(defaultKeys) ? defaultKeys : [defaultKeys];
653
- for (const key of keys) {
654
- shortcuts.add(normalizeShortcut(key));
655
- }
656
- }
657
-
658
- return shortcuts;
659
- }
660
-
661
- function isValidShortcutKeyPart(keyPart: string): boolean {
662
- const lowerKeyPart = keyPart.toLowerCase();
663
-
664
- if (/^[a-z0-9]$/i.test(keyPart)) return true;
665
- if (/^f([1-9]|1[0-2])$/i.test(keyPart)) return true;
666
- if (SHORTCUT_NAMED_KEYS.has(lowerKeyPart)) return true;
667
-
668
- return SHORTCUT_SYMBOL_KEYS.has(keyPart);
669
- }
670
-
671
- function parseShortcutOverride(value: unknown): string | null {
672
- if (typeof value !== "string") {
673
- return null;
674
- }
675
-
676
- const trimmed = value.trim();
677
- if (!trimmed || /\s/.test(trimmed)) {
678
- return null;
679
- }
680
-
681
- const parts = trimmed.split("+");
682
- if (parts.some((part) => part.length === 0)) {
683
- return null;
684
- }
685
-
686
- const modifierParts = parts.slice(0, -1).map((part) => {
687
- const modifier = part.toLowerCase();
688
- return modifier === "cmd" || modifier === "command" ? "super" : modifier;
689
- });
690
- if (new Set(modifierParts).size !== modifierParts.length) {
691
- return null;
692
- }
693
-
694
- for (const modifier of modifierParts) {
695
- if (!SHORTCUT_MODIFIERS.has(modifier)) {
696
- return null;
697
- }
698
- }
699
-
700
- const keyPart = parts[parts.length - 1];
701
- if (!isValidShortcutKeyPart(keyPart)) {
702
- return null;
703
- }
704
-
705
- const normalizedKey = SHORTCUT_SYMBOL_KEYS.has(keyPart) ? keyPart : keyPart.toLowerCase();
706
- const normalizedShortcut = normalizeShortcut([...modifierParts, normalizedKey].join("+"));
707
- if (shortcutUsesSuper(normalizedShortcut) && !isSupportedSuperShortcut(normalizedShortcut)) {
708
- return null;
709
- }
710
-
711
- return normalizedShortcut;
712
- }
713
-
714
- function shortcutUsageKey(shortcut: string): string {
715
- return shortcutConflictKey(normalizeShortcut(shortcut));
716
- }
717
-
718
- function parseShortcutSetting(value: unknown): ShortcutBinding | undefined {
719
- if (value === null || value === undefined) return null;
720
- if (typeof value === "string" && value.trim() === "") return null;
721
- return parseShortcutOverride(value) ?? undefined;
722
- }
723
-
724
- function findShortcutReplacement(key: PowerlineShortcutKey, used: Set<string>): string | null {
725
- const preferred = DEFAULT_SHORTCUTS[key];
726
- if (preferred && !used.has(shortcutUsageKey(preferred))) {
727
- return preferred;
728
- }
729
-
730
- for (const shortcutKey of SHORTCUT_KEYS) {
731
- const candidate = DEFAULT_SHORTCUTS[shortcutKey];
732
- if (candidate && !used.has(shortcutUsageKey(candidate))) {
733
- return candidate;
734
- }
735
- }
736
-
737
- return null;
738
- }
739
-
740
- function bashToggleShortcutReservation(settings: Record<string, unknown>): ShortcutBinding {
741
- const raw = isRecord(settings.bashMode) ? settings.bashMode : {};
742
- if (!Object.prototype.hasOwnProperty.call(raw, "toggleShortcut")) {
743
- return DEFAULT_BASH_MODE_SETTINGS.toggleShortcut;
744
- }
745
-
746
- const parsed = parseShortcutSetting(raw.toggleShortcut);
747
- return parsed === undefined ? DEFAULT_BASH_MODE_SETTINGS.toggleShortcut : parsed;
748
- }
749
-
750
- export function resolveShortcutConfig(settings: Record<string, unknown>): PowerlineShortcuts {
751
- const resolved: PowerlineShortcuts = { ...DEFAULT_SHORTCUTS };
752
- const shortcutSettings = settings.powerlineShortcuts;
753
-
754
- if (isRecord(shortcutSettings)) {
755
- for (const key of SHORTCUT_KEYS) {
756
- if (!Object.prototype.hasOwnProperty.call(shortcutSettings, key)) {
757
- continue;
758
- }
759
-
760
- const override = parseShortcutSetting(shortcutSettings[key]);
761
- if (override !== undefined) {
762
- resolved[key] = override;
763
- }
764
- }
765
- }
766
-
767
- const used = new Set(Array.from(reservedShortcuts(), shortcutUsageKey));
768
- const reservedBashToggle = bashToggleShortcutReservation(settings);
769
- if (reservedBashToggle) {
770
- used.add(shortcutUsageKey(reservedBashToggle));
771
- }
772
-
773
- for (const key of SHORTCUT_KEYS) {
774
- const configured = resolved[key];
775
- if (configured === null) {
776
- continue;
777
- }
778
-
779
- const configuredUsageKey = shortcutUsageKey(configured);
780
-
781
- if (!used.has(configuredUsageKey)) {
782
- used.add(configuredUsageKey);
783
- continue;
784
- }
785
-
786
- const replacement = findShortcutReplacement(key, used);
787
- if (!replacement) {
788
- console.debug(`[powerline-footer] Shortcut conflict for ${key}: "${configured}" is already in use`);
789
- continue;
790
- }
791
-
792
- console.debug(
793
- `[powerline-footer] Shortcut conflict for ${key}: "${configured}" replaced with "${replacement}"`,
794
- );
795
-
796
- resolved[key] = replacement;
797
- used.add(shortcutUsageKey(replacement));
798
- }
799
-
800
- return resolved;
801
- }
802
-
803
- export function parseBashModeSettings(settings: Record<string, unknown>, powerlineShortcuts?: PowerlineShortcuts): BashModeSettings {
804
- const raw = isRecord(settings.bashMode) ? settings.bashMode : {};
805
- const used = new Set(Array.from(reservedShortcuts(), shortcutUsageKey));
806
- if (powerlineShortcuts) {
807
- for (const shortcut of Object.values(powerlineShortcuts)) {
808
- if (shortcut) {
809
- used.add(shortcutUsageKey(shortcut));
810
- }
811
- }
812
- }
813
-
814
- const configuredToggleShortcut = Object.prototype.hasOwnProperty.call(raw, "toggleShortcut")
815
- ? parseShortcutSetting(raw.toggleShortcut)
816
- : undefined;
817
- const fallbackToggleShortcut = used.has(shortcutUsageKey(DEFAULT_BASH_MODE_SETTINGS.toggleShortcut))
818
- ? null
819
- : DEFAULT_BASH_MODE_SETTINGS.toggleShortcut;
820
- const toggleShortcut = configuredToggleShortcut === null
821
- ? null
822
- : configuredToggleShortcut
823
- && !used.has(shortcutUsageKey(configuredToggleShortcut))
824
- ? configuredToggleShortcut
825
- : fallbackToggleShortcut;
826
-
827
- if (configuredToggleShortcut && toggleShortcut !== configuredToggleShortcut) {
828
- console.debug(
829
- `[powerline-footer] Bash mode shortcut conflict: "${configuredToggleShortcut}" replaced with "${toggleShortcut ?? "disabled"}"`,
830
- );
831
- }
832
- const completions = typeof raw.completions === "boolean"
833
- ? raw.completions
834
- : DEFAULT_BASH_MODE_SETTINGS.completions;
835
- const transcriptMaxLines = typeof raw.transcriptMaxLines === "number" && Number.isFinite(raw.transcriptMaxLines)
836
- ? Math.max(100, Math.floor(raw.transcriptMaxLines))
837
- : DEFAULT_BASH_MODE_SETTINGS.transcriptMaxLines;
838
- const transcriptMaxBytes = typeof raw.transcriptMaxBytes === "number" && Number.isFinite(raw.transcriptMaxBytes)
839
- ? Math.max(16 * 1024, Math.floor(raw.transcriptMaxBytes))
840
- : DEFAULT_BASH_MODE_SETTINGS.transcriptMaxBytes;
841
-
842
- return {
843
- toggleShortcut,
844
- completions,
845
- transcriptMaxLines,
846
- transcriptMaxBytes,
847
- };
848
- }
849
-
850
- const FAST_EDITOR_RENDER_LINE_THRESHOLD = 80;
851
- const FAST_EDITOR_RENDER_COLUMN_THRESHOLD = 1200;
852
-
853
- interface FastEditorState {
854
- lines: string[];
855
- cursorLine: number;
856
- cursorCol: number;
857
- }
858
-
859
- interface FastEditorVisualLine {
860
- text: string;
861
- cursorCol?: number;
862
- }
863
-
864
- function readFastEditorState(editor: unknown): FastEditorState | null {
865
- const state = Reflect.get(editor as object, "state");
866
- if (!isRecord(state) || !Array.isArray(state.lines)) return null;
867
- if (typeof state.cursorLine !== "number" || typeof state.cursorCol !== "number") return null;
868
-
869
- const lines = state.lines as string[];
870
- const cursorLine = Math.max(0, Math.min(lines.length - 1, Math.floor(state.cursorLine)));
871
- const cursorText = lines[cursorLine] ?? "";
872
- if (typeof cursorText !== "string") return null;
873
- const cursorCol = Math.max(0, Math.min(cursorText.length, Math.floor(state.cursorCol)));
874
- return { lines, cursorLine, cursorCol };
875
- }
876
-
877
- function fastChunkCount(line: string, width: number): number {
878
- return Math.max(1, Math.ceil(Math.max(1, line.length) / width));
879
- }
880
-
881
- function fastChunk(line: string, width: number, chunkIndex: number): { text: string; startCol: number } {
882
- const startCol = chunkIndex * width;
883
- return { text: line.slice(startCol, startCol + width), startCol };
884
- }
885
-
886
- function isFastRenderableText(text: string): boolean {
887
- return /^[\x20-\x7E]*$/.test(text);
888
- }
889
-
890
- function pushTrailingChunks(target: FastEditorVisualLine[], line: string, width: number, maxCount: number): void {
891
- const count = fastChunkCount(line, width);
892
- const start = Math.max(0, count - maxCount);
893
- for (let index = start; index < count; index++) {
894
- target.push({ text: fastChunk(line, width, index).text });
895
- }
896
- }
897
-
898
- function collectFastEditorVisualLines(state: FastEditorState, layoutWidth: number, maxVisibleLines: number): {
899
- lines: FastEditorVisualLine[];
900
- hasBefore: boolean;
901
- hasAfter: boolean;
902
- } {
903
- const cursorText = state.lines[state.cursorLine] ?? "";
904
- const cursorChunkIndex = Math.floor(state.cursorCol / layoutWidth);
905
- const cursorChunkCount = Math.max(fastChunkCount(cursorText, layoutWidth), cursorChunkIndex + 1);
906
- const firstCursorChunk = Math.max(0, cursorChunkIndex - maxVisibleLines + 1);
907
-
908
- const visualLines: FastEditorVisualLine[] = [];
909
- for (let lineIndex = state.cursorLine - 1; lineIndex >= 0 && visualLines.length < maxVisibleLines - 1; lineIndex--) {
910
- const chunks: FastEditorVisualLine[] = [];
911
- pushTrailingChunks(chunks, state.lines[lineIndex] ?? "", layoutWidth, maxVisibleLines - 1 - visualLines.length);
912
- visualLines.unshift(...chunks);
913
- }
914
-
915
- for (let chunkIndex = firstCursorChunk; chunkIndex < cursorChunkIndex && visualLines.length < maxVisibleLines - 1; chunkIndex++) {
916
- visualLines.push({ text: fastChunk(cursorText, layoutWidth, chunkIndex).text });
917
- }
918
-
919
- const cursorChunk = fastChunk(cursorText, layoutWidth, cursorChunkIndex);
920
- visualLines.push({
921
- text: cursorChunk.text,
922
- cursorCol: state.cursorCol - cursorChunk.startCol,
923
- });
924
-
925
- for (let chunkIndex = cursorChunkIndex + 1; chunkIndex < cursorChunkCount && visualLines.length < maxVisibleLines; chunkIndex++) {
926
- visualLines.push({ text: fastChunk(cursorText, layoutWidth, chunkIndex).text });
927
- }
928
-
929
- for (let lineIndex = state.cursorLine + 1; lineIndex < state.lines.length && visualLines.length < maxVisibleLines; lineIndex++) {
930
- const line = state.lines[lineIndex] ?? "";
931
- const count = fastChunkCount(line, layoutWidth);
932
- for (let chunkIndex = 0; chunkIndex < count && visualLines.length < maxVisibleLines; chunkIndex++) {
933
- visualLines.push({ text: fastChunk(line, layoutWidth, chunkIndex).text });
934
- }
935
- }
936
-
937
- return {
938
- lines: visualLines.slice(-maxVisibleLines),
939
- hasBefore: state.cursorLine > 0 || firstCursorChunk > 0,
940
- hasAfter: state.cursorLine < state.lines.length - 1 || cursorChunkIndex < cursorChunkCount - 1,
941
- };
942
- }
943
-
944
- function renderFastCursorLine(line: string, cursorCol: number, focused: boolean): string {
945
- const before = line.slice(0, cursorCol);
946
- const target = line[cursorCol];
947
- const marker = focused ? CURSOR_MARKER : "";
948
- if (target) {
949
- return `${before}${marker}\x1b[7m${target}\x1b[0m${line.slice(cursorCol + target.length)}`;
950
- }
951
- return `${before}${marker}\x1b[7m \x1b[0m`;
952
- }
953
-
954
- function padToWidth(line: string, width: number): string {
955
- return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
956
- }
957
-
958
- export function renderFastPowerlineEditor(
959
- editor: unknown,
960
- width: number,
961
- options: { bashModeActive: boolean; completionsEnabled: boolean },
962
- ): string[] | null {
963
- if (width < 10 || options.completionsEnabled) return null;
964
- if (Reflect.get(editor as object, "isInPaste") === true || Reflect.get(editor as object, "jumpMode") != null) return null;
965
- if (Reflect.get(editor as object, "autocompleteState") != null) return null;
966
-
967
- const isShowingAutocomplete = Reflect.get(editor as object, "isShowingAutocomplete");
968
- if (typeof isShowingAutocomplete === "function" && isShowingAutocomplete.call(editor)) return null;
969
-
970
- const state = readFastEditorState(editor);
971
- if (!state) return null;
972
-
973
- const cursorText = state.lines[state.cursorLine] ?? "";
974
- if (state.lines.length < FAST_EDITOR_RENDER_LINE_THRESHOLD && cursorText.length < FAST_EDITOR_RENDER_COLUMN_THRESHOLD) {
975
- return null;
976
- }
977
-
978
- const terminalRows = Reflect.get(Reflect.get(editor as object, "tui") ?? {}, "terminal")?.rows;
979
- const maxVisibleLines = Math.max(5, Math.floor((typeof terminalRows === "number" ? terminalRows : 24) * 0.3));
980
- const innerWidth = Math.max(1, width - 3);
981
- const layoutWidth = Math.max(1, innerWidth - 1);
982
- const viewport = collectFastEditorVisualLines(state, layoutWidth, maxVisibleLines);
983
- if (viewport.lines.some((line) => !isFastRenderableText(line.text))) return null;
984
-
985
- Reflect.set(editor as object, "lastWidth", layoutWidth);
986
-
987
- const bc = (s: string) =>
988
- typeof (editor as { borderColor?: unknown }).borderColor === "function"
989
- ? (editor as { borderColor: (s: string) => string }).borderColor(s)
990
- : `${getFgAnsiCode("sep")}${s}${ansi.reset}`;
991
- const border = (marker: "↑" | "↓" | "─") => {
992
- const text = marker === "─" ? "─".repeat(width - 2) : `${marker}${"─".repeat(Math.max(0, width - 3))}`;
993
- return ` ${bc(text)}`;
994
- };
995
- const promptGlyph = options.bashModeActive ? "$" : config.fixedEditorPromptGlyph;
996
- const prompt = promptGlyph ? `${ansi.getFgAnsi(200, 200, 200)}${promptGlyph}${ansi.reset}` : "";
997
- const promptPrefix = prompt ? ` ${prompt} ` : " ";
998
- const contPrefix = " ";
999
-
1000
- const lines = [border(viewport.hasBefore ? "↑" : "─")];
1001
- for (let index = 0; index < viewport.lines.length; index++) {
1002
- const visual = viewport.lines[index]!;
1003
- const content = visual.cursorCol === undefined
1004
- ? visual.text
1005
- : renderFastCursorLine(visual.text, visual.cursorCol, Reflect.get(editor as object, "focused") === true);
1006
- lines.push(`${index === 0 ? promptPrefix : contPrefix}${padToWidth(content, innerWidth)}`);
1007
- }
1008
- lines.push(border(viewport.hasAfter ? "↓" : "─"));
1009
- return lines;
1010
- }
1011
-
1012
- // ═══════════════════════════════════════════════════════════════════════════
1013
- // Status Line Builder
1014
- // ═══════════════════════════════════════════════════════════════════════════
1015
-
1016
- /** Render a single segment and return its content with width */
1017
- function renderSegmentWithWidth(
1018
- segId: StatusLineSegmentId,
1019
- ctx: SegmentContext
1020
- ): { content: string; width: number; visible: boolean } {
1021
- const rendered = renderSegment(segId, ctx);
1022
- if (!rendered.visible || !rendered.content) {
1023
- return { content: "", width: 0, visible: false };
1024
- }
1025
- return { content: rendered.content, width: visibleWidth(rendered.content), visible: true };
1026
- }
1027
-
1028
- /** Build content string from pre-rendered parts */
1029
- function buildContentFromParts(
1030
- parts: string[],
1031
- separatorStyle: StatusLineSeparatorStyle,
1032
- ): string {
1033
- if (parts.length === 0) return "";
1034
- const separatorDef = getSeparator(separatorStyle);
1035
- const sepAnsi = getFgAnsiCode("sep");
1036
- const sep = separatorDef.left;
1037
- return " " + parts.join(`${sepAnsi}${sep}${ansi.reset}`) + ansi.reset + " ";
1038
- }
1039
-
1040
- /**
1041
- * Responsive segment layout - fits segments into top bar, overflows to secondary row.
1042
- * When terminal is wide enough, secondary segments move up to top bar.
1043
- * When narrow, top bar segments overflow down to secondary row.
1044
- */
1045
- function computeResponsiveLayout(
1046
- ctx: SegmentContext,
1047
- presetDef: ReturnType<typeof getPreset>,
1048
- availableWidth: number
1049
- ): { topContent: string; secondaryContent: string } {
1050
- const separatorStyle = config.separator ?? presetDef.separator;
1051
- const separatorDef = getSeparator(separatorStyle);
1052
- const sepWidth = visibleWidth(separatorDef.left) + 2; // separator + spaces around it
1053
-
1054
- // Get all segments: primary first, then secondary
1055
- const mergedSegments = mergeSegmentsWithCustomItems(presetDef, config.customItems, {
1056
- layout: config.layout,
1057
- disabledSegments: config.disabledSegments,
1058
- });
1059
- const primaryIds = [...mergedSegments.leftSegments, ...mergedSegments.rightSegments];
1060
- const secondaryIds = mergedSegments.secondarySegments;
1061
- const allSegmentIds = [...primaryIds, ...secondaryIds];
1062
-
1063
- // Render all segments and get their widths
1064
- const renderedSegments: { content: string; width: number }[] = [];
1065
- for (const segId of allSegmentIds) {
1066
- const { content, width, visible } = renderSegmentWithWidth(segId, ctx);
1067
- if (visible) {
1068
- renderedSegments.push({ content, width });
1069
- }
1070
- }
1071
-
1072
- if (renderedSegments.length === 0) {
1073
- return { topContent: "", secondaryContent: "" };
1074
- }
1075
-
1076
- // Calculate how many segments fit in top bar
1077
- // Account for: leading space (1) + trailing space (1) = 2 chars overhead
1078
- const baseOverhead = 2;
1079
- let currentWidth = baseOverhead;
1080
- let topSegments: string[] = [];
1081
- let overflowSegments: { content: string; width: number }[] = [];
1082
- let overflow = false;
1083
-
1084
- for (const seg of renderedSegments) {
1085
- const neededWidth = seg.width + (topSegments.length > 0 ? sepWidth : 0);
1086
-
1087
- if (!overflow && currentWidth + neededWidth <= availableWidth) {
1088
- topSegments.push(seg.content);
1089
- currentWidth += neededWidth;
1090
- } else {
1091
- overflow = true;
1092
- overflowSegments.push(seg);
1093
- }
1094
- }
1095
-
1096
- // Fit overflow segments into secondary row (same width constraint)
1097
- // Stop at first non-fitting segment to preserve ordering
1098
- let secondaryWidth = baseOverhead;
1099
- let secondarySegments: string[] = [];
1100
-
1101
- for (const seg of overflowSegments) {
1102
- const neededWidth = seg.width + (secondarySegments.length > 0 ? sepWidth : 0);
1103
- if (secondaryWidth + neededWidth <= availableWidth) {
1104
- secondarySegments.push(seg.content);
1105
- secondaryWidth += neededWidth;
1106
- } else {
1107
- break;
1108
- }
1109
- }
1110
-
1111
- return {
1112
- topContent: buildContentFromParts(topSegments, separatorStyle),
1113
- secondaryContent: buildContentFromParts(secondarySegments, separatorStyle),
1114
- };
1115
- }
1116
-
1117
- // ═══════════════════════════════════════════════════════════════════════════
1118
- // Extension
1119
- // ═══════════════════════════════════════════════════════════════════════════
1120
-
1121
- function warnInvalidSegmentSettings(ctx: any): void {
1122
- if (config.invalidDisabledSegments.length > 0) {
1123
- const invalid = config.invalidDisabledSegments.map((id) => JSON.stringify(id)).join(", ");
1124
- const message = `Ignoring unknown powerline disabled segment${config.invalidDisabledSegments.length === 1 ? "" : "s"}: ${invalid}`;
1125
- console.warn(`[powerline-footer] ${message}`);
1126
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
1127
- }
1128
-
1129
- if (config.invalidLayoutSegments.length > 0) {
1130
- const invalid = config.invalidLayoutSegments.map((id) => JSON.stringify(id)).join(", ");
1131
- const message = `Ignoring unknown powerline layout segment${config.invalidLayoutSegments.length === 1 ? "" : "s"}: ${invalid}`;
1132
- console.warn(`[powerline-footer] ${message}`);
1133
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
1134
- }
1135
-
1136
- if (config.invalidPlacement !== null) {
1137
- const message = `Ignoring invalid powerline placement: ${JSON.stringify(config.invalidPlacement)}`;
1138
- console.warn(`[powerline-footer] ${message}`);
1139
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
1140
- }
1141
- }
1142
-
1143
- export default function powerlineFooter(pi: ExtensionAPI) {
1144
- const editorPerf = new EditorPerfProfiler(readEditorPerfOptions());
1145
- const startupSettings = readSettings();
1146
- config = parsePowerlineConfig(startupSettings.powerline, PRESET_NAMES);
1147
- let resolvedShortcuts = resolveShortcutConfig(startupSettings);
1148
- let bashModeSettings = parseBashModeSettings(startupSettings, resolvedShortcuts);
1149
-
1150
- let enabled = true;
1151
- let sessionStartTime = Date.now();
1152
- let sessionGeneration = 0;
1153
- let currentCtx: any = null;
1154
- let footerDataRef: ReadonlyFooterDataProvider | null = null;
1155
- let getThinkingLevelFn: (() => string) | null = null;
1156
- let currentThinkingLevel: string | null = null;
1157
- let liveAssistantUsage: SessionAssistantUsage | null = null;
1158
- const completedAsyncSubagentUsage = new Map<string, { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }>();
1159
- let approximateContextUsage: CoreContextUsage | null = null;
1160
- let isStreaming = false;
1161
- let tuiRef: any = null;
1162
- let restoreFooterStatusRepaintHook: (() => void) | null = null;
1163
- let stashShortcutInputUnsubscribe: (() => void) | null = null;
1164
- let dismissWelcomeOverlay: (() => void) | null = null;
1165
- let guideOverlayBlocking = false;
1166
- let welcomeHeaderActive = false;
1167
- let welcomeOverlayShouldDismiss = false;
1168
- let lastUserPrompt = "";
1169
- let showLastPrompt = true;
1170
- let lastPromptRenderCache: {
1171
- source: string;
1172
- compact: string;
1173
- width: number;
1174
- color: string;
1175
- lines: string[];
1176
- } | null = null;
1177
- let stashedEditorText: string | null = null;
1178
- let stashedPromptHistory: string[] = readPersistedStashHistory();
1179
- let currentEditor: any = null;
1180
- let bashModeActive = false;
1181
- let bashTranscript = new BashTranscriptStore(bashModeSettings);
1182
- let bashCompletionEngine = new BashCompletionEngine();
1183
- let shellSession: ManagedShellSession | null = null;
1184
- const queueStore = new PowerlineQueueStore();
1185
- let queueSummaryCache: {
1186
- cwd: string;
1187
- sessionId?: string;
1188
- compacting: boolean;
1189
- expiresAt: number;
1190
- summary: QueueSummary;
1191
- } | null = null;
1192
- let powerlineCompacting = false;
1193
- let deliverAfterRetrySettles = false;
1194
- let queueDeliveryTimer: ReturnType<typeof setTimeout> | null = null;
1195
-
1196
- // Cache for the top and secondary powerline widgets.
1197
- let lastLayoutWidth = 0;
1198
- let lastLayoutResult: { topContent: string; secondaryContent: string } | null = null;
1199
- let lastLayoutTimestamp = 0;
1200
- let layoutDirty = true;
1201
- let forceNextLayoutRecompute = false;
1202
- let lastEditorInputAt = 0;
1203
- let lastLayoutSessionName: string | undefined;
1204
-
1205
- // Cache for token counting: avoid re-scanning the full session event list
1206
- // on every render (250ms-1s cadence). Revalidates the trailing event's
1207
- // stats signature so in-place streaming updates are not served stale.
1208
- const sessionBranchCache = new SessionBranchCache();
1209
- const tokenStatsCache = new SessionTokenStatsCache();
1210
- const coreContextUsageCache = new CoreContextUsageCache();
1211
-
1212
- const getShellPath = () => process.env.SHELL || "/bin/sh";
1213
- const getShellCwd = () => shellSession?.state.cwd ?? currentCtx?.cwd ?? process.cwd();
1214
-
1215
- const statusRenderScheduler = createRenderScheduler(() => {
1216
- const msSinceInput = Date.now() - lastEditorInputAt;
1217
- if (layoutDirty && !forceNextLayoutRecompute && msSinceInput < EDITOR_STATUS_DEFER_MS) {
1218
- statusRenderScheduler.schedule(Math.max(0, EDITOR_STATUS_DEFER_MS - msSinceInput));
1219
- return;
1220
- }
1221
-
1222
- tuiRef?.requestRender();
1223
- }, STATUS_RENDER_DEBOUNCE_MS);
1224
-
1225
- const welcomeDismissScheduler = createWelcomeDismissScheduler({
1226
- dismiss: (ctx: unknown) => dismissWelcome(ctx),
1227
- getGeneration: () => sessionGeneration,
1228
- isEnabled: () => enabled,
1229
- });
1230
-
1231
- const resetLayoutCache = () => {
1232
- lastLayoutResult = null;
1233
- lastLayoutSessionName = undefined;
1234
- layoutDirty = true;
1235
- sessionBranchCache.reset();
1236
- tokenStatsCache.reset();
1237
- coreContextUsageCache.reset();
1238
- };
1239
-
1240
- const requestStatusRender = (delayMs?: number) => {
1241
- layoutDirty = true;
1242
- statusRenderScheduler.schedule(delayMs);
1243
- };
1244
-
1245
- const requestImmediateStatusRender = (options: { deferDuringTyping?: boolean } = {}) => {
1246
- layoutDirty = true;
1247
- if (options.deferDuringTyping !== false && Date.now() - lastEditorInputAt < EDITOR_STATUS_DEFER_MS) {
1248
- statusRenderScheduler.schedule();
1249
- return;
1250
- }
1251
-
1252
- forceNextLayoutRecompute = true;
1253
- statusRenderScheduler.cancel();
1254
- statusRenderScheduler.schedule(0);
1255
- };
1256
-
1257
- const installFooterStatusRepaintHook = (footerData: ReadonlyFooterDataProvider) => {
1258
- restoreFooterStatusRepaintHook?.();
1259
- restoreFooterStatusRepaintHook = null;
1260
-
1261
- const writableFooterData = footerData as ReadonlyFooterDataProvider & {
1262
- setExtensionStatus?: (key: string, text: string | undefined) => void;
1263
- clearExtensionStatuses?: () => void;
1264
- };
1265
- if (typeof writableFooterData.setExtensionStatus !== "function") return;
1266
-
1267
- const originalSetExtensionStatus = writableFooterData.setExtensionStatus;
1268
- const originalClearExtensionStatuses = writableFooterData.clearExtensionStatuses;
1269
- const setExtensionStatusAndRepaint = function setExtensionStatusAndRepaint(this: unknown, key: string, text: string | undefined) {
1270
- originalSetExtensionStatus.call(this, key, text);
1271
- requestImmediateStatusRender();
1272
- };
1273
- writableFooterData.setExtensionStatus = setExtensionStatusAndRepaint;
1274
-
1275
- let clearExtensionStatusesAndRepaint: (() => void) | null = null;
1276
- if (typeof originalClearExtensionStatuses === "function") {
1277
- clearExtensionStatusesAndRepaint = function clearExtensionStatusesAndRepaint(this: unknown) {
1278
- originalClearExtensionStatuses.call(this);
1279
- requestImmediateStatusRender();
1280
- };
1281
- writableFooterData.clearExtensionStatuses = clearExtensionStatusesAndRepaint;
1282
- }
1283
-
1284
- restoreFooterStatusRepaintHook = () => {
1285
- if (writableFooterData.setExtensionStatus === setExtensionStatusAndRepaint) {
1286
- writableFooterData.setExtensionStatus = originalSetExtensionStatus;
1287
- }
1288
- if (clearExtensionStatusesAndRepaint && writableFooterData.clearExtensionStatuses === clearExtensionStatusesAndRepaint) {
1289
- writableFooterData.clearExtensionStatuses = originalClearExtensionStatuses;
1290
- }
1291
- };
1292
- };
1293
-
1294
- const getShellHistoryEntries = (prefix: string): string[] => {
1295
- const project = matchHistoryEntries(
1296
- readProjectHistory(currentCtx?.cwd ?? process.cwd()).map((entry) => entry.command),
1297
- prefix,
1298
- 50,
1299
- );
1300
- const global = matchHistoryEntries(readGlobalShellHistory(getShellPath()), prefix, 50);
1301
- return [...new Set([...project, ...global])];
1302
- };
1303
-
1304
- const ensureShellSession = async (): Promise<ManagedShellSession> => {
1305
- if (!shellSession) {
1306
- shellSession = new ManagedShellSession(
1307
- getShellPath(),
1308
- currentCtx?.cwd ?? process.cwd(),
1309
- bashTranscript,
1310
- requestStatusRender,
1311
- (command, cwd) => appendProjectHistory(currentCtx?.cwd ?? process.cwd(), command, cwd),
1312
- );
1313
- }
1314
- await shellSession.ensureReady();
1315
- return shellSession;
1316
- };
1317
-
1318
- const runShellCommand = async (command: string, ctx: any): Promise<void> => {
1319
- try {
1320
- const session = await ensureShellSession();
1321
- await session.runCommand(command);
1322
- requestStatusRender();
1323
- } catch (error) {
1324
- const message = error instanceof Error ? error.message : String(error);
1325
- ctx.ui.notify(`Failed to run shell command: ${message}`, "error");
1326
- }
1327
- };
1328
-
1329
- const setBashModeActive = async (value: boolean, ctx: any): Promise<void> => {
1330
- if (value === bashModeActive) return;
1331
- if (!value && shellSession?.state.running) {
1332
- ctx.ui.notify("Wait for the current shell command to finish before leaving bash mode", "warning");
1333
- return;
1334
- }
1335
-
1336
- if (value) {
1337
- try {
1338
- const session = await ensureShellSession();
1339
- bashModeActive = true;
1340
- currentEditor?.dismissBashModeUi?.();
1341
- currentEditor?.refreshGhostSuggestion?.();
1342
- requestStatusRender();
1343
- ctx.ui.notify(`Bash mode enabled (${session.state.shellName})`, "info");
1344
- } catch (error) {
1345
- shellSession?.dispose();
1346
- shellSession = null;
1347
- bashModeActive = false;
1348
- requestStatusRender();
1349
- const message = error instanceof Error ? error.message : String(error);
1350
- ctx.ui.notify(`Failed to start shell session: ${message}`, "error");
1351
- }
1352
- return;
1353
- }
1354
-
1355
- bashModeActive = value;
1356
- currentEditor?.dismissBashModeUi?.();
1357
- requestStatusRender();
1358
- ctx.ui.notify("Bash mode disabled", "info");
1359
- };
1360
-
1361
- function overlaySelectListTheme(theme: Theme) {
1362
- return {
1363
- selectedPrefix: (text: string) => theme.fg("accent", text),
1364
- selectedText: (text: string) => theme.fg("accent", text),
1365
- description: (text: string) => theme.fg("muted", text),
1366
- scrollInfo: (text: string) => theme.fg("dim", text),
1367
- noMatch: (text: string) => theme.fg("warning", text),
1368
- };
1369
- }
1370
-
1371
- async function showSelectOverlay(
1372
- ctx: any,
1373
- title: string,
1374
- hint: string,
1375
- items: SelectItem[],
1376
- maxVisible: number,
1377
- ): Promise<SelectItem | null> {
1378
- return ctx.ui.custom(
1379
- (tui: any, theme: Theme, _keybindings: any, done: (result: SelectItem | null) => void) => {
1380
- const selectList = new SelectList(items, maxVisible, overlaySelectListTheme(theme));
1381
- const border = (text: string) => theme.fg("dim", text);
1382
- const wrapRow = (text: string, innerWidth: number): string => {
1383
- return `${border("│")}${truncateToWidth(text, innerWidth, "…", true)}${border("│")}`;
1384
- };
1385
-
1386
- selectList.onSelect = (item) => done(item);
1387
- selectList.onCancel = () => done(null);
1388
-
1389
- return {
1390
- render: (width: number) => {
1391
- const innerWidth = Math.max(1, width - 2);
1392
- const lines: string[] = [];
1393
-
1394
- lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
1395
- lines.push(wrapRow(theme.fg("accent", theme.bold(title)), innerWidth));
1396
- lines.push(border(`├${"─".repeat(innerWidth)}┤`));
1397
-
1398
- for (const line of selectList.render(innerWidth)) {
1399
- lines.push(wrapRow(line, innerWidth));
1400
- }
1401
-
1402
- lines.push(border(`├${"─".repeat(innerWidth)}┤`));
1403
- lines.push(wrapRow(theme.fg("dim", hint), innerWidth));
1404
- lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
1405
-
1406
- return lines;
1407
- },
1408
- invalidate: () => selectList.invalidate(),
1409
- handleInput: (data: string) => {
1410
- selectList.handleInput(data);
1411
- tui.requestRender();
1412
- },
1413
- };
1414
- },
1415
- {
1416
- overlay: true,
1417
- overlayOptions: () => ({
1418
- verticalAlign: "center",
1419
- horizontalAlign: "center",
1420
- }),
1421
- },
1422
- );
1423
- }
1424
-
1425
- function getQueueSessionId(ctx: any): string | undefined {
1426
- const sessionId = ctx.sessionManager?.getSessionId?.();
1427
- return typeof sessionId === "string" && sessionId.trim() ? sessionId : undefined;
1428
- }
1429
-
1430
- function getQueueContext(ctx: any): QueueContext {
1431
- return currentQueueContext(ctx.cwd ?? process.cwd(), getQueueSessionId(ctx));
1432
- }
1433
-
1434
- function getQueueSummary(ctx: any): QueueSummary {
1435
- const context = getQueueContext(ctx);
1436
- const now = Date.now();
1437
- if (
1438
- queueSummaryCache
1439
- && queueSummaryCache.cwd === context.cwd
1440
- && queueSummaryCache.sessionId === context.sessionId
1441
- && queueSummaryCache.compacting === powerlineCompacting
1442
- && now < queueSummaryCache.expiresAt
1443
- ) {
1444
- return queueSummaryCache.summary;
1445
- }
1446
-
1447
- const summary = queueStore.summarize(context, powerlineCompacting);
1448
- queueSummaryCache = {
1449
- cwd: context.cwd,
1450
- ...(context.sessionId ? { sessionId: context.sessionId } : {}),
1451
- compacting: powerlineCompacting,
1452
- expiresAt: now + QUEUE_SUMMARY_CACHE_TTL_MS,
1453
- summary,
1454
- };
1455
- return summary;
1456
- }
1457
-
1458
- function requestQueueRender(): void {
1459
- queueSummaryCache = null;
1460
- requestImmediateStatusRender({ deferDuringTyping: false });
1461
- }
1462
-
1463
- function queueItemLabel(item: PowerlineQueueItem): string {
1464
- const status = item.status === "queued" ? item.intent : `${item.intent}/${item.status}`;
1465
- return `${item.id} ${status} ${buildStashPreview(item.text, 56)}`;
1466
- }
1467
-
1468
- function queueItemDescription(item: PowerlineQueueItem): string {
1469
- if (item.target.kind === "global") return "global";
1470
- if (item.target.kind === "current-session") return "current session";
1471
- return item.target.alias ? `@${item.target.alias}` : item.target.cwd;
1472
- }
1473
-
1474
- function captureQueueItem(ctx: any, text: string, intent: QueueIntent, target: QueueTarget): PowerlineQueueItem {
1475
- const item = queueStore.add({
1476
- text,
1477
- intent,
1478
- target,
1479
- source: getQueueContext(ctx),
1480
- });
1481
- requestQueueRender();
1482
- return item;
1483
- }
1484
-
1485
- function capturePostCompactPrompt(ctx: any, text: string): PowerlineQueueItem | null {
1486
- const trimmed = text.trim();
1487
- if (!trimmed) return null;
1488
-
1489
- const item = captureQueueItem(ctx, trimmed, "post-compact", { kind: "current-session" });
1490
- ctx.ui.notify(`Queued for after compaction (${item.id})`, "info");
1491
- return item;
1492
- }
1493
-
1494
- function deliveryModeForItem(ctx: any, item: PowerlineQueueItem): "steer" | "followUp" | undefined {
1495
- const idle = typeof ctx.isIdle === "function" ? ctx.isIdle() : true;
1496
- if (idle) return undefined;
1497
- return item.intent === "steer" ? "steer" : "followUp";
1498
- }
1499
-
1500
- function deliverQueueItem(ctx: any, item: PowerlineQueueItem): boolean {
1501
- if (powerlineCompacting) {
1502
- queueStore.update(item.id, { status: "queued" });
1503
- requestQueueRender();
1504
- return false;
1505
- }
1506
-
1507
- queueStore.update(item.id, { status: "delivering", error: undefined });
1508
- requestQueueRender();
1509
-
1510
- let sent = false;
1511
- try {
1512
- const deliverAs = deliveryModeForItem(ctx, item);
1513
- const deliveryText = formatQueueDeliveryText(item);
1514
- if (deliverAs) {
1515
- pi.sendUserMessage(deliveryText, { deliverAs });
1516
- } else {
1517
- pi.sendUserMessage(deliveryText);
1518
- }
1519
- sent = true;
1520
- queueStore.update(item.id, { status: "sent", error: undefined });
1521
- try {
1522
- ctx.ui.notify(`Sent queued item ${item.id}`, "info");
1523
- } catch (error) {
1524
- if (!isStaleExtensionContextError(error)) throw error;
1525
- currentCtx = null;
1526
- }
1527
- requestQueueRender();
1528
- return true;
1529
- } catch (error) {
1530
- if (isStaleExtensionContextError(error)) {
1531
- if (!sent) queueStore.update(item.id, { status: "queued", error: undefined });
1532
- currentCtx = null;
1533
- throw error;
1534
- }
1535
- const message = error instanceof Error ? error.message : String(error);
1536
- queueStore.update(item.id, { status: "failed", error: message });
1537
- ctx.ui.notify(`Failed to send ${item.id}: ${message}`, "error");
1538
- requestQueueRender();
1539
- return false;
1540
- }
1541
- }
1542
-
1543
- function schedulePostCompactionDelivery(ctx: any): void {
1544
- if (queueDeliveryTimer) clearTimeout(queueDeliveryTimer);
1545
- const queueContext = getQueueContext(ctx);
1546
- const scheduledGeneration = sessionGeneration;
1547
- queueDeliveryTimer = setTimeout(() => {
1548
- queueDeliveryTimer = null;
1549
- if (scheduledGeneration !== sessionGeneration) return;
1550
- try {
1551
- const item = queueStore.queuedDeliveryItems(queueContext, "post-compact")[0];
1552
- if (item) deliverQueueItem(ctx, item);
1553
- } catch (error) {
1554
- if (!isStaleExtensionContextError(error)) throw error;
1555
- currentCtx = null;
1556
- }
1557
- }, 50);
1558
- }
1559
-
1560
- function blockPostCompactionQueue(ctx: any, errorMessage: string): void {
1561
- const items = queueStore.queuedDeliveryItems(getQueueContext(ctx), "post-compact");
1562
- for (const item of items) {
1563
- queueStore.update(item.id, { status: "blocked", error: errorMessage });
1564
- }
1565
- if (items.length > 0) {
1566
- ctx.ui.notify(`Kept ${items.length} post-compaction item${items.length === 1 ? "" : "s"} blocked`, "warning");
1567
- requestQueueRender();
1568
- }
1569
- }
1570
-
1571
- function finishFailedCompaction(ctx: any, errorMessage: string): void {
1572
- powerlineCompacting = false;
1573
- deliverAfterRetrySettles = false;
1574
- blockPostCompactionQueue(ctx, errorMessage);
1575
- requestQueueRender();
1576
- }
1577
-
1578
- async function chooseQueueAction(ctx: any, item: PowerlineQueueItem): Promise<void> {
1579
- const actions: SelectItem[] = [
1580
- { value: "send", label: "Send to current session", description: "Deliver as prompt/follow-up" },
1581
- { value: "edit", label: "Edit in prompt", description: "Move text into the editor" },
1582
- { value: "retry", label: "Retry", description: "Mark queued and deliver" },
1583
- { value: "clear", label: "Clear", description: "Mark item done" },
1584
- { value: "cancel", label: "Cancel" },
1585
- ];
1586
- const selected = await showSelectOverlay(ctx, `Queue item ${item.id}`, buildStashPreview(item.text, 72), actions, actions.length);
1587
- if (!selected || selected.value === "cancel") return;
1588
-
1589
- if (selected.value === "send") {
1590
- deliverQueueItem(ctx, item);
1591
- return;
1592
- }
1593
-
1594
- if (selected.value === "retry") {
1595
- const updated = queueStore.update(item.id, { status: "queued", error: undefined });
1596
- if (updated) deliverQueueItem(ctx, updated);
1597
- return;
1598
- }
1599
-
1600
- if (selected.value === "edit") {
1601
- ctx.ui.setEditorText(item.text);
1602
- queueStore.clear(item.id);
1603
- requestQueueRender();
1604
- return;
1605
- }
1606
-
1607
- if (selected.value === "clear") {
1608
- queueStore.clear(item.id);
1609
- ctx.ui.notify(`Cleared ${item.id}`, "info");
1610
- requestQueueRender();
1611
- }
1612
- }
1613
-
1614
- async function openQueuePicker(ctx: any): Promise<void> {
1615
- const active = queueStore.activeItems(getQueueContext(ctx));
1616
- if (active.length === 0) {
1617
- ctx.ui.notify("No queued items", "info");
1618
- return;
1619
- }
1620
-
1621
- const items: SelectItem[] = active.map((item) => ({
1622
- value: item.id,
1623
- label: queueItemLabel(item),
1624
- description: queueItemDescription(item),
1625
- }));
1626
- const selected = await showSelectOverlay(
1627
- ctx,
1628
- "Powerline queue",
1629
- "↑↓ navigate • enter manage • esc cancel",
1630
- items,
1631
- Math.min(active.length, 12),
1632
- );
1633
- if (!selected) return;
1634
-
1635
- const item = active.find((candidate) => candidate.id === selected.value);
1636
- if (item) await chooseQueueAction(ctx, item);
1637
- }
1638
-
1639
- function resolveCommandTarget(ctx: any, spec: string): QueueTarget {
1640
- const normalized = spec.trim().replace(/^@/, "");
1641
- if (normalized === "current") return { kind: "current-session" };
1642
- if (normalized === "global") return { kind: "global" };
1643
-
1644
- const cwd = queueStore.resolveAlias(normalized);
1645
- if (!cwd) throw new Error(`Unknown project alias @${normalized}. Use /queue alias ${normalized} <path> first.`);
1646
- return { kind: "project", cwd, alias: normalized };
1647
- }
1648
-
1649
- function sendOrRetryQueueItem(ctx: any, idPrefix: string): void {
1650
- const item = queueStore.get(idPrefix);
1651
- if (!item) {
1652
- ctx.ui.notify(`No unique queue item matches ${idPrefix}`, "warning");
1653
- return;
1654
- }
1655
- const updated = queueStore.update(item.id, { status: "queued", error: undefined });
1656
- if (updated) deliverQueueItem(ctx, updated);
1657
- }
1658
-
1659
- // Track session start
1660
- setupTpsTracker(pi);
1661
-
1662
- pi.on("session_start", async (event, ctx) => {
1663
- shellSession?.dispose();
1664
- shellSession = null;
1665
- sessionGeneration++;
1666
- sessionStartTime = Date.now();
1667
- currentCtx = ctx;
1668
- customCompactionEnabled = detectCustomCompactionEnabled(ctx.cwd);
1669
- lastUserPrompt = "";
1670
- isStreaming = false;
1671
- liveAssistantUsage = null;
1672
- completedAsyncSubagentUsage.clear();
1673
- approximateContextUsage = event.reason === "reload" ? estimateUnknownContextUsage(ctx) : null;
1674
- powerlineCompacting = false;
1675
- deliverAfterRetrySettles = false;
1676
- stashedEditorText = null;
1677
-
1678
- const settings = readSettings(ctx.cwd);
1679
- const guidePreferences = getGuidePreferences(settings);
1680
- const guideDisplayMode = resolveGuideDisplayMode({
1681
- reason: event.reason,
1682
- hasUI: ctx.hasUI,
1683
- guideMode: guidePreferences.mode,
1684
- });
1685
- const newGuideFeatures = getNewGuideFeatures(guidePreferences.lastSeenVersion, VERSION);
1686
- resolvedShortcuts = resolveShortcutConfig(settings);
1687
- bashModeSettings = parseBashModeSettings(settings, resolvedShortcuts);
1688
- showLastPrompt = settings.showLastPrompt !== false;
1689
- config = parsePowerlineConfig(settings.powerline, PRESET_NAMES);
1690
- warnInvalidSegmentSettings(ctx);
1691
- stashedPromptHistory = readPersistedStashHistory();
1692
- bashModeActive = false;
1693
- bashTranscript = new BashTranscriptStore(bashModeSettings);
1694
- bashCompletionEngine = new BashCompletionEngine();
1695
-
1696
- getThinkingLevelFn = () => ctx.thinkingLevel ?? "off";
1697
- currentThinkingLevel = getThinkingLevelFn();
1698
-
1699
- if (ctx.hasUI) {
1700
- ctx.ui.setStatus("stash", undefined);
1701
- }
1702
-
1703
- // Initialize vibe manager (needs modelRegistry from ctx)
1704
- initVibeManager(ctx);
1705
-
1706
- if (enabled && ctx.hasUI) {
1707
- setupCustomEditor(ctx);
1708
- if (guideDisplayMode === "full") {
1709
- setupWelcomeOverlay(ctx, newGuideFeatures);
1710
- } else if (guideDisplayMode === "compact") {
1711
- setupWelcomeHeader(ctx, newGuideFeatures);
1712
- } else if (shouldShowStartupWelcome(event.reason, config.welcome)) {
1713
- if (settings.quietStartup === true) {
1714
- setupWelcomeHeader(ctx, newGuideFeatures);
1715
- } else {
1716
- setupWelcomeOverlay(ctx, newGuideFeatures);
1717
- }
1718
- } else {
1719
- dismissWelcome(ctx);
1720
- }
1721
- }
1722
-
1723
- });
1724
-
1725
- pi.on("session_shutdown", async (_event, ctx) => {
1726
- sessionGeneration++;
1727
- completedAsyncSubagentUsage.clear();
1728
- dismissWelcomeOverlay?.();
1729
- dismissWelcomeOverlay = null;
1730
- guideOverlayBlocking = false;
1731
- welcomeHeaderActive = false;
1732
- welcomeOverlayShouldDismiss = false;
1733
- statusRenderScheduler.cancel();
1734
- restoreFooterStatusRepaintHook?.();
1735
- restoreFooterStatusRepaintHook = null;
1736
- stashShortcutInputUnsubscribe?.();
1737
- stashShortcutInputUnsubscribe = null;
1738
- shellSession?.dispose();
1739
- shellSession = null;
1740
- if (queueDeliveryTimer) {
1741
- clearTimeout(queueDeliveryTimer);
1742
- queueDeliveryTimer = null;
1743
- }
1744
- powerlineCompacting = false;
1745
- deliverAfterRetrySettles = false;
1746
- bashModeActive = false;
1747
- currentCtx = null;
1748
- footerDataRef = null;
1749
- getThinkingLevelFn = null;
1750
- currentThinkingLevel = null;
1751
- liveAssistantUsage = null;
1752
- tuiRef = null;
1753
- currentEditor = null;
1754
- resetLayoutCache();
1755
- });
1756
-
1757
- pi.events.on("subagent:async-complete", (event) => {
1758
- const completed = readAsyncSubagentUsage(event);
1759
- const sessionId = currentCtx?.sessionManager?.getSessionId?.();
1760
- if (!completed || !sessionId || completed.sessionId !== sessionId) return;
1761
-
1762
- completedAsyncSubagentUsage.set(completed.runId, completed.usage);
1763
- layoutDirty = true;
1764
- requestImmediateStatusRender({ deferDuringTyping: false });
1765
- });
1766
-
1767
- // Check if a bash command might change git branch
1768
- const mightChangeGitBranch = (cmd: string): boolean => {
1769
- const gitBranchPatterns = [
1770
- /\bgit\s+(checkout|switch|branch\s+-[dDmM]|merge|rebase|pull|reset|worktree)/,
1771
- /\bgit\s+stash\s+(pop|apply)/,
1772
- ];
1773
- return gitBranchPatterns.some(p => p.test(cmd));
1774
- };
1775
-
1776
- // Invalidate git status on file changes, trigger re-render on potential branch changes
1777
- pi.on("tool_result", async (event, ctx) => {
1778
- if (event.toolName === "subagent") {
1779
- currentCtx = ctx;
1780
- layoutDirty = true;
1781
- requestImmediateStatusRender({ deferDuringTyping: false });
1782
- }
1783
- if (event.toolName === "write" || event.toolName === "edit") {
1784
- invalidateGitStatus();
1785
- requestStatusRender();
1786
- }
1787
- // Check for bash commands that might change git branch
1788
- if (event.toolName === "bash" && event.input?.command) {
1789
- const cmd = String(event.input.command);
1790
- if (mightChangeGitBranch(cmd)) {
1791
- // The command has completed, so start refreshing immediately.
1792
- invalidateGitStatus();
1793
- invalidateGitBranch();
1794
- requestStatusRender();
1795
- }
1796
- }
1797
- });
1798
-
1799
- // Also catch user escape commands (! prefix)
1800
- // Note: This fires BEFORE execution, so we use a longer delay and multiple re-renders
1801
- // to ensure we catch the update after the command completes.
1802
- pi.on("user_bash", async (event) => {
1803
- if (mightChangeGitBranch(event.command)) {
1804
- // Invalidate immediately so next render fetches fresh data
1805
- invalidateGitStatus();
1806
- invalidateGitBranch();
1807
- // Multiple staggered re-renders to catch fast and slow commands
1808
- setTimeout(() => requestStatusRender(), 100);
1809
- setTimeout(() => requestStatusRender(), 300);
1810
- setTimeout(() => requestStatusRender(), 500);
1811
- }
1812
- });
1813
-
1814
- pi.on("model_select", async (_event, ctx) => {
1815
- currentCtx = ctx;
1816
- coreContextUsageCache.reset();
1817
- requestStatusRender();
1818
- });
1819
-
1820
- pi.on("thinking_level_select", async (event, ctx) => {
1821
- currentCtx = ctx;
1822
- currentThinkingLevel = getThinkingLevelFn?.() ?? (typeof event.level === "string" ? event.level : null);
1823
- requestImmediateStatusRender({ deferDuringTyping: false });
1824
- });
1825
-
1826
- pi.on("session_tree", async (_event, ctx) => {
1827
- currentCtx = ctx;
1828
- currentThinkingLevel = null;
1829
- liveAssistantUsage = null;
1830
- requestImmediateStatusRender({ deferDuringTyping: false });
1831
- });
1832
-
1833
- // Generate themed working message before agent starts (has access to user's prompt)
1834
- pi.on("before_agent_start", async (event, ctx) => {
1835
- lastUserPrompt = event.prompt;
1836
- if (ctx.hasUI) {
1837
- onVibeBeforeAgentStart(event.prompt, ctx.ui.setWorkingMessage);
1838
- }
1839
- });
1840
-
1841
- // Track streaming state (footer only shows status during streaming).
1842
- // A visible guide remains blocking until its own key handler dismisses it.
1843
- pi.on("agent_start", async (_event, ctx) => {
1844
- isStreaming = true;
1845
- liveAssistantUsage = null;
1846
- onVibeAgentStart();
1847
- currentCtx = ctx;
1848
- });
1849
-
1850
- pi.on("message_update", async (event, ctx) => {
1851
- if (isSessionAssistantMessage(event.message)
1852
- && event.message.stopReason !== "error"
1853
- && event.message.stopReason !== "aborted"
1854
- && getUsageTokenTotal(event.message.usage) > 0) {
1855
- liveAssistantUsage = event.message.usage;
1856
- currentCtx = ctx;
1857
- layoutDirty = true;
1858
- statusRenderScheduler.schedule(CONTEXT_STATUS_RENDER_MS);
1859
- }
1860
- });
1861
-
1862
- pi.on("message_end", async (event, ctx) => {
1863
- currentCtx = ctx;
1864
- coreContextUsageCache.reset();
1865
- if (isSessionAssistantMessage(event.message)) {
1866
- if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
1867
- liveAssistantUsage = null;
1868
- } else if (getUsageTokenTotal(event.message.usage) > 0) {
1869
- liveAssistantUsage = event.message.usage;
1870
- }
1871
- }
1872
- requestImmediateStatusRender({ deferDuringTyping: false });
1873
- });
1874
-
1875
- pi.on("turn_end", async (_event, ctx) => {
1876
- currentCtx = ctx;
1877
- coreContextUsageCache.reset();
1878
- requestImmediateStatusRender({ deferDuringTyping: false });
1879
- });
1880
-
1881
- pi.on("session_before_compact", async (_event, ctx) => {
1882
- powerlineCompacting = true;
1883
- currentCtx = ctx;
1884
- isStreaming = false;
1885
- liveAssistantUsage = null;
1886
- approximateContextUsage = null;
1887
- coreContextUsageCache.reset();
1888
- requestQueueRender();
1889
- });
1890
-
1891
- pi.on("session_compact", async (event, ctx) => {
1892
- powerlineCompacting = false;
1893
- currentCtx = ctx;
1894
- isStreaming = false;
1895
- liveAssistantUsage = null;
1896
- approximateContextUsage = estimateUnknownContextUsage(ctx);
1897
- coreContextUsageCache.reset();
1898
- if (event.willRetry) {
1899
- deliverAfterRetrySettles = true;
1900
- } else {
1901
- deliverAfterRetrySettles = false;
1902
- schedulePostCompactionDelivery(ctx);
1903
- }
1904
- requestQueueRender();
1905
- });
1906
-
1907
- pi.on("agent_settled", async (_event, ctx) => {
1908
- if (powerlineCompacting) {
1909
- finishFailedCompaction(ctx, "Compaction did not complete");
1910
- return;
1911
- }
1912
- if (deliverAfterRetrySettles) {
1913
- deliverAfterRetrySettles = false;
1914
- schedulePostCompactionDelivery(ctx);
1915
- }
1916
- });
1917
-
1918
- // Also dismiss on tool calls (agent is working) + refresh vibe if rate limit allows
1919
- pi.on("tool_call", async (event, ctx) => {
1920
- if (ctx.hasUI) {
1921
- // Extract recent agent context from session for richer vibe generation
1922
- const agentContext = getRecentAgentContext(ctx);
1923
- onVibeToolCall(event.toolName, event.input, ctx.ui.setWorkingMessage, agentContext);
1924
- }
1925
- });
1926
-
1927
- // Helper to extract recent agent response text (skipping thinking blocks)
1928
- function getRecentAgentContext(ctx: any): string | undefined {
1929
- const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
1930
-
1931
- // Find the most recent assistant message
1932
- for (let i = sessionEvents.length - 1; i >= 0; i--) {
1933
- const e = sessionEvents[i];
1934
- if (e.type === "message" && e.message?.role === "assistant") {
1935
- const content = e.message.content;
1936
- if (!Array.isArray(content)) continue;
1937
-
1938
- // Extract text content, skip thinking blocks
1939
- for (const block of content) {
1940
- if (block.type === "text" && block.text) {
1941
- // Return first ~200 chars of non-empty text
1942
- const text = block.text.trim();
1943
- if (text.length > 0) {
1944
- return text.slice(0, 200);
1945
- }
1946
- }
1947
- }
1948
- }
1949
- }
1950
- return undefined;
1951
- }
1952
-
1953
- function dismissWelcome(ctx: any) {
1954
- if (guideOverlayBlocking) return;
1955
-
1956
- if (dismissWelcomeOverlay) {
1957
- dismissWelcomeOverlay();
1958
- dismissWelcomeOverlay = null;
1959
- } else {
1960
- // The startup overlay mounts after a delay; dismiss it immediately if it appears later.
1961
- welcomeOverlayShouldDismiss = true;
1962
- }
1963
- if (welcomeHeaderActive) {
1964
- welcomeHeaderActive = false;
1965
- ctx.ui.setHeader(undefined);
1966
- }
1967
- }
1968
-
1969
- function scheduleDismissWelcome(ctx: any) {
1970
- if (!dismissWelcomeOverlay && welcomeOverlayShouldDismiss && !welcomeHeaderActive) return;
1971
- welcomeDismissScheduler.schedule(ctx);
1972
- }
1973
-
1974
- function closeWelcome(ctx: any): void {
1975
- dismissWelcomeOverlay?.();
1976
- dismissWelcomeOverlay = null;
1977
- guideOverlayBlocking = false;
1978
- if (welcomeHeaderActive) {
1979
- welcomeHeaderActive = false;
1980
- ctx.ui.setHeader(undefined);
1981
- }
1982
- welcomeOverlayShouldDismiss = false;
1983
- }
1984
-
1985
- function currentGuideFeatures(ctx: any): GuideFeature[] {
1986
- const preferences = getGuidePreferences(readSettings(ctx.cwd));
1987
- return getNewGuideFeatures(preferences.lastSeenVersion, VERSION);
1988
- }
1989
-
1990
- function addStashHistoryEntry(text: string): void {
1991
- const changed = pushStashHistory(stashedPromptHistory, text);
1992
- if (!changed) {
1993
- return;
1994
- }
1995
-
1996
- persistStashHistory(stashedPromptHistory);
1997
- }
1998
-
1999
- function copyTextToClipboard(ctx: any, text: string, successMessage?: string): void {
2000
- copyToClipboard(text);
2001
- if (successMessage) {
2002
- ctx.ui.notify(successMessage, "info");
2003
- }
2004
- }
2005
-
2006
- function getEditorTextForClipboard(ctx: any): string | null {
2007
- const text = getCurrentEditorText(ctx, currentEditor);
2008
- if (hasNonWhitespaceText(text)) {
2009
- return text;
2010
- }
2011
-
2012
- ctx.ui.notify("Editor is empty", "info");
2013
- return null;
2014
- }
2015
-
2016
- async function selectStashedPromptFromHistory(ctx: any): Promise<string | null> {
2017
- const historyItems = [...stashedPromptHistory];
2018
- const items: SelectItem[] = historyItems.map((entry, index) => ({
2019
- value: String(index),
2020
- label: `#${index + 1} ${buildStashPreview(entry, STASH_PREVIEW_WIDTH)}`,
2021
- }));
2022
-
2023
- const selected = await showSelectOverlay(
2024
- ctx, "Stash history", "↑↓ navigate • enter insert • esc cancel",
2025
- items, Math.min(items.length, 10));
2026
- if (!selected) return null;
2027
-
2028
- const i = Number.parseInt(selected.value, 10);
2029
- return historyItems[i] ?? null;
2030
- }
2031
-
2032
- async function selectProjectPromptFromHistory(ctx: any, prompts: string[]): Promise<string | null> {
2033
- const items: SelectItem[] = prompts.map((entry, index) => ({
2034
- value: String(index),
2035
- label: `#${index + 1} ${buildStashPreview(entry, STASH_PREVIEW_WIDTH)}`,
2036
- }));
2037
-
2038
- const selected = await showSelectOverlay(
2039
- ctx, "Recent project prompts", "↑↓ navigate • enter insert • esc cancel",
2040
- items, Math.min(items.length, 10));
2041
- if (!selected) return null;
2042
-
2043
- const i = Number.parseInt(selected.value, 10);
2044
- return prompts[i] ?? null;
2045
- }
2046
-
2047
- async function selectPromptHistorySource(
2048
- ctx: any,
2049
- stashCount: number,
2050
- projectPromptCount: number,
2051
- ): Promise<"stash" | "project" | null> {
2052
- const items: SelectItem[] = [];
2053
-
2054
- if (stashCount > 0) {
2055
- items.push({
2056
- value: "stash",
2057
- label: "Stashed prompts",
2058
- description: `${stashCount} saved`,
2059
- });
2060
- }
2061
-
2062
- if (projectPromptCount > 0) {
2063
- items.push({
2064
- value: "project",
2065
- label: "Recent project prompts",
2066
- description: `${projectPromptCount} recent`,
2067
- });
2068
- }
2069
-
2070
- if (items.length === 0) {
2071
- return null;
2072
- }
2073
-
2074
- if (items.length === 1) {
2075
- return items[0]?.value === "project" ? "project" : "stash";
2076
- }
2077
-
2078
- const selected = await showSelectOverlay(
2079
- ctx, "Prompt history", "↑↓ navigate • enter open • esc cancel",
2080
- items, items.length);
2081
- if (!selected) return null;
2082
-
2083
- return selected.value === "project" ? "project" : "stash";
2084
- }
2085
-
2086
- async function insertSelectedPromptHistoryEntry(ctx: any, selected: string): Promise<void> {
2087
- const currentText = getCurrentEditorText(ctx, currentEditor);
2088
- if (!hasNonWhitespaceText(currentText)) {
2089
- ctx.ui.setEditorText(selected);
2090
- ctx.ui.notify("Inserted prompt", "info");
2091
- return;
2092
- }
2093
-
2094
- const action = await ctx.ui.select("Insert prompt", ["Replace", "Append", "Cancel"]);
2095
-
2096
- if (action === "Replace") {
2097
- ctx.ui.setEditorText(selected);
2098
- ctx.ui.notify("Replaced editor with prompt", "info");
2099
- return;
2100
- }
2101
-
2102
- if (action === "Append") {
2103
- const separator = currentText.endsWith("\n") || selected.startsWith("\n") ? "" : "\n";
2104
- ctx.ui.setEditorText(`${currentText}${separator}${selected}`);
2105
- ctx.ui.notify("Appended prompt", "info");
2106
- }
2107
- }
2108
-
2109
- async function handleSelectedStashHistoryEntry(ctx: any, selected: string): Promise<void> {
2110
- const action = await ctx.ui.select("Stashed prompt", ["Insert", "Cancel"]);
2111
- if (action === "Insert") await insertSelectedPromptHistoryEntry(ctx, selected);
2112
- }
2113
-
2114
- function isStashShortcutInput(data: string): boolean {
2115
- return matchesStashShortcutInput(data, { includePrintableSharpS: config.stashSharpSShortcut });
2116
- }
2117
-
2118
- function isPromptHistoryShortcutInput(data: string): boolean {
2119
- return matchesConfiguredShortcut(data, resolvedShortcuts.stashHistory)
2120
- || (resolvedShortcuts.stashHistory === "ctrl+alt+h" && (
2121
- /^\x1b\[104(?::\d*)?(?::\d*)?;7(?::\d+)?u$/.test(data)
2122
- || data === "\x1b[27;7;104~"
2123
- || data === "\x1b[27;7;72~"
2124
- ));
2125
- }
2126
-
2127
- function getPowerlineShortcutAction(data: string): PowerlineShortcutAction | null {
2128
- if (isKeyRelease(data)) return null;
2129
-
2130
- if (isPromptHistoryShortcutInput(data)) {
2131
- return { kind: "stashHistory" };
2132
- }
2133
- if (matchesConfiguredShortcut(data, resolvedShortcuts.copyEditor)) {
2134
- return { kind: "copyEditor" };
2135
- }
2136
- if (matchesConfiguredShortcut(data, resolvedShortcuts.cutEditor)) {
2137
- return { kind: "cutEditor" };
2138
- }
2139
- if (matchesConfiguredShortcut(data, resolvedShortcuts.queueOpen)) {
2140
- return { kind: "queueOpen" };
2141
- }
2142
- if (matchesConfiguredShortcut(data, bashModeSettings.toggleShortcut)) {
2143
- return { kind: "bashMode" };
2144
- }
2145
-
2146
- return null;
2147
- }
2148
-
2149
- function runPowerlineShortcut(ctx: any, action: PowerlineShortcutAction): void {
2150
- if (action.kind === "stashHistory") {
2151
- void openStashHistory(ctx);
2152
- return;
2153
- }
2154
-
2155
- if (action.kind === "copyEditor" || action.kind === "cutEditor") {
2156
- const text = getEditorTextForClipboard(ctx);
2157
- if (!text) return;
2158
-
2159
- copyTextToClipboard(ctx, text, action.kind === "copyEditor" ? "Copied editor text" : undefined);
2160
- if (action.kind === "cutEditor") {
2161
- ctx.ui.setEditorText("");
2162
- ctx.ui.notify("Cut editor text", "info");
2163
- }
2164
- return;
2165
- }
2166
-
2167
- if (action.kind === "queueOpen") {
2168
- void openQueuePicker(ctx);
2169
- return;
2170
- }
2171
-
2172
- if (action.kind === "bashMode") {
2173
- void setBashModeActive(!bashModeActive, ctx);
2174
- return;
2175
- }
2176
- }
2177
-
2178
- function stashOrRestoreEditorText(ctx: any): void {
2179
- const rawText = getCurrentEditorText(ctx, currentEditor);
2180
- const hasStash = stashedEditorText !== null;
2181
-
2182
- if (!hasNonWhitespaceText(rawText)) {
2183
- if (!hasStash) {
2184
- ctx.ui.notify("Nothing to stash", "info");
2185
- return;
2186
- }
2187
-
2188
- ctx.ui.setEditorText(stashedEditorText);
2189
- stashedEditorText = null;
2190
- ctx.ui.setStatus("stash", undefined);
2191
- ctx.ui.notify("Stash restored", "info");
2192
- return;
2193
- }
2194
-
2195
- stashedEditorText = rawText;
2196
- addStashHistoryEntry(rawText);
2197
- ctx.ui.setEditorText("");
2198
- ctx.ui.setStatus("stash", "stash");
2199
- ctx.ui.notify(hasStash ? "Stash updated" : "Text stashed", "info");
2200
- }
2201
-
2202
- async function openStashHistory(ctx: any): Promise<void> {
2203
- let projectPrompts: string[] = [];
2204
-
2205
- try {
2206
- projectPrompts = readRecentProjectPrompts(ctx.cwd, PROJECT_PROMPT_HISTORY_LIMIT);
2207
- } catch (error) {
2208
- const message = error instanceof Error ? error.message : String(error);
2209
- ctx.ui.notify(`Failed to load project prompts: ${message}`, "warning");
2210
- }
2211
-
2212
- if (stashedPromptHistory.length === 0 && projectPrompts.length === 0) {
2213
- ctx.ui.notify("No prompt history yet", "info");
2214
- return;
2215
- }
2216
-
2217
- const source = await selectPromptHistorySource(ctx, stashedPromptHistory.length, projectPrompts.length);
2218
- if (!source) {
2219
- return;
2220
- }
2221
-
2222
- const selected = source === "project"
2223
- ? await selectProjectPromptFromHistory(ctx, projectPrompts)
2224
- : await selectStashedPromptFromHistory(ctx);
2225
- if (!selected) return;
2226
-
2227
- if (source === "stash") {
2228
- await handleSelectedStashHistoryEntry(ctx, selected);
2229
- return;
2230
- }
2231
-
2232
- await insertSelectedPromptHistoryEntry(ctx, selected);
2233
- }
2234
-
2235
- pi.on("agent_end", async (_event, ctx) => {
2236
- isStreaming = false;
2237
- liveAssistantUsage = null;
2238
- coreContextUsageCache.reset();
2239
-
2240
- let hasUI = false;
2241
- try {
2242
- hasUI = Boolean(ctx.hasUI);
2243
- } catch (error) {
2244
- if (!isStaleExtensionContextError(error)) throw error;
2245
- currentCtx = null;
2246
- return;
2247
- }
2248
-
2249
- currentCtx = ctx;
2250
- try {
2251
- if (hasUI) {
2252
- onVibeAgentEnd(ctx.ui.setWorkingMessage); // working-vibes internal state + reset message
2253
- if (stashedEditorText !== null) {
2254
- if (ctx.ui.getEditorText().trim() === "") {
2255
- ctx.ui.setEditorText(stashedEditorText);
2256
- stashedEditorText = null;
2257
- ctx.ui.setStatus("stash", undefined);
2258
- ctx.ui.notify("Stash restored", "info");
2259
- } else {
2260
- ctx.ui.notify("Stash preserved — clear editor then Alt+S to restore", "info");
2261
- }
2262
- }
2263
- }
2264
- } catch (error) {
2265
- if (!isStaleExtensionContextError(error)) throw error;
2266
- currentCtx = null;
2267
- return;
2268
- }
2269
-
2270
- requestStatusRender();
2271
- if (!powerlineCompacting && !deliverAfterRetrySettles) {
2272
- schedulePostCompactionDelivery(ctx);
2273
- }
2274
- });
2275
-
2276
- registerCdCommand(pi, () => currentCtx?.cwd ?? process.cwd());
2277
-
2278
- pi.registerCommand("queue", {
2279
- description: "Manage Powerline queued prompts and project aliases",
2280
- handler: async (args, ctx) => {
2281
- currentCtx = ctx;
2282
- const parts = args.trim().split(/\s+/).filter(Boolean);
2283
- const action = parts[0];
2284
-
2285
- if (!action) {
2286
- await openQueuePicker(ctx);
2287
- return;
2288
- }
2289
-
2290
- if (action === "alias") {
2291
- const alias = parts[1];
2292
- const aliasPath = parts.slice(2).join(" ") || ctx.cwd || process.cwd();
2293
- if (!alias) {
2294
- ctx.ui.notify("Usage: /queue alias <name> [path]", "info");
2295
- return;
2296
- }
2297
- try {
2298
- queueStore.setAlias(alias, aliasPath);
2299
- ctx.ui.notify(`Alias @${alias} → ${aliasPath}`, "info");
2300
- } catch (error) {
2301
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2302
- }
2303
- return;
2304
- }
2305
-
2306
- if (action === "send" || action === "retry") {
2307
- const id = parts[1];
2308
- if (!id) {
2309
- const item = queueStore.queuedDeliveryItems(getQueueContext(ctx))[0];
2310
- if (!item) {
2311
- ctx.ui.notify("No queued item to send", "info");
2312
- return;
2313
- }
2314
- sendOrRetryQueueItem(ctx, item.id);
2315
- return;
2316
- }
2317
- sendOrRetryQueueItem(ctx, id);
2318
- return;
2319
- }
2320
-
2321
- if (action === "clear") {
2322
- const id = parts[1];
2323
- if (id === "all") {
2324
- const active = queueStore.activeItems(getQueueContext(ctx));
2325
- for (const item of active) queueStore.clear(item.id);
2326
- ctx.ui.notify(`Cleared ${active.length} queued item${active.length === 1 ? "" : "s"}`, "info");
2327
- requestQueueRender();
2328
- return;
2329
- }
2330
- if (!id) {
2331
- ctx.ui.notify("Usage: /queue clear <id|all>", "info");
2332
- return;
2333
- }
2334
- const item = queueStore.get(id);
2335
- if (!item) {
2336
- ctx.ui.notify(`No unique queued item matches ${id}`, "warning");
2337
- return;
2338
- }
2339
- queueStore.clear(item.id);
2340
- ctx.ui.notify(`Cleared ${item.id}`, "info");
2341
- requestQueueRender();
2342
- return;
2343
- }
2344
-
2345
- if (action === "target") {
2346
- const id = parts[1];
2347
- const spec = parts[2];
2348
- if (!id || !spec) {
2349
- ctx.ui.notify("Usage: /queue target <id> @alias|global|current", "info");
2350
- return;
2351
- }
2352
- const item = queueStore.get(id);
2353
- if (!item) {
2354
- ctx.ui.notify(`No unique queue item matches ${id}`, "warning");
2355
- return;
2356
- }
2357
- try {
2358
- const target = resolveCommandTarget(ctx, spec);
2359
- queueStore.update(item.id, { target });
2360
- ctx.ui.notify(`Retargeted ${item.id}`, "info");
2361
- requestQueueRender();
2362
- } catch (error) {
2363
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2364
- }
2365
- return;
2366
- }
2367
-
2368
- ctx.ui.notify("Usage: /queue [send|retry|clear|target|alias]", "info");
2369
- },
2370
- });
2371
-
2372
- pi.registerCommand("guide", {
2373
- description: "Open the recurring Selesai feature guide",
2374
- handler: async (args, ctx) => {
2375
- if (!ctx.hasUI) return;
2376
-
2377
- const mode = args?.trim().toLowerCase() ?? "";
2378
- if (mode !== "" && !["full", "compact", "off", "reset"].includes(mode)) {
2379
- ctx.ui.notify("Usage: /guide [full|compact|off|reset]", "warning");
2380
- return;
2381
- }
2382
-
2383
- if (!enabled) {
2384
- ctx.ui.notify("Powerline is disabled; use /powerline first", "info");
2385
- return;
2386
- }
2387
-
2388
- if (mode === "off") {
2389
- const saved = saveGuidePreferences({ mode: "off" }, getSettingsPath());
2390
- closeWelcome(ctx);
2391
- ctx.ui.notify(saved ? "Guide hidden for future sessions" : "Guide hidden for this session; settings.json was not updated", saved ? "info" : "warning");
2392
- return;
2393
- }
2394
-
2395
- if (mode === "compact") {
2396
- const saved = saveGuidePreferences({ mode: "compact" }, getSettingsPath());
2397
- closeWelcome(ctx);
2398
- setupWelcomeHeader(ctx, currentGuideFeatures(ctx));
2399
- if (!saved) ctx.ui.notify("Compact guide enabled for this session; settings.json was not updated", "warning");
2400
- return;
2401
- }
2402
-
2403
- if (mode === "full" || mode === "reset") {
2404
- const saved = saveGuidePreferences({
2405
- mode: "full",
2406
- ...(mode === "reset" ? { lastSeenVersion: null } : {}),
2407
- }, getSettingsPath());
2408
- if (!saved) ctx.ui.notify("Guide opened, but settings.json was not updated", "warning");
2409
- }
2410
-
2411
- closeWelcome(ctx);
2412
- setupWelcomeOverlay(ctx, currentGuideFeatures(ctx));
2413
- },
2414
- });
2415
-
2416
- // Command to toggle/configure
2417
- pi.registerCommand("powerline", {
2418
- description: "Configure powerline status (toggle, preset)",
2419
- handler: async (args, ctx) => {
2420
- // Update context reference (command ctx may have more methods)
2421
- currentCtx = ctx;
2422
-
2423
- if (!args?.trim()) {
2424
- // Toggle
2425
- enabled = !enabled;
2426
- if (enabled) {
2427
- setupCustomEditor(ctx);
2428
- ctx.ui.notify("Powerline enabled", "info");
2429
- } else {
2430
- shellSession?.dispose();
2431
- shellSession = null;
2432
- bashTranscript.clear();
2433
- bashModeActive = false;
2434
- dismissWelcomeOverlay?.();
2435
- dismissWelcomeOverlay = null;
2436
- welcomeHeaderActive = false;
2437
- welcomeOverlayShouldDismiss = false;
2438
- getPromptHistoryState().savedPromptHistory = [];
2439
- stashedEditorText = null;
2440
- ctx.ui.setStatus("stash", undefined);
2441
- restoreFooterStatusRepaintHook?.();
2442
- restoreFooterStatusRepaintHook = null;
2443
- stashShortcutInputUnsubscribe?.();
2444
- stashShortcutInputUnsubscribe = null;
2445
- // Clear all custom UI components
2446
- ctx.ui.setEditorComponent(undefined);
2447
- ctx.ui.setFooter(undefined);
2448
- ctx.ui.setHeader(undefined);
2449
- ctx.ui.setWidget("powerline-top", undefined);
2450
- ctx.ui.setWidget("powerline-secondary", undefined);
2451
- ctx.ui.setWidget("powerline-bash-transcript", undefined);
2452
- ctx.ui.setWidget("powerline-status", undefined);
2453
- ctx.ui.setWidget("powerline-queue-preview", undefined);
2454
- ctx.ui.setWidget("powerline-last-prompt", undefined);
2455
- footerDataRef = null;
2456
- tuiRef = null;
2457
- currentEditor = null;
2458
- statusRenderScheduler.cancel();
2459
- resetLayoutCache();
2460
- ctx.ui.notify("Powerline disabled", "info");
2461
- }
2462
- return;
2463
- }
2464
-
2465
- const normalizedArgs = args.trim().toLowerCase();
2466
- const placementMatch = /^placement(?:\s+(above|below|toggle))?$/.exec(normalizedArgs);
2467
- if (placementMatch) {
2468
- const requestedPlacement = placementMatch[1];
2469
- config.placement = requestedPlacement === "above" || requestedPlacement === "below"
2470
- ? requestedPlacement
2471
- : config.placement === "above" ? "below" : "above";
2472
- config.invalidPlacement = null;
2473
- if (enabled && ctx.hasUI) setupCustomEditor(ctx);
2474
-
2475
- if (writePowerlineOptionSetting(ctx.cwd, { placement: config.placement }, config.preset)) {
2476
- ctx.ui.notify(`Powerline placement set to: ${config.placement}`, "info");
2477
- } else {
2478
- ctx.ui.notify(`Powerline placement set to: ${config.placement} (not persisted; check settings.json)`, "warning");
2479
- }
2480
- return;
2481
- }
2482
-
2483
- const preset = normalizePreset(args);
2484
- if (preset) {
2485
- config.preset = preset;
2486
- resetLayoutCache();
2487
- if (enabled) {
2488
- setupCustomEditor(ctx);
2489
- }
2490
-
2491
- if (writePowerlinePresetSetting(preset, ctx.cwd)) {
2492
- ctx.ui.notify(`Preset set to: ${preset}`, "info");
2493
- } else {
2494
- ctx.ui.notify(`Preset set to: ${preset} (not persisted; check settings.json)`, "warning");
2495
- }
2496
- return;
2497
- }
2498
-
2499
- // Show available presets
2500
- const presetList = Object.keys(PRESETS).join(", ");
2501
- ctx.ui.notify(`Available presets: ${presetList}`, "info");
2502
- },
2503
- });
2504
-
2505
- pi.registerCommand("stash-history", {
2506
- description: "Open prompt history picker",
2507
- handler: async (_args, ctx) => {
2508
- if (!ctx.hasUI) return;
2509
- if (!enabled) {
2510
- ctx.ui.notify("Powerline is disabled", "info");
2511
- return;
2512
- }
2513
-
2514
- await openStashHistory(ctx);
2515
- },
2516
- });
2517
-
2518
- pi.registerCommand("bash-mode", {
2519
- description: "Toggle sticky bash mode (on, off, toggle)",
2520
- handler: async (args, ctx) => {
2521
- const mode = args?.trim().toLowerCase() || "toggle";
2522
- if (mode === "on") {
2523
- await setBashModeActive(true, ctx);
2524
- return;
2525
- }
2526
- if (mode === "off") {
2527
- await setBashModeActive(false, ctx);
2528
- return;
2529
- }
2530
- if (mode === "toggle") {
2531
- await setBashModeActive(!bashModeActive, ctx);
2532
- return;
2533
- }
2534
- ctx.ui.notify("Usage: /bash-mode [on|off|toggle]", "warning");
2535
- },
2536
- });
2537
-
2538
- pi.registerCommand("powerline-perf", {
2539
- description: "Show or reset opt-in editor performance profiling",
2540
- handler: async (args, ctx) => {
2541
- if (!editorPerf.options.enabled) {
2542
- ctx.ui.notify("Set POWERLINE_DEBUG_PERF=1 and reload to enable editor profiling", "info");
2543
- return;
2544
- }
2545
- if (args.trim().toLowerCase() === "reset") {
2546
- editorPerf.reset();
2547
- ctx.ui.notify("Powerline editor performance counters reset", "info");
2548
- return;
2549
- }
2550
- ctx.ui.notify(editorPerf.report(), "info");
2551
- },
2552
- });
2553
-
2554
- pi.registerCommand("bash-reset", {
2555
- description: "Reset the managed bash session",
2556
- handler: async (_args, ctx) => {
2557
- shellSession?.dispose();
2558
- shellSession = null;
2559
- bashTranscript.clear();
2560
- if (bashModeActive) {
2561
- try {
2562
- await ensureShellSession();
2563
- } catch (error) {
2564
- bashModeActive = false;
2565
- const message = error instanceof Error ? error.message : String(error);
2566
- ctx.ui.notify(`Failed to restart shell session: ${message}`, "error");
2567
- requestStatusRender();
2568
- return;
2569
- }
2570
- }
2571
- requestStatusRender();
2572
- ctx.ui.notify("Bash session reset", "info");
2573
- },
2574
- });
2575
-
2576
-
2577
- // Command to set working message theme
2578
- pi.registerCommand("vibe", {
2579
- description: "Set working message theme. Usage: /vibe [theme|off|mode|model|generate]",
2580
- handler: async (args, ctx) => {
2581
- const parts = args?.trim().split(/\s+/) || [];
2582
- const subcommand = parts[0]?.toLowerCase();
2583
-
2584
- // No args: show current status
2585
- if (!args || !args.trim()) {
2586
- const theme = getVibeTheme();
2587
- const mode = getVibeMode();
2588
- const model = getVibeModel();
2589
- let status = `Vibe: ${theme || "off"} | Mode: ${mode} | Model: ${model}`;
2590
- if (theme && mode === "file") {
2591
- const count = getVibeFileCount(theme);
2592
- status += count > 0 ? ` | File: ${count} vibes` : " | File: not found";
2593
- }
2594
- ctx.ui.notify(status, "info");
2595
- return;
2596
- }
2597
-
2598
- // /vibe model [spec] - show or set model
2599
- if (subcommand === "model") {
2600
- const modelSpec = parts.slice(1).join(" ");
2601
- if (!modelSpec) {
2602
- ctx.ui.notify(`Current vibe model: ${getVibeModel()}`, "info");
2603
- return;
2604
- }
2605
- // Validate format (provider/modelId)
2606
- if (!modelSpec.includes("/")) {
2607
- ctx.ui.notify("Invalid model format. Use: provider/modelId (e.g., openai-codex/gpt-5.4-mini)", "error");
2608
- return;
2609
- }
2610
- const persisted = setVibeModel(modelSpec);
2611
- if (persisted) {
2612
- ctx.ui.notify(`Vibe model set to: ${modelSpec}`, "info");
2613
- } else {
2614
- ctx.ui.notify(`Vibe model set to: ${modelSpec} (not persisted; check settings.json)`, "warning");
2615
- }
2616
- return;
2617
- }
2618
-
2619
- // /vibe mode [generate|file] - show or set mode
2620
- if (subcommand === "mode") {
2621
- const newMode = parts[1]?.toLowerCase();
2622
- if (!newMode) {
2623
- ctx.ui.notify(`Current vibe mode: ${getVibeMode()}`, "info");
2624
- return;
2625
- }
2626
- if (newMode !== "generate" && newMode !== "file") {
2627
- ctx.ui.notify("Invalid mode. Use: generate or file", "error");
2628
- return;
2629
- }
2630
- // Check if file exists when switching to file mode
2631
- const theme = getVibeTheme();
2632
- if (newMode === "file" && theme && !hasVibeFile(theme)) {
2633
- ctx.ui.notify(`No vibe file for "${theme}". Run /vibe generate ${theme} first`, "error");
2634
- return;
2635
- }
2636
- const persisted = setVibeMode(newMode);
2637
- if (persisted) {
2638
- ctx.ui.notify(`Vibe mode set to: ${newMode}`, "info");
2639
- } else {
2640
- ctx.ui.notify(`Vibe mode set to: ${newMode} (not persisted; check settings.json)`, "warning");
2641
- }
2642
- return;
2643
- }
2644
-
2645
- // /vibe generate <theme> [count] - generate vibes and save to file
2646
- if (subcommand === "generate") {
2647
- const parsed = parseVibeGenerateArgs(parts.slice(1));
2648
- if (!parsed) {
2649
- ctx.ui.notify("Usage: /vibe generate <theme> [count]", "error");
2650
- return;
2651
- }
2652
-
2653
- const { theme, count } = parsed;
2654
- ctx.ui.notify(`Generating ${count} vibes for "${theme}"...`, "info");
2655
-
2656
- const result = await generateVibesBatch(theme, count);
2657
-
2658
- if (result.success) {
2659
- ctx.ui.notify(`Generated ${result.count} vibes for "${theme}" → ${result.filePath}`, "info");
2660
- } else {
2661
- ctx.ui.notify(`Failed to generate vibes: ${result.error}`, "error");
2662
- }
2663
- return;
2664
- }
2665
-
2666
- // /vibe off - disable
2667
- if (subcommand === "off") {
2668
- const persisted = setVibeTheme(null);
2669
- if (persisted) {
2670
- ctx.ui.notify("Vibe disabled", "info");
2671
- } else {
2672
- ctx.ui.notify("Vibe disabled (not persisted; check settings.json)", "warning");
2673
- }
2674
- return;
2675
- }
2676
-
2677
- // /vibe <theme> - set theme (preserve original casing)
2678
- const theme = args.trim();
2679
- const persisted = setVibeTheme(theme);
2680
- const mode = getVibeMode();
2681
- if (mode === "file" && !hasVibeFile(theme)) {
2682
- const suffix = persisted ? "" : " (not persisted; check settings.json)";
2683
- ctx.ui.notify(`Vibe set to: ${theme} (file mode, but no file found - run /vibe generate ${theme})${suffix}`, "warning");
2684
- } else if (persisted) {
2685
- ctx.ui.notify(`Vibe set to: ${theme}`, "info");
2686
- } else {
2687
- ctx.ui.notify(`Vibe set to: ${theme} (not persisted; check settings.json)`, "warning");
2688
- }
2689
- },
2690
- });
2691
-
2692
- function getCurrentSessionName(ctx: any): string | undefined {
2693
- const direct = pi.getSessionName?.() ?? ctx?.sessionManager?.getSessionName?.();
2694
- if (direct?.trim()) return direct.trim();
2695
-
2696
- const entries = ctx?.sessionManager?.getEntries?.() ?? ctx?.sessionManager?.getBranch?.() ?? [];
2697
- for (let i = entries.length - 1; i >= 0; i--) {
2698
- const entry = entries[i];
2699
- if (entry?.type === "session_info") {
2700
- return entry.name?.trim() || undefined;
2701
- }
2702
- }
2703
-
2704
- return undefined;
2705
- }
2706
-
2707
- function buildSegmentContext(ctx: any, theme: Theme): SegmentContext {
2708
- const presetDef = getPreset(config.preset);
2709
- const colors: ColorScheme = presetDef.colors ?? getDefaultColors();
2710
-
2711
- // Build usage stats and get thinking level from session (cached; the full
2712
- // event list is only re-scanned when events are appended or the trailing
2713
- // event's stats-relevant fields change, e.g. in-place streaming updates)
2714
- const sessionEvents = sessionBranchCache.get(ctx.sessionManager);
2715
- const tokenStats = tokenStatsCache.get(sessionEvents);
2716
- let { input, output, cacheRead, cacheWrite, cost, subagentCost } = tokenStats;
2717
- const lastAssistant = tokenStats.lastAssistant;
2718
- const thinkingLevelFromSession = tokenStats.thinkingLevelFromSession;
2719
-
2720
- for (const subagentUsage of completedAsyncSubagentUsage.values()) {
2721
- input += subagentUsage.input;
2722
- output += subagentUsage.output;
2723
- cacheRead += subagentUsage.cacheRead;
2724
- cacheWrite += subagentUsage.cacheWrite;
2725
- cost += subagentUsage.cost;
2726
- }
2727
-
2728
- // Calculate context percentage.
2729
- const latestUsage = isStreaming ? liveAssistantUsage ?? lastAssistant?.usage : lastAssistant?.usage;
2730
- const coreContextUsage = isStreaming && liveAssistantUsage ? null : coreContextUsageCache.get(ctx);
2731
- const fallbackContextTokens = latestUsage ? getUsageTokenTotal(latestUsage) : 0;
2732
- const {
2733
- contextTokens,
2734
- contextWindow,
2735
- contextPercent,
2736
- } = resolveDisplayContextUsage({
2737
- coreContextUsage,
2738
- unknownCoreFallback: approximateContextUsage,
2739
- fallbackContextTokens,
2740
- fallbackContextWindow: ctx.model?.contextWindow ?? 0,
2741
- });
2742
- const contextApproximate = coreContextUsage?.contextTokens === null && approximateContextUsage !== null;
2743
-
2744
- const segmentOptions = mergeSegmentOptions(presetDef.segmentOptions, config.segmentOptions);
2745
-
2746
- // Get git status (cached)
2747
- const gitBranch = footerDataRef?.getGitBranch() ?? null;
2748
- const gitStatus = getGitStatus(gitBranch, segmentOptions.git?.polling);
2749
- const extensionStatuses = footerDataRef?.getExtensionStatuses() ?? new Map();
2750
- const customItemsById = new Map(config.customItems.map((item) => [item.id, item]));
2751
- const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(config.customItems);
2752
-
2753
- // Check if using OAuth subscription
2754
- const usingSubscription = ctx.model
2755
- ? ctx.modelRegistry?.isUsingOAuth?.(ctx.model) ?? false
2756
- : false;
2757
-
2758
- const thinkingLevel = currentThinkingLevel ?? thinkingLevelFromSession ?? getThinkingLevelFn?.() ?? "off";
2759
- const queueSummary = getQueueSummary(ctx);
2760
-
2761
- return {
2762
- model: ctx.model,
2763
- thinkingLevel,
2764
- sessionId: ctx.sessionManager?.getSessionId?.(),
2765
- sessionName: getCurrentSessionName(ctx),
2766
- cwd: ctx.cwd,
2767
- usageStats: { input, output, cacheRead, cacheWrite, cost, subagentCost },
2768
- contextTokens,
2769
- contextPercent,
2770
- contextWindow,
2771
- contextApproximate,
2772
- autoCompactEnabled: ctx.settingsManager?.getCompactionSettings?.()?.enabled ?? true,
2773
- customCompactionEnabled: customCompactionEnabled || extensionStatuses.has(CUSTOM_COMPACTION_STATUS_KEY),
2774
- usingSubscription,
2775
- queueSummary,
2776
- sessionStartTime,
2777
- shellModeActive: bashModeActive,
2778
- shellRunning: shellSession?.state.running ?? false,
2779
- shellName: shellSession?.state.shellName ?? null,
2780
- shellCwd: shellSession?.state.cwd ?? null,
2781
- git: gitStatus,
2782
- extensionStatuses,
2783
- hiddenExtensionStatusKeys,
2784
- customItemsById,
2785
- options: segmentOptions,
2786
- theme,
2787
- colors,
2788
- };
2789
- }
2790
-
2791
- /**
2792
- * Get cached responsive layout or compute fresh one.
2793
- * The segment context scans session state, so keep it stable across render bursts.
2794
- */
2795
- function getResponsiveLayout(width: number, theme: Theme): { topContent: string; secondaryContent: string } {
2796
- const now = Date.now();
2797
- const cacheTtl = isStreaming ? STREAMING_LAYOUT_CACHE_TTL_MS : LAYOUT_CACHE_TTL_MS;
2798
-
2799
- const currentSessionName = getCurrentSessionName(currentCtx);
2800
- if (lastLayoutResult && lastLayoutWidth === width && currentSessionName === lastLayoutSessionName) {
2801
- const msSinceInput = now - lastEditorInputAt;
2802
- const typingRecently = msSinceInput < EDITOR_STATUS_DEFER_MS;
2803
-
2804
- if (!forceNextLayoutRecompute && typingRecently && (layoutDirty || now - lastLayoutTimestamp >= cacheTtl)) {
2805
- return lastLayoutResult;
2806
- }
2807
-
2808
- if (!layoutDirty && now - lastLayoutTimestamp < cacheTtl) {
2809
- return lastLayoutResult;
2810
- }
2811
- }
2812
-
2813
- const presetDef = getPreset(config.preset);
2814
- let segmentCtx: SegmentContext;
2815
- try {
2816
- segmentCtx = editorPerf.options.enabled
2817
- ? editorPerf.measure("layout.segment-context", () => buildSegmentContext(currentCtx, theme))
2818
- : buildSegmentContext(currentCtx, theme);
2819
- } catch (error) {
2820
- if (!isStaleExtensionContextError(error)) throw error;
2821
- currentCtx = null;
2822
- lastLayoutWidth = width;
2823
- lastLayoutResult = { topContent: "", secondaryContent: "" };
2824
- lastLayoutTimestamp = now;
2825
- layoutDirty = false;
2826
- forceNextLayoutRecompute = false;
2827
- return lastLayoutResult;
2828
- }
2829
-
2830
- lastLayoutWidth = width;
2831
- lastLayoutResult = computeResponsiveLayout(segmentCtx, presetDef, width);
2832
- lastLayoutSessionName = currentSessionName;
2833
- lastLayoutTimestamp = now;
2834
- layoutDirty = false;
2835
- forceNextLayoutRecompute = false;
2836
-
2837
- return lastLayoutResult;
2838
- }
2839
-
2840
- function renderPowerlineStatusLines(width: number): string[] {
2841
- if (!currentCtx || !footerDataRef) return [];
2842
-
2843
- const statuses = footerDataRef.getExtensionStatuses();
2844
- if (!statuses || statuses.size === 0) return [];
2845
- const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(config.customItems);
2846
-
2847
- const notifications: string[] = [];
2848
- for (const value of getNotificationExtensionStatuses(statuses, hiddenExtensionStatusKeys)) {
2849
- const lineContent = ` ${value}`;
2850
- if (visibleWidth(lineContent) <= width) {
2851
- notifications.push(lineContent);
2852
- }
2853
- }
2854
-
2855
- return notifications;
2856
- }
2857
-
2858
- function renderPowerlinePrimaryLines(width: number, theme: Theme): string[] {
2859
- if (!currentCtx) return [];
2860
-
2861
- const layout = getResponsiveLayout(width, theme);
2862
- return layout.topContent ? [layout.topContent] : [];
2863
- }
2864
-
2865
- function renderPowerlineSecondaryLines(width: number, theme: Theme): string[] {
2866
- if (!currentCtx) return [];
2867
-
2868
- const layout = getResponsiveLayout(width, theme);
2869
- return layout.secondaryContent ? [layout.secondaryContent] : [];
2870
- }
2871
-
2872
- function renderPowerlineQueuePreviewLines(width: number, theme: Theme): string[] {
2873
- if (!currentCtx) return [];
2874
- const summary = getQueueSummary(currentCtx);
2875
- if (!summary.leadingText) return [];
2876
-
2877
- const prefix = summary.leadingStatus === "blocked" || summary.leadingStatus === "failed"
2878
- ? "blocked: "
2879
- : summary.leadingStatus === "delivering"
2880
- ? "sending: "
2881
- : "queued: ";
2882
- const text = `${prefix}${summary.leadingText.replace(/\s+/g, " ").trim()}`;
2883
- const color = summary.leadingStatus === "blocked" || summary.leadingStatus === "failed" ? "warning" : "dim";
2884
- return [` ${theme.fg(color, truncateToWidth(text, Math.max(1, width - 1), "…"))}`];
2885
- }
2886
-
2887
- function renderBashTranscriptLines(width: number, theme: Theme): string[] {
2888
- if (!bashModeActive) return [];
2889
-
2890
- const snapshot = bashTranscript.getSnapshot();
2891
- if (snapshot.commands.length === 0) return [];
2892
-
2893
- const lines: string[] = [];
2894
- if (snapshot.truncatedCommands > 0) {
2895
- lines.push(` ${theme.fg("dim", `… ${snapshot.truncatedCommands} earlier command${snapshot.truncatedCommands === 1 ? "" : "s"} truncated`)}`);
2896
- }
2897
-
2898
- const recentCommands = snapshot.commands.slice(-4);
2899
- for (const command of recentCommands) {
2900
- const promptGlyph = (shellSession?.state.shellName ?? "shell") === "fish" ? ">" : "$";
2901
- const status = command.exitCode === null
2902
- ? theme.fg("accent", "running")
2903
- : command.exitCode === 0
2904
- ? theme.fg("success", "ok")
2905
- : theme.fg("error", `exit ${command.exitCode}`);
2906
- const commandLine = truncateToWidth(command.command.replace(/\s+/g, " ").trim(), Math.max(8, width - 8), "…");
2907
- lines.push(` ${theme.fg("accent", promptGlyph)} ${commandLine} ${theme.fg("dim", "(")}${status}${theme.fg("dim", ")")}`);
2908
-
2909
- const outputTail = command.output.slice(-6);
2910
- for (const outputLine of outputTail) {
2911
- lines.push(` ${truncateToWidth(outputLine, Math.max(1, width - 3), "…")}`);
2912
- }
2913
- }
2914
-
2915
- return lines.slice(-16);
2916
- }
2917
-
2918
- function renderLastPromptLines(width: number): string[] {
2919
- if (bashModeActive || !showLastPrompt || !lastUserPrompt) return [];
2920
-
2921
- const color = getFgAnsiCode("sep");
2922
- if (
2923
- lastPromptRenderCache
2924
- && lastPromptRenderCache.source === lastUserPrompt
2925
- && lastPromptRenderCache.width === width
2926
- && lastPromptRenderCache.color === color
2927
- ) {
2928
- return lastPromptRenderCache.lines;
2929
- }
2930
-
2931
- const compact = lastPromptRenderCache?.source === lastUserPrompt
2932
- ? lastPromptRenderCache.compact
2933
- : lastUserPrompt.replace(/\s+/g, " ").trim();
2934
- const prefix = ` ${color}↳${ansi.reset} `;
2935
- const availableWidth = width - visibleWidth(prefix);
2936
- const lines = compact && availableWidth >= 10
2937
- ? [truncateToWidth(`${prefix}${color}${truncateToWidth(compact, availableWidth, "…")}${ansi.reset}`, width, "…")]
2938
- : [];
2939
-
2940
- lastPromptRenderCache = { source: lastUserPrompt, compact, width, color, lines };
2941
- return lines;
2942
- }
2943
-
2944
- function installPowerlineWidgets(ctx: any) {
2945
- if (!editorPerf.options.widgets) return;
2946
-
2947
- const measureWidget = (name: string, render: () => string[]): string[] => {
2948
- return editorPerf.options.enabled ? editorPerf.measure(`widget.${name}`, render) : render();
2949
- };
2950
-
2951
- ctx.ui.setWidget("powerline-status", () => ({
2952
- dispose() {},
2953
- invalidate() {
2954
- requestStatusRender();
2955
- },
2956
- render(width: number): string[] {
2957
- return measureWidget("status", () => renderPowerlineStatusLines(width));
2958
- },
2959
- }), { placement: "aboveEditor" });
2960
-
2961
- ctx.ui.setWidget("powerline-top", (_tui: any, theme: Theme) => ({
2962
- dispose() {},
2963
- invalidate() {
2964
- resetLayoutCache();
2965
- },
2966
- render(width: number): string[] {
2967
- return measureWidget("primary", () => renderPowerlinePrimaryLines(width, theme));
2968
- },
2969
- }), { placement: config.placement === "below" ? "belowEditor" : "aboveEditor" });
2970
-
2971
- ctx.ui.setWidget("powerline-secondary", (_tui: any, theme: Theme) => ({
2972
- dispose() {},
2973
- invalidate() {
2974
- resetLayoutCache();
2975
- },
2976
- render(width: number): string[] {
2977
- return measureWidget("secondary", () => renderPowerlineSecondaryLines(width, theme));
2978
- },
2979
- }), { placement: "belowEditor" });
2980
-
2981
- if (editorPerf.options.bashWidgets) {
2982
- ctx.ui.setWidget("powerline-bash-transcript", (_tui: any, theme: Theme) => ({
2983
- dispose() {},
2984
- invalidate() {},
2985
- render(width: number): string[] {
2986
- return measureWidget("bash-transcript", () => renderBashTranscriptLines(width, theme));
2987
- },
2988
- }), { placement: "belowEditor" });
2989
- }
2990
-
2991
- ctx.ui.setWidget("powerline-queue-preview", (_tui: any, theme: Theme) => ({
2992
- dispose() {},
2993
- invalidate() {},
2994
- render(width: number): string[] {
2995
- return measureWidget("queue-preview", () => renderPowerlineQueuePreviewLines(width, theme));
2996
- },
2997
- }), { placement: "belowEditor" });
2998
-
2999
- if (editorPerf.options.lastPrompt) {
3000
- ctx.ui.setWidget("powerline-last-prompt", () => ({
3001
- dispose() {},
3002
- invalidate() {},
3003
- render(width: number): string[] {
3004
- return measureWidget("last-prompt", () => renderLastPromptLines(width));
3005
- },
3006
- }), { placement: "belowEditor" });
3007
- }
3008
- }
3009
-
3010
- function setupCustomEditor(ctx: any) {
3011
- snapshotPromptHistory(currentEditor);
3012
- if (!enabled) {
3013
- return;
3014
- }
3015
-
3016
- stashShortcutInputUnsubscribe?.();
3017
- stashShortcutInputUnsubscribe = typeof ctx.ui.onTerminalInput === "function"
3018
- ? ctx.ui.onTerminalInput((data: string) => {
3019
- if (!enabled || !ctx.hasUI || tuiRef?.hasOverlay?.()) {
3020
- return undefined;
3021
- }
3022
- if (isStashShortcutInput(data)) {
3023
- stashOrRestoreEditorText(ctx);
3024
- tuiRef?.requestRender();
3025
- return { consume: true };
3026
- }
3027
-
3028
- const powerlineShortcutAction = getPowerlineShortcutAction(data);
3029
- if (!powerlineShortcutAction) {
3030
- return undefined;
3031
- }
3032
-
3033
- runPowerlineShortcut(ctx, powerlineShortcutAction);
3034
- tuiRef?.requestRender();
3035
- return { consume: true };
3036
- })
3037
- : null;
3038
-
3039
- ctx.ui.setWidget("powerline-top", undefined);
3040
- ctx.ui.setWidget("powerline-secondary", undefined);
3041
- ctx.ui.setWidget("powerline-bash-transcript", undefined);
3042
- ctx.ui.setWidget("powerline-status", undefined);
3043
- ctx.ui.setWidget("powerline-queue-preview", undefined);
3044
- ctx.ui.setWidget("powerline-last-prompt", undefined);
3045
-
3046
- let autocompleteFixed = !bashModeSettings.completions;
3047
- const previousEditorFactory = typeof ctx.ui.getEditorComponent === "function" ? ctx.ui.getEditorComponent() : undefined;
3048
-
3049
- const editorFactory = (tui: any, editorTheme: any, keybindings: any) => {
3050
- const previousEditor = previousEditorFactory?.(tui, editorTheme, keybindings);
3051
- const editor = new BashModeEditor(tui, editorTheme, keybindings, {
3052
- keybindings,
3053
- isBashModeActive: () => bashModeActive,
3054
- isShellRunning: () => shellSession?.state.running ?? false,
3055
- onExitBashMode: () => {
3056
- void setBashModeActive(false, ctx);
3057
- },
3058
- onSubmitCommand: (command) => void runShellCommand(command, ctx),
3059
- editorBoundaryShortcuts: {
3060
- start: resolvedShortcuts.editorStart,
3061
- end: resolvedShortcuts.editorEnd,
3062
- },
3063
- onInterrupt: () => {
3064
- shellSession?.interrupt();
3065
- ctx.ui.notify("Sent interrupt to shell", "info");
3066
- },
3067
- onNotify: (message, level = "info") => ctx.ui.notify(message, level),
3068
- getHistoryEntries: (prefix) => getShellHistoryEntries(prefix),
3069
- areCompletionsEnabled: () => bashModeSettings.completions,
3070
- resolveGhostSuggestion: async (text, signal) => {
3071
- const oneOffBash = getOneOffBashCommandContext(text);
3072
- if (oneOffBash) {
3073
- const ghost = await bashCompletionEngine.getGhostSuggestion(
3074
- oneOffBash.command,
3075
- getShellCwd(),
3076
- getShellPath(),
3077
- signal,
3078
- );
3079
- return ghost ? { ...ghost, value: `${oneOffBash.prefix}${ghost.value}` } : null;
3080
- }
3081
-
3082
- return bashCompletionEngine.getGhostSuggestion(text, getShellCwd(), getShellPath(), signal);
3083
- },
3084
- });
3085
-
3086
- let installingPowerlineAutocompleteProvider = false;
3087
- const originalSetAutocompleteProvider = editor.setAutocompleteProvider.bind(editor);
3088
- editor.setAutocompleteProvider = (provider: AutocompleteProvider) => {
3089
- if (installingPowerlineAutocompleteProvider) {
3090
- originalSetAutocompleteProvider(provider);
3091
- return;
3092
- }
3093
-
3094
- originalSetAutocompleteProvider(passAutocompleteProviderThroughPreviousEditor(provider, previousEditor));
3095
- attachAutocompleteProvider();
3096
- };
3097
-
3098
- const getInstalledAutocompleteProvider = (): AutocompleteProvider | undefined => {
3099
- return getEditorAutocompleteProvider(editor) ?? getEditorAutocompleteProvider(previousEditor);
3100
- };
3101
-
3102
- const attachAutocompleteProvider = (): boolean => {
3103
- if (!bashModeSettings.completions) return false;
3104
- if (editor.hasWrappedProvider()) return true;
3105
- const defaultProvider = getInstalledAutocompleteProvider();
3106
- if (!defaultProvider) return false;
3107
-
3108
- const bashProvider = new BashAutocompleteProvider();
3109
- const oneOffBashProvider = new OneOffBashAutocompleteProvider();
3110
- installingPowerlineAutocompleteProvider = true;
3111
- try {
3112
- editor.installAutocompleteProvider(
3113
- new ModeAwareAutocompleteProvider(defaultProvider, bashProvider, oneOffBashProvider, () => bashModeActive),
3114
- );
3115
- } finally {
3116
- installingPowerlineAutocompleteProvider = false;
3117
- }
3118
- return true;
3119
- };
3120
-
3121
- currentEditor = editor;
3122
- trackPromptHistory(editor);
3123
- restorePromptHistory(editor);
3124
- attachAutocompleteProvider();
3125
-
3126
- const baseHandleInput = editor.handleInput.bind(editor);
3127
- const originalHandleInput = editorPerf.options.enabled
3128
- ? (data: string) => editorPerf.measure("input.base-editor", () => baseHandleInput(data))
3129
- : baseHandleInput;
3130
- const handlePowerlineEditorInput = (data: string) => {
3131
- lastEditorInputAt = Date.now();
3132
-
3133
- if (isPrintableInput(data)) {
3134
- scheduleDismissWelcome(ctx);
3135
- originalHandleInput(data);
3136
- return;
3137
- }
3138
-
3139
- const isSubmit = keybindings.matches(data, "tui.input.submit") && !keybindings.matches(data, "tui.input.newLine");
3140
- const isFollowUpSubmit = keybindings.matches(data, "app.message.followUp");
3141
- if (!powerlineCompacting && !bashModeActive && isSubmit && typeof ctx.compact === "function") {
3142
- const editorText = editor.getExpandedText().trim();
3143
- const compactQueuedPrompt = parseCompactQueuedPrompt(editorText);
3144
- if (editorText === "/compact" || compactQueuedPrompt) {
3145
- editor.addToHistory?.(editorText);
3146
- editor.setText("");
3147
- if (compactQueuedPrompt) {
3148
- capturePostCompactPrompt(ctx, compactQueuedPrompt);
3149
- }
3150
- powerlineCompacting = true;
3151
- deliverAfterRetrySettles = false;
3152
- requestQueueRender();
3153
- ctx.compact({
3154
- onError: (error: Error) => {
3155
- finishFailedCompaction(ctx, error.message);
3156
- ctx.ui.notify(error.message, "error");
3157
- },
3158
- });
3159
- scheduleDismissWelcome(ctx);
3160
- return;
3161
- }
3162
- }
3163
-
3164
- if (powerlineCompacting && !bashModeActive && (isSubmit || isFollowUpSubmit)) {
3165
- const text = editor.getExpandedText().trim();
3166
- if (!text) return;
3167
- if (text.startsWith("/")) {
3168
- originalHandleInput(data);
3169
- return;
3170
- }
3171
- editor.addToHistory?.(text);
3172
- editor.setText("");
3173
- capturePostCompactPrompt(ctx, text);
3174
- scheduleDismissWelcome(ctx);
3175
- return;
3176
- }
3177
-
3178
- if (isStashShortcutInput(data)) {
3179
- stashOrRestoreEditorText(ctx);
3180
- return;
3181
- }
3182
-
3183
- const powerlineShortcutAction = getPowerlineShortcutAction(data);
3184
- if (powerlineShortcutAction) {
3185
- runPowerlineShortcut(ctx, powerlineShortcutAction);
3186
- return;
3187
- }
3188
-
3189
- if (!autocompleteFixed && !getInstalledAutocompleteProvider()) {
3190
- autocompleteFixed = true;
3191
- snapshotPromptHistory(editor);
3192
- ctx.ui.setEditorComponent(editorFactory);
3193
- currentEditor?.handleInput(data);
3194
- return;
3195
- }
3196
-
3197
- if (bashModeSettings.completions) {
3198
- attachAutocompleteProvider();
3199
- }
3200
- scheduleDismissWelcome(ctx);
3201
- originalHandleInput(data);
3202
- };
3203
- editor.handleInput = editorPerf.options.enabled
3204
- ? (data: string) => {
3205
- editorPerf.measure("input.total", () => handlePowerlineEditorInput(data));
3206
- const state = Reflect.get(editor, "state");
3207
- const lines = state && typeof state === "object" ? Reflect.get(state, "lines") : null;
3208
- if (Array.isArray(lines)) editorPerf.observeDraft(lines);
3209
- }
3210
- : handlePowerlineEditorInput;
3211
-
3212
- const originalRender = editor.render.bind(editor);
3213
- editor.render = (width: number): string[] => {
3214
- const renderPowerlineEditor = (): string[] => {
3215
- if (!editorPerf.options.editorChrome) {
3216
- return editorPerf.options.enabled
3217
- ? editorPerf.measure("editor.render.base", () => originalRender(width))
3218
- : originalRender(width);
3219
- }
3220
-
3221
- if (editorPerf.options.fastRender) {
3222
- const fastLines = editorPerf.options.enabled
3223
- ? editorPerf.measure("editor.render.fast-probe", () => renderFastPowerlineEditor(editor, width, {
3224
- bashModeActive,
3225
- completionsEnabled: bashModeSettings.completions,
3226
- }))
3227
- : renderFastPowerlineEditor(editor, width, {
3228
- bashModeActive,
3229
- completionsEnabled: bashModeSettings.completions,
3230
- });
3231
- if (fastLines) {
3232
- if (editorPerf.options.enabled) editorPerf.count("editor.render.fast-hit");
3233
- return fastLines;
3234
- }
3235
- }
3236
-
3237
- if (width < 10) {
3238
- return editorPerf.options.enabled
3239
- ? editorPerf.measure("editor.render.base", () => originalRender(width))
3240
- : originalRender(width);
3241
- }
3242
-
3243
- const bc = (s: string) =>
3244
- typeof editor.borderColor === "function"
3245
- ? editor.borderColor(s)
3246
- : `${getFgAnsiCode("sep")}${s}${ansi.reset}`;
3247
- const promptGlyph = bashModeActive ? "$" : config.fixedEditorPromptGlyph;
3248
- const promptColor = ansi.getFgAnsi(200, 200, 200);
3249
- const prompt = promptGlyph ? `${promptColor}${promptGlyph}${ansi.reset}` : "";
3250
- const promptPrefix = prompt ? ` ${prompt} ` : " ";
3251
- const contPrefix = " ";
3252
- const contentWidth = Math.max(1, width - 1);
3253
- const lines = editorPerf.options.enabled
3254
- ? editorPerf.measure("editor.render.base", () => originalRender(contentWidth))
3255
- : originalRender(contentWidth);
3256
-
3257
- if (lines.length === 0) return lines;
3258
-
3259
- let bottomBorderIndex = lines.length - 1;
3260
- for (let i = lines.length - 1; i >= 1; i--) {
3261
- const stripped = lines[i]?.replace(/\x1b\[[0-9;]*m/g, "") || "";
3262
- if (stripped.length > 0 && /^─{3,}/.test(stripped)) {
3263
- bottomBorderIndex = i;
3264
- break;
3265
- }
3266
- }
3267
-
3268
- const result: string[] = [];
3269
- result.push(" " + bc("─".repeat(width - 2)));
3270
-
3271
- for (let i = 1; i < bottomBorderIndex; i++) {
3272
- const prefix = i === 1 ? promptPrefix : contPrefix;
3273
- result.push(`${prefix}${lines[i] || ""}`);
3274
- }
3275
-
3276
- if (bottomBorderIndex === 1) {
3277
- result.push(`${promptPrefix}${" ".repeat(contentWidth)}`);
3278
- }
3279
-
3280
- result.push(" " + bc("─".repeat(width - 2)));
3281
-
3282
- for (let i = bottomBorderIndex + 1; i < lines.length; i++) {
3283
- result.push(lines[i] || "");
3284
- }
3285
-
3286
- return result;
3287
- };
3288
-
3289
- return editorPerf.options.enabled
3290
- ? editorPerf.measure("editor.render.total", renderPowerlineEditor)
3291
- : renderPowerlineEditor();
3292
- };
3293
-
3294
- return editor;
3295
- };
3296
-
3297
- ctx.ui.setEditorComponent(editorFactory);
3298
-
3299
- ctx.ui.setFooter((tui: any, _theme: Theme, footerData: ReadonlyFooterDataProvider) => {
3300
- footerDataRef = footerData;
3301
- tuiRef = tui;
3302
- installFooterStatusRepaintHook(footerData);
3303
- const unsub = footerData.onBranchChange(() => requestStatusRender());
3304
- const unsubGitUpdates = subscribeGitUpdates(() => requestStatusRender());
3305
-
3306
- return {
3307
- dispose() {
3308
- unsub();
3309
- unsubGitUpdates();
3310
- restoreFooterStatusRepaintHook?.();
3311
- restoreFooterStatusRepaintHook = null;
3312
- },
3313
- invalidate() {
3314
- requestStatusRender();
3315
- },
3316
- render(): string[] {
3317
- return [""];
3318
- },
3319
- };
3320
- });
3321
-
3322
- installPowerlineWidgets(ctx);
3323
- }
3324
-
3325
- function setupWelcomeHeader(ctx: any, newFeatures: GuideFeature[] = []) {
3326
- const modelName = ctx.model?.name || ctx.model?.id || "No model";
3327
- const providerName = ctx.model?.provider || "Unknown";
3328
- const loadedCounts = discoverLoadedCounts();
3329
- const recentSessions = getRecentSessions(3);
3330
- const initialContextTokens = estimateInitialContextTokens(ctx);
3331
-
3332
- const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts, "compact", newFeatures, initialContextTokens);
3333
- welcomeHeaderActive = true;
3334
- markGuideVersionSeen(VERSION, getSettingsPath());
3335
-
3336
- ctx.ui.setHeader(() => {
3337
- return {
3338
- render(width: number): string[] {
3339
- return header.render(width);
3340
- },
3341
- invalidate() {
3342
- header.invalidate();
3343
- },
3344
- };
3345
- });
3346
- }
3347
-
3348
- function setupWelcomeOverlay(ctx: any, newFeatures: GuideFeature[] = []) {
3349
- const modelName = ctx.model?.name || ctx.model?.id || "No model";
3350
- const providerName = ctx.model?.provider || "Unknown";
3351
- const loadedCounts = discoverLoadedCounts();
3352
- const recentSessions = getRecentSessions(3);
3353
-
3354
- const overlaySessionGeneration = sessionGeneration;
3355
-
3356
- // Small delay to let pi-mono finish initialization
3357
- setTimeout(() => {
3358
- if (!enabled || welcomeOverlayShouldDismiss || isStreaming || overlaySessionGeneration !== sessionGeneration) {
3359
- welcomeOverlayShouldDismiss = false;
3360
- return;
3361
- }
3362
-
3363
- const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
3364
- const hasActivity = sessionEvents.some((entry: unknown) => {
3365
- if (!isRecord(entry)) return false;
3366
- if (entry.type === "tool_call" || entry.type === "tool_result") return true;
3367
- return entry.type === "message" && isRecord(entry.message) && entry.message.role === "assistant";
3368
- });
3369
- if (hasActivity) {
3370
- return;
3371
- }
3372
-
3373
- const initialContextTokens = estimateInitialContextTokens(ctx);
3374
-
3375
- ctx.ui.custom(
3376
- (_tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
3377
- const welcome = new WelcomeComponent(
3378
- modelName,
3379
- providerName,
3380
- recentSessions,
3381
- loadedCounts,
3382
- "full",
3383
- newFeatures,
3384
- initialContextTokens,
3385
- );
3386
- markGuideVersionSeen(VERSION, getSettingsPath());
3387
- guideOverlayBlocking = true;
3388
-
3389
- let dismissed = false;
3390
-
3391
- const dismiss = () => {
3392
- if (dismissed) return;
3393
- dismissed = true;
3394
- dismissWelcomeOverlay = null;
3395
- guideOverlayBlocking = false;
3396
- done();
3397
- };
3398
-
3399
- dismissWelcomeOverlay = dismiss;
3400
-
3401
- if (welcomeOverlayShouldDismiss) {
3402
- welcomeOverlayShouldDismiss = false;
3403
- dismiss();
3404
- }
3405
-
3406
- return {
3407
- focused: true,
3408
- invalidate: () => welcome.invalidate(),
3409
- render: (width: number) => welcome.render(width),
3410
- handleInput: () => dismiss(),
3411
- dispose: () => {
3412
- dismissed = true;
3413
- },
3414
- };
3415
- },
3416
- {
3417
- overlay: true,
3418
- overlayOptions: () => ({
3419
- verticalAlign: "center",
3420
- horizontalAlign: "center",
3421
- }),
3422
- },
3423
- ).catch((error: unknown) => {
3424
- console.debug("[powerline-footer] Welcome overlay failed:", error);
3425
- });
3426
- }, 100);
3427
- }
3428
- }