gloomberb 0.17.0 → 0.17.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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/data.ts +5 -1
  4. package/src/api-client/doe.ts +19 -0
  5. package/src/api-client/earnings.ts +4 -2
  6. package/src/api-client/index.ts +1 -0
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app/runtime/update.ts +16 -0
  11. package/src/app.tsx +1 -1
  12. package/src/architecture/core-builtin-allowlist.json +4 -9
  13. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  14. package/src/cli/commands/market.ts +87 -17
  15. package/src/cli/commands/options-chain.ts +93 -23
  16. package/src/cli/commands/options-leaps.ts +462 -63
  17. package/src/cli/commands/search.ts +10 -7
  18. package/src/cli/commands/ticker.ts +19 -10
  19. package/src/cli/glossary.ts +1 -1
  20. package/src/cli/index.ts +14 -6
  21. package/src/cli/known-gaps.ts +22 -0
  22. package/src/cli/pane-functions/catalog.ts +45 -10
  23. package/src/cli/pane-functions/dom.ts +36 -10
  24. package/src/cli/pane-functions/function-help.ts +4 -2
  25. package/src/cli/pane-functions/headless.ts +12 -1
  26. package/src/cli/pane-functions/index.ts +44 -3
  27. package/src/cli/pane-functions/lock-note.ts +56 -0
  28. package/src/cli/pane-functions/report-notices.ts +15 -0
  29. package/src/cli/symbol-args.ts +19 -0
  30. package/src/components/command-bar/list/view.tsx +2 -1
  31. package/src/components/command-bar/workflow/builtin.ts +2 -2
  32. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  33. package/src/components/feedback-dialog.tsx +1 -1
  34. package/src/components/index.ts +3 -2
  35. package/src/components/layout/header.tsx +58 -14
  36. package/src/components/layout/shell/chrome.ts +10 -4
  37. package/src/components/layout/shell/index.tsx +1 -1
  38. package/src/components/layout/status-bar-groups.ts +1 -1
  39. package/src/components/layout/status-bar.tsx +2 -2
  40. package/src/components/markdown-text.tsx +14 -2
  41. package/src/components/reader/article-reader.tsx +1 -1
  42. package/src/components/reader/detail-reader.tsx +10 -7
  43. package/src/components/reader/reader-text.tsx +32 -17
  44. package/src/components/ui/column-grid.tsx +206 -0
  45. package/src/components/ui/display.tsx +40 -3
  46. package/src/components/ui/fill.ts +8 -0
  47. package/src/components/ui/index.ts +2 -0
  48. package/src/layout-marketplace/gallery.tsx +2 -2
  49. package/src/layout-marketplace/linked-sync.ts +1 -1
  50. package/src/market-data/market/freshness.ts +3 -0
  51. package/src/market-data/published-apac-sessions.ts +12 -2
  52. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  53. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  54. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  55. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  56. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  57. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  58. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  59. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  60. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  61. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  62. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  63. package/src/plugins/builtin/chat/conversations.ts +1 -1
  64. package/src/plugins/builtin/chat/index.tsx +2 -2
  65. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  66. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  67. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  68. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  69. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  70. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  71. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  72. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  73. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  74. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  75. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  76. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  77. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  78. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  79. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  80. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  81. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  82. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  83. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  84. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  85. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  86. package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
  87. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  88. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  89. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  90. package/src/plugins/builtin/connections/pane.tsx +16 -5
  91. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  92. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  93. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  94. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  95. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  96. package/src/plugins/builtin/docs/pane.tsx +6 -5
  97. package/src/plugins/builtin/doe/index.tsx +11 -4
  98. package/src/plugins/builtin/doe/pane.tsx +5 -4
  99. package/src/plugins/builtin/doe/prices.ts +84 -0
  100. package/src/plugins/builtin/doe/series.ts +4 -1
  101. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  102. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  103. package/src/plugins/builtin/earnings/headless.ts +13 -2
  104. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  105. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  106. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  107. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  108. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  109. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  110. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  111. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  112. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  113. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  114. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  115. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  116. package/src/plugins/builtin/executives/index.tsx +18 -0
  117. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  118. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  119. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  120. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  121. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  122. package/src/plugins/builtin/iv-history/format.ts +3 -0
  123. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  124. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  125. package/src/plugins/builtin/iv-history/model.ts +3 -1
  126. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  127. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  128. package/src/plugins/builtin/jobs/headless.ts +6 -20
  129. package/src/plugins/builtin/jobs/index.tsx +2 -0
  130. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  131. package/src/plugins/builtin/notes/index.tsx +1 -1
  132. package/src/plugins/builtin/notes/owner.tsx +2 -2
  133. package/src/plugins/builtin/options/carry.ts +36 -0
  134. package/src/plugins/builtin/options/index.tsx +2 -1
  135. package/src/plugins/builtin/options/model-spot.ts +94 -0
  136. package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
  137. package/src/plugins/builtin/options/view.tsx +69 -11
  138. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  139. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  140. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  141. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  142. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  143. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  144. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  145. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  146. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  147. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  148. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  149. package/src/plugins/builtin/research/event-model.ts +7 -4
  150. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  151. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  152. package/src/plugins/builtin/scanner/headless.ts +9 -2
  153. package/src/plugins/builtin/scanner/index.ts +3 -0
  154. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  155. package/src/plugins/builtin/series-inventory.ts +2 -1
  156. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  157. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  158. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  159. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  160. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  161. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  162. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  163. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  164. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  165. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  166. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  167. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  168. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  169. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  170. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  171. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  172. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  173. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  174. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  175. package/src/renderers/dom/data-table/index.tsx +48 -15
  176. package/src/renderers/dom/data-table/row.tsx +1 -1
  177. package/src/renderers/dom/desktop/popover.tsx +6 -15
  178. package/src/renderers/dom/host/input.tsx +27 -3
  179. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  180. package/src/renderers/dom/styles.css +26 -6
  181. package/src/renderers/electrobun/view/app-services.ts +2 -22
  182. package/src/renderers/electrobun/view/news-source.ts +42 -0
  183. package/src/series/validate.ts +5 -0
  184. package/src/shares/dialog.tsx +2 -1
  185. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  186. package/src/sources/provider-router/financials.ts +5 -0
  187. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  188. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  189. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  190. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  191. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  192. package/src/types/data-provider.ts +3 -0
  193. package/src/types/financials.ts +28 -0
  194. package/src/types/plugin.ts +17 -4
  195. package/src/utils/decimal-comma.ts +18 -0
  196. package/src/utils/exchanges.ts +7 -0
  197. package/src/utils/financial-statements.ts +25 -0
  198. package/src/utils/forward-dividend.ts +39 -9
  199. package/src/utils/fundamentals.ts +41 -1
  200. package/src/utils/option-expiry.ts +16 -0
  201. package/src/utils/sec.ts +2 -1
