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
@@ -26,6 +26,21 @@ export function renderedReportNotices(semanticUi: readonly RemoteUiNodeSnapshot[
26
26
  return { notices, facts };
27
27
  }
28
28
 
29
+ /**
30
+ * Why a rendered pane cannot give the report it was asked for, from the
31
+ * `report-failure` node it publishes: `{ message, details? }`. OMON opened on
32
+ * an expiry the chain does not list says so in its body; the report fails with
33
+ * the error the plain command (`options --expiration`) prints, instead of
34
+ * reading that body back as a view. Null when the pane publishes none.
35
+ */
36
+ export function renderedReportFailure(semanticUi: readonly RemoteUiNodeSnapshot[]): { message: string; details?: string } | null {
37
+ const metadata = semanticUi.find((node) => node.role === "report-failure")?.metadata;
38
+ const message = typeof metadata?.message === "string" ? metadata.message.trim() : "";
39
+ if (!message) return null;
40
+ const details = typeof metadata?.details === "string" && metadata.details.trim() ? metadata.details.trim() : undefined;
41
+ return details ? { message, details } : { message };
42
+ }
43
+
29
44
  const FRESHNESS_STATUSES: readonly HeadlessFreshnessStatus[] = ["live", "delayed", "stale", "not-a-feed"];
30
45
 
31
46
  /** The `sources` a pane publishes: each a name and a time. */
@@ -13,6 +13,25 @@ export function symbolInputKey(symbol: string): string {
13
13
  return symbol.trim().replace(/^\$/, "").toUpperCase();
14
14
  }
15
15
 
16
+ /**
17
+ * How a command's symbol list spots one security typed twice: `symbolInputKey`,
18
+ * with a share class written with a dot or a dash as one (BRK.B and BRK-B).
19
+ */
20
+ function securityInputKey(symbol: string): string {
21
+ return symbolInputKey(symbol).replace(/-/g, ".");
22
+ }
23
+
24
+ /** The securities of a symbol list, each once, in the first spelling typed: `AAPL aapl BRK.B BRK-B` is AAPL and BRK.B. */
25
+ export function uniqueSecuritySymbols(symbols: readonly string[]): string[] {
26
+ const seen = new Set<string>();
27
+ return symbols.filter((symbol) => {
28
+ const key = securityInputKey(symbol);
29
+ if (!key || seen.has(key)) return false;
30
+ seen.add(key);
31
+ return true;
32
+ });
33
+ }
34
+
16
35
  /** The symbols without repeats, first spelling kept, and how many repeats were dropped. */
17
36
  export function uniqueSymbols(symbols: readonly string[]): { symbols: string[]; repeated: number } {
18
37
  const seen = new Set<string>();
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../../ui";
14
14
  import { displayWidth, truncateTextSegments, truncateToDisplayWidth } from "../../../utils/format";
15
15
  import { Spinner } from "../../ui";
16
+ import { ClippedText } from "../../ui/display";
16
17
  import { useCommandBarPalette } from "../panel/palette";
17
18
  import { getRowPresentation } from "../view-model";
18
19
  import {
@@ -306,7 +307,7 @@ export const CommandBarListBody = memo(function CommandBarListBody({
306
307
  if (row.kind === "message") {
307
308
  return (
308
309
  <Box key={row.id} height={1} paddingLeft={labelEdgePadding} paddingRight={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
309
- <Text fg={palette.text}>{truncateToDisplayWidth(t(row.label), labelEdgeWidth)}</Text>
310
+ <ClippedText fg={palette.text} text={t(row.label)} width={labelEdgeWidth} />
310
311
  </Box>
311
312
  );
312
313
  }
@@ -5,8 +5,8 @@ import {
5
5
  } from "../../../plugins/builtin/portfolio-list/command-bar";
6
6
  import type { CommandBarFieldOption, CommandBarWorkflowField, CommandBarWorkflowRoute } from "./types";
7
7
  import { buildCommandBarWorkflowRoute } from "./route-builder";
8
- import { teamPrefix } from "../../../plugins/builtin/cloud/team/model";
9
- import { teamStore } from "../../../plugins/builtin/cloud/team/store";
8
+ import { teamPrefix } from "../../../team/model";
9
+ import { teamStore } from "../../../team/store";
10
10
 
11
11
  /**
12
12
  * Where a new collection lives: me, or one of my teams. Only asked when there
@@ -1,7 +1,7 @@
1
1
  import type { Dispatch } from "react";
2
2
  import { apiClient } from "../../../api-client";
3
3
  import { t } from "../../../i18n";
4
- import { teamCollectionLocalId } from "../../../plugins/builtin/cloud/team/collections";
4
+ import { teamCollectionLocalId } from "../../../team/collections";
5
5
  import type { DataProvider } from "../../../types/data-provider";
6
6
  import type { AppTickerRepositoryPort } from "../../../core/app-service-ports";
7
7
  import type { PluginRegistry } from "../../../plugins/registry";
@@ -19,7 +19,7 @@ import {
19
19
  } from "../feedback/types";
20
20
  import { t, tf } from "../i18n";
21
21
  import { useAppLanguage } from "../i18n/react";
22
- import { requestAuthDialog } from "../plugins/builtin/cloud/auth-dialog";
22
+ import { requestAuthDialog } from "../account/auth-dialog";
23
23
  import { getSharedRegistry } from "../plugins/registry";
24
24
  import { useViewport } from "../react/input";
25
25
  import { useAppStateRef } from "../state/app/context";
@@ -125,7 +125,7 @@ export { MarkdownText } from "./markdown-text";
125
125
  // this file is the public `gloomberb/components` surface: a kit component
126
126
  // reaches plugins only once it is added here.
127
127
  export {
128
- ActionRow, Badge, BulletList, Button, ButtonActionScope, Checkbox, ChoiceDialog, ConfirmDialog, confirmDialog, DataTable,
128
+ ActionRow, Badge, BulletList, Button, ButtonActionScope, Checkbox, ChoiceDialog, ColumnGrid, ColumnGridPair, ConfirmDialog, confirmDialog, DataTable,
129
129
  DetailScrollBody, DialogFrame, DisclosureMarker, Divider, EmptyState, ExternalLink, ExternalLinkText, FieldGrid, FieldLabel,
130
130
  fieldGridColumns, fieldGridRows, FigureList, getMessageComposerBlockHeight, GridFieldView, Icon, ICON_GLYPHS,
131
131
  IconButton, InlineQuickAddRow, KeyValueRow, ListView, loadingText, Menu, MenuPopover,
@@ -136,7 +136,8 @@ export {
136
136
  useFieldRing, usePaneLinkMenuEntry, useQueryBarSearch,
137
137
  } from "./ui";
138
138
  export type {
139
- ActionRowProps, BadgeProps, BulletListProps, ButtonProps, ButtonVariant, CheckboxProps, ChoiceDialogChoice, ConfirmDialogOptions,
139
+ ActionRowProps, BadgeProps, BulletListProps, ButtonProps, ButtonVariant, CheckboxProps, ChoiceDialogChoice, ColumnGridCell,
140
+ ColumnGridColumn, ColumnGridPairProps, ColumnGridProps, ConfirmDialogOptions,
140
141
  DataTableCell, DataTableColumn, DataTableProps, DataTableVisibleRange, DetailScrollBodyProps, DialogFrameProps,
141
142
  DisclosureMarkerProps, DividerProps, EmptyStateProps, FieldGridProps, FieldLabelProps, FieldRing, FieldRingOptions,
142
143
  FigureListItem, FigureListProps, GridField,
@@ -28,14 +28,29 @@ import { useAppDispatch, useAppSelector } from "../../state/app/context";
28
28
  import { useViewport } from "../../react/input";
29
29
  import { canRetryUpdate } from "../../app/global-shortcuts";
30
30
  import { t, tf } from "../../i18n";
31
- import { truncateToDisplayWidth } from "../../utils/format";
31
+ import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
32
+ import { ClippedText } from "../ui/display";
32
33
  import { resolveMarketSummaryFit, useMarketSummary } from "./market-summary";
33
34
  import { resolveHeaderPromptGeometry } from "./shell/chrome";
34
35
  import { useWindowFullscreen } from "./window-fullscreen";
35
36
  import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
36
- import { Button } from "../ui/button";
37
+ import { Button, terminalButtonColumns } from "../ui/button";
37
38
 
38
39
  const UPDATE_NOTICE_DURATION_MS = 5_000;
40
+ /**
41
+ * The failed-update message is cut to the room it has, but not below this many
42
+ * columns ("Update fa..."): narrower says nothing, so the banner is left out.
43
+ * The retry hint gives way before the message goes under it.
44
+ */
45
+ const UPDATE_ERROR_MIN_COLUMNS = 12;
46
+ /** Kept clear after the failed-update banner, so its last character never touches the Help pill. */
47
+ const UPDATE_ERROR_GAP_COLUMNS = 1;
48
+ /** Left padding of the update status's slot, in cells; the desktop's title-bar layout also pads its right. */
49
+ const UPDATE_SLOT_PADDING_COLUMNS = 2;
50
+ /** The Help pill's own props, shared by its button and the width the update status leaves it. */
51
+ const helpButtonProps = () => ({ label: "Open Help", displayLabel: t("Help"), shortcut: "?", compact: true });
52
+ /** The terminal pads the pill's right edge by a cell; the desktop pads it in px. */
53
+ const TERMINAL_HELP_PADDING_COLUMNS = 1;
39
54
 
40
55
  type HeaderActionEvent = {
41
56
  key?: string;
@@ -157,11 +172,17 @@ function HeaderPromptInput({
157
172
  placeholderColor={subtleColor}
158
173
  cursorColor={colors.textBright}
159
174
  />
160
- {ghostSuffix && (
175
+ {ghostSuffix && (nativePaneChrome ? (
176
+ // The desktop sets the suggestion right after the typed text, one
177
+ // character per `ch` of the field's font, and cuts it where the field ends.
178
+ <Box position="absolute" top={0} height={1} flexDirection="row" style={{ left: `${displayWidth(query)}ch`, right: 0 }}>
179
+ <ClippedText fg={subtleColor} text={ghostSuffix} width={0} />
180
+ </Box>
181
+ ) : (
161
182
  <Box position="absolute" top={0} left={ghostLeft} width={Math.max(0, width - ghostLeft)} height={1}>
162
183
  <Text fg={subtleColor}>{truncateToDisplayWidth(ghostSuffix, Math.max(0, width - ghostLeft))}</Text>
163
184
  </Box>
164
- )}
185
+ ))}
165
186
  </Box>
166
187
  );
167
188
  }
@@ -244,8 +265,14 @@ function HeaderCommandPrompt({
244
265
  );
245
266
  }
246
267
 
247
- function UpdateStatus({ onRestart }: { onRestart?: () => void }) {
268
+ /**
269
+ * What the header says about an update. In the terminal it is cut to `columns`,
270
+ * the room its slot is guaranteed (the desktop clips it in CSS where the slot
271
+ * ends), so it never runs into the Help pill or the market cluster beside it.
272
+ */
273
+ function UpdateStatus({ columns, onRestart }: { columns: number; onRestart?: () => void }) {
248
274
  const colors = useThemeColors();
275
+ const { nativePaneChrome = false } = useUiCapabilities();
249
276
  const dispatch = useAppDispatch();
250
277
  const updateAvailable = useAppSelector((state) => state.updateAvailable);
251
278
  const updateProgress = useAppSelector((state) => state.updateProgress);
@@ -301,11 +328,20 @@ function UpdateStatus({ onRestart }: { onRestart?: () => void }) {
301
328
  return <Text fg={colors.headerText}>{t(updateProgress.message ?? "Update ready, restart to apply")}</Text>;
302
329
  }
303
330
  if (updateProgress.phase === "error") {
331
+ // One line: an error that carries a stack or a command's output must not grow the header.
332
+ const message = tf("Update failed: {error}", { error: (updateProgress.error ?? "Unknown error").replace(/\s+/g, " ").trim() });
304
333
  const retry = updateKey && canRetryUpdate({ updateAvailable, updateProgress, updateCheckInProgress });
334
+ const hint = retry ? ` ${updateKey} ${t("retry")}` : "";
335
+ const hintColumns = displayWidth(hint);
336
+ // The message goes first, and keeps its room before the hint takes any.
337
+ const room = columns - UPDATE_ERROR_GAP_COLUMNS;
338
+ const showHint = hintColumns > 0 && (nativePaneChrome || room - hintColumns >= UPDATE_ERROR_MIN_COLUMNS);
339
+ const messageColumns = room - (showHint ? hintColumns : 0);
340
+ if (!nativePaneChrome && messageColumns < UPDATE_ERROR_MIN_COLUMNS) return null;
305
341
  return (
306
- <Box flexDirection="row">
307
- <Text fg={colors.headerText}>{tf("Update failed: {error}", { error: updateProgress.error ?? "Unknown error" })}</Text>
308
- {retry ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{` ${updateKey} ${t("retry")}`}</Text> : null}
342
+ <Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_ERROR_GAP_COLUMNS}>
343
+ <ClippedText fg={colors.headerText} text={message} width={messageColumns} />
344
+ {showHint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
309
345
  </Box>
310
346
  );
311
347
  }
@@ -404,11 +440,11 @@ function HeaderHelpButton({ nativePaneChrome, onPress }: { nativePaneChrome: boo
404
440
  return (
405
441
  <Box
406
442
  flexShrink={0}
407
- paddingRight={nativePaneChrome ? undefined : 1}
443
+ paddingRight={nativePaneChrome ? undefined : TERMINAL_HELP_PADDING_COLUMNS}
408
444
  data-gloom-role="header-help-action"
409
445
  style={nativePaneChrome ? { marginRight: 8 } : undefined}
410
446
  >
411
- <Button compact label="Open Help" displayLabel={t("Help")} shortcut="?" onPress={onPress} stopPropagation />
447
+ <Button {...helpButtonProps()} onPress={onPress} stopPropagation />
412
448
  </Box>
413
449
  );
414
450
  }
@@ -437,6 +473,14 @@ export function Header({
437
473
  windowFullscreen,
438
474
  });
439
475
  const shortcutLabel = useCommandBarShortcut();
476
+ // The terminal lays the header out by cell counts, so it works out what is left
477
+ // for the update status once the Help pill and the slot's padding are paid for.
478
+ const updateStatusColumns = Math.max(
479
+ 0,
480
+ prompt.updateColumns
481
+ - (onOpenHelp ? terminalButtonColumns(helpButtonProps()) + TERMINAL_HELP_PADDING_COLUMNS : 0)
482
+ - UPDATE_SLOT_PADDING_COLUMNS * (titleBarOverlay ? 2 : 1),
483
+ );
440
484
  // Desktop has its own window chrome to sit on; the terminal has none, so the
441
485
  // bar itself wears the prompt's fill and reads as a single strip.
442
486
  const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
@@ -483,8 +527,8 @@ export function Header({
483
527
  >
484
528
  <Box width={prompt.left} />
485
529
  {commandPrompt}
486
- <Box flexGrow={1} paddingLeft={2} paddingRight={2} minWidth={0}>
487
- <UpdateStatus onRestart={onRestartForUpdate} />
530
+ <Box flexGrow={1} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS} paddingRight={UPDATE_SLOT_PADDING_COLUMNS} minWidth={0}>
531
+ <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
488
532
  </Box>
489
533
  {onOpenHelp ? <HeaderHelpButton nativePaneChrome onPress={onOpenHelp} /> : null}
490
534
  {marketSummary}
@@ -505,8 +549,8 @@ export function Header({
505
549
  >
506
550
  <Box width={prompt.left} />
507
551
  {commandPrompt}
508
- <Box flexGrow={1} minWidth={0} paddingLeft={2}>
509
- <UpdateStatus onRestart={onRestartForUpdate} />
552
+ <Box flexGrow={1} minWidth={0} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS}>
553
+ <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
510
554
  </Box>
511
555
  {onOpenHelp ? <HeaderHelpButton nativePaneChrome={nativePaneChrome} onPress={onOpenHelp} /> : null}
512
556
  {marketSummary}
@@ -73,6 +73,12 @@ export interface HeaderPromptGeometry {
73
73
  width: number;
74
74
  /** Columns held at the header's right edge for the market cluster. */
75
75
  marketColumns: number;
76
+ /**
77
+ * Columns between the prompt and the market cluster: the update status, its
78
+ * padding and the Help pill share them. Never negative, so a window too narrow
79
+ * for the header reads as no room rather than a negative width.
80
+ */
81
+ updateColumns: number;
76
82
  }
77
83
 
78
84
  /**
@@ -95,14 +101,14 @@ export function resolveHeaderPromptGeometry(options: {
95
101
  ? getTitlebarLeadingInset({ platform, windowFullscreen })
96
102
  : 0;
97
103
  const left = leadingInset + 1;
98
- const updateColumns = headerPromptUpdateColumns(termWidth);
99
- const marketColumns = headerMarketColumns(termWidth, left, updateColumns);
104
+ const updateReserve = headerPromptUpdateColumns(termWidth);
105
+ const marketColumns = headerMarketColumns(termWidth, left, updateReserve);
100
106
  const width = Math.max(
101
107
  HEADER_PROMPT_MIN_WIDTH,
102
108
  Math.min(
103
109
  resolveCommandSurfaceWidth({ nativePaneChrome, termWidth }),
104
- termWidth - left - updateColumns - marketColumns,
110
+ termWidth - left - updateReserve - marketColumns,
105
111
  ),
106
112
  );
107
- return { left, width, marketColumns };
113
+ return { left, width, marketColumns, updateColumns: Math.max(0, termWidth - left - width - marketColumns) };
108
114
  }
@@ -61,7 +61,7 @@ import {
61
61
  useShellVisibleLayout,
62
62
  } from "./layout-state";
63
63
  import { restoreShellHiddenPanes } from "./visible-layout";
64
- import { AuthDialogHost } from "../../../plugins/builtin/cloud/auth-dialog";
64
+ import { AuthDialogHost } from "../../../account/auth-dialog";
65
65
  import { DeviceSignInDialogHost } from "../../../account/device-signin-dialog";
66
66
  import { McpConnectDialogHost } from "../../../plugins/builtin/cloud/mcp-connect/dialog";
67
67
  import { BrokerSignInDialogHost } from "../../../brokers/signed-in/sign-in-dialog";
@@ -1,6 +1,6 @@
1
1
  import type { TeamSummary } from "../../api-client";
2
2
  import type { SavedLayout } from "../../types/config";
3
- import type { TeamStoreSnapshot } from "../../plugins/builtin/cloud/team/store";
3
+ import type { TeamStoreSnapshot } from "../../team/store";
4
4
 
5
5
  export type TabGroupId = "personal" | `team:${string}`;
6
6
 
@@ -43,8 +43,8 @@ import { confirmDialog } from "../ui/confirm-dialog";
43
43
  import { Tabs } from "../ui/tabs";
44
44
  import { useTransientLayout } from "./transient-layout";
45
45
  import { linkedLayoutMarker, linkedLayoutStatus, linkedLayoutUpdates } from "../../layout-marketplace/linked";
46
- import { teamAccentHex } from "../../plugins/builtin/cloud/team/model";
47
- import { teamStore } from "../../plugins/builtin/cloud/team/store";
46
+ import { teamAccentHex } from "../../team/model";
47
+ import { teamStore } from "../../team/store";
48
48
  import { buildStatusBarTabGroups, groupIdFromMarkerValue, groupMarkerValue } from "./status-bar-groups";
49
49
  import { requestFeedbackDialog } from "../feedback-dialog";
50
50
  import { openFormModal } from "../form-modal";
@@ -17,6 +17,8 @@ export interface MarkdownTextProps {
17
17
  lineWidth?: number;
18
18
  /** Desktop and web: the space a blank line leaves between paragraphs, in CSS units; one line when unset. */
19
19
  paragraphGap?: string;
20
+ /** Desktop and web: the size, weight and space of a heading line, such as a reader's section heading; the text's own when unset. */
21
+ headingStyle?: Readonly<Record<string, string | number>>;
20
22
  catalog?: Record<string, InlineTickerCatalogEntry>;
21
23
  textColor?: string;
22
24
  openTicker?: (symbol: string) => void;
@@ -86,9 +88,11 @@ function parseInlineMarkdown(text: string, links = false): StyledSegment[] {
86
88
  return segments;
87
89
  }
88
90
 
91
+ const HEADING_RE = /^(#{1,3})\s+(.+)$/;
92
+
89
93
  function parseLine(line: string, links: boolean): ParsedLine {
90
94
  // Headings
91
- const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
95
+ const headingMatch = line.match(HEADING_RE);
92
96
  if (headingMatch) {
93
97
  return {
94
98
  heading: true,
@@ -595,6 +599,7 @@ export function MarkdownText({
595
599
  openTicker = () => {},
596
600
  onOpenLink,
597
601
  paragraphGap,
602
+ headingStyle,
598
603
  }: MarkdownTextProps) {
599
604
  const { nativePaneChrome } = useUiCapabilities();
600
605
  const links = onOpenLink !== undefined;
@@ -620,12 +625,15 @@ export function MarkdownText({
620
625
  }
621
626
  const line = block.text;
622
627
  if (line.trim() === "") {
628
+ // Under a heading set in its own style, the heading's margin is the space: it sits with what it names.
629
+ const previous = blocks[index - 1];
630
+ if (nativePaneChrome && headingStyle && previous?.kind === "line" && HEADING_RE.test(previous.text)) return null;
623
631
  return nativePaneChrome && paragraphGap
624
632
  ? <Box key={index} flexShrink={0} style={{ height: paragraphGap }} />
625
633
  : <Text key={index}>{" "}</Text>;
626
634
  }
627
635
  const parsed = parseLine(line, links);
628
- return (
636
+ const markdownLine = (
629
637
  <MarkdownLine
630
638
  key={index}
631
639
  parsed={parsed}
@@ -638,6 +646,10 @@ export function MarkdownText({
638
646
  onOpenLink={onOpenLink}
639
647
  />
640
648
  );
649
+ // A line's height comes from --cell-h, in em of its own size.
650
+ return nativePaneChrome && headingStyle && parsed.heading
651
+ ? <Box key={index} flexDirection="column" width="100%" style={{ ...headingStyle, "--cell-h": `${headingStyle.lineHeight ?? 1.35}em` }}>{markdownLine}</Box>
652
+ : markdownLine;
641
653
  })}
642
654
  </Box>
643
655
  );
@@ -375,7 +375,7 @@ function ArticleReaderView({
375
375
  <ReaderText text={lead} geometry={geometry} color={colors.textBright} variant="lead" />
376
376
  ) : null}
377
377
  {!raw && facts.length > 0 ? (
378
- <Box flexDirection="column" style={desktop ? { margin: "0.3em 0 1.3em" } : undefined}>
378
+ <Box flexDirection="column" style={desktop ? { margin: "0.3em 0 1em" } : undefined}>
379
379
  <ReaderText text={readerLabel("Key facts")} geometry={geometry} color={colors.textDim} variant="label" />
380
380
  {facts.map((fact, index) => (
381
381
  <ReaderText key={index} text={fact} geometry={geometry} color={colors.text} variant="fact" segments={figureSegments(fact)} figureColor={colors.textBright} />
@@ -9,7 +9,7 @@ import { DetailScrollBody } from "../ui/detail-scroll-body";
9
9
  import { KeyValueRow } from "../ui/display";
10
10
  import { ExternalLink, usePaneLinkMenuEntry } from "../ui/external-link";
11
11
  import { parseArticle } from "./article-text";
12
- import { DESKTOP_LINE_HEIGHT, READER_APP_FONT, READER_COLUMN_FONT, ReaderBlock, ReaderText, type ReaderGeometry } from "./reader-text";
12
+ import { DESKTOP_LINE_HEIGHT, READER_APP_FONT, READER_COLUMN_FONT, READER_HEADING_STYLE, ReaderBlock, ReaderText, type ReaderGeometry } from "./reader-text";
13
13
 
14
14
  export interface DetailFact {
15
15
  label: string;
@@ -50,14 +50,17 @@ const URL_LINE = /^https?:\/\/\S+$/;
50
50
 
51
51
  const COLUMN_STYLE = { fontSize: READER_COLUMN_FONT, lineHeight: DESKTOP_LINE_HEIGHT } as const;
52
52
  const META_STYLE = { marginBottom: "0.8em" } as const;
53
- /** Its columns come from styles.css: one pair to a row, or two in a wide reader. */
53
+ /**
54
+ * At the app's own size, as metadata is in every reader. Its columns come from
55
+ * styles.css: one pair to a row, or two in a wide reader.
56
+ */
54
57
  const FACTS_STYLE = {
55
58
  display: "grid",
56
- columnGap: "1.6em",
57
- rowGap: "0.2em",
58
- fontSize: "0.8em",
59
+ columnGap: "2em",
60
+ rowGap: "0.3em",
61
+ fontSize: READER_APP_FONT,
59
62
  lineHeight: 1.5,
60
- marginBottom: "1.4em",
63
+ marginBottom: "1.6em",
61
64
  } as const;
62
65
  const FACT_TEXT_STYLE = { display: "block", lineHeight: "inherit", minWidth: 0 } as const;
63
66
  /** Markdown lines take the reader's line height (the host sets Text line height from --cell-h). */
@@ -118,7 +121,7 @@ function NativeDetail({ meta, facts, body, bodyFormat, note }: DetailContentProp
118
121
  ) : null}
119
122
  {bodyFormat === "markdown" && body ? (
120
123
  <Box flexDirection="column" width="100%" style={MARKDOWN_STYLE}>
121
- <MarkdownText text={body} paragraphGap={PARAGRAPH_GAP} />
124
+ <MarkdownText text={body} paragraphGap={PARAGRAPH_GAP} headingStyle={READER_HEADING_STYLE} />
122
125
  </Box>
123
126
  ) : blocks.map((block, index) => (
124
127
  <Box key={index} flexDirection="column">
@@ -12,18 +12,32 @@ import type { ArticleBlock } from "./article-text";
12
12
  * detail reader of list panes.
13
13
  */
14
14
 
15
- /** Line height of reading text on the desktop, against its own size. */
16
- export const DESKTOP_LINE_HEIGHT = 1.65;
17
-
18
15
  /**
19
- * The reading size on the desktop against the app's own (12px by default):
20
- * 17.4px, or 16.2px in a column under 480px, where a phone sets its body. A
21
- * container query in styles.css picks it from the column's own width, so a
22
- * resize changes it with the layout and nothing counts cells.
16
+ * The reader's type scale on the desktop and the web, one for every reader
17
+ * (the article reader, the detail reader, the markdown and docs headings in
18
+ * them). Reading text sits a step above the app's own size (12px by default)
19
+ * and every other level is a modest step from it, so the page reads as part
20
+ * of the app and the levels differ by weight and space as much as by size:
21
+ *
22
+ * | Level | Wide | Column under 480px |
23
+ * |---|---|---|
24
+ * | Metadata, facts, tables, coverage | 12px, dimmer | 12px |
25
+ * | Body | 14px, line height 1.6 | 13.2px |
26
+ * | Lead | 15px | 14.1px |
27
+ * | Section heading | 16px bold | 15.2px |
28
+ * | Title | 20px bold | 18.9px |
29
+ *
30
+ * The body size comes from `--reader-em`, which a container query in
31
+ * styles.css lowers in a narrow column, so a resize changes it with the
32
+ * layout and nothing counts cells. The levels above it are in em of it.
23
33
  */
24
- export const READER_COLUMN_FONT = "calc(var(--reader-em, 1.45) * 1em)";
25
- /** Back to the app's own size inside a reading column: chips, controls, tables. */
26
- export const READER_APP_FONT = "calc(1em / var(--reader-em, 1.45))";
34
+ export const READER_COLUMN_FONT = "calc(var(--reader-em, 1.1667) * 1em)";
35
+ /** Back to the app's own size inside a reading column: metadata, chips, controls, tables. */
36
+ export const READER_APP_FONT = "calc(1em / var(--reader-em, 1.1667))";
37
+ /** Line height of reading text on the desktop, against its own size. */
38
+ export const DESKTOP_LINE_HEIGHT = 1.6;
39
+ /** A section heading in reading text: a step above the body, set apart by weight and the space above it. */
40
+ export const READER_HEADING_STYLE = { fontSize: "1.15em", fontWeight: 700, lineHeight: 1.35, margin: "0.9em 0 0.4em" } as const;
27
41
 
28
42
  export type Segment = { text: string; marked?: boolean; figure?: boolean };
29
43
 
@@ -58,14 +72,15 @@ function hasHeaderRow(rows: readonly string[][]): boolean {
58
72
  export type TextVariant = "headline" | "meta" | "lead" | "body" | "heading" | "label" | "fact" | "item";
59
73
 
60
74
  const DESKTOP_VARIANT_STYLE: Record<TextVariant, Record<string, string | number>> = {
61
- headline: { fontSize: "1.45em", lineHeight: 1.22, letterSpacing: "-0.01em" },
62
- meta: { fontSize: "0.78em", lineHeight: 1.5 },
63
- lead: { fontSize: "1.1em", lineHeight: 1.55, marginBottom: "1.1em" },
75
+ headline: { fontSize: "1.43em", lineHeight: 1.25 },
76
+ meta: { fontSize: READER_APP_FONT, lineHeight: 1.5 },
77
+ lead: { fontSize: "1.07em", lineHeight: 1.55, marginBottom: "1em" },
64
78
  body: { lineHeight: DESKTOP_LINE_HEIGHT, marginBottom: "0.9em" },
65
- heading: { fontSize: "1.08em", lineHeight: 1.35, margin: "0.6em 0 0.35em" },
66
- label: { fontSize: "0.8em", lineHeight: 1.6, marginBottom: "0.3em" },
67
- fact: { fontSize: "0.95em", lineHeight: 1.55, marginBottom: "0.35em" },
68
- item: { lineHeight: 1.6, marginBottom: "0.3em" },
79
+ heading: READER_HEADING_STYLE,
80
+ // Names what follows (key facts, coverage) as a section heading does, without the air above a section.
81
+ label: { ...READER_HEADING_STYLE, margin: "0 0 0.4em" },
82
+ fact: { fontSize: "0.93em", lineHeight: 1.55, marginBottom: "0.35em" },
83
+ item: { lineHeight: DESKTOP_LINE_HEIGHT, marginBottom: "0.3em" },
69
84
  };
70
85
 
71
86
  function styled(segments: readonly Segment[], color: string, markColor: string, figureColor: string | undefined, attributes: number): StyledText {