gloomberb 0.2.0 → 0.3.2

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 (311) 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 +1365 -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 +1546 -0
  45. package/src/components/command-bar/command-bar.tsx +4841 -0
  46. package/src/components/command-bar/command-registry.ts +269 -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 +151 -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 +651 -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/markdown-text.tsx +218 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +347 -0
  97. package/src/data/config-store.ts +507 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +55 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +350 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +460 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +270 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +224 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +162 -0
  259. package/src/state/app-context.tsx +981 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +444 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +282 -0
  285. package/src/updater.ts +303 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -68
@@ -0,0 +1,1365 @@
1
+ import { useState, useCallback, useEffect, useRef, useMemo, type RefObject } from "react";
2
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
3
+ import { TextAttributes } from "@opentui/core";
4
+ import type { InputRenderable } from "@opentui/core";
5
+ import { colors, applyTheme } from "../../theme/colors";
6
+ import { themes, getThemeIds } from "../../theme/themes";
7
+ import { saveConfig } from "../../data/config-store";
8
+ import type { AppConfig } from "../../types/config";
9
+ import type { PluginRegistry } from "../../plugins/registry";
10
+ import { resolveBrokerConfigFields, type BrokerAdapter, type BrokerConfigField } from "../../types/broker";
11
+ import { buildIbkrConfigFromValues } from "../../plugins/ibkr/config";
12
+ import { createBrokerInstanceId } from "../../utils/broker-instances";
13
+ import { syncBrokerInstance } from "../../brokers/sync-broker-instance";
14
+ import { debugLog } from "../../utils/debug-log";
15
+ import { ToggleList, type ToggleListItem } from "../toggle-list";
16
+ import { TextField, ExternalLink } from "../ui";
17
+
18
+ interface OnboardingWizardProps {
19
+ config: AppConfig;
20
+ pluginRegistry: PluginRegistry;
21
+ onComplete: (config: AppConfig) => void | Promise<void>;
22
+ }
23
+
24
+ type Step = "welcome" | "theme" | "portfolio" | "plugins" | "shortcuts" | "ready";
25
+ const STEPS: Step[] = ["welcome", "theme", "portfolio", "plugins", "shortcuts", "ready"];
26
+
27
+ // Sub-steps within the portfolio step
28
+ type PortfolioSub = "choose" | "manual-name" | "broker-setup" | "broker-fields" | "broker-sync";
29
+
30
+ interface BrokerOption {
31
+ id: string;
32
+ name: string;
33
+ adapter: BrokerAdapter;
34
+ }
35
+
36
+ const LOGO = [
37
+ "\u259E\u2580\u2596\u259C \u258C \u258C ",
38
+ "\u258C\u2584\u2596\u2590 \u259E\u2580\u2596\u259E\u2580\u2596\u259B\u259A\u2580\u2596\u259B\u2580\u2596\u259E\u2580\u2596\u2599\u2580\u2596\u259B\u2580\u2596",
39
+ "\u258C \u258C\u2590 \u258C \u258C\u258C \u258C\u258C\u2590 \u258C\u258C \u258C\u259B\u2580 \u258C \u258C \u258C",
40
+ "\u259D\u2580 \u2598\u259D\u2580 \u259D\u2580 \u2598\u259D \u2598\u2580\u2580 \u259D\u2580\u2598\u2598 \u2580\u2580 ",
41
+ ];
42
+
43
+ const PASSWORD_MASK_CHAR = "*";
44
+ const onboardingLog = debugLog.createLogger("onboarding");
45
+
46
+ interface BrokerSyncSummary {
47
+ portfolioId: string | null;
48
+ positionsImported: number;
49
+ }
50
+
51
+ function getToggleablePlugins(pluginRegistry: PluginRegistry) {
52
+ const result: Array<{ id: string; name: string; description: string; order: number }> = [];
53
+ for (const [, plugin] of pluginRegistry.allPlugins) {
54
+ if (plugin.toggleable) {
55
+ result.push({
56
+ id: plugin.id,
57
+ name: plugin.name,
58
+ description: plugin.description ?? "",
59
+ order: plugin.order ?? Number.MAX_SAFE_INTEGER,
60
+ });
61
+ }
62
+ }
63
+ return result
64
+ .sort((left, right) => left.order - right.order || left.name.localeCompare(right.name))
65
+ .map(({ order: _order, ...plugin }) => plugin);
66
+ }
67
+
68
+ function summarizeError(error: unknown): string {
69
+ if (error instanceof Error && error.message.trim()) {
70
+ return error.message.trim();
71
+ }
72
+ return "Unable to finish onboarding.";
73
+ }
74
+
75
+ function focusPortfolioListCollection(config: AppConfig, collectionId: string): AppConfig {
76
+ const nextInstances = config.layout.instances.map((instance) => {
77
+ if (instance.paneId !== "portfolio-list") {
78
+ return instance;
79
+ }
80
+
81
+ const visibleCollectionIds = Array.isArray(instance.settings?.visibleCollectionIds)
82
+ ? instance.settings.visibleCollectionIds.filter((value): value is string => typeof value === "string")
83
+ : [];
84
+
85
+ return {
86
+ ...instance,
87
+ params: {
88
+ ...instance.params,
89
+ collectionId,
90
+ },
91
+ settings: {
92
+ ...instance.settings,
93
+ lockedCollectionId: collectionId,
94
+ visibleCollectionIds: [collectionId, ...visibleCollectionIds.filter((value) => value !== collectionId)],
95
+ },
96
+ };
97
+ });
98
+
99
+ return {
100
+ ...config,
101
+ layout: {
102
+ ...config.layout,
103
+ instances: nextInstances,
104
+ },
105
+ };
106
+ }
107
+
108
+ export function OnboardingWizard({ config, pluginRegistry, onComplete }: OnboardingWizardProps) {
109
+ const { width: termWidth, height: termHeight } = useTerminalDimensions();
110
+ const [step, setStep] = useState<Step>("welcome");
111
+ const [themeIdx, setThemeIdx] = useState(0);
112
+
113
+ // Portfolio state
114
+ const [portfolioSub, setPortfolioSub] = useState<PortfolioSub>("choose");
115
+ const [portfolioOptionIdx, setPortfolioOptionIdx] = useState(0);
116
+ const [portfolioName, setPortfolioName] = useState("Main Portfolio");
117
+ // Broker credentials: brokerId -> { field_key -> value }
118
+ const [brokerValues, setBrokerValues] = useState<Record<string, Record<string, string>>>({});
119
+ const [selectedBrokerId, setSelectedBrokerId] = useState<string | null>(null);
120
+ const [brokerFieldIdx, setBrokerFieldIdx] = useState(0);
121
+ const [brokerSelectIdx, setBrokerSelectIdx] = useState(0);
122
+ const [editingField, setEditingField] = useState(false);
123
+ const [isBrokerSyncing, setIsBrokerSyncing] = useState(false);
124
+ const [brokerSyncError, setBrokerSyncError] = useState<string | null>(null);
125
+ const [brokerSyncedConfig, setBrokerSyncedConfig] = useState<AppConfig | null>(null);
126
+ const [brokerSyncSummary, setBrokerSyncSummary] = useState<BrokerSyncSummary | null>(null);
127
+ const [isFinishing, setIsFinishing] = useState(false);
128
+ const [finishError, setFinishError] = useState<string | null>(null);
129
+
130
+ // Plugin state
131
+ const toggleablePlugins = useMemo(() => getToggleablePlugins(pluginRegistry), [pluginRegistry]);
132
+ const [disabledPlugins, setDisabledPlugins] = useState<string[]>(() => (
133
+ config.disabledPlugins.filter((pluginId) => pluginId !== "gloomberb-cloud")
134
+ ));
135
+ const [pluginIdx, setPluginIdx] = useState(0);
136
+
137
+ const inputRef = useRef<InputRenderable>(null);
138
+ const brokerSyncAttemptRef = useRef(0);
139
+ const themeIds = getThemeIds();
140
+ const stepIdx = STEPS.indexOf(step);
141
+
142
+ // Discover connectable brokers from plugin registry (those with config fields)
143
+ const brokerOptions = useMemo((): BrokerOption[] => {
144
+ const options: BrokerOption[] = [];
145
+ for (const [id, adapter] of pluginRegistry.brokers) {
146
+ if (adapter.configSchema.length > 0) {
147
+ options.push({ id, name: adapter.name, adapter });
148
+ }
149
+ }
150
+ return options;
151
+ }, [pluginRegistry.brokers]);
152
+
153
+ // Build the list of choices for the portfolio step: manual + all connectable brokers
154
+ const portfolioChoices = useMemo(() => {
155
+ const choices: Array<{ id: string; label: string; desc: string }> = [
156
+ { id: "manual", label: "Create Manual Portfolio", desc: "Add tickers and positions by hand" },
157
+ ];
158
+ for (const broker of brokerOptions) {
159
+ choices.push({
160
+ id: broker.id,
161
+ label: `Connect ${broker.name}`,
162
+ desc: `Auto-import positions via ${broker.name}`,
163
+ });
164
+ }
165
+ return choices;
166
+ }, [brokerOptions]);
167
+
168
+ // Get the currently selected broker's required fields
169
+ const activeBrokerFields = useMemo((): BrokerConfigField[] => {
170
+ if (!selectedBrokerId) return [];
171
+ const broker = brokerOptions.find((b) => b.id === selectedBrokerId);
172
+ return broker
173
+ ? resolveBrokerConfigFields(broker.adapter, brokerValues[selectedBrokerId] ?? {}).filter((field) => field.required)
174
+ : [];
175
+ }, [selectedBrokerId, brokerOptions, brokerValues]);
176
+
177
+ // Apply theme preview synchronously so colors are correct for this render
178
+ if (step === "theme") {
179
+ applyTheme(themeIds[themeIdx]!);
180
+ }
181
+
182
+ // Focus input when entering editing mode
183
+ useEffect(() => {
184
+ if (editingField) {
185
+ setTimeout(() => inputRef.current?.focus(), 10);
186
+ }
187
+ }, [editingField, portfolioSub, brokerFieldIdx]);
188
+
189
+ useEffect(() => {
190
+ if (!selectedBrokerId) return;
191
+ const field = activeBrokerFields[brokerFieldIdx];
192
+ if (!field || field.type !== "select") return;
193
+ const currentValue = brokerValues[selectedBrokerId]?.[field.key] ?? field.options?.[0]?.value ?? "";
194
+ const index = Math.max(0, field.options?.findIndex((option) => option.value === currentValue) ?? 0);
195
+ setBrokerSelectIdx(index);
196
+ }, [selectedBrokerId, brokerFieldIdx, activeBrokerFields, brokerValues]);
197
+
198
+ useEffect(() => {
199
+ if (step !== "ready" && finishError) {
200
+ setFinishError(null);
201
+ }
202
+ }, [finishError, step]);
203
+
204
+ const nextStep = useCallback(() => {
205
+ const idx = STEPS.indexOf(step);
206
+ if (idx < STEPS.length - 1) {
207
+ setEditingField(false);
208
+ setStep(STEPS[idx + 1]!);
209
+ }
210
+ }, [step]);
211
+
212
+ const prevStep = useCallback(() => {
213
+ const idx = STEPS.indexOf(step);
214
+ if (idx > 0) {
215
+ setStep(STEPS[idx - 1]!);
216
+ }
217
+ }, [step]);
218
+
219
+ const resetBrokerSync = useCallback(() => {
220
+ brokerSyncAttemptRef.current += 1;
221
+ setIsBrokerSyncing(false);
222
+ setBrokerSyncError(null);
223
+ setBrokerSyncedConfig(null);
224
+ setBrokerSyncSummary(null);
225
+ }, []);
226
+
227
+ const buildDraftBrokerConfig = useCallback((brokerValueOverrides?: Record<string, string>) => {
228
+ const selectedValues = selectedBrokerId ? (brokerValueOverrides ?? brokerValues[selectedBrokerId]) : null;
229
+ if (!selectedBrokerId || selectedBrokerId === "manual" || !selectedValues) {
230
+ throw new Error("Broker setup is incomplete.");
231
+ }
232
+
233
+ const brokerOption = brokerOptions.find((option) => option.id === selectedBrokerId);
234
+ const label = brokerOption?.name || selectedBrokerId;
235
+ const brokerConfig = (selectedBrokerId === "ibkr"
236
+ ? buildIbkrConfigFromValues(selectedValues)
237
+ : { ...selectedValues }) as unknown as Record<string, unknown>;
238
+ const instanceId = createBrokerInstanceId(
239
+ selectedBrokerId,
240
+ label,
241
+ config.brokerInstances.map((instance) => instance.id),
242
+ );
243
+
244
+ return {
245
+ instanceId,
246
+ config: {
247
+ ...config,
248
+ brokerInstances: [
249
+ ...config.brokerInstances,
250
+ {
251
+ id: instanceId,
252
+ brokerType: selectedBrokerId,
253
+ label,
254
+ connectionMode: typeof brokerConfig.connectionMode === "string" ? brokerConfig.connectionMode : undefined,
255
+ config: brokerConfig,
256
+ enabled: true,
257
+ },
258
+ ],
259
+ } satisfies AppConfig,
260
+ };
261
+ }, [brokerOptions, brokerValues, config, selectedBrokerId]);
262
+
263
+ const syncSelectedBroker = useCallback(async (brokerValueOverrides?: Record<string, string>) => {
264
+ const attemptId = brokerSyncAttemptRef.current + 1;
265
+ brokerSyncAttemptRef.current = attemptId;
266
+ setEditingField(false);
267
+ setPortfolioSub("broker-sync");
268
+ setIsBrokerSyncing(true);
269
+ setBrokerSyncError(null);
270
+ setBrokerSyncedConfig(null);
271
+ setBrokerSyncSummary(null);
272
+
273
+ try {
274
+ const { config: draftConfig, instanceId } = buildDraftBrokerConfig(brokerValueOverrides);
275
+ onboardingLog.info("Syncing broker during onboarding", { instanceId, brokerId: selectedBrokerId });
276
+ const result = await syncBrokerInstance({
277
+ config: draftConfig,
278
+ instanceId,
279
+ brokers: pluginRegistry.brokers,
280
+ tickerRepository: pluginRegistry.tickerRepository,
281
+ resources: pluginRegistry.persistence.resources,
282
+ persistResolvedIbkrConnection: true,
283
+ });
284
+ if (brokerSyncAttemptRef.current !== attemptId) {
285
+ return;
286
+ }
287
+
288
+ const portfolioId = result.portfolioIds[0] ?? null;
289
+ const nextConfig = portfolioId
290
+ ? focusPortfolioListCollection(result.config, portfolioId)
291
+ : result.config;
292
+ setBrokerSyncedConfig(nextConfig);
293
+ setBrokerSyncSummary({
294
+ portfolioId,
295
+ positionsImported: result.positions.length,
296
+ });
297
+ setBrokerSyncError(null);
298
+ setIsBrokerSyncing(false);
299
+ setPortfolioSub("broker-fields");
300
+ onboardingLog.info("Broker onboarding sync completed", {
301
+ instanceId,
302
+ portfolioId,
303
+ positionsImported: result.positions.length,
304
+ });
305
+ nextStep();
306
+ } catch (error) {
307
+ if (brokerSyncAttemptRef.current !== attemptId) {
308
+ return;
309
+ }
310
+
311
+ onboardingLog.error("Broker onboarding sync failed", { error: summarizeError(error), brokerId: selectedBrokerId });
312
+ setBrokerSyncError(summarizeError(error));
313
+ setIsBrokerSyncing(false);
314
+ setPortfolioSub("broker-sync");
315
+ }
316
+ }, [
317
+ buildDraftBrokerConfig,
318
+ nextStep,
319
+ pluginRegistry.brokers,
320
+ pluginRegistry.persistence.resources,
321
+ pluginRegistry.tickerRepository,
322
+ selectedBrokerId,
323
+ ]);
324
+
325
+ const finish = useCallback(async () => {
326
+ if (isFinishing) {
327
+ return;
328
+ }
329
+
330
+ setFinishError(null);
331
+ const selectedTheme = themeIds[themeIdx]!;
332
+ applyTheme(selectedTheme);
333
+
334
+ const isBroker = selectedBrokerId && selectedBrokerId !== "manual";
335
+ const baseConfig = isBroker ? brokerSyncedConfig : config;
336
+
337
+ setIsFinishing(true);
338
+ try {
339
+ if (isBroker && !baseConfig) {
340
+ throw new Error("Connect and sync the broker before finishing onboarding.");
341
+ }
342
+
343
+ const updatedConfig: AppConfig = {
344
+ ...(baseConfig ?? config),
345
+ theme: selectedTheme,
346
+ portfolios: isBroker
347
+ ? (baseConfig ?? config).portfolios
348
+ : [{ id: "main", name: portfolioName || "Main Portfolio", currency: "USD" }],
349
+ disabledPlugins,
350
+ onboardingComplete: true,
351
+ };
352
+
353
+ await saveConfig(updatedConfig);
354
+ await Promise.resolve(onComplete(updatedConfig));
355
+ } catch (error) {
356
+ setFinishError(summarizeError(error));
357
+ setIsFinishing(false);
358
+ }
359
+ }, [
360
+ brokerSyncedConfig,
361
+ config,
362
+ disabledPlugins,
363
+ isFinishing,
364
+ onComplete,
365
+ portfolioName,
366
+ selectedBrokerId,
367
+ themeIds,
368
+ themeIdx,
369
+ ]);
370
+
371
+ // Helper to update a broker field value
372
+ const setBrokerFieldValue = useCallback((brokerId: string, key: string, value: string) => {
373
+ resetBrokerSync();
374
+ setBrokerValues((prev) => ({
375
+ ...prev,
376
+ [brokerId]: { ...prev[brokerId], [key]: value },
377
+ }));
378
+ }, [resetBrokerSync]);
379
+
380
+ useKeyboard((event) => {
381
+ if (isFinishing) {
382
+ return;
383
+ }
384
+
385
+ // --- Input field handling ---
386
+ if (editingField) {
387
+ if (event.name === "return") {
388
+ setEditingField(false);
389
+ if (portfolioSub === "manual-name") {
390
+ nextStep();
391
+ } else if (portfolioSub === "broker-fields" && selectedBrokerId) {
392
+ const currentField = activeBrokerFields[brokerFieldIdx];
393
+ if (!currentField) {
394
+ void syncSelectedBroker();
395
+ return;
396
+ }
397
+ const rawValue = brokerValues[selectedBrokerId]?.[currentField.key] ?? "";
398
+ const currentValue = rawValue.trim() || currentField.defaultValue || "";
399
+ if (currentValue) {
400
+ const nextValues = {
401
+ ...(brokerValues[selectedBrokerId] ?? {}),
402
+ [currentField.key]: currentValue,
403
+ };
404
+ if (!rawValue.trim() && currentField.defaultValue) {
405
+ setBrokerFieldValue(selectedBrokerId, currentField.key, currentField.defaultValue);
406
+ }
407
+ // Move to next field, or advance to next step if all fields done
408
+ if (brokerFieldIdx < activeBrokerFields.length - 1) {
409
+ const nextIndex = brokerFieldIdx + 1;
410
+ const nextField = activeBrokerFields[nextIndex];
411
+ setBrokerFieldIdx(nextIndex);
412
+ setEditingField(nextField?.type !== "select");
413
+ } else {
414
+ void syncSelectedBroker(nextValues);
415
+ }
416
+ }
417
+ }
418
+ } else if (event.name === "escape") {
419
+ setEditingField(false);
420
+ if (portfolioSub === "broker-fields") {
421
+ setPortfolioSub("broker-setup");
422
+ } else {
423
+ setPortfolioSub("choose");
424
+ setBrokerFieldIdx(0);
425
+ }
426
+ }
427
+ return; // Let input handle other keys
428
+ }
429
+
430
+ // --- Global nav ---
431
+ if (event.name === "return" || event.name === "enter") {
432
+ if (step === "ready") {
433
+ void finish();
434
+ return;
435
+ }
436
+ if (step === "portfolio") {
437
+ if (portfolioSub === "choose") {
438
+ const choice = portfolioChoices[portfolioOptionIdx]!;
439
+ if (choice.id === "manual") {
440
+ resetBrokerSync();
441
+ setSelectedBrokerId(null);
442
+ setPortfolioSub("manual-name");
443
+ setEditingField(true);
444
+ } else {
445
+ resetBrokerSync();
446
+ setSelectedBrokerId(choice.id);
447
+ setBrokerFieldIdx(0);
448
+ setPortfolioSub("broker-fields");
449
+ const broker = brokerOptions.find((option) => option.id === choice.id);
450
+ const initialField = broker
451
+ ? resolveBrokerConfigFields(broker.adapter, brokerValues[choice.id] ?? {}).filter((field) => field.required)[0]
452
+ : null;
453
+ setEditingField(initialField?.type !== "select");
454
+ }
455
+ return;
456
+ }
457
+ if (portfolioSub === "broker-setup" && selectedBrokerId) {
458
+ setPortfolioSub("broker-fields");
459
+ const currentField = activeBrokerFields[brokerFieldIdx];
460
+ setEditingField(currentField?.type !== "select");
461
+ return;
462
+ }
463
+ if (portfolioSub === "broker-fields" && selectedBrokerId) {
464
+ const currentField = activeBrokerFields[brokerFieldIdx];
465
+ if (!currentField) {
466
+ void syncSelectedBroker();
467
+ return;
468
+ }
469
+ if (currentField.type === "select") {
470
+ const option = currentField.options?.[brokerSelectIdx];
471
+ if (!option) return;
472
+ setBrokerFieldValue(selectedBrokerId, currentField.key, option.value);
473
+ const nextValues = {
474
+ ...(brokerValues[selectedBrokerId] ?? {}),
475
+ [currentField.key]: option.value,
476
+ };
477
+ const broker = brokerOptions.find((entry) => entry.id === selectedBrokerId);
478
+ const fields = broker
479
+ ? resolveBrokerConfigFields(broker.adapter, nextValues).filter((field) => field.required)
480
+ : activeBrokerFields;
481
+ if (brokerFieldIdx < fields.length - 1) {
482
+ const nextIndex = brokerFieldIdx + 1;
483
+ const nextField = fields[nextIndex];
484
+ setBrokerFieldIdx(nextIndex);
485
+ // Show setup guide after connection mode is selected
486
+ if (currentField.key === "connectionMode") {
487
+ setPortfolioSub("broker-setup");
488
+ } else {
489
+ setEditingField(nextField?.type !== "select");
490
+ }
491
+ } else {
492
+ void syncSelectedBroker(nextValues);
493
+ }
494
+ return;
495
+ }
496
+ if (!editingField) {
497
+ setEditingField(true);
498
+ return;
499
+ }
500
+ }
501
+ if (portfolioSub === "broker-sync") {
502
+ if (!isBrokerSyncing && brokerSyncError) {
503
+ void syncSelectedBroker();
504
+ }
505
+ return;
506
+ }
507
+ }
508
+ nextStep();
509
+ } else if (event.name === "escape") {
510
+ if (step === "portfolio" && portfolioSub === "broker-sync") {
511
+ resetBrokerSync();
512
+ setPortfolioSub("broker-fields");
513
+ return;
514
+ }
515
+ if (step === "portfolio" && portfolioSub === "broker-fields") {
516
+ setPortfolioSub("broker-setup");
517
+ return;
518
+ }
519
+ if (step === "portfolio" && portfolioSub === "broker-setup") {
520
+ setPortfolioSub("broker-fields");
521
+ setBrokerFieldIdx(0);
522
+ return;
523
+ }
524
+ if (step === "portfolio" && portfolioSub !== "choose") {
525
+ setPortfolioSub("choose");
526
+ setBrokerFieldIdx(0);
527
+ return;
528
+ }
529
+ prevStep();
530
+ } else if (event.name === "left") {
531
+ if (step === "portfolio" && portfolioSub === "broker-sync") {
532
+ resetBrokerSync();
533
+ setPortfolioSub("broker-fields");
534
+ return;
535
+ }
536
+ if (step === "portfolio" && portfolioSub === "broker-fields") {
537
+ setPortfolioSub("broker-setup");
538
+ return;
539
+ }
540
+ if (step === "portfolio" && portfolioSub === "broker-setup") {
541
+ setPortfolioSub("broker-fields");
542
+ setBrokerFieldIdx(0);
543
+ return;
544
+ }
545
+ if (step === "portfolio" && portfolioSub !== "choose") {
546
+ setPortfolioSub("choose");
547
+ setBrokerFieldIdx(0);
548
+ return;
549
+ }
550
+ prevStep();
551
+ } else if (event.name === "right") {
552
+ nextStep();
553
+ }
554
+
555
+ // --- Step-specific controls ---
556
+ if (step === "theme") {
557
+ if (event.name === "up" || event.name === "k") {
558
+ setThemeIdx((i) => Math.max(0, i - 1));
559
+ } else if (event.name === "down" || event.name === "j") {
560
+ setThemeIdx((i) => Math.min(themeIds.length - 1, i + 1));
561
+ }
562
+ } else if (step === "portfolio" && portfolioSub === "choose") {
563
+ if (event.name === "up" || event.name === "k") {
564
+ setPortfolioOptionIdx((i) => Math.max(0, i - 1));
565
+ } else if (event.name === "down" || event.name === "j") {
566
+ setPortfolioOptionIdx((i) => Math.min(portfolioChoices.length - 1, i + 1));
567
+ }
568
+ } else if (step === "portfolio" && portfolioSub === "broker-fields" && selectedBrokerId) {
569
+ const currentField = activeBrokerFields[brokerFieldIdx];
570
+ if (currentField?.type === "select") {
571
+ const optionCount = currentField.options?.length ?? 0;
572
+ if (event.name === "up" || event.name === "k") {
573
+ setBrokerSelectIdx((index) => Math.max(0, index - 1));
574
+ } else if (event.name === "down" || event.name === "j") {
575
+ setBrokerSelectIdx((index) => Math.min(optionCount - 1, index + 1));
576
+ }
577
+ }
578
+ } else if (step === "plugins") {
579
+ if (event.name === "up" || event.name === "k") {
580
+ setPluginIdx((i) => Math.max(0, i - 1));
581
+ } else if (event.name === "down" || event.name === "j") {
582
+ setPluginIdx((i) => Math.min(toggleablePlugins.length - 1, i + 1));
583
+ } else if (event.name === "space" || event.name === " ") {
584
+ event.stopPropagation?.();
585
+ const plugin = toggleablePlugins[pluginIdx];
586
+ if (plugin) {
587
+ setDisabledPlugins((prev) =>
588
+ prev.includes(plugin.id)
589
+ ? prev.filter((id) => id !== plugin.id)
590
+ : [...prev, plugin.id]
591
+ );
592
+ }
593
+ }
594
+ }
595
+ });
596
+
597
+ const contentWidth = Math.min(60, termWidth - 4);
598
+ const contentLeft = Math.floor((termWidth - contentWidth) / 2);
599
+ const contentTop = Math.max(1, Math.floor((termHeight - 24) / 2));
600
+
601
+ // Progress bar
602
+ const progressDots = STEPS.map((s, i) => {
603
+ if (i < stepIdx) return "\u2501";
604
+ if (i === stepIdx) return "\u25cf";
605
+ return "\u00b7";
606
+ }).join(" ");
607
+
608
+ // Bottom hint text
609
+ let hintText = "enter ->";
610
+ if (step === "ready") {
611
+ hintText = "enter to launch";
612
+ }
613
+ if (step === "portfolio" && portfolioSub === "broker-sync") {
614
+ hintText = isBrokerSyncing ? "syncing broker..." : "enter to retry";
615
+ } else if (isFinishing) {
616
+ hintText = "launching...";
617
+ }
618
+
619
+ // Determine what the ready step should show
620
+ const connectedBrokerName = selectedBrokerId
621
+ ? brokerOptions.find((b) => b.id === selectedBrokerId)?.name
622
+ : null;
623
+
624
+ return (
625
+ <box
626
+ position="absolute"
627
+ top={0}
628
+ left={0}
629
+ width={termWidth}
630
+ height={termHeight}
631
+ backgroundColor={colors.bg}
632
+ zIndex={200}
633
+ >
634
+ <box
635
+ position="absolute"
636
+ top={contentTop}
637
+ left={contentLeft}
638
+ width={contentWidth}
639
+ flexDirection="column"
640
+ >
641
+ {step === "welcome" && <WelcomeStep />}
642
+ {step === "theme" && <ThemeStep themeIds={themeIds} selectedIdx={themeIdx} height={termHeight - contentTop - 4} />}
643
+ {step === "portfolio" && (
644
+ <PortfolioStep
645
+ sub={portfolioSub}
646
+ choices={portfolioChoices}
647
+ optionIdx={portfolioOptionIdx}
648
+ portfolioName={portfolioName}
649
+ onNameChange={setPortfolioName}
650
+ selectedBrokerId={selectedBrokerId}
651
+ brokerFields={activeBrokerFields}
652
+ brokerFieldIdx={brokerFieldIdx}
653
+ brokerSelectIdx={brokerSelectIdx}
654
+ brokerValues={brokerValues}
655
+ onBrokerFieldChange={setBrokerFieldValue}
656
+ editing={editingField}
657
+ inputRef={inputRef}
658
+ brokerSyncing={isBrokerSyncing}
659
+ brokerSyncError={brokerSyncError}
660
+ />
661
+ )}
662
+ {step === "plugins" && (
663
+ <PluginsStep
664
+ plugins={toggleablePlugins}
665
+ disabledPlugins={disabledPlugins}
666
+ selectedIdx={pluginIdx}
667
+ onToggle={(id) => {
668
+ setDisabledPlugins((prev) =>
669
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
670
+ );
671
+ }}
672
+ onSelect={setPluginIdx}
673
+ />
674
+ )}
675
+ {step === "shortcuts" && (
676
+ <ShortcutsStep pluginRegistry={pluginRegistry} disabledPlugins={disabledPlugins} />
677
+ )}
678
+ {step === "ready" && (
679
+ <ReadyStep
680
+ brokerName={connectedBrokerName ?? null}
681
+ portfolioName={portfolioName}
682
+ brokerSyncSummary={brokerSyncSummary}
683
+ isFinishing={isFinishing}
684
+ error={finishError}
685
+ />
686
+ )}
687
+
688
+ {/* Bottom: progress + nav hints */}
689
+ <box height={1} />
690
+ <box height={1} flexDirection="row" width={contentWidth}>
691
+ <box flexGrow={1}>
692
+ <text fg={colors.textMuted}>{progressDots}</text>
693
+ </box>
694
+ </box>
695
+ <box height={1} flexDirection="row" width={contentWidth}>
696
+ <box flexGrow={1}>
697
+ {stepIdx > 0 && <text fg={colors.textMuted}>{"<- back"}</text>}
698
+ </box>
699
+ <box>
700
+ <text fg={colors.textMuted}>{hintText}</text>
701
+ </box>
702
+ </box>
703
+ </box>
704
+ </box>
705
+ );
706
+ }
707
+
708
+ // --- Step Components ---
709
+
710
+ function WelcomeStep() {
711
+ return (
712
+ <box flexDirection="column" paddingX={2}>
713
+ <box height={1} />
714
+ {LOGO.map((line, i) => (
715
+ <box key={i} height={1}>
716
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{line}</text>
717
+ </box>
718
+ ))}
719
+ <box height={2} />
720
+ <box height={1}>
721
+ <text fg={colors.textDim}>{"The open terminal for modern finance."}</text>
722
+ </box>
723
+ <box height={2} />
724
+ <box height={1}>
725
+ <text fg={colors.textMuted}>{"Let's set things up (~30s)."}</text>
726
+ </box>
727
+ <box height={2} />
728
+
729
+ </box>
730
+ );
731
+ }
732
+
733
+ function ThemeStep({ themeIds, selectedIdx, height }: { themeIds: string[]; selectedIdx: number; height: number }) {
734
+ const maxVisible = Math.min(themeIds.length, Math.max(6, height - 12));
735
+ const halfWindow = Math.floor(maxVisible / 2);
736
+ let windowStart = Math.max(0, Math.min(selectedIdx - halfWindow, themeIds.length - maxVisible));
737
+ if (windowStart < 0) windowStart = 0;
738
+ const windowEnd = Math.min(themeIds.length, windowStart + maxVisible);
739
+
740
+ return (
741
+ <box flexDirection="column" paddingX={2}>
742
+ <box height={1}>
743
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Theme"}</text>
744
+ </box>
745
+ <box height={1} />
746
+ <box height={1} flexDirection="row">
747
+ <text fg={colors.textDim}>{"Change it later from the command bar with "}</text>
748
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>{"TH"}</text>
749
+ </box>
750
+ <box height={1} />
751
+ <box height={1}>
752
+ <text fg={colors.positive}>{" \u2588\u2588 "}</text>
753
+ <text fg={colors.negative}>{" \u2588\u2588 "}</text>
754
+ <text fg={colors.text}>{" \u2588\u2588 "}</text>
755
+ <text fg={colors.textBright}>{" \u2588\u2588 "}</text>
756
+ <text fg={colors.borderFocused}>{" \u2588\u2588 "}</text>
757
+ <text fg={colors.textDim}>{" \u2588\u2588 "}</text>
758
+ </box>
759
+ <box height={1} />
760
+
761
+ {windowStart > 0 && (
762
+ <box height={1}>
763
+ <text fg={colors.textMuted}>{"\u2191 more"}</text>
764
+ </box>
765
+ )}
766
+
767
+ {themeIds.slice(windowStart, windowEnd).map((id, i) => {
768
+ const theme = themes[id]!;
769
+ const globalIdx = windowStart + i;
770
+ const isSel = globalIdx === selectedIdx;
771
+ return (
772
+ <box key={id} height={1} backgroundColor={isSel ? colors.selected : colors.bg}>
773
+ <text fg={isSel ? colors.selectedText : colors.textDim}>
774
+ {isSel ? "\u25b8 " : " "}
775
+ </text>
776
+ <text fg={isSel ? colors.text : colors.textDim} attributes={isSel ? TextAttributes.BOLD : 0}>
777
+ {theme.name}
778
+ </text>
779
+ </box>
780
+ );
781
+ })}
782
+
783
+ {windowEnd < themeIds.length && (
784
+ <box height={1}>
785
+ <text fg={colors.textMuted}>{"\u2193 more"}</text>
786
+ </box>
787
+ )}
788
+
789
+ <box height={1} />
790
+ <box height={1}>
791
+ <text fg={colors.textMuted}>{"Use \u2191\u2193 to browse"}</text>
792
+ </box>
793
+ </box>
794
+ );
795
+ }
796
+
797
+ function PortfolioStep({
798
+ sub,
799
+ choices,
800
+ optionIdx,
801
+ portfolioName,
802
+ onNameChange,
803
+ selectedBrokerId,
804
+ brokerFields,
805
+ brokerFieldIdx,
806
+ brokerSelectIdx,
807
+ brokerValues,
808
+ onBrokerFieldChange,
809
+ editing,
810
+ inputRef,
811
+ brokerSyncing,
812
+ brokerSyncError,
813
+ }: {
814
+ sub: PortfolioSub;
815
+ choices: Array<{ id: string; label: string; desc: string }>;
816
+ optionIdx: number;
817
+ portfolioName: string;
818
+ onNameChange: (n: string) => void;
819
+ selectedBrokerId: string | null;
820
+ brokerFields: BrokerConfigField[];
821
+ brokerFieldIdx: number;
822
+ brokerSelectIdx: number;
823
+ brokerValues: Record<string, Record<string, string>>;
824
+ onBrokerFieldChange: (brokerId: string, key: string, value: string) => void;
825
+ editing: boolean;
826
+ inputRef: RefObject<InputRenderable | null>;
827
+ brokerSyncing: boolean;
828
+ brokerSyncError: string | null;
829
+ }) {
830
+ if (sub === "choose") {
831
+ return (
832
+ <box flexDirection="column" paddingX={2}>
833
+ <box height={1}>
834
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Set up a portfolio"}</text>
835
+ </box>
836
+ <box height={1} />
837
+ <box height={1}>
838
+ <text fg={colors.textDim}>{"How would you like to get started?"}</text>
839
+ </box>
840
+ <box height={2} />
841
+
842
+ {choices.map((opt, i) => {
843
+ const isSel = i === optionIdx;
844
+ return (
845
+ <box key={opt.id} height={1} backgroundColor={isSel ? colors.selected : colors.bg}>
846
+ <text fg={isSel ? colors.selectedText : colors.textDim}>
847
+ {isSel ? "\u25b8 " : " "}
848
+ </text>
849
+ <text fg={isSel ? colors.text : colors.textDim} attributes={isSel ? TextAttributes.BOLD : 0}>
850
+ {opt.label}
851
+ </text>
852
+ </box>
853
+ );
854
+ })}
855
+
856
+ <box height={1} />
857
+ <box height={1}>
858
+ <text fg={colors.textDim}>{choices[optionIdx]?.desc}</text>
859
+ </box>
860
+
861
+ <box height={1} />
862
+ <box height={1}>
863
+ <text fg={colors.textDim}>{"Edits later from the command bar."}</text>
864
+ </box>
865
+ <box height={1} />
866
+ <box height={1}>
867
+ <text fg={colors.textMuted}>{"Use \u2191\u2193 to choose"}</text>
868
+ </box>
869
+ </box>
870
+ );
871
+ }
872
+
873
+ if (sub === "manual-name") {
874
+ return (
875
+ <box flexDirection="column" paddingX={2}>
876
+ <box height={1}>
877
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Name your portfolio"}</text>
878
+ </box>
879
+ <box height={1} />
880
+ <box height={1}>
881
+ <text fg={colors.textDim}>{"Create watchlists later."}</text>
882
+ </box>
883
+ <box height={2} />
884
+ <box height={1}>
885
+ <text fg={colors.text}>{"Portfolio name:"}</text>
886
+ </box>
887
+ <box height={1}>
888
+ {editing ? (
889
+ <TextField
890
+ inputRef={inputRef}
891
+ value={portfolioName}
892
+ placeholder="Main Portfolio"
893
+ focused
894
+ backgroundColor={colors.panel}
895
+ textColor={colors.text}
896
+ placeholderColor={colors.textDim}
897
+ onChange={onNameChange}
898
+ onSubmit={() => {}}
899
+ />
900
+ ) : (
901
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>
902
+ {`> ${portfolioName}`}
903
+ </text>
904
+ )}
905
+ </box>
906
+ <box height={2} />
907
+ <box height={1} flexDirection="row">
908
+ <text fg={colors.textDim}>{"After setup, use the command bar ("}</text>
909
+ <text fg={colors.text}>{"Ctrl+P"}</text>
910
+ <text fg={colors.textDim}>{") and"}</text>
911
+ </box>
912
+ <box height={1} flexDirection="row">
913
+ <text fg={colors.textDim}>{"type "}</text>
914
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>{"T AAPL"}</text>
915
+ <text fg={colors.textDim}>{" to search and add any stock or ETF."}</text>
916
+ </box>
917
+ </box>
918
+ );
919
+ }
920
+
921
+ if (sub === "broker-setup" && selectedBrokerId) {
922
+ const brokerLabel = choices.find((c) => c.id === selectedBrokerId)?.label.replace("Connect ", "") ?? selectedBrokerId;
923
+ const connectionMode = brokerValues[selectedBrokerId]?.connectionMode;
924
+ const isGateway = connectionMode === "gateway";
925
+
926
+ return (
927
+ <box flexDirection="column" paddingX={2}>
928
+ <box height={1}>
929
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>
930
+ {`Setup Guide — ${brokerLabel}`}
931
+ </text>
932
+ </box>
933
+ <box height={1} />
934
+
935
+ {selectedBrokerId === "ibkr" && !isGateway && (
936
+ <>
937
+ <box height={1}>
938
+ <text fg={colors.textDim}>{"You'll need 2 things from IBKR Account Management:"}</text>
939
+ </box>
940
+ <box height={2} />
941
+ <box height={1}>
942
+ <text fg={colors.textDim}>{"1. Go to "}<u><span fg={colors.text}>{"Reports > Flex Queries"}</span></u></text>
943
+ </box>
944
+ <box height={1}>
945
+ <text fg={colors.textDim}>{"2. Create a Flex Query that includes positions data"}</text>
946
+ </box>
947
+ <box height={1}>
948
+ <text fg={colors.textDim}>{"3. Note the "}<strong><span fg={colors.text}>{"Query ID"}</span></strong>{" (numeric)"}</text>
949
+ </box>
950
+ <box height={1}>
951
+ <text fg={colors.textDim}>{"4. Under "}<u><span fg={colors.text}>{"Reports > Settings"}</span></u>{", generate a "}<strong><span fg={colors.text}>{"Flex Web Service Token"}</span></strong></text>
952
+ </box>
953
+ <box height={2} />
954
+ <ExternalLink url="https://www.ibkrguides.com/orgportal/performanceandstatements/flex.htm" />
955
+ </>
956
+ )}
957
+
958
+ {selectedBrokerId === "ibkr" && isGateway && (
959
+ <>
960
+ <box height={1}>
961
+ <text fg={colors.textDim}>{"You'll need IB Gateway or TWS running locally:"}</text>
962
+ </box>
963
+ <box height={2} />
964
+ <box height={1}>
965
+ <text fg={colors.textDim}>{"1. Download and install "}<strong><span fg={colors.text}>{"IB Gateway"}</span></strong>{" (or use TWS)"}</text>
966
+ </box>
967
+ <box height={1}>
968
+ <text fg={colors.textDim}>{"2. Log in with your IBKR credentials"}</text>
969
+ </box>
970
+ <box height={1}>
971
+ <text fg={colors.textDim}>{"3. In "}<u><span fg={colors.text}>{"Configuration > API > Settings"}</span></u>{":"}</text>
972
+ </box>
973
+ <box height={1}>
974
+ <text fg={colors.textDim}>{" Enable \"ActiveX and Socket Clients\""}</text>
975
+ </box>
976
+ <box height={1}>
977
+ <text fg={colors.textDim}>{" Gloomberb can auto-detect local API ports (4001, 4002, 7496, 7497)"}</text>
978
+ </box>
979
+ <box height={1}>
980
+ <text fg={colors.textDim}>{" Use Manual setup only if you need a custom host or exact socket port"}</text>
981
+ </box>
982
+ <box height={1}>
983
+ <text fg={colors.textDim}>{"4. Keep it running while using Gloomberb"}</text>
984
+ </box>
985
+ <box height={2} />
986
+ <ExternalLink url="https://www.interactivebrokers.com/en/trading/ibgateway-stable.php" />
987
+ </>
988
+ )}
989
+
990
+ {selectedBrokerId !== "ibkr" && (
991
+ <>
992
+ <box height={1}>
993
+ <text fg={colors.textDim}>{`You'll need your ${brokerLabel} API credentials.`}</text>
994
+ </box>
995
+ <box height={1}>
996
+ <text fg={colors.textDim}>{"Check your broker's documentation for setup instructions."}</text>
997
+ </box>
998
+ </>
999
+ )}
1000
+
1001
+ <box height={2} />
1002
+
1003
+ </box>
1004
+ );
1005
+ }
1006
+
1007
+ if (sub === "broker-sync" && selectedBrokerId) {
1008
+ const brokerLabel = choices.find((choice) => choice.id === selectedBrokerId)?.label.replace("Connect ", "") ?? selectedBrokerId;
1009
+ return (
1010
+ <box flexDirection="column" paddingX={2}>
1011
+ <box height={1}>
1012
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{`Connect ${brokerLabel}`}</text>
1013
+ </box>
1014
+ <box height={1} />
1015
+ <box height={1}>
1016
+ <text fg={brokerSyncing ? colors.text : colors.negative}>
1017
+ {brokerSyncing
1018
+ ? `Connecting to ${brokerLabel} and importing accounts and positions...`
1019
+ : brokerSyncError || `Unable to sync ${brokerLabel}.`}
1020
+ </text>
1021
+ </box>
1022
+ <box height={2} />
1023
+ <box height={1}>
1024
+ <text fg={colors.textDim}>
1025
+ {brokerSyncing
1026
+ ? "This happens now so your portfolio is ready before onboarding finishes."
1027
+ : "Press Enter to retry, or Esc to edit the broker settings."}
1028
+ </text>
1029
+ </box>
1030
+ </box>
1031
+ );
1032
+ }
1033
+
1034
+ // broker-fields: dynamically render fields from the broker's configSchema
1035
+ if (!selectedBrokerId) return null;
1036
+ const currentField = brokerFields[brokerFieldIdx];
1037
+ const values = brokerValues[selectedBrokerId] ?? {};
1038
+
1039
+ return (
1040
+ <box flexDirection="column" paddingX={2}>
1041
+ <box height={1}>
1042
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>
1043
+ {"Connect "}{choices.find((c) => c.id === selectedBrokerId)?.label.replace("Connect ", "") ?? selectedBrokerId}
1044
+ </text>
1045
+ </box>
1046
+ <box height={1} />
1047
+
1048
+ {/* Show completed fields */}
1049
+ {brokerFields.map((field, i) => {
1050
+ if (i > brokerFieldIdx) return null;
1051
+ const val = values[field.key] ?? "";
1052
+ const isActive = i === brokerFieldIdx;
1053
+
1054
+ if (!isActive && val) {
1055
+ const selectedLabel = field.type === "select"
1056
+ ? field.options?.find((option) => option.value === val)?.label ?? val
1057
+ : field.type === "password" ? PASSWORD_MASK_CHAR.repeat(val.length) : val;
1058
+ return (
1059
+ <box key={field.key} height={1}>
1060
+ <text fg={colors.positive}>{"\u2713 "}</text>
1061
+ <text fg={colors.text}>{`${field.label}: ${selectedLabel}`}</text>
1062
+ </box>
1063
+ );
1064
+ }
1065
+
1066
+ if (isActive) {
1067
+ const activeSelectValue = field.options?.[brokerSelectIdx]?.value ?? values[field.key] ?? "";
1068
+ const effectiveValue = val || field.defaultValue || "";
1069
+ return (
1070
+ <box key={field.key} flexDirection="column">
1071
+ {i > 0 && <box height={1} />}
1072
+ <box height={1}>
1073
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>
1074
+ {`Step ${i + 1}: `}
1075
+ </text>
1076
+ <text fg={colors.text}>{field.label}</text>
1077
+ </box>
1078
+ <box height={1}>
1079
+ {field.type !== "select" && (editing ? (
1080
+ <TextField
1081
+ inputRef={inputRef}
1082
+ value={val}
1083
+ type={field.type === "password" ? "password" : "text"}
1084
+ placeholder={field.placeholder || `Enter ${field.label.toLowerCase()}`}
1085
+ focused
1086
+ backgroundColor={colors.panel}
1087
+ textColor={colors.text}
1088
+ placeholderColor={colors.textDim}
1089
+ onChange={(nextValue) => onBrokerFieldChange(selectedBrokerId, field.key, nextValue)}
1090
+ onSubmit={() => {}}
1091
+ />
1092
+ ) : (
1093
+ <text fg={effectiveValue ? colors.positive : colors.textMuted}>
1094
+ {effectiveValue
1095
+ ? `\u2713 ${field.label}: ${field.type === "password" ? PASSWORD_MASK_CHAR.repeat(effectiveValue.length) : effectiveValue}`
1096
+ : "Press enter to type..."}
1097
+ </text>
1098
+ ))}
1099
+ </box>
1100
+ {field.type !== "select" && !val && field.defaultValue && (
1101
+ <box height={1}>
1102
+ <text fg={colors.textMuted}>{`Press enter to use ${field.defaultValue}`}</text>
1103
+ </box>
1104
+ )}
1105
+ {field.type === "select" && (
1106
+ <box flexDirection="column">
1107
+ {(field.options ?? []).map((option, optionIdx) => {
1108
+ const selected = optionIdx === brokerSelectIdx;
1109
+ return (
1110
+ <box key={option.value} flexDirection="column" backgroundColor={selected ? colors.selected : colors.bg}>
1111
+ <box height={1}>
1112
+ <text fg={selected ? colors.selectedText : colors.textDim}>{selected ? "\u25b8 " : " "}</text>
1113
+ <text
1114
+ fg={selected ? colors.text : colors.textDim}
1115
+ attributes={selected ? TextAttributes.BOLD : 0}
1116
+ >
1117
+ {option.label}
1118
+ </text>
1119
+ </box>
1120
+ {option.description && (
1121
+ <box height={1}>
1122
+ <text fg={colors.textMuted}>{` ${option.description}`}</text>
1123
+ </box>
1124
+ )}
1125
+ </box>
1126
+ );
1127
+ })}
1128
+ </box>
1129
+ )}
1130
+ {field.type === "select" && (
1131
+ <box height={1}>
1132
+ <text fg={colors.textMuted}>
1133
+ {activeSelectValue ? `Selected: ${field.options?.find((option) => option.value === activeSelectValue)?.label ?? activeSelectValue}` : "Use \u2191\u2193 to choose"}
1134
+ </text>
1135
+ </box>
1136
+ )}
1137
+ </box>
1138
+ );
1139
+ }
1140
+
1141
+ return null;
1142
+ })}
1143
+
1144
+ <box height={2} />
1145
+ <box height={1}>
1146
+ <text fg={colors.textDim}>{"Credentials are saved locally."}</text>
1147
+ </box>
1148
+ <box height={1} />
1149
+ <box height={1}>
1150
+ <text fg={colors.textMuted}>
1151
+ {`Field ${brokerFieldIdx + 1} of ${brokerFields.length}`}
1152
+ </text>
1153
+ </box>
1154
+ </box>
1155
+ );
1156
+ }
1157
+
1158
+ function PluginsStep({
1159
+ plugins,
1160
+ disabledPlugins,
1161
+ selectedIdx,
1162
+ onToggle,
1163
+ onSelect,
1164
+ }: {
1165
+ plugins: { id: string; name: string; description: string }[];
1166
+ disabledPlugins: string[];
1167
+ selectedIdx: number;
1168
+ onToggle: (id: string) => void;
1169
+ onSelect: (idx: number) => void;
1170
+ }) {
1171
+ const items: ToggleListItem[] = plugins.map((p) => ({
1172
+ id: p.id,
1173
+ label: p.name,
1174
+ enabled: !disabledPlugins.includes(p.id),
1175
+ description: p.description,
1176
+ }));
1177
+
1178
+ return (
1179
+ <box flexDirection="column" paddingX={2}>
1180
+ <box height={1}>
1181
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Select plugins to enable"}</text>
1182
+ </box>
1183
+
1184
+ <box height={2} />
1185
+
1186
+ <ToggleList
1187
+ items={items}
1188
+ selectedIdx={selectedIdx}
1189
+ onToggle={onToggle}
1190
+ onSelect={onSelect}
1191
+ />
1192
+
1193
+ <box height={1} />
1194
+ <box height={1}>
1195
+ <text fg={colors.textMuted}>{"Use \u2191\u2193 to navigate \u00b7 space to toggle"}</text>
1196
+ </box>
1197
+ <box height={1} />
1198
+ <box height={1}>
1199
+ <text fg={colors.textDim}>{"Toggle plugins anytime from the command bar"}</text>
1200
+ </box>
1201
+ <box height={1} flexDirection="row">
1202
+ <text fg={colors.textDim}>{"with the "}</text>
1203
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>{"PL"}</text>
1204
+ <text fg={colors.textDim}>{" prefix."}</text>
1205
+ </box>
1206
+ </box>
1207
+ );
1208
+ }
1209
+
1210
+ function ShortcutsStep({
1211
+ pluginRegistry,
1212
+ disabledPlugins,
1213
+ }: {
1214
+ pluginRegistry: PluginRegistry;
1215
+ disabledPlugins: string[];
1216
+ }) {
1217
+ const keyboardShortcuts = [
1218
+ { key: "Ctrl+P / `", desc: "Open the command bar" },
1219
+ { key: "Tab", desc: "Switch between panels" },
1220
+ { key: "Ctrl+W", desc: "Close the current window" },
1221
+ { key: "q", desc: "Quit" },
1222
+ ];
1223
+
1224
+ const disabledSet = useMemo(() => new Set(disabledPlugins), [disabledPlugins]);
1225
+
1226
+ const commandPrefixes = useMemo(() => {
1227
+ const builtIn = [
1228
+ { key: "T AAPL", desc: "Search and add any ticker" },
1229
+ { key: "TH", desc: "Switch theme" },
1230
+ { key: "PL", desc: "Toggle plugins" },
1231
+ { key: "PS", desc: "Edit the current window settings" },
1232
+ { key: "HELP", desc: "Open the help window" },
1233
+ ];
1234
+
1235
+ const builtInKeys = new Set(builtIn.map((b) => b.key.split(" ")[0]));
1236
+ const pluginPrefixes: { key: string; desc: string }[] = [];
1237
+
1238
+ for (const [, template] of pluginRegistry.paneTemplates) {
1239
+ if (!template.shortcut) continue;
1240
+ const pluginId = pluginRegistry.getPaneTemplatePluginId(template.id);
1241
+ if (pluginId && disabledSet.has(pluginId)) continue;
1242
+ if (builtInKeys.has(template.shortcut.prefix)) continue;
1243
+ const label = template.shortcut.argPlaceholder
1244
+ ? `${template.shortcut.prefix} <${template.shortcut.argPlaceholder}>`
1245
+ : template.shortcut.prefix;
1246
+ pluginPrefixes.push({ key: label, desc: template.label });
1247
+ }
1248
+
1249
+ pluginPrefixes.sort((a, b) => a.key.localeCompare(b.key));
1250
+ return [...builtIn, ...pluginPrefixes];
1251
+ }, [pluginRegistry, disabledSet]);
1252
+
1253
+ const COL = 14;
1254
+
1255
+ return (
1256
+ <box flexDirection="column" paddingX={2}>
1257
+ <box height={1}>
1258
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Useful shortcuts"}</text>
1259
+ </box>
1260
+ <box height={1} />
1261
+
1262
+ {keyboardShortcuts.map((s) => (
1263
+ <box key={s.key} height={1} flexDirection="row">
1264
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>{s.key.padEnd(COL)}</text>
1265
+ <text fg={colors.textDim}>{s.desc}</text>
1266
+ </box>
1267
+ ))}
1268
+
1269
+ <box height={2} />
1270
+ <box height={1}>
1271
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Command-bar prefixes"}</text>
1272
+ </box>
1273
+ <box height={1} />
1274
+ <box height={1}>
1275
+ <text fg={colors.textDim}>{"Type these in the command bar ("}<span fg={colors.text}><strong>{"Ctrl+P"}</strong></span>{" or "}<span fg={colors.text}><strong>{"`"}</strong></span>{"):"}</text>
1276
+ </box>
1277
+ <box height={1} />
1278
+
1279
+ {commandPrefixes.map((s) => (
1280
+ <box key={s.key} height={1} flexDirection="row">
1281
+ <text fg={colors.text} attributes={TextAttributes.BOLD}>{s.key.padEnd(COL)}</text>
1282
+ <text fg={colors.textDim}>{s.desc}</text>
1283
+ </box>
1284
+ ))}
1285
+
1286
+ <box height={2} />
1287
+ <box height={1}>
1288
+ <text fg={colors.textDim}>{"Everything is searchable, just type what you want."}</text>
1289
+ </box>
1290
+ </box>
1291
+ );
1292
+ }
1293
+
1294
+ function ReadyStep({
1295
+ brokerName,
1296
+ portfolioName,
1297
+ brokerSyncSummary,
1298
+ isFinishing,
1299
+ error,
1300
+ }: {
1301
+ brokerName: string | null;
1302
+ portfolioName: string;
1303
+ brokerSyncSummary: BrokerSyncSummary | null;
1304
+ isFinishing: boolean;
1305
+ error: string | null;
1306
+ }) {
1307
+ const positionsImported = brokerSyncSummary?.positionsImported ?? 0;
1308
+ const positionLabel = positionsImported === 1 ? "position" : "positions";
1309
+
1310
+ return (
1311
+ <box flexDirection="column" paddingX={2}>
1312
+ <box height={2} />
1313
+ <box height={1}>
1314
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"You're all set"}</text>
1315
+ </box>
1316
+ <box height={2} />
1317
+ <box height={1}>
1318
+ <text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</text>
1319
+ <text fg={colors.text}>{" Theme configured"}</text>
1320
+ </box>
1321
+ <box height={1}>
1322
+ <text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</text>
1323
+ <text fg={colors.text}>
1324
+ {brokerName
1325
+ ? ` ${brokerName} connected. Imported ${positionsImported} ${positionLabel}`
1326
+ : ` Portfolio "${portfolioName}" created`}
1327
+ </text>
1328
+ </box>
1329
+ <box height={1}>
1330
+ <text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</text>
1331
+ <text fg={colors.text}>{" Plugins selected"}</text>
1332
+ </box>
1333
+ <box height={2} />
1334
+ {brokerName ? (
1335
+ <box height={1}>
1336
+ <text fg={isFinishing ? colors.text : colors.textDim}>
1337
+ {isFinishing
1338
+ ? "Launching Gloomberb..."
1339
+ : positionsImported > 0
1340
+ ? "Your broker portfolio is ready and will open directly after launch."
1341
+ : "Broker sync finished. If you expected holdings, check the selected account or connection mode."}
1342
+ </text>
1343
+ </box>
1344
+ ) : (
1345
+ <box height={1}>
1346
+ <text fg={colors.textDim}>{"Search for broker names in the command bar to connect."}</text>
1347
+ </box>
1348
+ )}
1349
+ {error && (
1350
+ <>
1351
+ <box height={1} />
1352
+ <box height={2}>
1353
+ <text fg={colors.negative}>{error}</text>
1354
+ </box>
1355
+ </>
1356
+ )}
1357
+ <box height={2} />
1358
+ <box height={1} flexDirection="row">
1359
+ <text fg={colors.textDim}>{"Data stored in "}</text>
1360
+ <text fg={colors.text}>{"~/gloomberb/"}</text>
1361
+ </box>
1362
+ <box height={1} />
1363
+ </box>
1364
+ );
1365
+ }