@@ -0,0 +1,206 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { Box, Text, useUiCapabilities } from "../../ui";
3
+ import { useThemeColors } from "../../theme/theme-context";
4
+ import { displayWidth, padTo, truncateToDisplayWidth } from "../../utils/format";
5
+ import { ExternalLinkText } from "./external-link";
6
+
7
+ /** One column of a ColumnGrid. */
8
+ export interface ColumnGridColumn {
9
+ /** Cells the terminal gives the column, from the caller's own cell math. */
10
+ width: number;
11
+ /**
12
+ * On the desktop and the web the column takes an equal share of the room the
13
+ * fixed columns leave, and never less than this many characters. Without it
14
+ * the column holds `width` characters there too.
15
+ */
16
+ growFrom?: number;
17
+ /** Where the column's text sits; a number column is right-aligned. */
18
+ align?: "left" | "right";
19
+ /** Space before the column: cells on the terminal, characters on the desktop. */
20
+ gapBefore?: number;
21
+ }
22
+
23
+ /** Text the grid fits to its column (padded on the terminal, cut by CSS on the desktop), or content the column holds. */
24
+ export type ColumnGridCell =
25
+ | { text: string; color?: string }
26
+ | { content: ReactNode }
27
+ | null;
28
+
29
+ export interface ColumnGridProps {
30
+ columns: readonly ColumnGridColumn[];
31
+ /** Rows of cells, one per column. A header is the first row. */
32
+ rows: ReadonlyArray<ReadonlyArray<ColumnGridCell>>;
33
+ /** The terminal's width for the grid, in cells. The desktop fills its box. */
34
+ width?: number;
35
+ /** Names the grid for the pane chrome checks (`data-gloom-role`). */
36
+ role?: string;
37
+ }
38
+
39
+ /**
40
+ * Desktop text, in a box that takes what its track gives it. Whether its track
41
+ * can be narrower than the text is `clipOf`'s to say; where it can, the text
42
+ * carries `data-gloom-clip` and styles.css clips it, without a scroll container
43
+ * where the engine has `overflow: clip`, and ends it in an ellipsis.
44
+ */
45
+ const TEXT_BOX: CSSProperties = { display: "block", minWidth: 0 };
46
+ const CLIPPED = { "data-gloom-clip": "text" } as const;
47
+
48
+ /**
49
+ * The clip a cell's text gets: a fixed column is as wide as its characters at
50
+ * every frame, but a growing column can fall to `growFrom` characters at any
51
+ * frame of a resize, between two catch-ups, however far the pointer moved. Text
52
+ * longer than that is clipped, so a cell never draws outside its track.
53
+ */
54
+ function clipOf(text: string, column: ColumnGridColumn): typeof CLIPPED | undefined {
55
+ return displayWidth(text) > (column.growFrom ?? column.width) ? CLIPPED : undefined;
56
+ }
57
+
58
+ /**
59
+ * The grid's tracks as CSS: characters for fixed columns, an equal share for
60
+ * growing ones. Gaps that are all alike are the grid's column gap; otherwise
61
+ * each gap is a track of its own, which no cell sits in.
62
+ */
63
+ function gridTemplate(columns: readonly ColumnGridColumn[]): { template: string; gap: string | undefined; lines: number[] } {
64
+ const track = (column: ColumnGridColumn) => column.growFrom != null ? `minmax(${column.growFrom}ch, 1fr)` : `${column.width}ch`;
65
+ const gaps = columns.slice(1).map((column) => column.gapBefore ?? 0);
66
+ if ((columns[0]?.gapBefore ?? 0) === 0 && gaps.every((gap) => gap === gaps[0])) {
67
+ return { template: columns.map(track).join(" "), gap: gaps[0] ? `${gaps[0]}ch` : undefined, lines: columns.map((_, index) => index + 1) };
68
+ }
69
+ const tracks: string[] = [];
70
+ const lines: number[] = [];
71
+ for (const column of columns) {
72
+ if (column.gapBefore) tracks.push(`${column.gapBefore}ch`);
73
+ tracks.push(track(column));
74
+ lines.push(tracks.length);
75
+ }
76
+ return { template: tracks.join(" "), gap: undefined, lines };
77
+ }
78
+
79
+ /**
80
+ * Columns of single-line text that line up from row to row: a table of
81
+ * figures under its header, or label and value pairs set in columns. The terminal lays the
82
+ * columns out at the whole cells the caller worked out, as text padded to
83
+ * them. The desktop and the web lay out one CSS grid whose tracks are counted
84
+ * in characters, so the columns follow a resize to the pixel, every row and
85
+ * the header share the same tracks, and text that does not fit ends in an
86
+ * ellipsis instead of being cut to a count of cells.
87
+ */
88
+ export function ColumnGrid({ columns, rows, width, role }: ColumnGridProps) {
89
+ const { nativePaneChrome } = useUiCapabilities();
90
+ if (!nativePaneChrome) {
91
+ return (
92
+ <Box flexDirection="column" width={width} data-gloom-role={role}>
93
+ {rows.map((row, rowIndex) => (
94
+ <Box key={rowIndex} flexDirection="row" height={1}>
95
+ {columns.map((column, index) => {
96
+ const cell = row[index] ?? null;
97
+ return (
98
+ <Box key={index} flexDirection="row">
99
+ {column.gapBefore ? <Box width={column.gapBefore} /> : null}
100
+ {cell && "text" in cell
101
+ ? <Text fg={cell.color}>{padTo(cell.text, column.width, column.align)}</Text>
102
+ : <Box width={column.width} flexDirection="row">{cell?.content}</Box>}
103
+ </Box>
104
+ );
105
+ })}
106
+ </Box>
107
+ ))}
108
+ </Box>
109
+ );
110
+ }
111
+
112
+ const { template, gap, lines } = gridTemplate(columns);
113
+ return (
114
+ <Box
115
+ data-gloom-ui="column-grid"
116
+ data-gloom-role={role}
117
+ // A row is one line of text, as tall as the text's own line: rows of a set
118
+ // height spare the browser sizing them again on every frame of a resize.
119
+ // The grid clips what its tracks put past its edge (columns that cannot shrink under a pane that does).
120
+ data-gloom-clip="box"
121
+ style={{ display: "grid", gridTemplateColumns: template, gridAutoRows: "var(--cell-h)", ...(gap ? { columnGap: gap } : {}) }}
122
+ >
123
+ {rows.map((row, rowIndex) => (
124
+ <Box key={rowIndex} data-gloom-ui="column-grid-row" style={{ display: "contents" }}>
125
+ {columns.map((column, index) => {
126
+ const cell = row[index] ?? null;
127
+ if (!cell) return null;
128
+ const place: CSSProperties = { gridColumn: lines[index], gridRow: rowIndex + 1 };
129
+ return "text" in cell
130
+ ? (
131
+ <Text
132
+ key={index}
133
+ fg={cell.color}
134
+ data-gloom-ui="column-grid-text"
135
+ {...clipOf(cell.text, column)}
136
+ style={{ ...place, ...TEXT_BOX, textAlign: column.align ?? "left" }}
137
+ >
138
+ {cell.text}
139
+ </Text>
140
+ )
141
+ : <Box key={index} flexDirection="row" data-gloom-ui="column-grid-cell" style={{ ...place, minWidth: 0 }}>{cell.content}</Box>;
142
+ })}
143
+ </Box>
144
+ ))}
145
+ </Box>
146
+ );
147
+ }
148
+
149
+ export interface ColumnGridPairProps {
150
+ label: string;
151
+ /** Cells, and on the desktop characters, the label takes. */
152
+ labelWidth: number;
153
+ /** The terminal's cells for label and value together. */
154
+ width: number;
155
+ value: string;
156
+ valueColor?: string;
157
+ /** Muted context after the value, already chosen to fit. */
158
+ detail?: string;
159
+ /** The value opens this address. */
160
+ url?: string;
161
+ }
162
+
163
+ /**
164
+ * A label and its value in one ColumnGrid cell: the label at a fixed width,
165
+ * the value right-aligned in the rest and cut from its end when it is too
166
+ * long, and its muted detail after it. The detail gives way before the value.
167
+ */
168
+ export function ColumnGridPair({ label, labelWidth, width, value, valueColor, detail, url }: ColumnGridPairProps) {
169
+ const colors = useThemeColors();
170
+ const { nativePaneChrome } = useUiCapabilities();
171
+ const color = valueColor ?? colors.text;
172
+ if (!nativePaneChrome) {
173
+ const valueWidth = Math.max(1, width - labelWidth);
174
+ const shown = truncateToDisplayWidth(value, valueWidth);
175
+ return (
176
+ <>
177
+ <Box width={labelWidth} overflow="hidden">
178
+ <Text fg={colors.textDim}>{label}</Text>
179
+ </Box>
180
+ <Box flexDirection="row" width={valueWidth} justifyContent="flex-end" overflow="hidden">
181
+ {url ? <ExternalLinkText url={url} label={shown} color={color} /> : <Text fg={color}>{shown}</Text>}
182
+ {detail ? <Text fg={colors.textDim}>{` ${detail}`}</Text> : null}
183
+ </Box>
184
+ </>
185
+ );
186
+ }
187
+ // The value's box can fall to nothing at any frame of a resize (the columns hold their labels, not their
188
+ // values), so its text and detail are clipped, the detail first. A link is clipped by its box, from its end.
189
+ return (
190
+ <>
191
+ <Text
192
+ fg={colors.textDim}
193
+ {...(displayWidth(label) > labelWidth ? { "data-gloom-clip": "box" } : {})}
194
+ style={{ flexGrow: 0, flexShrink: 0, flexBasis: `${labelWidth}ch`, display: "block" }}
195
+ >
196
+ {label}
197
+ </Text>
198
+ <Box flexDirection="row" justifyContent="flex-end" flexGrow={1} flexShrink={1} flexBasis={0} {...(url ? { "data-gloom-clip": "box" } : {})}>
199
+ {url
200
+ ? <ExternalLinkText url={url} label={value} color={color} />
201
+ : <Text fg={color} {...CLIPPED} style={{ ...TEXT_BOX, flexShrink: 1 }}>{value}</Text>}
202
+ {detail ? <Text fg={colors.textDim} {...CLIPPED} style={{ ...TEXT_BOX, flexShrink: 1000 }}>{` ${detail}`}</Text> : null}
203
+ </Box>
204
+ </>
205
+ );
206
+ }
@@ -20,14 +20,20 @@ export interface SectionHeadingProps {
20
20
 
21
21
  export function SectionHeading({ title, marginTop = 0, width, wrap = false }: SectionHeadingProps) {
22
22
  const colors = useThemeColors();
23
+ const { nativePaneChrome } = useUiCapabilities();
23
24
  // English reads in title case; a translation keeps its own casing.
24
25
  const translated = t(title);
25
26
  const text = translated === title ? titleCase(title) : translated;
26
27
  return (
27
28
  <Box height={wrap ? undefined : 1} marginTop={marginTop} width={width} overflow="hidden" data-gloom-ui="section-heading">
28
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD} wrapText={wrap}>
29
- {wrap || width === undefined ? text : truncateToDisplayWidth(text, width)}
30
- </Text>
29
+ {nativePaneChrome && !wrap ? (
30
+ // The desktop cuts a long heading where its box ends, not at a count of columns.
31
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={{ ...NATIVE_CLIP_STYLE, flex: "1 1 auto" }}>{text}</Text>
32
+ ) : (
33
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD} wrapText={wrap}>
34
+ {wrap || width === undefined ? text : truncateToDisplayWidth(text, width)}
35
+ </Text>
36
+ )}
31
37
  </Box>
32
38
  );
