gloomberb 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +359 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +1 -1
  37. package/src/components/layout/pane-header.tsx +26 -2
  38. package/src/components/layout/shell.tsx +62 -50
  39. package/src/components/layout/status-bar.test.tsx +66 -0
  40. package/src/components/layout/status-bar.tsx +26 -5
  41. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  42. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  43. package/src/components/ticker-list-table.test.tsx +83 -0
  44. package/src/components/ticker-list-table.tsx +174 -97
  45. package/src/components/ui/data-table.tsx +27 -2
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/ui/page-stack-view.tsx +60 -0
  48. package/src/components/use-double-click-activation.ts +31 -0
  49. package/src/index.tsx +10 -10
  50. package/src/market-data/coordinator.test.ts +92 -4
  51. package/src/market-data/coordinator.ts +32 -5
  52. package/src/market-data/hooks.test.tsx +120 -0
  53. package/src/market-data/hooks.tsx +53 -16
  54. package/src/market-data/request-types.ts +4 -2
  55. package/src/market-data/selectors.ts +24 -58
  56. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  57. package/src/plugins/builtin/chat-controller.ts +310 -35
  58. package/src/plugins/builtin/chat.test.tsx +118 -6
  59. package/src/plugins/builtin/chat.tsx +175 -116
  60. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  61. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  62. package/src/plugins/builtin/news.tsx +1 -11
  63. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  64. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  65. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  66. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  67. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  68. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  69. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  70. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  71. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  72. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  73. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  74. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  75. package/src/plugins/builtin/sec.tsx +1 -11
  76. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  77. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  78. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  79. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  80. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  81. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  82. package/src/plugins/catalog.ts +65 -0
  83. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  84. package/src/plugins/ibkr/gateway-service.test.ts +45 -1
  85. package/src/plugins/ibkr/gateway-service.ts +141 -7
  86. package/src/plugins/pane-manager.test.ts +1 -1
  87. package/src/plugins/plugin-runtime.tsx +27 -7
  88. package/src/plugins/prediction-markets/cli.ts +28 -0
  89. package/src/plugins/prediction-markets/controller-data.ts +11 -3
  90. package/src/plugins/prediction-markets/controller.ts +141 -121
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/plugins/prediction-markets/launch.ts +51 -16
  93. package/src/plugins/prediction-markets/metrics.ts +9 -5
  94. package/src/plugins/prediction-markets/pane.test.tsx +119 -57
  95. package/src/plugins/prediction-markets/pane.tsx +74 -153
  96. package/src/plugins/prediction-markets/table.tsx +6 -3
  97. package/src/plugins/prediction-markets/types.ts +0 -1
  98. package/src/sources/gloomberb-cloud.test.ts +29 -0
  99. package/src/sources/gloomberb-cloud.ts +89 -1
  100. package/src/sources/provider-router.test.ts +265 -3
  101. package/src/sources/provider-router.ts +239 -11
  102. package/src/sources/yahoo-finance.test.ts +26 -0
  103. package/src/sources/yahoo-finance.ts +54 -3
  104. package/src/state/app-context-selector.test.tsx +99 -0
  105. package/src/state/app-context.test.ts +18 -0
  106. package/src/state/app-context.tsx +170 -65
  107. package/src/state/selectors.ts +25 -0
  108. package/src/theme/colors.ts +8 -8
  109. package/src/theme/themes.test.ts +2 -0
  110. package/src/theme/themes.ts +22 -10
  111. package/src/types/broker.ts +21 -0
  112. package/src/types/config.ts +17 -1
  113. package/src/types/data-provider.ts +18 -0
  114. package/src/types/financials.ts +31 -1
  115. package/src/types/plugin.ts +70 -0
  116. package/src/utils/api-client.test.ts +57 -0
  117. package/src/utils/api-client.ts +35 -12
  118. package/src/utils/back-navigation.ts +27 -0
  119. package/src/utils/format.test.ts +14 -0
  120. package/src/utils/format.ts +3 -3
  121. package/src/utils/market-status.ts +46 -1
  122. package/src/utils/quote-freshness.ts +127 -0
  123. package/src/utils/quote-resolution.test.ts +277 -0
  124. package/src/utils/quote-resolution.ts +641 -0
  125. package/src/utils/slugify.ts +3 -0
  126. package/src/utils/timestamp.ts +25 -0
  127. package/src/version.ts +1 -1
  128. package/src/cli/commands/portfolio.ts +0 -210
  129. package/src/cli/commands/watchlist.ts +0 -210
  130. package/src/plugins/builtin/data-providers.ts +0 -16
  131. package/src/sources/exchange-rates.ts +0 -18
  132. package/src/utils/ascii-chart.ts +0 -172
  133. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -82,6 +82,14 @@ export const commands: Command[] = [
82
82
  category: "Portfolio",
83
83
  execute: () => {},
84
84
  },
