gloomberb 0.5.0 → 0.6.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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,36 +1,165 @@
1
+ import { Box, Input, ScrollBox, Text, TextAttributes } from "../../ui";
1
2
  import { useRef, useEffect, useState, useCallback } from "react";
2
- import { useKeyboard } from "@opentui/react";
3
- import { TextAttributes } from "@opentui/core";
4
- import type { TextareaRenderable, InputRenderable } from "@opentui/core";
3
+ import { useShortcut } from "../../react/input";
4
+ import { type InputRenderable, type TextareaRenderable } from "../../ui";
5
5
  import type { GloomPlugin, DetailTabProps, PaneProps } from "../../types/plugin";
6
6
  import { usePaneTicker } from "../../state/app-context";
7
7
  import { colors } from "../../theme/colors";
8
8
  import { MarkdownEditor } from "../../components/markdown-editor";
9
+ import { MarkdownText } from "../../components/markdown-text";
10
+ import { DialogFrame, Tabs, usePaneFooter } from "../../components";
11
+ import { type PromptContext, useDialog, useDialogKeyboard } from "../../ui/dialog";
9
12
  import { NotesFiles, type QuickNoteEntry } from "./notes-files";
10
13
 
14
+ function useSyncedText(initialValue = "") {
15
+ const [text, setTextState] = useState(initialValue);
16
+ const textRef = useRef(initialValue);
17
+ const setText = useCallback((nextText: string) => {
18
+ textRef.current = nextText;
19
+ setTextState(nextText);
20
+ }, []);
21
+ return { text, textRef, setText };
22
+ }
23
+
24
+ function noteContentWidth(width: number): number {
25
+ return Math.max(1, Math.floor(width) - 2);
26
+ }
27
+
28
+ function formatLastEdited(updatedAt: number | undefined): string {
29
+ if (!updatedAt) return "not edited";
30
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - updatedAt) / 1000));
31
+ if (!Number.isFinite(elapsedSeconds)) return "not edited";
32
+ if (elapsedSeconds < 60) return "now";
33
+
34
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
35
+ if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`;
36
+
37
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
38
+ if (elapsedHours < 24) return `${elapsedHours}h ago`;
39
+
40
+ const elapsedDays = Math.floor(elapsedHours / 24);
41
+ if (elapsedDays === 1) return "yday";
42
+ if (elapsedDays < 7) return `${elapsedDays}d ago`;
43
+
44
+ const elapsedWeeks = Math.floor(elapsedDays / 7);
45
+ if (elapsedWeeks < 5) return `${elapsedWeeks}w ago`;
46
+
47
+ const elapsedMonths = Math.floor(elapsedDays / 30);
48
+ if (elapsedMonths < 12) return `${elapsedMonths}mo ago`;
49
+
50
+ return `${Math.floor(elapsedDays / 365)}y ago`;
51
+ }
52
+
53
+ function MarkdownNotePreview({
54
+ text,
55
+ width,
56
+ placeholder,
57
+ onActivate,
58
+ }: {
59
+ text: string;
60
+ width: number;
61
+ placeholder: string;
62
+ onActivate: () => void;
63
+ }) {
64
+ const lineWidth = noteContentWidth(width);
65
+ const hasText = text.trim().length > 0;
66
+
67
+ return (
68
+ <ScrollBox
69
+ flexGrow={1}
70
+ width="100%"
71
+ height="100%"
72
+ scrollY
73
+ focusable={false}
74
+ onMouseDown={onActivate}
75
+ >
76
+ <Box flexDirection="column" flexGrow={1} width={lineWidth}>
77
+ {hasText
78
+ ? <MarkdownText text={text} lineWidth={lineWidth} />
79
+ : <Text fg={colors.textDim}>{placeholder}</Text>}
80
+ </Box>
81
+ </ScrollBox>
82
+ );
83
+ }
84
+
85
+ function ConfirmDeleteNoteDialog({
86
+ resolve,
87
+ title,
88
+ }: PromptContext<boolean> & {
89
+ title: string;
90
+ }) {
91
+ const confirm = useCallback(() => resolve(true), [resolve]);
92
+ const cancel = useCallback(() => resolve(false), [resolve]);
93
+ const displayTitle = title.length > 28 ? `${title.slice(0, 25)}...` : title;
94
+
95
+ useDialogKeyboard((event) => {
96
+ event.stopPropagation();
97
+ if (event.name === "enter" || event.name === "return") {
98
+ confirm();
99
+ return;
100
+ }
101
+ if (event.name === "escape") {
102
+ cancel();
103
+ }
104
+ });
105
+
106
+ return (
107
+ <DialogFrame title="Delete note?" footer="Enter delete · Esc cancel">
108
+ <Box flexDirection="column" width={44}>
109
+ <Text fg={colors.text}>{`Delete "${displayTitle}"?`}</Text>
110
+ <Box height={1} />
111
+ <Text fg={colors.textDim}>This note has content.</Text>
112
+ <Text fg={colors.textDim}>Deleting it cannot be undone.</Text>
113
+ <Box height={1} />
114
+ <Box flexDirection="row" gap={1}>
115
+ <Box
116
+ backgroundColor={colors.negative}
117
+ onMouseDown={confirm}
118
+ data-gloom-interactive="true"
119
+ >
120
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{" Delete "}</Text>
121
+ </Box>
122
+ <Box
123
+ backgroundColor={colors.panel}
124
+ onMouseDown={cancel}
125
+ data-gloom-interactive="true"
126
+ >
127
+ <Text fg={colors.text}>{" Cancel "}</Text>
128
+ </Box>
129
+ </Box>
130
+ </Box>
131
+ </DialogFrame>
132
+ );
133
+ }
134
+
11
135
  function createNotesTab(notesFiles: NotesFiles) {
12
- return function NotesTab({ focused, onCapture }: DetailTabProps) {
136
+ return function NotesTab({ focused, width, onCapture }: DetailTabProps) {
13
137
  const { ticker } = usePaneTicker();
14
138
  const textareaRef = useRef<TextareaRenderable | null>(null);
15
139
  const [notesFocused, setNotesFocused] = useState(false);
16
- const [loadedNotes, setLoadedNotes] = useState("");
140
+ const { text: noteText, textRef: noteTextRef, setText: setNoteText } = useSyncedText("");
141
+ const wasNotesFocusedRef = useRef(false);
17
142
 
18
143
  const setNotesFocusedAndCapture = useCallback((value: boolean) => {
19
144
  setNotesFocused(value);
20
145
  onCapture(value);
21
146
  }, [onCapture]);
22
147
 
148
+ const getCurrentNoteText = useCallback(() => (
149
+ textareaRef.current?.editBuffer.getText() ?? noteTextRef.current
150
+ ), [noteTextRef]);
151
+
23
152
  const saveNotesFor = useCallback((symbol: string | null, text: string) => {
24
153
  if (!symbol) return;
25
154
  notesFiles.save(symbol, text).catch(() => {});
26
155
  }, [notesFiles]);
27
156
 
28
157
  useEffect(() => {
29
- const textarea = textareaRef.current;
30
- if (!notesFocused && textarea && ticker?.metadata.ticker) {
31
- saveNotesFor(ticker.metadata.ticker, textarea.editBuffer.getText());
158
+ if (wasNotesFocusedRef.current && !notesFocused && ticker?.metadata.ticker) {
159
+ saveNotesFor(ticker.metadata.ticker, getCurrentNoteText());
32
160
  }
33
- }, [notesFocused, ticker, saveNotesFor]);
161
+ wasNotesFocusedRef.current = notesFocused;
162
+ }, [getCurrentNoteText, notesFocused, ticker, saveNotesFor]);
34
163
 
35
164
  const tickerSymbol = ticker?.metadata.ticker ?? null;
36
165
  const prevSymbolRef = useRef<string | null>(null);
@@ -42,22 +171,22 @@ function createNotesTab(notesFiles: NotesFiles) {
42
171
  prevSymbolRef.current = tickerSymbol;
43
172
 
44
173
  if (!tickerSymbol) {
45
- setLoadedNotes("");
174
+ setNoteText("");
46
175
  textareaRef.current?.setText("");
47
176
  return;
48
177
  }
49
178
 
50
179
  notesFiles.load(tickerSymbol).then((nextNotes) => {
51
- setLoadedNotes(nextNotes);
180
+ setNoteText(nextNotes);
52
181
  textareaRef.current?.setText(nextNotes);
53
182
  }).catch(() => {
54
- setLoadedNotes("");
183
+ setNoteText("");
55
184
  textareaRef.current?.setText("");
56
185
  });
57
186
  }
58
- }, [tickerSymbol, saveNotesFor, notesFiles]);
187
+ }, [tickerSymbol, saveNotesFor, notesFiles, setNoteText]);
59
188
 
60
- useKeyboard((event) => {
189
+ useShortcut((event) => {
61
190
  if (!focused) return;
62
191
  const isEnter = event.name === "enter" || event.name === "return";
63
192
  if (isEnter && !notesFocused) {
@@ -70,28 +199,36 @@ function createNotesTab(notesFiles: NotesFiles) {
70
199
  }
71
200
  });
72
201
 
73
- if (!ticker) return <text fg={colors.textDim}>Select a ticker to view notes.</text>;
202
+ usePaneFooter("ticker-notes", () => ({
203
+ info: [
204
+ { id: "mode", parts: [{ text: notesFocused ? "editing" : "viewing", tone: "muted" }] },
205
+ ],
206
+ }), [notesFocused]);
207
+
208
+ if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view notes.</Text>;
74
209
 
75
210
  return (
76
- <box flexDirection="column" padding={1} flexGrow={1}>
77
- <box flexDirection="row" height={1}>
78
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>Notes</text>
79
- <box flexGrow={1} />
80
- <text fg={colors.textMuted}>
81
- {notesFocused ? "editing (Esc to stop)" : "Enter to edit"}
82
- </text>
83
- </box>
84
- <box height={1} />
85
- <box flexGrow={1} onMouseDown={() => { if (!notesFocused) setNotesFocusedAndCapture(true); }}>
86
- <MarkdownEditor
87
- textareaKey={tickerSymbol ?? "none"}
88
- focused={notesFocused}
89
- initialValue={loadedNotes}
90
- placeholder="Write notes about this ticker..."
91
- onRef={(ref) => { textareaRef.current = ref; }}
92
- />
93
- </box>
94
- </box>
211
+ <Box flexDirection="column" flexGrow={1}>
212
+ <Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!notesFocused) setNotesFocusedAndCapture(true); }}>
213
+ {notesFocused ? (
214
+ <MarkdownEditor
215
+ textareaKey={tickerSymbol ?? "none"}
216
+ focused={notesFocused}
217
+ initialValue={noteText}
218
+ placeholder="Write notes about this ticker..."
219
+ onRef={(ref) => { textareaRef.current = ref; }}
220
+ onChange={setNoteText}
221
+ />
222
+ ) : (
223
+ <MarkdownNotePreview
224
+ text={noteText}
225
+ width={width}
226
+ placeholder="Write notes about this ticker..."
227
+ onActivate={() => setNotesFocusedAndCapture(true)}
228
+ />
229
+ )}
230
+ </Box>
231
+ </Box>
95
232
  );
96
233
  };
97
234
  }
@@ -102,16 +239,46 @@ function generateNoteId(): string {
102
239
  }
103
240
 
104
241
  function createQuickNotesPane(notesFiles: NotesFiles) {
105
- return function QuickNotesPane({ focused }: PaneProps) {
242
+ return function QuickNotesPane({ focused, width }: PaneProps) {
243
+ const dialog = useDialog();
106
244
  const textareaRef = useRef<TextareaRenderable | null>(null);
107
245
  const [editing, setEditing] = useState(false);
108
246
  const [tabs, setTabs] = useState<QuickNoteEntry[]>([]);
109
247
  const [activeTabId, setActiveTabId] = useState<string | null>(null);
110
248
  const [renaming, setRenaming] = useState(false);
111
249
  const [renameValue, setRenameValue] = useState("");
250
+ const { text: noteText, textRef: noteTextRef, setText: setNoteText } = useSyncedText("");
112
251
  const renameInputRef = useRef<InputRenderable>(null);
113
252
  const prevTabRef = useRef<string | null>(null);
253
+ const lastSavedTextRef = useRef<Map<string, string>>(new Map());
114
254
  const loadedRef = useRef(false);
255
+ const activeTab = tabs.find((tab) => tab.id === activeTabId);
256
+
257
+ const saveQuickNotesIndex = useCallback((entries: QuickNoteEntry[]) => {
258
+ notesFiles.saveQuickNotesIndex(entries).catch(() => {});
259
+ }, [notesFiles]);
260
+
261
+ const readActiveNoteText = useCallback(() => (
262
+ textareaRef.current?.editBuffer.getText() ?? noteTextRef.current
263
+ ), [noteTextRef]);
264
+
265
+ const saveTab = useCallback((tabId: string | null) => {
266
+ if (!tabId) return;
267
+ if (!lastSavedTextRef.current.has(tabId)) return;
268
+ const text = tabId === activeTabId ? readActiveNoteText() : noteTextRef.current;
269
+ if (lastSavedTextRef.current.get(tabId) === text) return;
270
+
271
+ lastSavedTextRef.current.set(tabId, text);
272
+ notesFiles.save(notesFiles.quickNoteKey(tabId), text).catch(() => {});
273
+
274
+ const updatedAt = Date.now();
275
+ setTabs((prev) => {
276
+ if (!prev.some((tab) => tab.id === tabId)) return prev;
277
+ const next = prev.map((tab) => (tab.id === tabId ? { ...tab, updatedAt } : tab));
278
+ saveQuickNotesIndex(next);
279
+ return next;
280
+ });
281
+ }, [activeTabId, noteTextRef, notesFiles, readActiveNoteText, saveQuickNotesIndex]);
115
282
 
116
283
  // Load index on mount
117
284
  useEffect(() => {
@@ -121,88 +288,129 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
121
288
  if (entries.length === 0) {
122
289
  const id = generateNoteId();
123
290
  const initial: QuickNoteEntry[] = [{ id, title: "New" }];
291
+ lastSavedTextRef.current.set(id, "");
124
292
  setTabs(initial);
125
293
  setActiveTabId(id);
126
- notesFiles.saveQuickNotesIndex(initial).catch(() => {});
294
+ saveQuickNotesIndex(initial);
127
295
  } else {
128
296
  setTabs(entries);
129
297
  setActiveTabId(entries[0]!.id);
130
298
  }
131
299
  });
132
- }, []);
133
-
134
- // Save current tab content before switching away
135
- const saveCurrentTab = useCallback(() => {
136
- const tabId = prevTabRef.current;
137
- if (tabId && textareaRef.current) {
138
- const text = textareaRef.current.editBuffer.getText();
139
- notesFiles.save(notesFiles.quickNoteKey(tabId), text).catch(() => {});
140
- }
141
- }, []);
300
+ }, [notesFiles, saveQuickNotesIndex]);
142
301
 
143
302
  // Load content when active tab changes
144
303
  useEffect(() => {
145
- if (!activeTabId) return;
146
- if (prevTabRef.current && prevTabRef.current !== activeTabId) {
147
- saveCurrentTab();
304
+ if (!activeTabId) {
305
+ setNoteText("");
306
+ return;
148
307
  }
149
308
  prevTabRef.current = activeTabId;
309
+ let cancelled = false;
150
310
  notesFiles.load(notesFiles.quickNoteKey(activeTabId)).then((text) => {
311
+ if (cancelled) return;
312
+ lastSavedTextRef.current.set(activeTabId, text);
313
+ setNoteText(text);
151
314
  textareaRef.current?.setText(text);
152
315
  }).catch(() => {
316
+ if (cancelled) return;
317
+ lastSavedTextRef.current.set(activeTabId, "");
318
+ setNoteText("");
153
319
  textareaRef.current?.setText("");
154
320
  });
155
- }, [activeTabId, saveCurrentTab]);
321
+ return () => {
322
+ cancelled = true;
323
+ };
324
+ }, [activeTabId, notesFiles, setNoteText]);
156
325
 
157
326
  // Save on editing stop
158
327
  useEffect(() => {
159
- if (!editing) saveCurrentTab();
160
- }, [editing, saveCurrentTab]);
328
+ if (!editing) saveTab(activeTabId);
329
+ }, [activeTabId, editing, saveTab]);
161
330
 
162
331
  const addTab = useCallback(() => {
163
- saveCurrentTab();
332
+ saveTab(activeTabId);
164
333
  const id = generateNoteId();
165
334
  const entry: QuickNoteEntry = { id, title: "New" };
335
+ lastSavedTextRef.current.set(id, "");
166
336
  setTabs((prev) => {
167
337
  const next = [...prev, entry];
168
- notesFiles.saveQuickNotesIndex(next).catch(() => {});
338
+ saveQuickNotesIndex(next);
169
339
  return next;
170
340
  });
171
341
  setActiveTabId(id);
342
+ setNoteText("");
172
343
  setEditing(false);
173
- }, [saveCurrentTab]);
344
+ setRenaming(false);
345
+ }, [activeTabId, saveQuickNotesIndex, saveTab, setNoteText]);
174
346
 
175
347
  const removeTab = useCallback((id: string) => {
348
+ lastSavedTextRef.current.delete(id);
176
349
  setTabs((prev) => {
177
350
  const next = prev.filter((t) => t.id !== id);
178
351
  if (next.length === 0) {
179
352
  const newId = generateNoteId();
180
353
  const fresh: QuickNoteEntry[] = [{ id: newId, title: "New" }];
181
- notesFiles.saveQuickNotesIndex(fresh).catch(() => {});
354
+ lastSavedTextRef.current.set(newId, "");
355
+ saveQuickNotesIndex(fresh);
182
356
  setActiveTabId(newId);
357
+ setNoteText("");
183
358
  prevTabRef.current = null;
184
359
  return fresh;
185
360
  }
186
- notesFiles.saveQuickNotesIndex(next).catch(() => {});
361
+ saveQuickNotesIndex(next);
187
362
  if (activeTabId === id) {
188
363
  const idx = prev.findIndex((t) => t.id === id);
189
364
  const newActive = next[Math.min(idx, next.length - 1)]!;
190
365
  setActiveTabId(newActive.id);
366
+ setNoteText("");
191
367
  prevTabRef.current = null;
192
368
  }
193
369
  return next;
194
370
  });
195
371
  notesFiles.delete(notesFiles.quickNoteKey(id)).catch(() => {});
196
372
  setEditing(false);
197
- }, [activeTabId]);
373
+ setRenaming(false);
374
+ }, [activeTabId, notesFiles, saveQuickNotesIndex, setNoteText]);
375
+
376
+ const requestRemoveTab = useCallback(async (id: string) => {
377
+ const tab = tabs.find((entry) => entry.id === id);
378
+ const text = id === activeTabId
379
+ ? readActiveNoteText()
380
+ : await notesFiles.load(notesFiles.quickNoteKey(id));
381
+
382
+ if (text.trim().length > 0) {
383
+ const confirmed = await dialog.prompt<boolean>({
384
+ closeOnClickOutside: true,
385
+ content: (ctx: PromptContext<boolean>) => (
386
+ <ConfirmDeleteNoteDialog
387
+ {...ctx}
388
+ title={tab?.title ?? "Note"}
389
+ />
390
+ ),
391
+ }).catch(() => false);
392
+ if (confirmed !== true) return;
393
+ }
394
+
395
+ removeTab(id);
396
+ }, [activeTabId, dialog, notesFiles, readActiveNoteText, removeTab, tabs]);
198
397
 
199
398
  const startRename = useCallback(() => {
200
- const tab = tabs.find((t) => t.id === activeTabId);
399
+ if (!activeTab) return;
400
+ setRenameValue(activeTab.title);
401
+ setRenaming(true);
402
+ setEditing(false);
403
+ }, [activeTab]);
404
+
405
+ const startRenameTab = useCallback((id: string) => {
406
+ const tab = tabs.find((entry) => entry.id === id);
201
407
  if (!tab) return;
408
+ if (id !== activeTabId) saveTab(activeTabId);
409
+ setActiveTabId(id);
202
410
  setRenameValue(tab.title);
203
411
  setRenaming(true);
204
412
  setEditing(false);
205
- }, [tabs, activeTabId]);
413
+ }, [activeTabId, saveTab, tabs]);
206
414
 
207
415
  const commitRename = useCallback(() => {
208
416
  const value = renameInputRef.current?.editBuffer.getText().trim() || renameValue.trim();
@@ -218,7 +426,7 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
218
426
  setRenaming(false);
219
427
  }, [activeTabId, renameValue]);
220
428
 
221
- useKeyboard((event) => {
429
+ useShortcut((event) => {
222
430
  if (!focused) return;
223
431
 
224
432
  if (renaming) {
@@ -243,12 +451,12 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
243
451
  return;
244
452
  }
245
453
  if (!editing) {
246
- if (event.name === "t") {
454
+ if (event.name === "n" || event.name === "t") {
247
455
  addTab();
248
456
  return;
249
457
  }
250
458
  if (event.name === "w" && tabs.length > 0) {
251
- if (activeTabId) removeTab(activeTabId);
459
+ if (activeTabId) void requestRemoveTab(activeTabId);
252
460
  return;
253
461
  }
254
462
  if (event.name === "r") {
@@ -262,70 +470,53 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
262
470
  const next = event.name === "]"
263
471
  ? (idx + 1) % tabs.length
264
472
  : (idx - 1 + tabs.length) % tabs.length;
265
- saveCurrentTab();
473
+ saveTab(activeTabId);
266
474
  setActiveTabId(tabs[next]!.id);
267
475
  return;
268
476
  }
269
477
  }
270
478
  });
271
479
 
480
+ usePaneFooter("quick-notes", () => ({
481
+ info: [
482
+ { id: "edited", parts: [{ text: editing || renaming ? "editing" : formatLastEdited(activeTab?.updatedAt), tone: "muted" }] },
483
+ ],
484
+ hints: editing || renaming
485
+ ? []
486
+ : [
487
+ { id: "new", key: "n", label: "ew", onPress: addTab },
488
+ { id: "rename", key: "r", label: "ename", onPress: startRename, disabled: !activeTabId },
489
+ ],
490
+ }), [activeTab, activeTabId, addTab, editing, renaming, startRename]);
491
+
272
492
  return (
273
- <box flexDirection="column" flexGrow={1}>
274
- {/* Tab bar */}
275
- <box flexDirection="row" height={1}>
276
- {tabs.map((tab) => {
277
- const isActive = tab.id === activeTabId;
278
- return (
279
- <box key={tab.id} flexDirection="row">
280
- <text
281
- fg={isActive ? colors.textBright : colors.textDim}
282
- bg={isActive ? colors.selected : undefined}
283
- attributes={isActive ? TextAttributes.BOLD : 0}
284
- onMouseDown={() => {
285
- if (tab.id !== activeTabId) {
286
- saveCurrentTab();
287
- setActiveTabId(tab.id);
288
- setEditing(false);
289
- }
290
- }}
291
- onDoubleClick={() => {
292
- setActiveTabId(tab.id);
293
- startRename();
294
- }}
295
- >
296
- {` ${tab.title} `}
297
- </text>
298
- {isActive && tabs.length > 1 ? (
299
- <text
300
- fg={colors.textMuted}
301
- onMouseDown={() => removeTab(tab.id)}
302
- >
303
- {`x `}
304
- </text>
305
- ) : <text>{` `}</text>}
306
- </box>
307
- );
308
- })}
309
- <text
310
- fg={colors.textMuted}
311
- onMouseDown={addTab}
312
- >
313
- {` + `}
314
- </text>
315
- <box flexGrow={1} />
316
- <text fg={colors.textMuted}>
317
- {renaming
318
- ? "type name, Enter to confirm"
319
- : editing
320
- ? "editing (Esc to stop)"
321
- : "Enter edit | t new | w close | r rename"}
322
- </text>
323
- </box>
493
+ <Box flexDirection="column" flexGrow={1}>
494
+ <Box height={1}>
495
+ <Tabs
496
+ tabs={tabs.map((tab) => ({
497
+ label: tab.title,
498
+ value: tab.id,
499
+ onClose: tabs.length > 1 ? (id) => { void requestRemoveTab(id); } : undefined,
500
+ onDoubleClick: startRenameTab,
501
+ }))}
502
+ activeValue={activeTabId}
503
+ onSelect={(id) => {
504
+ if (id === activeTabId) return;
505
+ saveTab(activeTabId);
506
+ setActiveTabId(id);
507
+ setEditing(false);
508
+ }}
509
+ compact
510
+ variant="pill"
511
+ closeMode="active"
512
+ onAdd={addTab}
513
+ />
514
+ </Box>
324
515
  {/* Rename input */}
325
516
  {renaming && (
326
- <box height={1} flexDirection="row" paddingLeft={1}>
327
- <text fg={colors.textDim}>{"Rename: "}</text>
328
- <input
517
+ <Box height={1} flexDirection="row" paddingLeft={1}>
518
+ <Text fg={colors.textDim}>{"Rename: "}</Text>
519
+ <Input
329
520
  ref={renameInputRef}
330
521
  initialValue={renameValue}
331
522
  focused={renaming}
@@ -334,18 +525,28 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
334
525
  flexGrow={1}
335
526
  onChange={(val: string) => setRenameValue(val)}
336
527
  />
337
- </box>
528
+ </Box>
338
529
  )}
339
- {/* Editor */}
340
- <box flexGrow={1} padding={1} onMouseDown={() => { if (!editing && !renaming) setEditing(true); }}>
341
- <MarkdownEditor
342
- textareaKey={activeTabId ?? "none"}
343
- focused={editing && !renaming}
344
- placeholder="Write notes..."
345
- onRef={(ref) => { textareaRef.current = ref; }}
346
- />
347
- </box>
348
- </box>
530
+ <Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!editing && !renaming) setEditing(true); }}>
531
+ {editing && !renaming ? (
532
+ <MarkdownEditor
533
+ textareaKey={activeTabId ?? "none"}
534
+ focused
535
+ initialValue={noteText}
536
+ placeholder="Write notes..."
537
+ onRef={(ref) => { textareaRef.current = ref; }}
538
+ onChange={setNoteText}
539
+ />
540
+ ) : (
541
+ <MarkdownNotePreview
542
+ text={noteText}
543
+ width={width}
544
+ placeholder="Write notes..."
545
+ onActivate={() => { if (!renaming) setEditing(true); }}
546
+ />
547
+ )}
548
+ </Box>
549
+ </Box>
349
550
  );
350
551
  };
351
552
  }
@@ -375,7 +576,7 @@ export const notesPlugin: GloomPlugin = {
375
576
 
376
577
  ctx.registerPane({
377
578
  id: "quick-notes",
378
- name: "Quick Notes",
579
+ name: "Notes",
379
580
  icon: "N",
380
581
  component: QuickNotesPane,
381
582
  defaultPosition: "right",
@@ -386,7 +587,7 @@ export const notesPlugin: GloomPlugin = {
386
587
  ctx.registerPaneTemplate({
387
588
  id: "new-quick-notes-pane",
388
589
  paneId: "quick-notes",
389
- label: "Quick Notes",
590
+ label: "Notes",
390
591
  description: "Open a general-purpose notes scratchpad",
391
592
  keywords: ["notes", "quick", "scratchpad", "memo"],
392
593
  shortcut: { prefix: "NOTE" },