33
39
  }
@@ -70,6 +76,37 @@ export interface KeyValueRowProps {
70
76
  */
71
77
  export const NATIVE_CLIP_STYLE = { display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "pre" } as const;
72
78
 
79
+ export interface ClippedTextProps {
80
+ text: string;
81
+ /** The terminal's room for the text, in cells. */
82
+ width: number;
83
+ fg?: string;
84
+ attributes?: number;
85
+ /** How the text sits in a row on the desktop: by default it gives way to its siblings. */
86
+ flex?: string;
87
+ /** Kept whole before the text in both renderers (an indent, a separator). */
88
+ lead?: string;
89
+ /** How the terminal cuts it; the caller's own cut keeps its frames as they were. */
90
+ cut?: (text: string, width: number) => string;
91
+ /** The desktop wraps it onto more lines instead (a message, a description); the terminal still cuts it. */
92
+ wrap?: boolean;
93
+ }
94
+
95
+ /**
96
+ * One line of text cut where its room ends: in the terminal at `width` cells,
97
+ * on the desktop and the web by CSS at its box's edge, or wrapped there with
98
+ * `wrap`. Its box (or its row, with the siblings that keep their room) has to
99
+ * bound it there.
100
+ */
101
+ export function ClippedText({ text, width, fg, attributes, flex = "0 1 auto", lead = "", cut = truncateToDisplayWidth, wrap = false }: ClippedTextProps) {
102
+ const { nativePaneChrome } = useUiCapabilities();
103
+ const ink = { ...(fg === undefined ? {} : { fg }), ...(attributes === undefined ? {} : { attributes }) };
104
+ if (!nativePaneChrome) return <Text {...ink}>{`${lead}${cut(text, width)}`}</Text>;
105
+ return wrap
106
+ ? <Text {...ink} wrapText style={{ minWidth: 0, flex }}>{`${lead}${text}`}</Text>
107
+ : <Text {...ink} style={{ ...NATIVE_CLIP_STYLE, flex }}>{`${lead}${text}`}</Text>;
108
+ }
109
+
73
110
  /** Aligned labels and values; a bounded row reserves room for an optional detail. */
74
111
  export function KeyValueRow(props: KeyValueRowProps) {
75
112
  const { nativePaneChrome } = useUiCapabilities();
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Box props for what sits under a pane's header zone (tabs, query bar,
3
+ * figures) on the desktop and the web: it takes the space left in its column
4
+ * by flex and ends at the pane footer. The terminal counts the rows left
5
+ * instead; there a bar is one row, but on the desktop a chrome bar is taller
6
+ * than a row (`--chrome-h`), so a body given those rows overruns the pane.
7
+ */
8
+ export const FILL_REST = { flexGrow: 1, flexShrink: 1, flexBasis: 0, minHeight: 0 } as const;
@@ -35,6 +35,8 @@ export { QueryBar, useQueryBarSearch } from "./query-bar";
35
35
  export { FieldGrid, GridFieldView, fieldGridColumns, fieldGridRows } from "./field-grid";
36
36
  export type { FieldGridProps, GridField } from "./field-grid";
37
37
  export { StatGrid, statGridColumns, statGridRows } from "./stat-grid";
38
+ export { ColumnGrid, ColumnGridPair } from "./column-grid";
39
+ export type { ColumnGridCell, ColumnGridColumn, ColumnGridPairProps, ColumnGridProps } from "./column-grid";
38
40
  export type { StatGridProps, StatItem } from "./stat-grid";
39
41
  export { RatioBar } from "./ratio-bar";
40
42
  export type { RatioBarProps } from "./ratio-bar";
@@ -11,8 +11,8 @@ import { useAppDispatch, useAppSelector } from "../state/app/context";
11
11
  import { useDialog, useDialogState, type PromptContext } from "../ui/dialog";
12
12
  import { useRendererHost, useUiCapabilities } from "../ui";
13
13
  import { apiClient, type TeamSummary } from "../api-client";
14
- import { requestAuthDialog } from "../plugins/builtin/cloud/auth-dialog";
15
- import { teamStore } from "../plugins/builtin/cloud/team/store";
14
+ import { requestAuthDialog } from "../account/auth-dialog";
15
+ import { teamStore } from "../team/store";
16
16
  import { usePlanAccess } from "../api-client/plan-access";
17
17
  import { getMarketplaceHost } from "../plugins/marketplace/store";
18
18
  import { rememberLayoutRequirements } from "../components/layout/missing-pane";
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
2
2
  import { apiClient } from "../api-client";
3
3
  import { rememberLayoutRequirements } from "../components/layout/missing-pane";
4
4
  import type { PluginRegistry } from "../plugins/registry";
5
- import { teamStore } from "../plugins/builtin/cloud/team/store";
5
+ import { teamStore } from "../team/store";
6
6
  import { useAppDispatch, useAppSelector } from "../state/app/context";
7
7
  import { decideLinkedAction, linkedLayoutStatus, linkedLayoutUpdates, originFromEntry } from "./linked";
8
8
  import { materializeMarketplaceLayout } from "../shares/portable-layout";
@@ -50,6 +50,7 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
50
50
  FWB: 8 * 60,
51
51
  SWX: 9 * 60,
52
52
  VIE: 9 * 60,
53
+ WBDM: 9 * 60,
53
54
  OSL: 9 * 60,
54
55
  ICEX: 9 * 60 + 30,
55
56
  WSE: 9 * 60,
@@ -89,6 +90,8 @@ const REGULAR_CLOSE_MINUTES: Record<string, number> = {
89
90
  JPX: 15 * 60 + 30, HKEX: 16 * 60 + 10, TWSE: 14 * 60 + 30, TPEX: 14 * 60 + 30,
90
91
  NSE: 16 * 60, BSE: 16 * 60, ASX: 16 * 60 + 15, SGX: 17 * 60 + 20, KRX: 16 * 60, KOSDAQ: 16 * 60,
91
92
  NZX: 17 * 60, SSE: 15 * 60 + 30, SZSE: 15 * 60 + 30,
93
+ // The Vienna MTF's ETPs and notes trade in continuous auctions to 17:30, with no closing auction.
94
+ WBDM: 17 * 60 + 30,
92
95
  BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15,
93
96
  // Monday to Thursday: trading at last ends 17:30, after the closing auction.
94
97
  // Not later: a complete 5-minute copy ends with its 17:09 bar, which must
@@ -1,5 +1,5 @@
1
- // Published full-day closures (weekend dates omitted) for Asia-Pacific venues and EGX,
2
- // not a holiday-rule engine. A year a venue has not published is unknown, not
1
+ // Published full-day closures (weekend dates omitted) for Asia-Pacific venues, EGX and Wiener
2
+ // Boerse, not a holiday-rule engine. A year a venue has not published is unknown, not
3
3
  // open: every weekday of it reads as a session. Half-day sessions (HKEX and
4
4
  // SGX eves, ASX's 14:10 closes) are not modelled. Checked 2026-10-09:
5
5
  // KRX, also KOSDAQ: http://open.krx.co.kr/contents/MKD/01/0110/01100305/MKD01100305.jsp
@@ -9,6 +9,9 @@
9
9
  // (some moved, as its list warns), and on National Day, 16 and 17 Dec. Its week is Sunday to Thursday.
10
10
  // HKEX: https://www.hkex.com.hk/-/media/HKEX-Market/Services/Circulars-and-Notices/Participant-and-Members-Circulars/SEHK/2025/ce_SEHK_CT_075_2025.pdf
11
11
  // and https://www.hkex.com.hk/-/media/HKEX-Market/Services/Circulars-and-Notices/Participant-and-Members-Circulars/SEHK/2026/ce_SEHK_CT_077_2026.pdf
12
+ // Wiener Boerse closes its main market (VIE) and the Vienna MTF (WBDM) on its stock exchange holidays,
13
+ // and trades on Austria's other public holidays:
14
+ // https://www.wienerborse.at/uploads/u/cms/files/trading/stock-exchange-holidays-2026-en.pdf (and -2027-en.pdf)
12
15
  // SGX closes on Singapore's gazetted public holidays, one on a Sunday moving to the Monday:
13
16
  // https://www.sgx.com/stock-exchange/trading and https://www.mom.gov.sg/employment-practices/public-holidays
14
17
  // MSX closes on the holidays its site lists (msx.om exchange holidays, read 10 Oct 2026).
@@ -28,6 +31,11 @@ const KRX_CLOSURES: Record<number, readonly string[]> = {
28
31
  ],
29
32
  };
30
33
 
34
+ const WIENER_BOERSE_CLOSURES: Record<number, readonly string[]> = {
35
+ 2026: ["01-01", "04-03", "04-06", "05-01", "10-26", "12-24", "12-25", "12-31"],
36
+ 2027: ["01-01", "03-26", "03-29", "10-26", "12-24", "12-31"],
37
+ };
38
+
31
39
  const TWSE_CLOSURES: Record<number, readonly string[]> = {
32
40
  2026: [
33
41
  "01-01", "02-12", "02-13", "02-16", "02-17", "02-18", "02-19", "02-20", "02-27",
@@ -61,6 +69,8 @@ export const PUBLISHED_APAC_CLOSURES: Readonly<Record<string, Record<number, rea
61
69
  2026: ["01-01", "02-17", "02-18", "04-03", "05-01", "05-27", "06-01", "08-10", "11-09", "12-25"],
62
70
  2027: ["01-01", "02-08", "03-10", "03-26", "05-17", "05-20", "08-09", "10-28"],
63
71
  },
72
+ VIE: WIENER_BOERSE_CLOSURES,
73
+ WBDM: WIENER_BOERSE_CLOSURES,
64
74
  MSX: {
65
75
  2026: ["01-15", "01-18", "03-19", "03-22", "03-23", "05-26", "05-27", "05-28", "06-18", "08-27", "11-25", "11-26"],
66
76
  },
@@ -5,7 +5,7 @@ import { buildQuoteKey, resolveEntryData } from "../selectors";
5
5
  import type { QueryEntry } from "../result-types";
6
6
  import { resolveCurrencyUnit } from "../../utils/currency-units";
7
7
  import { isFiniteNumber } from "../../utils/guards";
8
- import { getCompletedRegularSessionDisplay, getTradedSessionMoveDisplay, type ExtendedSession } from "../market/status";
8
+ import { getCompletedRegularSessionDisplay, getTradedSessionMoveDisplay, type ActiveQuoteDisplay, type ExtendedSession } from "../market/status";
9
9
  import { quoteTradingDay } from "./day-range";
10
10
 
11
11
  const STREAM_FRESHNESS_MS = 2 * 60_000;
@@ -172,15 +172,32 @@ function overlaySessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote):
172
172
  };
