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,371 @@
1
+ import type {
2
+ CommandDef,
3
+ PaneSettingField,
4
+ PaneTemplateDef,
5
+ WizardStep,
6
+ } from "../../types/plugin";
7
+ import type {
8
+ CommandBarFieldOption,
9
+ CommandBarFieldValue,
10
+ CommandBarRoute,
11
+ CommandBarWorkflowField,
12
+ } from "./workflow-types";
13
+ import type { CollectionKind, CollectionMembershipAction } from "./workflow-ops";
14
+
15
+ export type RouteCommandId = "search-ticker" | "theme" | "plugins" | "layout" | "new-pane";
16
+ export type CollectionCommandId = "add-watchlist" | "add-portfolio" | "remove-watchlist" | "remove-portfolio";
17
+
18
+ export function summarizeError(error: unknown): Record<string, string> {
19
+ if (error instanceof Error) {
20
+ return {
21
+ name: error.name,
22
+ message: error.message,
23
+ stack: error.stack || "",
24
+ };
25
+ }
26
+ return { message: String(error) };
27
+ }
28
+
29
+ export function isRouteCommandId(commandId: string): commandId is RouteCommandId {
30
+ return commandId === "search-ticker"
31
+ || commandId === "theme"
32
+ || commandId === "plugins"
33
+ || commandId === "layout"
34
+ || commandId === "new-pane";
35
+ }
36
+
37
+ export function normalizeFieldOptions(
38
+ options: Array<{ label: string; value: string; description?: string }> | undefined,
39
+ ): CommandBarFieldOption[] {
40
+ return (options ?? []).map((option) => ({
41
+ label: option.label,
42
+ value: option.value,
43
+ description: option.description,
44
+ }));
45
+ }
46
+
47
+ export function getVisibleWorkflowFields(
48
+ fields: CommandBarWorkflowField[],
49
+ values: Record<string, CommandBarFieldValue>,
50
+ ): CommandBarWorkflowField[] {
51
+ return fields.filter((field) => {
52
+ if (!field.dependsOn || field.dependsOn.length === 0) return true;
53
+ return field.dependsOn.every((dependency) => String(values[dependency.key] ?? "") === dependency.value);
54
+ });
55
+ }
56
+
57
+ export function coerceFieldString(value: CommandBarFieldValue | undefined): string {
58
+ return typeof value === "string" ? value : "";
59
+ }
60
+
61
+ export function coerceFieldBoolean(value: CommandBarFieldValue | undefined): boolean {
62
+ return value === true;
63
+ }
64
+
65
+ export function coerceFieldValues(value: CommandBarFieldValue | undefined): string[] {
66
+ return Array.isArray(value)
67
+ ? value.filter((entry): entry is string => typeof entry === "string")
68
+ : [];
69
+ }
70
+
71
+ export function fieldOptionLabel(field: CommandBarWorkflowField, value: string): string {
72
+ if (field.type !== "select" && field.type !== "multi-select" && field.type !== "ordered-multi-select") {
73
+ return value;
74
+ }
75
+ return field.options.find((option) => option.value === value)?.label ?? value;
76
+ }
77
+
78
+ export function summarizeWorkflowFieldValue(
79
+ field: CommandBarWorkflowField,
80
+ value: CommandBarFieldValue | undefined,
81
+ ): string {
82
+ switch (field.type) {
83
+ case "toggle":
84
+ return coerceFieldBoolean(value) ? "On" : "Off";
85
+ case "text":
86
+ case "password":
87
+ case "number": {
88
+ const next = coerceFieldString(value).trim();
89
+ return next.length > 0 ? next : "Unset";
90
+ }
91
+ case "textarea": {
92
+ const next = coerceFieldString(value).replace(/\s+/g, " ").trim();
93
+ return next.length > 0 ? next : "Unset";
94
+ }
95
+ case "select": {
96
+ const selected = coerceFieldString(value);
97
+ return selected ? fieldOptionLabel(field, selected) : "Choose…";
98
+ }
99
+ case "multi-select":
100
+ case "ordered-multi-select": {
101
+ const selected = coerceFieldValues(value);
102
+ if (selected.length === 0) return "None";
103
+ const labels = selected.map((entry) => fieldOptionLabel(field, entry)).slice(0, 3);
104
+ const suffix = selected.length > 3 ? ` +${selected.length - 3}` : "";
105
+ return `${labels.join(", ")}${suffix}`;
106
+ }
107
+ default:
108
+ return "";
109
+ }
110
+ }
111
+
112
+ export function summarizePaneSettingValue(field: PaneSettingField, value: unknown): string {
113
+ switch (field.type) {
114
+ case "toggle":
115
+ return value === true ? "On" : "Off";
116
+ case "text":
117
+ return typeof value === "string" && value.trim().length > 0 ? value : "Unset";
118
+ case "select": {
119
+ const option = field.options.find((entry) => entry.value === value);
120
+ return option?.label ?? "Unset";
121
+ }
122
+ case "multi-select":
123
+ case "ordered-multi-select": {
124
+ const selected = Array.isArray(value)
125
+ ? value.filter((entry): entry is string => typeof entry === "string")
126
+ : [];
127
+ if (selected.length === 0) return "None";
128
+ const labels = selected
129
+ .map((entry) => field.options.find((option) => option.value === entry)?.label ?? entry)
130
+ .slice(0, 3);
131
+ const suffix = selected.length > 3 ? ` +${selected.length - 3}` : "";
132
+ return `${labels.join(", ")}${suffix}`;
133
+ }
134
+ default:
135
+ return "";
136
+ }
137
+ }
138
+
139
+ export function toggleSelectedValue(currentValues: string[], value: string): string[] {
140
+ return currentValues.includes(value)
141
+ ? currentValues.filter((entry) => entry !== value)
142
+ : [...currentValues, value];
143
+ }
144
+
145
+ export function moveSelectedValue(
146
+ field: { options: CommandBarFieldOption[] },
147
+ currentValues: string[],
148
+ selectedOption: string,
149
+ direction: "up" | "down",
150
+ ): string[] {
151
+ if (!currentValues.includes(selectedOption)) return currentValues;
152
+
153
+ const optionValueSet = new Set(field.options.map((option) => option.value));
154
+ const ordered = currentValues.filter((value) => optionValueSet.has(value));
155
+ const index = ordered.indexOf(selectedOption);
156
+ if (index < 0) return currentValues;
157
+
158
+ const targetIndex = direction === "up"
159
+ ? Math.max(0, index - 1)
160
+ : Math.min(ordered.length - 1, index + 1);
161
+ if (targetIndex === index) return currentValues;
162
+
163
+ const next = [...ordered];
164
+ const [entry] = next.splice(index, 1);
165
+ next.splice(targetIndex, 0, entry!);
166
+ const unknownValues = currentValues.filter((value) => !optionValueSet.has(value));
167
+ return [...next, ...unknownValues];
168
+ }
169
+
170
+ export function normalizeWizardFields(steps: WizardStep[]): {
171
+ fields: CommandBarWorkflowField[];
172
+ description: string[];
173
+ initialValues: Record<string, CommandBarFieldValue>;
174
+ pendingLabel?: string;
175
+ successLabel?: string;
176
+ } {
177
+ const fields: CommandBarWorkflowField[] = [];
178
+ const description: string[] = [];
179
+ const initialValues: Record<string, CommandBarFieldValue> = {};
180
+ let pendingLabel: string | undefined;
181
+ let successLabel: string | undefined;
182
+
183
+ for (const step of steps) {
184
+ if (step.type === "info") {
185
+ if (step.key.startsWith("_validate")) {
186
+ pendingLabel = step.body?.[0] || step.label;
187
+ successLabel = step.body?.[1];
188
+ } else if (step.body) {
189
+ description.push(...step.body);
190
+ }
191
+ continue;
192
+ }
193
+
194
+ const type = step.type === "password"
195
+ ? "password"
196
+ : step.type === "number"
197
+ ? "number"
198
+ : step.type === "textarea"
199
+ ? "textarea"
200
+ : step.type === "select"
201
+ ? "select"
202
+ : "text";
203
+ if (type === "select") {
204
+ fields.push({
205
+ id: step.key,
206
+ label: step.label,
207
+ type,
208
+ options: normalizeFieldOptions(step.options),
209
+ placeholder: step.placeholder,
210
+ description: step.body?.[0],
211
+ required: true,
212
+ dependsOn: step.dependsOn ? [{ key: step.dependsOn.key, value: step.dependsOn.value }] : undefined,
213
+ });
214
+ if (step.defaultValue) {
215
+ initialValues[step.key] = step.defaultValue;
216
+ } else if (step.options?.[0]?.value) {
217
+ initialValues[step.key] = step.options[0].value;
218
+ }
219
+ continue;
220
+ }
221
+
222
+ fields.push({
223
+ id: step.key,
224
+ label: step.label,
225
+ type,
226
+ placeholder: step.placeholder,
227
+ description: step.body?.[0],
228
+ required: true,
229
+ dependsOn: step.dependsOn ? [{ key: step.dependsOn.key, value: step.dependsOn.value }] : undefined,
230
+ });
231
+ if (step.defaultValue) {
232
+ initialValues[step.key] = step.defaultValue;
233
+ }
234
+ }
235
+
236
+ return { fields, description, initialValues, pendingLabel, successLabel };
237
+ }
238
+
239
+ export function routeCommandIdToScreen(commandId: RouteCommandId): "ticker-search" | "themes" | "plugins" | "layout" | "new-pane" {
240
+ switch (commandId) {
241
+ case "search-ticker":
242
+ return "ticker-search";
243
+ case "theme":
244
+ return "themes";
245
+ case "plugins":
246
+ return "plugins";
247
+ case "layout":
248
+ return "layout";
249
+ case "new-pane":
250
+ return "new-pane";
251
+ }
252
+ }
253
+
254
+ export function slugifyName(name: string, fallbackPrefix: string): string {
255
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `${fallbackPrefix}-${Date.now()}`;
256
+ }
257
+
258
+ export function getFirstVisibleFieldId(
259
+ fields: CommandBarWorkflowField[],
260
+ values: Record<string, CommandBarFieldValue>,
261
+ ): string | null {
262
+ return getVisibleWorkflowFields(fields, values)[0]?.id ?? null;
263
+ }
264
+
265
+ export function isWorkflowTextField(field: CommandBarWorkflowField | undefined): boolean {
266
+ return field?.type === "text"
267
+ || field?.type === "password"
268
+ || field?.type === "number"
269
+ || field?.type === "textarea";
270
+ }
271
+
272
+ export function buildGeneratedTemplateField(
273
+ template: PaneTemplateDef,
274
+ activeTicker: string | null,
275
+ ): {
276
+ field: CommandBarWorkflowField | null;
277
+ initialValue: CommandBarFieldValue | undefined;
278
+ } {
279
+ const placeholder = template.shortcut?.argPlaceholder;
280
+ if (placeholder === "ticker") {
281
+ return {
282
+ field: {
283
+ id: "ticker",
284
+ label: "Ticker",
285
+ type: "text",
286
+ required: true,
287
+ placeholder: activeTicker || "MSFT",
288
+ },
289
+ initialValue: activeTicker ?? "",
290
+ };
291
+ }
292
+ if (placeholder === "tickers") {
293
+ return {
294
+ field: {
295
+ id: "tickers",
296
+ label: "Tickers",
297
+ type: "text",
298
+ required: true,
299
+ placeholder: "AAPL, MSFT, NVDA",
300
+ },
301
+ initialValue: "",
302
+ };
303
+ }
304
+ return { field: null, initialValue: undefined };
305
+ }
306
+
307
+ export function getScreenFooterLeft(route: CommandBarRoute | null): string {
308
+ if (!route) return "up/down move enter select";
309
+ switch (route.kind) {
310
+ case "mode":
311
+ if (route.screen === "plugins") return "up/down move enter select space toggle";
312
+ return "up/down move enter select";
313
+ case "picker":
314
+ if (route.pickerId === "field-multi-select") {
315
+ const ordered = route.payload?.fieldType === "ordered-multi-select";
316
+ return ordered ? "up/down move space toggle [ ] reorder enter done" : "up/down move space toggle enter done";
317
+ }
318
+ return "up/down move enter select";
319
+ case "pane-settings":
320
+ return "up/down move enter edit";
321
+ case "workflow":
322
+ return "tab move enter act";
323
+ case "confirm":
324
+ return "enter confirm esc cancel";
325
+ default:
326
+ return "up/down move enter select";
327
+ }
328
+ }
329
+
330
+ export function getScreenFooterRight(route: CommandBarRoute | null): string {
331
+ return route ? "esc back" : "esc close";
332
+ }
333
+
334
+ export function isRootParsedCommand(commandId: string): boolean {
335
+ return commandId === "search-ticker"
336
+ || commandId === "add-watchlist"
337
+ || commandId === "add-portfolio"
338
+ || commandId === "remove-watchlist"
339
+ || commandId === "remove-portfolio";
340
+ }
341
+
342
+ export function isCollectionCommand(commandId: string): commandId is CollectionCommandId {
343
+ return commandId === "add-watchlist"
344
+ || commandId === "add-portfolio"
345
+ || commandId === "remove-watchlist"
346
+ || commandId === "remove-portfolio";
347
+ }
348
+
349
+ export function getCollectionCommandKind(commandId: CollectionCommandId): CollectionKind {
350
+ return commandId === "add-watchlist" || commandId === "remove-watchlist" ? "watchlist" : "portfolio";
351
+ }
352
+
353
+ export function getCollectionCommandAction(commandId: CollectionCommandId): CollectionMembershipAction {
354
+ return commandId === "add-watchlist" || commandId === "add-portfolio" ? "add" : "remove";
355
+ }
356
+
357
+ export function getCollectionCommandVerb(action: CollectionMembershipAction): string {
358
+ return action === "add" ? "Add" : "Remove";
359
+ }
360
+
361
+ export function looksDestructiveCommand(
362
+ command: Pick<CommandDef, "id" | "label" | "description" | "keywords">,
363
+ ): boolean {
364
+ const haystack = [
365
+ command.id,
366
+ command.label,
367
+ command.description || "",
368
+ ...(command.keywords || []),
369
+ ].join(" ").toLowerCase();
370
+ return /\b(delete|remove|disconnect|reset|close)\b/.test(haystack);
371
+ }
@@ -0,0 +1,158 @@
1
+ import type { PaneTemplateDef } from "../../types/plugin";
2
+ import type { Command } from "./command-registry";
3
+
4
+ export type RootShortcutArgKind = "text" | "ticker" | "ticker-list";
5
+ export type ShortcutIntentKind = "none" | "complete" | "inferred-complete" | "partial" | "ambiguous";
6
+
7
+ interface ShortcutIntentBase {
8
+ kind: Exclude<ShortcutIntentKind, "none">;
9
+ prefix: string;
10
+ label: string;
11
+ description: string;
12
+ argKind: RootShortcutArgKind | null;
13
+ argPlaceholder?: string;
14
+ argText: string;
15
+ completionQuery: string | null;
16
+ }
17
+
18
+ export interface CommandShortcutIntent extends ShortcutIntentBase {
19
+ source: "command";
20
+ command: Command;
21
+ }
22
+
23
+ export interface PaneTemplateShortcutIntent extends ShortcutIntentBase {
24
+ source: "pane-template";
25
+ template: PaneTemplateDef;
26
+ }
27
+
28
+ export type ShortcutIntent =
29
+ | { kind: "none" }
30
+ | CommandShortcutIntent
31
+ | PaneTemplateShortcutIntent;
32
+
33
+ interface ShortcutParseCandidate {
34
+ prefix: string;
35
+ label: string;
36
+ description: string;
37
+ argKind: RootShortcutArgKind | null;
38
+ argPlaceholder?: string;
39
+ source: "command" | "pane-template";
40
+ command?: Command;
41
+ template?: PaneTemplateDef;
42
+ }
43
+
44
+ function normalizeShortcutPrefix(value: string): string {
45
+ return value.trim().toUpperCase();
46
+ }
47
+
48
+ function mapPlaceholderToArgKind(value: string | undefined): RootShortcutArgKind | null {
49
+ if (value === "ticker") return "ticker";
50
+ if (value === "tickers") return "ticker-list";
51
+ if (value) return "text";
52
+ return null;
53
+ }
54
+
55
+ export function getPaneShortcutArgKind(template: PaneTemplateDef): RootShortcutArgKind | null {
56
+ return template.shortcut?.argKind ?? mapPlaceholderToArgKind(template.shortcut?.argPlaceholder);
57
+ }
58
+
59
+ export function getCommandShortcutArgKind(command: Command): RootShortcutArgKind | null {
60
+ return mapPlaceholderToArgKind(command.argPlaceholder) ?? (command.hasArg ? "text" : null);
61
+ }
62
+
63
+ function inferShortcutArg(argKind: RootShortcutArgKind | null, activeTicker: string | null): string | null {
64
+ if (!activeTicker) return null;
65
+ if (argKind === "ticker" || argKind === "ticker-list") {
66
+ return activeTicker;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function buildShortcutCandidates(
72
+ commands: Command[],
73
+ paneTemplates: PaneTemplateDef[],
74
+ ): ShortcutParseCandidate[] {
75
+ return [
76
+ ...commands
77
+ .filter((command) => command.prefix.trim().length > 0)
78
+ .map((command) => ({
79
+ prefix: normalizeShortcutPrefix(command.prefix),
80
+ label: command.label,
81
+ description: command.description,
82
+ argKind: getCommandShortcutArgKind(command),
83
+ argPlaceholder: command.argPlaceholder,
84
+ source: "command" as const,
85
+ command,
86
+ })),
87
+ ...paneTemplates
88
+ .filter((template) => template.shortcut?.prefix)
89
+ .map((template) => ({
90
+ prefix: normalizeShortcutPrefix(template.shortcut!.prefix),
91
+ label: template.label,
92
+ description: template.description,
93
+ argKind: getPaneShortcutArgKind(template),
94
+ argPlaceholder: template.shortcut?.argPlaceholder,
95
+ source: "pane-template" as const,
96
+ template,
97
+ })),
98
+ ].sort((a, b) => b.prefix.length - a.prefix.length);
99
+ }
100
+
101
+ export function parseRootShortcutIntent({
102
+ query,
103
+ commands,
104
+ paneTemplates,
105
+ activeTicker,
106
+ }: {
107
+ query: string;
108
+ commands: Command[];
109
+ paneTemplates: PaneTemplateDef[];
110
+ activeTicker: string | null;
111
+ }): ShortcutIntent {
112
+ const trimmed = query.trim();
113
+ if (!trimmed) return { kind: "none" };
114
+
115
+ const upper = trimmed.toUpperCase();
116
+ const match = buildShortcutCandidates(commands, paneTemplates).find((candidate) => (
117
+ upper === candidate.prefix || upper.startsWith(`${candidate.prefix} `)
118
+ ));
119
+ if (!match) return { kind: "none" };
120
+
121
+ const argText = trimmed.slice(match.prefix.length).trim();
122
+ const inferredArg = inferShortcutArg(match.argKind, activeTicker);
123
+ const completionQuery = inferredArg ? `${match.prefix} ${inferredArg}` : null;
124
+
125
+ let kind: Exclude<ShortcutIntentKind, "none">;
126
+ if (argText.length > 0) {
127
+ kind = match.argKind === "ticker-list" && /[,\n]\s*$/.test(trimmed) ? "partial" : "complete";
128
+ } else if (completionQuery) {
129
+ kind = "inferred-complete";
130
+ } else {
131
+ kind = "partial";
132
+ }
133
+
134
+ const base: ShortcutIntentBase = {
135
+ kind,
136
+ prefix: match.prefix,
137
+ label: match.label,
138
+ description: match.description,
139
+ argKind: match.argKind,
140
+ argPlaceholder: match.argPlaceholder,
141
+ argText,
142
+ completionQuery,
143
+ };
144
+
145
+ if (match.source === "command" && match.command) {
146
+ return {
147
+ ...base,
148
+ source: "command",
149
+ command: match.command,
150
+ };
151
+ }
152
+
153
+ return {
154
+ ...base,
155
+ source: "pane-template",
156
+ template: match.template!,
157
+ };
158
+ }
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildSections,
4
+ getEmptyState,
5
+ getFooterHints,
6
+ getRowPresentation,
7
+ rankTickerSearchItems,
8
+ resolveCommandBarMode,
9
+ } from "./view-model";
10
+
11
+ describe("command bar view model helpers", () => {
12
+ test("resolves prefix-driven modes", () => {
13
+ expect(resolveCommandBarMode("")).toMatchObject({ kind: "default", badge: "BROWSE" });
14
+ expect(resolveCommandBarMode("T NVDA")).toMatchObject({ kind: "search", badge: "SEARCH" });
15
+ expect(resolveCommandBarMode("TH ")).toMatchObject({ kind: "themes", badge: "THEMES" });
16
+ expect(resolveCommandBarMode("PL notes")).toMatchObject({ kind: "plugins", badge: "PLUGINS" });
17
+ expect(resolveCommandBarMode("LAY ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
18
+ expect(resolveCommandBarMode("NP ")).toMatchObject({ kind: "new-pane", badge: "NEW PANE" });
19
+ expect(resolveCommandBarMode("PS")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
20
+ expect(resolveCommandBarMode("AW")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
21
+ });
22
+
23
+ test("builds sections while preserving order", () => {
24
+ const sections = buildSections([
25
+ { id: "a", category: "Tickers" },
26
+ { id: "b", category: "Commands" },
27
+ { id: "c", category: "Tickers" },
28
+ ]);
29
+
30
+ expect(sections.map((section) => section.category)).toEqual(["Tickers", "Commands"]);
31
+ expect(sections[0]?.items.map((item) => item.id)).toEqual(["a", "c"]);
32
+ });
33
+
34
+ test("moves danger and debug sections to the end", () => {
35
+ const sections = buildSections([
36
+ { id: "a", category: "Tickers" },
37
+ { id: "b", category: "Danger" },
38
+ { id: "c", category: "Debug" },
39
+ { id: "d", category: "Config" },
40
+ ]);
41
+
42
+ expect(sections.map((section) => section.category)).toEqual(["Tickers", "Config", "Danger", "Debug"]);
43
+ });
44
+
45
+ test("returns footer hints for plugin toggles", () => {
46
+ expect(getFooterHints("plugins", false)).toEqual({
47
+ left: "up/down move enter select space toggle",
48
+ right: "esc close",
49
+ });
50
+ expect(getFooterHints("layout", false)).toEqual({
51
+ left: "up/down move enter select",
52
+ right: "esc close",
53
+ });
54
+ expect(getFooterHints("new-pane", false)).toEqual({
55
+ left: "up/down move enter select",
56
+ right: "esc close",
57
+ });
58
+ });
59
+
60
+ test("returns specific empty states", () => {
61
+ expect(getEmptyState("search", "T ", "")).toEqual({
62
+ label: "Type a ticker symbol",
63
+ detail: "Search Yahoo Finance and connected brokers",
64
+ });
65
+ expect(getEmptyState("search", "T zom", "zom")).toEqual({
66
+ label: 'No matches for "zom"',
67
+ detail: "Try a symbol, company name, or exchange variant",
68
+ });
69
+ expect(getEmptyState("default", "abc")).toEqual({
70
+ label: 'No matches for "abc"',
71
+ detail: "Try a ticker, command name, or prefix",
72
+ });
73
+ expect(getEmptyState("layout", "LAY ")).toEqual({
74
+ label: "No layout actions match",
75
+ detail: "LAY",
76
+ });
77
+ expect(getEmptyState("new-pane", "NP ")).toEqual({
78
+ label: "No pane templates match",
79
+ detail: "NP",
80
+ });
81
+ });
82
+
83
+ test("derives row presentation for toggles and current theme rows", () => {
84
+ expect(getRowPresentation({
85
+ id: "plugin:news",
86
+ label: "News",
87
+ detail: "Latest headlines",
88
+ category: "Plugins",
89
+ kind: "plugin",
90
+ checked: true,
91
+ }, false, true)).toMatchObject({
92
+ glyph: " ",
93
+ trailing: "on",
94
+ primaryMuted: false,
95
+ });
96
+
97
+ expect(getRowPresentation({
98
+ id: "theme:amber",
99
+ label: "Amber",
100
+ detail: "Warm terminal palette",
101
+ category: "Themes",
102
+ kind: "theme",
103
+ right: "amber",
104
+ current: true,
105
+ }, false, true)).toMatchObject({
106
+ glyph: " ",
107
+ trailing: "current",
108
+ });
109
+ });
110
+
111
+ test("ranks ticker search matches by symbol relevance and hides duplicate open symbols", () => {
112
+ const items = rankTickerSearchItems([
113
+ {
114
+ id: "search:IVSX",
115
+ label: "IVSX",
116
+ detail: "Invsivx Holdings | ETF",
117
+ category: "Search Results",
118
+ kind: "search",
119
+ right: "NYSE",
120
+ },
121
+ {
122
+ id: "goto:AAPL",
123
+ label: "AAPL",
124
+ detail: "Apple Inc.",
125
+ category: "Open",
126
+ kind: "ticker",
127
+ right: "NASDAQ",
128
+ },
129
+ {
130
+ id: "search:APP",
131
+ label: "APP",
132
+ detail: "AppLovin Corp | EQUITY",
133
+ category: "Search Results",
134
+ kind: "search",
135
+ right: "NASDAQ",
136
+ },
137
+ {
138
+ id: "search:AAPL",
139
+ label: "AAPL",
140
+ detail: "Apple Inc | EQUITY",
141
+ category: "Search Results",
142
+ kind: "search",
143
+ right: "NASDAQ",
144
+ },
145
+ ], "appl");
146
+
147
+ expect(items.map((item) => item.id)).toEqual([
148
+ "goto:AAPL",
149
+ "search:APP",
150
+ ]);
151
+ });
152
+ });