gloomberb 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1330 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1524 -0
  45. package/src/components/command-bar/command-bar.tsx +4823 -0
  46. package/src/components/command-bar/command-registry.ts +261 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +129 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +625 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  70. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  71. package/src/components/pane-settings-dialog.tsx +468 -0
  72. package/src/components/pane-template-wizard.test.tsx +52 -0
  73. package/src/components/pane-template-wizard.tsx +199 -0
  74. package/src/components/price-selector-dialog.tsx +195 -0
  75. package/src/components/spinner.tsx +2 -0
  76. package/src/components/tab-bar.tsx +17 -0
  77. package/src/components/ticker-badge-text.test.tsx +160 -0
  78. package/src/components/ticker-badge-text.tsx +58 -0
  79. package/src/components/ticker-badge.tsx +88 -0
  80. package/src/components/ticker-list-table.tsx +188 -0
  81. package/src/components/toggle-list.tsx +74 -0
  82. package/src/components/ui/button.tsx +77 -0
  83. package/src/components/ui/data-table.tsx +210 -0
  84. package/src/components/ui/external-link.tsx +22 -0
  85. package/src/components/ui/fields.tsx +187 -0
  86. package/src/components/ui/frame.tsx +73 -0
  87. package/src/components/ui/index.ts +53 -0
  88. package/src/components/ui/list-view.tsx +166 -0
  89. package/src/components/ui/loading.tsx +70 -0
  90. package/src/components/ui/status.tsx +83 -0
  91. package/src/components/ui/tabs.tsx +47 -0
  92. package/src/components/ui/toggle.tsx +164 -0
  93. package/src/components/ui/ui.test.tsx +134 -0
  94. package/src/data/app-persistence.ts +25 -0
  95. package/src/data/config-store.test.ts +310 -0
  96. package/src/data/config-store.ts +469 -0
  97. package/src/data/plugin-state-store.ts +102 -0
  98. package/src/data/resource-store.ts +323 -0
  99. package/src/data/session-store.ts +52 -0
  100. package/src/data/sqlite-cache.test.ts +103 -0
  101. package/src/data/sqlite-database.ts +62 -0
  102. package/src/data/sqlite-json.ts +19 -0
  103. package/src/data/ticker-repository.ts +45 -0
  104. package/src/data/ticker-store.ts +46 -0
  105. package/src/index.tsx +67 -0
  106. package/src/market-data/coordinator.test.ts +369 -0
  107. package/src/market-data/coordinator.ts +580 -0
  108. package/src/market-data/hooks.tsx +234 -0
  109. package/src/market-data/query-store.ts +28 -0
  110. package/src/market-data/request-types.test.ts +64 -0
  111. package/src/market-data/request-types.ts +70 -0
  112. package/src/market-data/result-types.ts +56 -0
  113. package/src/market-data/selectors.ts +166 -0
  114. package/src/market-data/trace.ts +7 -0
  115. package/src/pane-settings.test.ts +71 -0
  116. package/src/pane-settings.ts +73 -0
  117. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  118. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  119. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  120. package/src/plugins/builtin/ai/index.tsx +86 -0
  121. package/src/plugins/builtin/ai/providers.ts +99 -0
  122. package/src/plugins/builtin/ai/runner.ts +82 -0
  123. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  124. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  125. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  126. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  127. package/src/plugins/builtin/ai/settings.ts +79 -0
  128. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  129. package/src/plugins/builtin/ai/utils.ts +71 -0
  130. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  131. package/src/plugins/builtin/ask-ai.tsx +9 -0
  132. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  133. package/src/plugins/builtin/chat-controller.ts +340 -0
  134. package/src/plugins/builtin/chat.test.tsx +451 -0
  135. package/src/plugins/builtin/chat.tsx +620 -0
  136. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  137. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  138. package/src/plugins/builtin/data-providers.ts +16 -0
  139. package/src/plugins/builtin/debug.tsx +351 -0
  140. package/src/plugins/builtin/help.test.tsx +110 -0
  141. package/src/plugins/builtin/help.tsx +281 -0
  142. package/src/plugins/builtin/layout-manager.tsx +264 -0
  143. package/src/plugins/builtin/manual-entry.tsx +25 -0
  144. package/src/plugins/builtin/news.tsx +143 -0
  145. package/src/plugins/builtin/notes-files.ts +58 -0
  146. package/src/plugins/builtin/notes.tsx +396 -0
  147. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  148. package/src/plugins/builtin/options-availability.ts +158 -0
  149. package/src/plugins/builtin/options.tsx +364 -0
  150. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  151. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  152. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  153. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  154. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  155. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  156. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  157. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  158. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  159. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  160. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  161. package/src/plugins/builtin/sec.tsx +250 -0
  162. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  163. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  164. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  165. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  166. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  167. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  168. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  169. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  170. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  171. package/src/plugins/builtin/yahoo.ts +18 -0
  172. package/src/plugins/event-bus.ts +37 -0
  173. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  174. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  175. package/src/plugins/ibkr/account-cache.ts +137 -0
  176. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  177. package/src/plugins/ibkr/config.test.ts +68 -0
  178. package/src/plugins/ibkr/config.ts +262 -0
  179. package/src/plugins/ibkr/dialogs.tsx +83 -0
  180. package/src/plugins/ibkr/flex.test.ts +107 -0
  181. package/src/plugins/ibkr/flex.ts +267 -0
  182. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  183. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  184. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  185. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  186. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  187. package/src/plugins/ibkr/index.test.tsx +183 -0
  188. package/src/plugins/ibkr/index.tsx +153 -0
  189. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  190. package/src/plugins/ibkr/instance-selection.ts +36 -0
  191. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  192. package/src/plugins/ibkr/price-normalization.ts +59 -0
  193. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  194. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  195. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  196. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  197. package/src/plugins/ibkr/trade-utils.ts +149 -0
  198. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  199. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  200. package/src/plugins/ibkr/trading-state.ts +339 -0
  201. package/src/plugins/loader.ts +71 -0
  202. package/src/plugins/pane-manager.test.ts +103 -0
  203. package/src/plugins/pane-manager.ts +974 -0
  204. package/src/plugins/plugin-persistence.ts +85 -0
  205. package/src/plugins/plugin-runtime.test.tsx +185 -0
  206. package/src/plugins/plugin-runtime.tsx +175 -0
  207. package/src/plugins/prediction-markets/cache.ts +114 -0
  208. package/src/plugins/prediction-markets/categories.ts +223 -0
  209. package/src/plugins/prediction-markets/chart.tsx +512 -0
  210. package/src/plugins/prediction-markets/columns.ts +133 -0
  211. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  212. package/src/plugins/prediction-markets/controller.ts +725 -0
  213. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  214. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  215. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  216. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  217. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  218. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  219. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  220. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  221. package/src/plugins/prediction-markets/index.tsx +102 -0
  222. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  223. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  224. package/src/plugins/prediction-markets/launch.ts +240 -0
  225. package/src/plugins/prediction-markets/metrics.ts +311 -0
  226. package/src/plugins/prediction-markets/navigation.ts +90 -0
  227. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  228. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  229. package/src/plugins/prediction-markets/pane.tsx +294 -0
  230. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  231. package/src/plugins/prediction-markets/rows.ts +229 -0
  232. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  233. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  234. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  235. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  236. package/src/plugins/prediction-markets/settings.ts +164 -0
  237. package/src/plugins/prediction-markets/status.ts +24 -0
  238. package/src/plugins/prediction-markets/table.tsx +84 -0
  239. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  240. package/src/plugins/prediction-markets/types.ts +186 -0
  241. package/src/plugins/registry.ts +633 -0
  242. package/src/sources/exchange-rates.ts +18 -0
  243. package/src/sources/gloomberb-cloud.test.ts +199 -0
  244. package/src/sources/gloomberb-cloud.ts +326 -0
  245. package/src/sources/provider-errors.ts +19 -0
  246. package/src/sources/provider-router.test.ts +1476 -0
  247. package/src/sources/provider-router.ts +1405 -0
  248. package/src/sources/sec-edgar.test.ts +286 -0
  249. package/src/sources/sec-edgar.ts +526 -0
  250. package/src/sources/yahoo-finance.test.ts +14 -0
  251. package/src/sources/yahoo-finance.ts +1017 -0
  252. package/src/state/app-activity.test.ts +41 -0
  253. package/src/state/app-activity.ts +106 -0
  254. package/src/state/app-bootstrap.test.ts +471 -0
  255. package/src/state/app-bootstrap.ts +308 -0
  256. package/src/state/app-context.test.ts +406 -0
  257. package/src/state/app-context.test.tsx +132 -0
  258. package/src/state/app-context.tsx +969 -0
  259. package/src/state/selectors.ts +67 -0
  260. package/src/state/session-persistence.test.ts +128 -0
  261. package/src/state/session-persistence.ts +161 -0
  262. package/src/state/ticker-refresh-queue.test.ts +46 -0
  263. package/src/state/ticker-refresh-queue.ts +40 -0
  264. package/src/state/use-inline-tickers.ts +174 -0
  265. package/src/state/use-quote-streaming.test.ts +37 -0
  266. package/src/state/use-quote-streaming.test.tsx +70 -0
  267. package/src/state/use-quote-streaming.ts +80 -0
  268. package/src/state/use-session-snapshot.ts +84 -0
  269. package/src/test-support/data-provider.ts +22 -0
  270. package/src/theme/color-utils.ts +67 -0
  271. package/src/theme/colors.ts +128 -0
  272. package/src/theme/themes.test.ts +45 -0
  273. package/src/theme/themes.ts +491 -0
  274. package/src/types/broker.ts +105 -0
  275. package/src/types/config.ts +434 -0
  276. package/src/types/data-provider.ts +81 -0
  277. package/src/types/financials.ts +142 -0
  278. package/src/types/instrument.ts +29 -0
  279. package/src/types/persistence.ts +21 -0
  280. package/src/types/plugin.ts +354 -0
  281. package/src/types/ticker.ts +58 -0
  282. package/src/types/trading.ts +85 -0
  283. package/src/updater.test.ts +162 -0
  284. package/src/updater.ts +247 -0
  285. package/src/utils/api-client.ts +666 -0
  286. package/src/utils/ascii-chart.ts +172 -0
  287. package/src/utils/broker-instances.ts +47 -0
  288. package/src/utils/cli-output.ts +83 -0
  289. package/src/utils/currency-units.test.ts +49 -0
  290. package/src/utils/currency-units.ts +82 -0
  291. package/src/utils/debug-log.ts +133 -0
  292. package/src/utils/exchange-rate-map.ts +8 -0
  293. package/src/utils/format.ts +158 -0
  294. package/src/utils/fuzzy-search.ts +34 -0
  295. package/src/utils/market-status.ts +95 -0
  296. package/src/utils/options.ts +86 -0
  297. package/src/utils/price-history.ts +39 -0
  298. package/src/utils/quote-time.test.ts +22 -0
  299. package/src/utils/quote-time.ts +50 -0
  300. package/src/utils/sec.test.ts +55 -0
  301. package/src/utils/sec.ts +45 -0
  302. package/src/utils/ticker-list.test.ts +22 -0
  303. package/src/utils/ticker-list.ts +35 -0
  304. package/src/utils/ticker-metadata.ts +51 -0
  305. package/src/utils/ticker-search.test.ts +227 -0
  306. package/src/utils/ticker-search.ts +764 -0
  307. package/src/utils/ticker-tokenizer.test.ts +48 -0
  308. package/src/utils/ticker-tokenizer.ts +72 -0
  309. package/src/version.ts +1 -0
  310. package/install.mjs +0 -68