173
173
  }
174
174
 
175
- function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote, completedSessionDate: string | null): T {
175
+ /** The regular session as a board reads it from one quote (getRegularSessionQuoteDisplay). */
176
+ export type RegularSessionQuoteDisplay = ActiveQuoteDisplay & (
177
+ | { completed: false }
178
+ /** The completed session's close and move, and its date when the quote can say it. */
179
+ | { completed: true; closedSession: string | null }
180
+ );
181
+
182
+ /**
183
+ * What a board that headlines only the regular session reads from a quote
184
+ * (the `regularSession` overlay): the live quote while the session trades,
185
+ * then the completed session's close and its move. Null when the quote
186
+ * leaves the row as it was: outside the session without a close it can name,
187
+ * or, on a board of `completedSessionDate`, a quote that is no news for it.
188
+ */
189
+ export function getRegularSessionQuoteDisplay(
190
+ quote: Quote,
191
+ completedSessionDate: string | null = null,
192
+ ): RegularSessionQuoteDisplay | null {
176
193
  const state = quote.marketState;
177
194
  if (state == null || state === "REGULAR") {
178
195
  // On a board of a completed session, only a session that trades after it is news.
179
196
  if (completedSessionDate) {
180
197
  const day = state === "REGULAR" ? quoteTradingDay(quote) : null;
181
- if (day == null || day <= completedSessionDate) return row;
198
+ if (day == null || day <= completedSessionDate) return null;
182
199
  }
183
- return overlayQuote(row, quote);
200
+ return { price: quote.price, change: quote.change, changePercent: quote.changePercent, completed: false };
184
201
  }
185
202
  const display = getCompletedRegularSessionDisplay(quote);
186
203
  const closeDate = quote.regularCloseSessionDate;
@@ -190,14 +207,21 @@ function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Q
190
207
  || !isFiniteNumber(display.changePercent)
191
208
  || (completedSessionDate != null && closeDate != null && closeDate < completedSessionDate)
192
209
  ) {
193
- return row;
210
+ return null;
194
211
  }
212
+ return { ...display, completed: true, closedSession: closeDate ?? quoteTradingDay(quote) };
213
+ }
214
+
215
+ function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote, completedSessionDate: string | null): T {
216
+ const display = getRegularSessionQuoteDisplay(quote, completedSessionDate);
217
+ if (!display) return row;
218
+ if (!display.completed) return overlayQuote(row, quote);
195
219
  return {
196
220
  ...overlayQuote(row, quote),
197
221
  price: display.price,
198
222
  change: isFiniteNumber(display.change) ? display.change : null,
199
- changePercent: display.changePercent,
200
- regularChangePercent: display.changePercent,
223
+ changePercent: display.changePercent ?? null,
224
+ regularChangePercent: display.changePercent ?? null,
201
225
  };
202
226
  }