85
+ {
86
+ id: "set-portfolio-position",
87
+ prefix: "",
88
+ label: "Set Portfolio Position",
89
+ description: "Create or update a manual position in a portfolio",
90
+ category: "Portfolio",
91
+ execute: () => {},
92
+ },
85
93
 
86
94
  // Create / Delete
87
95
  {
@@ -188,10 +188,32 @@ describe("command-bar helpers", () => {
188
188
  options: [],
189
189
  payload: { fieldType: "ordered-multi-select" },
190
190
  };
191
+ const filteredPickerRoute: CommandBarRoute = {
192
+ ...orderedPickerRoute,
193
+ query: "vol",
194
+ };
195
+ const workflowRoute: CommandBarRoute = {
196
+ kind: "workflow",
197
+ title: "Create Pane",
198
+ fields: [],
199
+ values: {},
200
+ activeFieldId: null,
201
+ };
202
+ const confirmRoute: CommandBarRoute = {
203
+ kind: "confirm",
204
+ title: "Delete",
205
+ body: ["Delete item?"],
206
+ confirmLabel: "Delete",
207
+ cancelLabel: "Back",
208
+ tone: "danger",
209
+ };
191
210
 
192
211
  expect(getScreenFooterLeft(null)).toBe("up/down move enter select");
193
212
  expect(getScreenFooterLeft(orderedPickerRoute)).toBe("up/down move space toggle [ ] reorder enter done");
194
- expect(getScreenFooterRight(orderedPickerRoute)).toBe("esc back");
213
+ expect(getScreenFooterRight(orderedPickerRoute)).toBe("backspace/esc back");
214
+ expect(getScreenFooterRight(filteredPickerRoute)).toBe("backspace delete esc back");
215
+ expect(getScreenFooterRight(workflowRoute)).toBe("esc back");
216
+ expect(getScreenFooterRight(confirmRoute)).toBe("backspace/esc back");
195
217
  });
196
218
 