@@ -0,0 +1,127 @@
1
+ export interface CommandBarMainSnapshot {
2
+ query: string;
3
+ selectedIdx: number;
4
+ }
5
+
6
+ export interface CommandBarFieldOption {
7
+ label: string;
8
+ value: string;
9
+ description?: string;
10
+ }
11
+
12
+ export interface CommandBarFieldDependency {
13
+ key: string;
14
+ value: string;
15
+ }
16
+
17
+ interface CommandBarFieldBase {
18
+ id: string;
19
+ label: string;
20
+ description?: string;
21
+ placeholder?: string;
22
+ required?: boolean;
23
+ dependsOn?: CommandBarFieldDependency[];
24
+ }
25
+
26
+ export type CommandBarWorkflowField =
27
+ | (CommandBarFieldBase & { type: "text" | "password" | "number" | "textarea" })
28
+ | (CommandBarFieldBase & { type: "toggle" })
29
+ | (CommandBarFieldBase & { type: "select"; options: CommandBarFieldOption[] })
30
+ | (CommandBarFieldBase & { type: "multi-select" | "ordered-multi-select"; options: CommandBarFieldOption[] });
31
+
32
+ export type CommandBarFieldValue = string | boolean | string[];
33
+
34
+ export interface CommandBarRouteBase {
35
+ restoreMain?: CommandBarMainSnapshot;
36
+ }
37
+
38
+ export interface CommandBarModeRoute extends CommandBarRouteBase {
39
+ kind: "mode";
40
+ screen: "ticker-search" | "themes" | "plugins" | "layout" | "new-pane";
41
+ query: string;
42
+ selectedIdx: number;
43
+ hoveredIdx: number | null;
44
+ themeBaseId?: string;
45
+ payload?: Record<string, unknown>;
46
+ }
47
+
48
+ export interface CommandBarPickerOption {
49
+ id: string;
50
+ label: string;
51
+ detail?: string;
52
+ description?: string;
53
+ disabled?: boolean;
54
+ }
55
+
56
+ export interface CommandBarPickerRoute extends CommandBarRouteBase {
57
+ kind: "picker";
58
+ pickerId:
59
+ | "layout-swap"
60
+ | "delete-watchlist"
61
+ | "delete-portfolio"
62
+ | "disconnect-broker"
63
+ | "collection-target"
64
+ | "broker-type"
65
+ | "field-select"
66
+ | "field-multi-select";
67
+ title: string;
68
+ query: string;
69
+ selectedIdx: number;
70
+ hoveredIdx: number | null;
71
+ options: CommandBarPickerOption[];
72
+ payload?: Record<string, unknown>;
73
+ }
74
+
75
+ export interface CommandBarConfirmRoute extends CommandBarRouteBase {
76
+ kind: "confirm";
77
+ confirmId: string;
78
+ title: string;
79
+ body: string[];
80
+ confirmLabel: string;
81
+ cancelLabel?: string;
82
+ tone: "default" | "danger";
83
+ onConfirm: () => void | Promise<void>;
84
+ pending: boolean;
85
+ error: string | null;
86
+ successBehavior?: "close" | "back" | "stay";
87
+ }
88
+
89
+ export interface CommandBarWorkflowRoute extends CommandBarRouteBase {
90
+ kind: "workflow";
91
+ workflowId: string;
92
+ title: string;
93
+ subtitle?: string;
94
+ description?: string[];
95
+ fields: CommandBarWorkflowField[];
96
+ values: Record<string, CommandBarFieldValue>;
97
+ activeFieldId: string | null;
98
+ submitLabel: string;
99
+ cancelLabel?: string;
100
+ pendingLabel?: string;
101
+ successLabel?: string;
102
+ pending: boolean;
103
+ error: string | null;
104
+ successBehavior?: "close" | "back";
105
+ payload: {
106
+ kind: "builtin" | "plugin-command" | "pane-template" | "pane-setting";
107
+ actionId: string;
108
+ };
109
+ payloadMeta?: Record<string, unknown>;
110
+ }
111
+
112
+ export interface CommandBarPaneSettingsRoute extends CommandBarRouteBase {
113
+ kind: "pane-settings";
114
+ paneId: string;
115
+ query: string;
116
+ selectedIdx: number;
117
+ hoveredIdx: number | null;
118
+ error: string | null;
119
+ pendingFieldKey: string | null;
120
+ }
121
+
122
+ export type CommandBarRoute =
123
+ | CommandBarModeRoute
124
+ | CommandBarPickerRoute
125
+ | CommandBarConfirmRoute
126
+ | CommandBarWorkflowRoute
127
+ | CommandBarPaneSettingsRoute;
@@ -0,0 +1,105 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { testRender } from "@opentui/react/test-utils";
3
+ import { DetailFeedView } from "./detail-feed-view";
4
+
5
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
6
+
7
+ afterEach(() => {
8
+ if (testSetup) {
9
+ testSetup.renderer.destroy();
10
+ testSetup = undefined;
11
+ }
12
+ });
13
+
14
+ const items = [
15
+ {
16
+ id: "1",
17
+ eyebrow: "Reuters",
18
+ title: "Long headline with enough words to prove the list wraps instead of collapsing into fixed columns",
19
+ timestamp: new Date("2024-08-01T12:00:00Z"),
20
+ preview: "Preview text with a little more color.",
21
+ detailTitle: "Long headline with enough words to prove the list wraps",
22
+ detailMeta: ["Reuters", "Published Aug 1, 2024"],
23
+ detailBody: "This body should stay readable when the pane is narrow and also when the pane is wide enough for a split layout.",
24
+ detailNote: "https://example.com/story",
25
+ },
26
+ {
27
+ id: "2",
28
+ eyebrow: "SEC",
29
+ title: "10-Q filing",
30
+ timestamp: new Date("2024-08-02T12:00:00Z"),
31
+ preview: "Quarterly report",
32
+ detailTitle: "10-Q filing",
33
+ detailMeta: ["Filed Aug 2, 2024"],
34
+ detailBody: "Quarterly report details.",
35
+ },
36
+ ];
37
+
38
+ describe("DetailFeedView", () => {
39
+ test("renders readable stacked content in narrow panes", async () => {
40
+ testSetup = await testRender(
41
+ <DetailFeedView
42
+ width={60}
43
+ height={16}
44
+ items={items}
45
+ selectedIdx={0}
46
+ hoveredIdx={null}
47
+ onSelect={() => {}}
48
+ onHover={() => {}}
49
+ />,
50
+ { width: 60, height: 16 },
51
+ );
52
+
53
+ await testSetup.renderOnce();
54
+ const frame = testSetup.captureCharFrame();
55
+
56
+ expect(frame).toContain("Long headline with enough words");
57
+ expect(frame).toContain("This body should stay readable");
58
+ expect(frame).toContain("Published Aug 1, 2024");
59
+ });
60
+
61
+ test("renders split list and detail content in wide panes", async () => {
62
+ testSetup = await testRender(
63
+ <DetailFeedView
64
+ width={110}
65
+ height={16}
66
+ items={items}
67
+ selectedIdx={0}
68
+ hoveredIdx={1}
69
+ onSelect={() => {}}
70
+ onHover={() => {}}
71
+ />,
72
+ { width: 110, height: 16 },
73
+ );
74
+
75
+ await testSetup.renderOnce();
76
+ const frame = testSetup.captureCharFrame();
77
+
78
+ expect(frame).toContain("Reuters");
79
+ expect(frame).toContain("10-Q filing");
80
+ expect(frame).toContain("j/k navigate");
81
+ });
82
+
83
+ test("renders a single-line list variant", async () => {
84
+ testSetup = await testRender(
85
+ <DetailFeedView
86
+ width={110}
87
+ height={12}
88
+ items={items}
89
+ selectedIdx={0}
90
+ hoveredIdx={null}
91
+ onSelect={() => {}}
92
+ onHover={() => {}}
93
+ listVariant="single-line"
94
+ splitListWidthRatio={0.33}
95
+ />,
96
+ { width: 110, height: 12 },
97
+ );
98
+
99
+ await testSetup.renderOnce();
100
+ const frame = testSetup.captureCharFrame();
101
+
102
+ expect(frame).toContain("Long headline with enough words");
103
+ expect(frame).not.toContain("Preview text with a little more color.");
104
+ });
105
+ });
@@ -0,0 +1,295 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { colors, hoverBg } from "../theme/colors";
3
+ import { formatTimeAgo } from "../utils/format";
4
+ import { ExternalLink } from "./ui";
5
+
6
+ export interface DetailFeedItem {
7
+ id: string;
8
+ eyebrow?: string;
9
+ title: string;
10
+ timestamp?: Date | string | null;
11
+ preview?: string | null;
12
+ detailTitle?: string;
13
+ detailMeta?: string[];
14
+ detailBody?: string | null;
15
+ detailNote?: string | null;
16
+ }
17
+
18
+ interface DetailFeedViewProps {
19
+ width: number;
20
+ height: number;
21
+ items: DetailFeedItem[];
22
+ selectedIdx: number;
23
+ hoveredIdx: number | null;
24
+ onSelect: (index: number) => void;
25
+ onHover: (index: number) => void;
26
+ helpText?: string;
27
+ listVariant?: "comfortable" | "compact" | "single-line";
28
+ splitListWidthRatio?: number;
29
+ }
30
+
31
+ function truncateWithEllipsis(text: string, width: number): string {
32
+ if (width <= 0) return "";
33
+ if (text.length <= width) return text;
34
+ if (width <= 3) return text.slice(0, width);
35
+ return `${text.slice(0, width - 3)}...`;
36
+ }
37
+
38
+ function wrapTextLines(text: string, width: number, maxLines = Number.MAX_SAFE_INTEGER): string[] {
39
+ if (width <= 0) return [];
40
+
41
+ const paragraphs = text
42
+ .split(/\r?\n/)
43
+ .map((paragraph) => paragraph.replace(/\s+/g, " ").trim());
44
+ const lines: string[] = [];
45
+
46
+ const pushLine = (line: string) => {
47
+ if (lines.length >= maxLines) return;
48
+ lines.push(line);
49
+ };
50
+
51
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex += 1) {
52
+ const paragraph = paragraphs[paragraphIndex]!;
53
+ if (!paragraph) {
54
+ pushLine("");
55
+ continue;
56
+ }
57
+
58
+ let current = "";
59
+ for (const rawWord of paragraph.split(" ")) {
60
+ let word = rawWord;
61
+ while (word.length > width) {
62
+ const available = current ? width - current.length - 1 : width;
63
+ if (available <= 0) {
64
+ pushLine(current);
65
+ current = "";
66
+ continue;
67
+ }
68
+ if (word.length <= available) break;
69
+ const piece = word.slice(0, available);
70
+ word = word.slice(available);
71
+ pushLine(current ? `${current} ${piece}` : piece);
72
+ current = "";
73
+ }
74
+
75
+ if (!current) {
76
+ current = word;
77
+ continue;
78
+ }
79
+
80
+ if ((current.length + 1 + word.length) <= width) {
81
+ current = `${current} ${word}`;
82
+ } else {
83
+ pushLine(current);
84
+ current = word;
85
+ }
86
+ }
87
+
88
+ if (current) pushLine(current);
89
+ if (paragraphIndex < (paragraphs.length - 1)) pushLine("");
90
+ if (lines.length >= maxLines) break;
91
+ }
92
+
93
+ if (lines.length > maxLines) {
94
+ lines.length = maxLines;
95
+ }
96
+ if (lines.length === maxLines && paragraphs.join(" ").length > lines.join(" ").length) {
97
+ lines[maxLines - 1] = truncateWithEllipsis(lines[maxLines - 1] ?? "", width);
98
+ }
99
+
100
+ return lines;
101
+ }
102
+
103
+ function contentWidth(value: number): number {
104
+ return Math.max(value - 2, 12);
105
+ }
106
+
107
+ function lineOrBlank(lines: string[], index: number): string {
108
+ return lines[index] ?? "";
109
+ }
110
+
111
+ export function DetailFeedView({
112
+ width,
113
+ height,
114
+ items,
115
+ selectedIdx,
116
+ hoveredIdx,
117
+ onSelect,
118
+ onHover,
119
+ helpText = "j/k navigate",
120
+ listVariant = "comfortable",
121
+ splitListWidthRatio = 0.38,
122
+ }: DetailFeedViewProps) {
123
+ const selected = items[Math.max(0, Math.min(selectedIdx, items.length - 1))];
124
+ const split = width >= 92 && height >= 14;
125
+ const footerHeight = helpText ? 1 : 0;
126
+ const availableHeight = Math.max(height - footerHeight, 6);
127
+ const compactList = listVariant === "compact";
128
+ const singleLineList = listVariant === "single-line";
129
+ const listWidth = split ? Math.max(28, Math.min(Math.floor(width * splitListWidthRatio), 44)) : Math.max(width - 2, 20);
130
+ const detailWidth = split ? Math.max(width - listWidth - 4, 28) : Math.max(width - 2, 20);
131
+ const listHeight = split ? availableHeight : Math.max(5, Math.floor((availableHeight - 1) * 0.45));
132
+ const detailHeight = split ? availableHeight : Math.max(3, availableHeight - listHeight - 1);
133
+ const listTextWidth = contentWidth(listWidth);
134
+ const detailTextWidth = contentWidth(detailWidth);
135
+ const showPreviewLine = !singleLineList && listTextWidth >= (compactList ? 18 : 28);
136
+ const cardHeight = compactList
137
+ ? (showPreviewLine ? 2 : 1)
138
+ : singleLineList
139
+ ? 1
140
+ : (showPreviewLine ? 4 : 3);
141
+
142
+ return (
143
+ <box flexDirection="column" flexGrow={1} paddingX={1}>
144
+ <box flexDirection={split ? "row" : "column"} gap={1}>
145
+ <box width={split ? listWidth : undefined}>
146
+ <scrollbox height={listHeight} scrollY>
147
+ <box flexDirection="column">
148
+ {items.map((item, index) => {
149
+ const isSelected = index === selectedIdx;
150
+ const isHovered = index === hoveredIdx && !isSelected;
151
+ const titleLines = wrapTextLines(item.title, listTextWidth, compactList ? 1 : 2);
152
+ const previewLine = item.preview ? truncateWithEllipsis(item.preview, listTextWidth) : "";
153
+ const timestampText = item.timestamp ? formatTimeAgo(item.timestamp) : "";
154
+ const inlineEyebrow = item.eyebrow ? `${item.eyebrow} | ` : "";
155
+ const compactTitleWidth = Math.max(listTextWidth - (timestampText ? timestampText.length + 1 : 0), 8);
156
+ const compactTitle = truncateWithEllipsis(`${inlineEyebrow}${item.title}`, compactTitleWidth);
157
+ const singleLineTitle = truncateWithEllipsis(item.title, compactTitleWidth);
158
+ return (
159
+ <box
160
+ key={item.id}
161
+ flexDirection="column"
162
+ height={cardHeight}
163
+ paddingX={1}
164
+ backgroundColor={isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg}
165
+ onMouseMove={() => onHover(index)}
166
+ onMouseDown={() => onSelect(index)}
167
+ >
168
+ {singleLineList ? (
169
+ <box flexDirection="row" height={1}>
170
+ <box flexGrow={1}>
171
+ <text
172
+ attributes={isSelected ? TextAttributes.BOLD : 0}
173
+ fg={isSelected ? colors.selectedText : colors.textBright}
174
+ >
175
+ {singleLineTitle}
176
+ </text>
177
+ </box>
178
+ {timestampText && (
179
+ <text fg={colors.textDim}>{timestampText}</text>
180
+ )}
181
+ </box>
182
+ ) : compactList ? (
183
+ <>
184
+ <box flexDirection="row" height={1}>
185
+ <box flexGrow={1}>
186
+ <text
187
+ attributes={isSelected ? TextAttributes.BOLD : 0}
188
+ fg={isSelected ? colors.selectedText : colors.textBright}
189
+ >
190
+ {compactTitle}
191
+ </text>
192
+ </box>
193
+ {timestampText && (
194
+ <text fg={colors.textDim}>{timestampText}</text>
195
+ )}
196
+ </box>
197
+ {showPreviewLine && (
198
+ <box height={1}>
199
+ <text fg={colors.textMuted}>
200
+ {previewLine || truncateWithEllipsis(item.eyebrow ?? "", listTextWidth)}
201
+ </text>
202
+ </box>
203
+ )}
204
+ </>
205
+ ) : (
206
+ <>
207
+ <box flexDirection="row" height={1}>
208
+ <box flexGrow={1}>
209
+ <text fg={colors.textMuted}>{truncateWithEllipsis(item.eyebrow ?? "", listTextWidth)}</text>
210
+ </box>
211
+ {timestampText && (
212
+ <text fg={colors.textDim}>{timestampText}</text>
213
+ )}
214
+ </box>
215
+ <box height={1}>
216
+ <text
217
+ attributes={isSelected ? TextAttributes.BOLD : 0}
218
+ fg={isSelected ? colors.selectedText : colors.textBright}
219
+ >
220
+ {lineOrBlank(titleLines, 0)}
221
+ </text>
222
+ </box>
223
+ <box height={1}>
224
+ <text fg={isSelected ? colors.selectedText : colors.text}>
225
+ {lineOrBlank(titleLines, 1)}
226
+ </text>
227
+ </box>
228
+ {showPreviewLine && (
229
+ <box height={1}>
230
+ <text fg={colors.textMuted}>{previewLine}</text>
231
+ </box>
232
+ )}
233
+ </>
234
+ )}
235
+ </box>
236
+ );
237
+ })}
238
+ </box>
239
+ </scrollbox>
240
+ </box>
241
+
242
+ <box width={split ? detailWidth : undefined} flexGrow={1}>
243
+ <scrollbox height={detailHeight} scrollY>
244
+ {selected ? (
245
+ <box flexDirection="column">
246
+ {wrapTextLines(selected.detailTitle ?? selected.title, detailTextWidth, 4).map((line, index) => (
247
+ <box key={`title-${index}`} height={1}>
248
+ <text attributes={TextAttributes.BOLD} fg={colors.textBright}>{line}</text>
249
+ </box>
250
+ ))}
251
+
252
+ {(selected.detailMeta ?? []).flatMap((entry) => wrapTextLines(entry, detailTextWidth, 2)).map((line, index) => (
253
+ <box key={`meta-${index}`} height={1}>
254
+ <text fg={colors.textMuted}>{line}</text>
255
+ </box>
256
+ ))}
257
+
258
+ <box height={1} />
259
+
260
+ {wrapTextLines(selected.detailBody ?? "", detailTextWidth).map((line, index) => (
261
+ <box key={`body-${index}`} height={1}>
262
+ <text fg={colors.text}>{line}</text>
263
+ </box>
264
+ ))}
265
+
266
+ {selected.detailNote && (
267
+ <>
268
+ <box height={1} />
269
+ {wrapTextLines(selected.detailNote, detailTextWidth).map((line, index) =>
270
+ /^https?:\/\/\S+$/.test(line.trim()) ? (
271
+ <ExternalLink key={`note-${index}`} url={line.trim()} />
272
+ ) : (
273
+ <box key={`note-${index}`} height={1}>
274
+ <text fg={colors.textDim}>{line}</text>
275
+ </box>
276
+ )
277
+ )}
278
+ </>
279
+ )}
280
+ </box>
281
+ ) : (
282
+ <text fg={colors.textDim}>Nothing selected.</text>
283
+ )}
284
+ </scrollbox>
285
+ </box>
286
+ </box>
287
+
288
+ {helpText && (
289
+ <box height={1}>
290
+ <text fg={colors.textMuted}>{helpText}</text>
291
+ </box>
292
+ )}
293
+ </box>
294
+ );
295
+ }
@@ -0,0 +1,32 @@
1
+ // Reusable components for plugins
2
+ export { PriceSelectorDialog } from "./price-selector-dialog";
3
+ export type { PriceSelectorDialogProps } from "./price-selector-dialog";
4
+ export { StockChart } from "./chart/stock-chart";
5
+ export type { ChartViewState, TimeRange, ChartRenderMode, ChartColors } from "./chart/chart-types";
6
+ export * from "./ui";
7
+ export { ToggleList } from "./toggle-list";
8
+ export type { ToggleListItem, ToggleListProps } from "./toggle-list";
9
+ export { TabBar } from "./tab-bar";
10
+ export type { Tab, TabBarProps } from "./tab-bar";
11
+ export { Spinner } from "./spinner";
12
+ export type { SpinnerProps } from "./spinner";
13
+ export { TickerListTable } from "./ticker-list-table";
14
+ export type { QuoteFlashDirection, TickerTableCell } from "./ticker-list-table";
15
+
16
+ // Theme
17
+ export { colors, priceColor, hoverBg } from "../theme/colors";
18
+
19
+ // Hooks
20
+ export {
21
+ useAppState,
22
+ useFocusedTicker,
23
+ usePaneSettingValue,
24
+ usePaneTicker,
25
+ useSelectedTicker,
26
+ } from "../state/app-context";
27
+
28
+ // Format utilities
29
+ export {
30
+ formatCurrency, formatCompact, formatPercent, formatPercentRaw,
31
+ formatNumber, padTo,
32
+ } from "../utils/format";
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from "react";
2
+ import { colors, floatingPaneBg } from "../../theme/colors";
3
+ import { PaneHeader } from "./pane-header";
4
+
5
+ interface FloatingPaneWrapperProps {
6
+ title: string;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ zIndex: number;
12
+ focused: boolean;
13
+ showActions?: boolean;
14
+ onMouseMove?: (event: any) => void;
15
+ children: ReactNode;
16
+ }
17
+
18
+ /** Pure visual wrapper — all mouse handling done by Shell at root level */
19
+ export function FloatingPaneWrapper({
20
+ title, x, y, width, height, zIndex, focused, showActions = false, onMouseMove, children,
21
+ }: FloatingPaneWrapperProps) {
22
+ const bg = floatingPaneBg(focused);
23
+
24
+ return (
25
+ <box
26
+ position="absolute"
27
+ top={y}
28
+ left={x}
29
+ width={width}
30
+ height={height}
31
+ zIndex={zIndex}
32
+ backgroundColor={bg}
33
+ flexDirection="column"
34
+ onMouseMove={onMouseMove}
35
+ >
36
+ <PaneHeader title={title} width={width} focused={focused} floating showActions={showActions} />
37
+
38
+ {/* Content */}
39
+ <box flexGrow={1} overflow="hidden">
40
+ {children}
41
+ </box>
42
+
43
+ {/* Resize handle at bottom-right corner */}
44
+ <box position="absolute" bottom={0} right={0} width={2} height={1}>
45
+ <text fg={colors.textDim} selectable={false}> ◢</text>
46
+ </box>
47
+ </box>
48
+ );
49
+ }