203
227
 
@@ -15,6 +15,7 @@ import { truncateWithEllipsis } from "../../../utils/text-wrap";
15
15
  import type { SelectControl } from "../../../components/ui/select-button";
16
16
  import type { ProfileAnalyticsPreview } from "./model";
17
17
  import { t } from "../../../i18n";
18
+ import { ClippedText } from "../../../components/ui/display";
18
19
 
19
20
  export function accountFieldLabelWidth(width: number) {
20
21
  const preferred = width >= 28 ? 16 : Math.max(10, Math.floor(width * 0.42));
@@ -132,16 +133,12 @@ export function PublicAnalyticsGroup({
132
133
  const valueWidth = Math.max(1, metricWidth - labelTextWidth - 1);
133
134
  return (
134
135
  <Box key={metric.id} width={metricWidth} height={1} flexDirection="row" gap={1}>
135
- <Text fg={colors.textDim}>{truncateWithEllipsis(metric.label, labelTextWidth)}</Text>
136
- <Text fg={metricColor(metric.tone)} attributes={TextAttributes.BOLD}>
137
- {truncateWithEllipsis(metric.value, valueWidth)}
138
- </Text>
136
+ <ClippedText fg={colors.textDim} text={metric.label} width={labelTextWidth} cut={truncateWithEllipsis} />
137
+ <ClippedText fg={metricColor(metric.tone)} attributes={TextAttributes.BOLD} text={metric.value} width={valueWidth} cut={truncateWithEllipsis} />
139
138
  </Box>
140
139
  );
141
140
  }) : detail ? (
142
- <Text fg={colors.textMuted}>
143
- {truncateWithEllipsis(detail, detailWidth)}
144
- </Text>
141
+ <ClippedText fg={colors.textMuted} text={detail} width={detailWidth} cut={truncateWithEllipsis} />
145
142
  ) : null}