197
219
  test("maps route and collection command ids", () => {
@@ -4,6 +4,7 @@ import type {
4
4
  PaneTemplateDef,
5
5
  WizardStep,
6
6
  } from "../../types/plugin";
7
+ export { slugifyName } from "../../utils/slugify";
7
8
  import type {
8
9
  CommandBarFieldOption,
9
10
  CommandBarFieldValue,
@@ -251,10 +252,6 @@ export function routeCommandIdToScreen(commandId: RouteCommandId): "ticker-searc
251
252
  }
252
253
  }
253
254
 
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
255
  export function getFirstVisibleFieldId(
259
256
  fields: CommandBarWorkflowField[],
260
257
  values: Record<string, CommandBarFieldValue>,
@@ -328,7 +325,19 @@ export function getScreenFooterLeft(route: CommandBarRoute | null): string {
328
325
  }
329
326
 
330
327
  export function getScreenFooterRight(route: CommandBarRoute | null): string {
331
- return route ? "esc back" : "esc close";
328
+ if (!route) return "esc close";
329
+
330
+ if (route.kind === "workflow") return "esc back";
331
+ if (route.kind === "confirm") return "backspace/esc back";
332
+
333
+ if (
334
+ (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings")
335
+ && route.query.trim().length > 0
336
+ ) {
337
+ return "backspace delete esc back";
338
+ }
339
+
340
+ return "backspace/esc back";
332
341
  }
333
342
 
334
343
  export function isRootParsedCommand(commandId: string): boolean {
@@ -0,0 +1,12 @@
1
+ import type { PaneTemplateDef } from "../../types/plugin";
2
+
3
+ export function formatPaneTemplateLabel(label: string): string {
4
+ let normalized = label.trim();
5
+ if (normalized.startsWith("New ")) normalized = normalized.slice(4);
6
+ if (normalized.endsWith(" Pane")) normalized = normalized.slice(0, -5);
7
+ return normalized;
8
+ }
9
+
10
+ export function getPaneTemplateDisplayLabel(template: Pick<PaneTemplateDef, "label">): string {
11
+ return formatPaneTemplateLabel(template.label);
12
+ }
@@ -1,5 +1,6 @@
1
1
  import type { PaneTemplateDef } from "../../types/plugin";
2
2
  import type { Command } from "./command-registry";
3
+ import { getPaneTemplateDisplayLabel } from "./pane-template-display";
3
4
 
4
5
  export type RootShortcutArgKind = "text" | "ticker" | "ticker-list";
5
6
  export type ShortcutIntentKind = "none" | "complete" | "inferred-complete" | "partial" | "ambiguous";
@@ -88,7 +89,7 @@ function buildShortcutCandidates(
88
89
  .filter((template) => template.shortcut?.prefix)
89
90
  .map((template) => ({
90
91
  prefix: normalizeShortcutPrefix(template.shortcut!.prefix),
91
- label: template.label,
92
+ label: getPaneTemplateDisplayLabel(template),
92
93
  description: template.description,
93
94
  argKind: getPaneShortcutArgKind(template),
94
95
  argPlaceholder: template.shortcut?.argPlaceholder,
@@ -9,10 +9,12 @@ import type { TickerRepository } from "../../data/ticker-repository";
9
9
  import { resolveTickerSearch, upsertTickerFromSearchResult } from "../../utils/ticker-search";
10
10
  import { formatTickerListInput, parseTickerListInput } from "../../utils/ticker-list";
11
11
  import { updatePaneInstance, setPaneSettings } from "../../pane-settings";
12
+ import { isManualPortfolio } from "../../plugins/builtin/portfolio-list/mutations";
12
13
  import {
13
14
  buildComparisonChartPaneTitle,
14
15
  COMPARISON_CHART_PANE_ID,
15
16
  } from "../../plugins/builtin/comparison-chart";
17
+ import { getPaneTemplateDisplayLabel } from "./pane-template-display";
16
18
 
17
19
  export interface SharedWorkflowDeps {
18
20
  dataProvider: DataProvider;
@@ -109,7 +111,7 @@ function getCollectionOptions(
109
111
  if (kind === "watchlist") {
110
112
  return state.config.watchlists;
111
113
  }
112
- return state.config.portfolios.filter((portfolio) => !portfolio.brokerId && !portfolio.brokerInstanceId);
114
+ return state.config.portfolios.filter(isManualPortfolio);
113
115
  }
114
116
 
115
117
  export function getCollectionTargetOptions(
@@ -302,7 +304,7 @@ export async function createPaneTemplateOrThrow(
302
304
  const { context, resolvedOptions } = await resolvePaneTemplateOptions(template, options, deps);
303
305
 
304
306
  if (template.canCreate && !template.canCreate(context, resolvedOptions)) {
305
- throw new Error(`Can't create ${template.label.toLowerCase()} right now.`);
307
+ throw new Error(`Can't create ${getPaneTemplateDisplayLabel(template).toLowerCase()} right now.`);
306
308
  }
307
309
 
308
310
  const createInstanceResult = await template.createInstance?.(context, resolvedOptions);
@@ -1,26 +1,55 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useState } from "react";
2
3
  import { testRender } from "@opentui/react/test-utils";
3
- import { DetailFeedView } from "./detail-feed-view";
4
+ import {
5
+ DetailFeedView,
6
+ type DetailFeedItem,
7
+ } from "./detail-feed-view";
4
8
 
5
9
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
+ let setHarnessItems:
11
+ | ((items: DetailFeedItem[]) => void)
12
+ | undefined;
13
+ let harnessSelectedIdx = 0;
6
14
 
7
- afterEach(() => {
15
+ afterEach(async () => {
8
16
  if (testSetup) {
9
- testSetup.renderer.destroy();
17
+ await act(async () => {
18
+ testSetup!.renderer.destroy();
19
+ });
10
20
  testSetup = undefined;
11
21
  }
22
+ setHarnessItems = undefined;
23
+ harnessSelectedIdx = 0;
12
24
  });
13
25
 
14
- const items = [
26
+ async function renderSettled() {
27
+ await act(async () => {
28
+ await testSetup!.renderOnce();
29
+ await testSetup!.renderOnce();
30
+ });
31
+ }
32
+
33
+ async function clickAt(x: number, y: number) {
34
+ await act(async () => {
35
+ await testSetup!.mockMouse.click(x, y);
36
+ await testSetup!.renderOnce();
37
+ await testSetup!.renderOnce();
38
+ });
39
+ }
40
+
41
+ const items: DetailFeedItem[] = [
15
42
  {
16
43
  id: "1",
17
44
  eyebrow: "Reuters",
18
- title: "Long headline with enough words to prove the list wraps instead of collapsing into fixed columns",
45
+ title:
46
+ "Long headline with enough words to prove the list wraps instead of collapsing into fixed columns",
19
47
  timestamp: new Date("2024-08-01T12:00:00Z"),
20
48
  preview: "Preview text with a little more color.",
21
49
  detailTitle: "Long headline with enough words to prove the list wraps",
22
50
  detailMeta: ["Reuters", "Published Aug 1, 2024"],
23
- detailBody: "This body should stay readable when the pane is narrow and also when the pane is wide enough for a split layout.",
51
+ detailBody:
52
+ "This body should stay readable when the pane is narrow and also when the pane is wide enough for a split layout.",
24
53
  detailNote: "https://example.com/story",
25
54
  },
26
55
  {
@@ -35,68 +64,114 @@ const items = [
35
64
  },
36
65
  ];
37
66
 
67
+ function Harness({
68
+ width,
69
+ height,
70
+ listVariant = "comfortable",
71
+ }: {
72
+ width: number;
73
+ height: number;
74
+ listVariant?: "comfortable" | "compact" | "single-line";
75
+ }) {
76
+ const [activeItems, setActiveItems] = useState(items);
77
+ const [selectedIdx, setSelectedIdx] = useState(0);
78
+ const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
79
+
80
+ setHarnessItems = setActiveItems;
81
+ harnessSelectedIdx = selectedIdx;
82
+
83
+ return (
84
+ <DetailFeedView
85
+ width={width}
86
+ height={height}
87
+ focused
88
+ items={activeItems}
89
+ selectedIdx={selectedIdx}
90
+ hoveredIdx={hoveredIdx}
91
+ onSelect={setSelectedIdx}
92
+ onHover={setHoveredIdx}
93
+ listVariant={listVariant}
94
+ />
95
+ );
96
+ }
97
+
38
98
  describe("DetailFeedView", () => {
39
- test("renders readable stacked content in narrow panes", async () => {
40
- testSetup = await testRender(
41
- <DetailFeedView
42
- width={60}
43
- height={16}
44
- items={items}
45
- selectedIdx={0}
46
- hoveredIdx={null}
47
- onSelect={() => {}}
48
- onHover={() => {}}
49
- />,
50
- { width: 60, height: 16 },
51
- );
99
+ test("renders the browse list by default", async () => {
100
+ testSetup = await testRender(<Harness width={60} height={16} />, {
101
+ width: 60,
102
+ height: 16,
103
+ });
52
104
 
53
- await testSetup.renderOnce();
105
+ await renderSettled();
54
106
  const frame = testSetup.captureCharFrame();
55
107
 
56
108
  expect(frame).toContain("Long headline with enough words");
57
- expect(frame).toContain("This body should stay readable");
58
- expect(frame).toContain("Published Aug 1, 2024");
109
+ expect(frame).toContain("j/k move enter or click selected row");
110
+ expect(frame).not.toContain("This body should stay readable");
59
111
  });
60
112
 
61
- test("renders split list and detail content in wide panes", async () => {
113
+ test("selects an item on single click without opening detail", async () => {
62
114
  testSetup = await testRender(
63
- <DetailFeedView
64
- width={110}
65
- height={16}
66
- items={items}
67
- selectedIdx={0}
68
- hoveredIdx={1}
69
- onSelect={() => {}}
70
- onHover={() => {}}
71
- />,
72
- { width: 110, height: 16 },
115
+ <Harness width={90} height={12} listVariant="single-line" />,
116
+ {
117
+ width: 90,
118
+ height: 12,
119
+ },
73
120
  );
74
121
 
75
- await testSetup.renderOnce();
122
+ await renderSettled();
123
+
76
124
  const frame = testSetup.captureCharFrame();
125
+ const lines = frame.split("\n");
126
+ const filingRow = lines.findIndex((line) => line.includes("10-Q filing"));
127
+ const filingCol = lines[filingRow]?.indexOf("10-Q filing") ?? -1;
128
+
129
+ await clickAt(filingCol + 1, filingRow);
130
+
131
+ const nextFrame = testSetup.captureCharFrame();
132
+ expect(harnessSelectedIdx).toBe(1);
133
+ expect(nextFrame).not.toContain("<- Back");
134
+ expect(nextFrame).not.toContain("Quarterly report details.");
135
+ });
136
+
137
+ test("closes the open detail page when the feed items change", async () => {
138
+ testSetup = await testRender(<Harness width={90} height={16} />, {
139
+ width: 90,
140
+ height: 16,
141
+ });
77
142
 
78
- expect(frame).toContain("Reuters");
79
- expect(frame).toContain("10-Q filing");
80
- expect(frame).toContain("j/k navigate");
143
+ await renderSettled();
144
+
145
+ await act(async () => {
146
+ testSetup!.mockInput.pressEnter();
147
+ await testSetup!.renderOnce();
148
+ await testSetup!.renderOnce();
149
+ });
150
+
151
+ await act(async () => {
152
+ setHarnessItems?.([
153
+ {
154
+ id: "9",
155
+ title: "Fresh item after source change",
156
+ detailBody: "Replacement detail",
157
+ },
158
+ ]);
159
+ });
160
+ await renderSettled();
161
+
162
+ const frame = testSetup.captureCharFrame();
163
+ expect(frame).toContain("Fresh item after source change");
164
+ expect(frame).not.toContain("<- Back");
165
+ expect(frame).not.toContain("Replacement detail");
81
166
  });
82
167
 
83
168
  test("renders a single-line list variant", async () => {
84
169
  testSetup = await testRender(
85
- <DetailFeedView
86
- width={110}
87
- height={12}
88
- items={items}
89
- selectedIdx={0}
90
- hoveredIdx={null}
91
- onSelect={() => {}}
92
- onHover={() => {}}
93
- listVariant="single-line"
94
- splitListWidthRatio={0.33}
95
- />,
170
+ <Harness width={110} height={12} listVariant="single-line" />,
96
171
  { width: 110, height: 12 },
97
172
  );
98
173
 
99
- await testSetup.renderOnce();
174
+ await renderSettled();
100
175
  const frame = testSetup.captureCharFrame();
101
176
 
102
177
  expect(frame).toContain("Long headline with enough words");