146
143
  </Box>
147
144
  {metrics.length === 0 && displayPreview.subtitle ? (
@@ -1,13 +1,14 @@
1
1
  import { useCallback, useState } from "react";
2
2
  import { Button, TextField } from "../../../components";
3
3
  import { DialogFrame } from "../../../components/ui/frame";
4
- import { Box, Text } from "../../../ui";
4
+ import { Box } from "../../../ui";
5
5
  import { useDialogKeyboard, type AlertContext } from "../../../ui/dialog";
6
6
  import { colors } from "../../../theme/colors";
7
7
  import { isPlainKey } from "../../../utils/keyboard";
8
8
  import { truncateWithEllipsis } from "../../../utils/text-wrap";
9
9
  import { t } from "../../../i18n";
10
10
  import { MIN_ACCOUNT_PASSWORD_LENGTH } from "../../../account/auth-model";
11
+ import { ClippedText } from "../../../components/ui/display";
11
12
 
12
13
  type PasswordDialogField = "current" | "new" | "confirm";
13
14
 
@@ -111,7 +112,7 @@ export function PasswordChangeDialog({
111
112
  onChange={setConfirmPassword}
112
113
  onSubmit={() => { void submit(); }}
113
114
  />
114
- {error ? <Text fg={colors.negative}>{truncateWithEllipsis(error, fieldWidth)}</Text> : null}
115
+ {error ? <ClippedText fg={colors.negative} text={error} width={fieldWidth} cut={truncateWithEllipsis} wrap /> : null}
115
116
  <Box flexDirection="row" justifyContent="flex-end">
116
117
  <Button
117
118
  label={submitting ? t("Changing...") : t("Update Password")}
@@ -22,7 +22,8 @@ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
22
22
  import { usePaneInstanceId, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
23
23
  import { useThemeColors } from "../../../theme/theme-context";
24
24
  import type { PaneProps } from "../../../types/plugin";
25
- import { Box } from "../../../ui";
25
+ import { Box, useUiCapabilities } from "../../../ui";
26
+ import { FILL_REST } from "../../../components/ui/fill";
26
27
  import { useAutoRefresh } from "../../../react/auto-refresh";
27
28
  import { loadBacktestHistory } from "./client";
28
29
  import { runBacktest, type BacktestResult, type BacktestTrade } from "./engine";
@@ -146,6 +147,7 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
146
147
  }), [history.loading, result, view, paneId, symbol]);
147
148
 
148
149
  const { strip: tabStrip, rows: tabRows } = usePaneTabs(symbol ? { tabs: TABS, activeValue: view, onSelect: setView, focused, dense: true } : null);
150
+ const { nativePaneChrome } = useUiCapabilities();
149
151
  if (!symbol) return <EmptyState title="Select a ticker." hint="Open BT with a symbol, for example BT AAPL." />;
150
152
  const bodyHeight = Math.max(4, height - 1 - tabRows);
151
153
  const wide = width - SUMMARY_WIDTH - 1 >= MIN_WIDE_CHART_COLS;
@@ -187,7 +189,9 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
187
189
  />
188
190
  );
189
191
  return (
190
- <Box width={width} height={height} flexDirection="column">
192
+ // On the desktop the pane's box fills the pane by flex, so what fills it
193
+ // follows the pane's frame by CSS, the chart beside the metrics too.
194
+ <Box width={width} flexDirection="column" {...(nativePaneChrome ? FILL_REST : { height })}>
191
195
  {tabStrip}
192
196
  <QueryBar
193
197
  width={width}
@@ -242,7 +246,9 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
242
246
  emptyStateTitle="No trades in this window."
243
247
  />
244
248
  ) : wide ? (
245
- <Box flexDirection="row" width={width} height={bodyHeight}>
249
+ // The desktop's query bar is taller than a row: the chart and the
250
+ // metrics take what is left under it, down to the pane footer.
251
+ <Box flexDirection="row" width={width} {...(nativePaneChrome ? FILL_REST : { height: bodyHeight })}>
246
252
  {chart(width - SUMMARY_WIDTH - 1, bodyHeight)}
247
253
  <Box width={1} />
248
254
  {summaryTable(SUMMARY_WIDTH, bodyHeight)}
@@ -1,11 +1,12 @@
1
1
  import { Section, TickerBadgeList } from "../../../../components";
2
+ import { ClippedText } from "../../../../components/ui/display";
2
3
  import { formatMarketPriceWithCurrency, liveQuoteFormatOptions } from "../../../../market-data/market/format";
3
4
  import { useInlineTickerQuote } from "../../../../state/hooks/inline-tickers";
4
5
  import { useTickerQuoteStream } from "../../../../state/hooks/live-ticker-financials";
5
6
  import { colors } from "../../../../theme/colors";
6
7
  import type { Quote } from "../../../../types/financials";
7
8
  import type { TickerRecord } from "../../../../types/ticker";
8
- import { Box, Text } from "../../../../ui";
9
+ import { Box } from "../../../../ui";
9
10
  import { formatCompact } from "../../../../utils/format";
10
11
  import { truncateWithEllipsis } from "../../../../utils/text-wrap";
11
12
  import { activityLabel, criticalityColor, dateShort, metricColor, metricNumber, textOrNull, tickerSymbol } from "../format";
@@ -249,7 +250,7 @@ export function CompanyDetail({
249
250
  ].filter(Boolean);
250
251
  return (
251
252
  <Box key={`${site.name}-${index}`} flexDirection="column" marginTop={index === 0 ? 0 : 1}>
252
- <Text fg={colors.textMuted}>{truncateWithEllipsis(parts.join(" - "), bodyWidth)}</Text>
253
+ <ClippedText fg={colors.textMuted} text={parts.join(" - ")} width={bodyWidth} cut={truncateWithEllipsis} />
253
254
  {site.involvementSummary ? (
254
255
  <MarkdownBlock text={site.involvementSummary} width={bodyWidth} catalog={catalog} openTicker={openTicker} marginTop={0} />
255
256
  ) : null}
@@ -262,7 +263,7 @@ export function CompanyDetail({
262
263
  <Section title="Recent Intel" width={bodyWidth}>
263
264
  {company.intelligence!.slice(0, 5).map((item, index) => (
264
265
  <Box key={`${item.headline ?? "intel"}:${index}`} flexDirection="column" marginTop={index === 0 ? 0 : 1}>
265
- <Text fg={colors.textDim}>{truncateWithEllipsis(`${dateShort(item.publishedAt)} ${item.headline ?? ""}`, bodyWidth)}</Text>
266
+ <ClippedText fg={colors.textDim} text={`${dateShort(item.publishedAt)} ${item.headline ?? ""}`} width={bodyWidth} cut={truncateWithEllipsis} />
266
267
  <MarkdownBlock text={item.content} width={bodyWidth} catalog={catalog} openTicker={openTicker} marginTop={0} />
267
268
  </Box>
268
269
  ))}