gloomberb 0.16.0 → 0.16.1

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 (167) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/data.ts +18 -3
  3. package/src/api-client/index.ts +3 -0
  4. package/src/api-client/members.ts +26 -0
  5. package/src/api-client/perps.ts +4 -0
  6. package/src/api-client/types.ts +7 -1
  7. package/src/app/global-shortcuts.ts +14 -6
  8. package/src/app/keybindings/actions.ts +1 -1
  9. package/src/app/runtime/update.ts +38 -2
  10. package/src/app.tsx +10 -6
  11. package/src/cli/pane-functions/function-help.ts +9 -2
  12. package/src/components/command-bar/commands/collection.ts +5 -1
  13. package/src/components/command-bar/routes/root/command-items.ts +9 -2
  14. package/src/components/command-bar/routes/root/plugin-install.ts +2 -2
  15. package/src/components/command-bar/routes/root/provider-search.ts +19 -5
  16. package/src/components/command-bar/routes/root/runtime.ts +14 -5
  17. package/src/components/command-bar/routes/ticker-search/results.ts +31 -5
  18. package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
  19. package/src/components/command-bar/routing/actions.ts +2 -0
  20. package/src/components/command-bar/selection-runtime.ts +38 -0
  21. package/src/components/command-bar/surface/index.tsx +7 -2
  22. package/src/components/index.ts +2 -2
  23. package/src/components/layout/floating-pane.tsx +6 -0
  24. package/src/components/layout/header.tsx +24 -7
  25. package/src/components/layout/missing-pane.tsx +2 -2
  26. package/src/components/layout/pane/header.tsx +63 -30
  27. package/src/components/layout/pane/index.tsx +6 -0
  28. package/src/components/layout/pane/title.ts +31 -7
  29. package/src/components/layout/shell/drag/index.ts +6 -5
  30. package/src/components/layout/shell/drag/runtime.ts +3 -0
  31. package/src/components/layout/shell/index.tsx +6 -0
  32. package/src/components/layout/shell/pane/layers.tsx +15 -7
  33. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -3
  34. package/src/components/layout/shell/ticker-link-menu.ts +23 -9
  35. package/src/components/metric-treemap/heat-surface.tsx +718 -0
  36. package/src/components/metric-treemap/heat-tiles.ts +210 -0
  37. package/src/components/metric-treemap/index.tsx +2 -0
  38. package/src/components/metric-treemap/layout.ts +6 -0
  39. package/src/components/metric-treemap/scene.ts +432 -0
  40. package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -0
  41. package/src/components/toggle-list.tsx +38 -12
  42. package/src/components/ui/country-flag-art.tsx +449 -0
  43. package/src/components/ui/country-flag.tsx +41 -0
  44. package/src/components/ui/data-table/types.ts +13 -0
  45. package/src/components/ui/icon.tsx +2 -0
  46. package/src/components/ui/index.ts +4 -0
  47. package/src/components/ui/multi-select/dialog.tsx +108 -22
  48. package/src/components/ui/range-track.tsx +46 -0
  49. package/src/components/ui/tabs.tsx +4 -0
  50. package/src/core/state/app/types.ts +10 -1
  51. package/src/i18n/es.ts +5 -3
  52. package/src/i18n/ja.ts +5 -3
  53. package/src/i18n/ko.ts +5 -3
  54. package/src/i18n/zh-cn.ts +5 -3
  55. package/src/i18n/zh-tw.ts +5 -3
  56. package/src/layout/ticker-navigation.ts +28 -2
  57. package/src/market-data/quotes/contributions.ts +6 -0
  58. package/src/market-data/quotes/freshness.ts +36 -0
  59. package/src/market-data/quotes/resolution.ts +22 -8
  60. package/src/market-data/quotes/time.ts +16 -0
  61. package/src/plugins/builtin/analytics/risk-client.ts +185 -59
  62. package/src/plugins/builtin/analytics/risk-headless.ts +55 -49
  63. package/src/plugins/builtin/analytics/risk-math.ts +33 -13
  64. package/src/plugins/builtin/analytics/risk-model.ts +250 -60
  65. package/src/plugins/builtin/analytics/risk-pane.tsx +61 -30
  66. package/src/plugins/builtin/analytics/risk-test-data.ts +182 -0
  67. package/src/plugins/builtin/buildout/cells.tsx +15 -12
  68. package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
  69. package/src/plugins/builtin/buildout/detail/index.tsx +0 -1
  70. package/src/plugins/builtin/buildout/detail/intel.tsx +6 -6
  71. package/src/plugins/builtin/buildout/detail/site.tsx +4 -7
  72. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -19
  73. package/src/plugins/builtin/catalysts/index.tsx +2 -2
  74. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  75. package/src/plugins/builtin/cds/index.tsx +1 -0
  76. package/src/plugins/builtin/debt-maturities/wall-bar.tsx +14 -41
  77. package/src/plugins/builtin/dividend-yield/client.ts +4 -2
  78. package/src/plugins/builtin/earnings/client.ts +2 -2
  79. package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
  80. package/src/plugins/builtin/fx-matrix/direction.ts +81 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +71 -33
  82. package/src/plugins/builtin/fx-matrix/live-legs.ts +17 -0
  83. package/src/plugins/builtin/fx-matrix/pairs.ts +68 -14
  84. package/src/plugins/builtin/fx-matrix/settings.ts +62 -0
  85. package/src/plugins/builtin/jobs/index.tsx +1 -0
  86. package/src/plugins/builtin/jobs/share-bars.tsx +5 -28
  87. package/src/plugins/builtin/market-heatmap/data.ts +28 -2
  88. package/src/plugins/builtin/market-heatmap/index.tsx +250 -79
  89. package/src/plugins/builtin/market-heatmap/live.ts +88 -0
  90. package/src/plugins/builtin/market-heatmap/portfolio.ts +27 -7
  91. package/src/plugins/builtin/market-movers/client.ts +3 -2
  92. package/src/plugins/builtin/market-movers/headless.ts +8 -1
  93. package/src/plugins/builtin/market-movers/screener.ts +18 -5
  94. package/src/plugins/builtin/members/client.ts +36 -0
  95. package/src/plugins/builtin/members/headless.ts +33 -0
  96. package/src/plugins/builtin/members/index.tsx +20 -0
  97. package/src/plugins/builtin/members/model.ts +60 -0
  98. package/src/plugins/builtin/members/pane.tsx +168 -0
  99. package/src/plugins/builtin/mna/index.tsx +2 -1
  100. package/src/plugins/builtin/mna/pane.tsx +5 -2
  101. package/src/plugins/builtin/options/index.tsx +2 -1
  102. package/src/plugins/builtin/pe-band/client.ts +29 -2
  103. package/src/plugins/builtin/pe-band/headless.ts +2 -2
  104. package/src/plugins/builtin/pe-band/model.ts +11 -7
  105. package/src/plugins/builtin/pe-band/pane.tsx +2 -1
  106. package/src/plugins/builtin/pe-band/report-dates.ts +78 -0
  107. package/src/plugins/builtin/perps/client.ts +17 -3
  108. package/src/plugins/builtin/perps/equity-identity.ts +16 -0
  109. package/src/plugins/builtin/perps/equity-row.tsx +6 -4
  110. package/src/plugins/builtin/plugin-marketplace/activation.ts +19 -0
  111. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -2
  112. package/src/plugins/builtin/plugin-marketplace/store.ts +2 -0
  113. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +43 -2
  114. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +48 -5
  115. package/src/plugins/builtin/scanner/hilo-bars.tsx +12 -70
  116. package/src/plugins/builtin/scanner/hilo-model.ts +0 -18
  117. package/src/plugins/builtin/sectors/move-bar.tsx +10 -32
  118. package/src/plugins/builtin/social-mentions/index.tsx +1 -1
  119. package/src/plugins/builtin/thirteenf/index.tsx +2 -1
  120. package/src/plugins/builtin/ticker-detail/overview/components.tsx +5 -60
  121. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +1 -1
  122. package/src/plugins/builtin/ticker-detail/pane.tsx +1 -1
  123. package/src/plugins/builtin/ticker-detail/quote-footer.ts +7 -1
  124. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -4
  125. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -2
  126. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  127. package/src/plugins/catalog-browser.ts +2 -0
  128. package/src/plugins/catalog-manifest.ts +1 -0
  129. package/src/plugins/catalog-ui.ts +3 -1
  130. package/src/plugins/catalog.ts +1 -1
  131. package/src/plugins/host-link.ts +33 -1
  132. package/src/plugins/installer.ts +48 -12
  133. package/src/plugins/loader.ts +18 -3
  134. package/src/plugins/manager-node.ts +15 -5
  135. package/src/plugins/registry/index.ts +1 -1
  136. package/src/plugins/registry/pane-settings.ts +25 -7
  137. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  138. package/src/renderers/dom/data-table/row.tsx +50 -16
  139. package/src/renderers/dom/desktop/icons.tsx +1 -0
  140. package/src/renderers/dom/host/box.tsx +69 -26
  141. package/src/renderers/dom/host/tabs.tsx +37 -1
  142. package/src/renderers/dom/styles.css +40 -5
  143. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +11 -6
  144. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  145. package/src/renderers/electrobun/bun/desktop/update.ts +181 -87
  146. package/src/renderers/electrobun/bun/index.ts +73 -6
  147. package/src/renderers/electrobun/bun/window/remembered-frame.ts +122 -0
  148. package/src/renderers/electrobun/shared/protocol.ts +3 -0
  149. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +2 -1
  150. package/src/renderers/electrobun/view/main.tsx +7 -1
  151. package/src/renderers/electrobun/view/renderer-window-error.ts +18 -0
  152. package/src/renderers/electrobun/view/update-host.ts +45 -21
  153. package/src/sources/gloomberb-cloud/normalizers.ts +9 -2
  154. package/src/sources/provider-router/financials.ts +3 -1
  155. package/src/telemetry/benign-window-error.ts +7 -0
  156. package/src/telemetry/crash-reports-dom.ts +2 -0
  157. package/src/test-support/daily-nav.ts +25 -0
  158. package/src/theme/heat-colors.ts +174 -1
  159. package/src/tickers/instrument-kind.ts +34 -7
  160. package/src/tickers/search/asset-classes.ts +24 -9
  161. package/src/tickers/search/index.ts +44 -11
  162. package/src/tickers/search/ranking.ts +23 -3
  163. package/src/types/financials.ts +2 -0
  164. package/src/types/plugin.ts +18 -5
  165. package/src/ui/context-menu.tsx +1 -0
  166. package/src/ui/host.tsx +2 -0
  167. package/src/updater/index.ts +29 -2
@@ -122,12 +122,14 @@ export function useCommandBarRouteActions({
122
122
  rawInput?: string,
123
123
  explicitTargetId?: string | null,
124
124
  selectedTicker?: TickerRecord,
125
+ directMembership?: boolean,
125
126
  ) => executeCollectionCommandAction({
126
127
  activeCollectionId,
127
128
  activeTickerSymbol,
128
129
  buildWorkflowDeps: buildSharedWorkflowDeps,
129
130
  closeAll,
130
131
  commandId,
132
+ directMembership,
131
133
  explicitTargetId,
132
134
  getState: () => stateRef.current,
133
135
  notify,
@@ -1,5 +1,8 @@
1
1
  import { useCallback, type MutableRefObject } from "react";
2
2
  import type { AppState } from "../../state/app/context";
3
+ import type { TickerRecord } from "../../types/ticker";
4
+ import { t } from "../../i18n";
5
+ import { listingChoiceQuery } from "../../tickers/search";
3
6
  import type { PluginRegistry } from "../../plugins/registry";
4
7
  import type { Command } from "./commands/registry";
5
8
  import type {
@@ -30,6 +33,8 @@ type ExecuteCollectionCommandFn = (
30
33
  commandId: CollectionCommandId,
31
34
  rawInput?: string,
32
35
  explicitTargetId?: string | null,
36
+ selectedTicker?: TickerRecord,
37
+ directMembership?: boolean,
33
38
  ) => Promise<void>;
34
39
 
35
40
  interface UseCommandBarSelectionRuntimeOptions {
@@ -270,6 +275,38 @@ export function useCommandBarSelectionRuntime({
270
275
  const selected = options?.item ?? typed ?? listState.results[listState.selectedIdx];
271
276
  if (!selected || selected.disabled) return;
272
277
 
278
+ // Opened from an add row: a venue picked from the colon list joins that
279
+ // collection. Any other text or screen runs as usual.
280
+ const addListing = stateRef.current.commandBarLaunchRequest;
281
+ if (
282
+ !options?.secondary
283
+ && addListing?.kind === "add-listing"
284
+ && !currentRoute
285
+ && listingChoiceQuery(rootQueryRef.current)
286
+ && (selected.kind === "ticker" || selected.kind === "search")
287
+ && selected.resolveTicker
288
+ ) {
289
+ const launch = addListing;
290
+ void (async () => {
291
+ try {
292
+ const ticker = await selected.resolveTicker!();
293
+ await executeCollectionCommand(
294
+ launch.collectionKind === "watchlist" ? "add-watchlist" : "add-portfolio",
295
+ ticker.metadata.ticker,
296
+ launch.collectionId,
297
+ ticker,
298
+ true,
299
+ );
300
+ } catch (error) {
301
+ pluginRegistry.notify({
302
+ type: "error",
303
+ body: error instanceof Error ? error.message : t("Could not add the selected listing."),
304
+ });
305
+ }
306
+ })();
307
+ return;
308
+ }
309
+
273
310
  const run = () => {
274
311
  if (options?.secondary && selected.secondaryAction) {
275
312
  void selected.secondaryAction();
@@ -320,6 +357,7 @@ export function useCommandBarSelectionRuntime({
320
357
  rootQueryRef,
321
358
  runRootRow,
322
359
  stateConfigLayout,
360
+ stateRef,
323
361
  visibleListStateRef,
324
362
  ]);
325
363
 
@@ -20,6 +20,7 @@ import { useCommandBarRootRuntime } from "../routes/root/runtime";
20
20
  import { createQuickLookTickerCandidates } from "../routes/ticker-search/results";
21
21
  import { useCommandSearchReport } from "../routes/root/search-report";
22
22
  import { useAppStateRef } from "../../../state/app/context";
23
+ import { listingChoiceQuery } from "../../../tickers/search";
23
24
  import { parseRootShortcutIntent } from "../routes/root/shortcuts";
24
25
  import { useRootPluginInstallItem } from "../routes/root/plugin-install";
25
26
  import { useCommandBarThemePreview } from "../theme-preview";
@@ -230,6 +231,9 @@ export function CommandBar({
230
231
  paneTemplates: getAvailablePaneShortcutTemplates(rootQuery),
231
232
  activeTicker: activeTickerSymbol,
232
233
  }), [activeTickerSymbol, availableCommands, getAvailablePaneShortcutTemplates, getAvailablePluginCommands, rootQuery]);
234
+ const listingChoice = !currentRoute && rootShortcutIntent.kind === "none"
235
+ ? listingChoiceQuery(rootQuery)
236
+ : null;
233
237
 
234
238
  // Runs the typed text again once a plugin installed from the bar is in, the
235
239
  // way a key bound to it would. Not over a dialog opened since: the bar would
@@ -258,6 +262,7 @@ export function CommandBar({
258
262
  // Only the root list asks on its own, and only for text the prefix parser
259
263
  // could not claim — otherwise the user is mid-command, not mid-question.
260
264
  const assistAutoAsk = !currentRoute
265
+ && !listingChoice
261
266
  && planAccess.emailVerified
262
267
  && shouldAutoAskAssist({ query: rootQuery, hasShortcutIntent: rootShortcutIntent.kind !== "none" });
263
268
  // The server keeps a question unless told not to, so the answer always
@@ -373,8 +378,8 @@ export function CommandBar({
373
378
  providers: searchProviders,
374
379
  query: rootQuery,
375
380
  // A resolved prefix means the user is running a command, so free-text
376
- // providers neither ask the network nor add rows.
377
- enabled: !currentRoute && rootShortcutIntent.kind === "none",
381
+ // providers neither ask the network nor add rows. A colon is the venue list.
382
+ enabled: !currentRoute && rootShortcutIntent.kind === "none" && !listingChoice,
378
383
  context: searchProviderContext,
379
384
  onExecuted: closeAfterProviderResult,
380
385
  });
@@ -131,7 +131,7 @@ export {
131
131
  IconButton, InlineQuickAddRow, KeyValueRow, ListView, loadingText, Menu, MenuPopover,
132
132
  MessageComposer, MultiSelectDialogButton, MultiSelectDialogContent, Notice, NumberField,
133
133
  NumberPromptDialog, openUrl, PageStackView, PaneLinkMenu, PaneStatusBody, Popover, Prose,
134
- QueryBar, READING_WIDTH, RemoteImage, Section, SectionHeading, SegmentedControl, SelectButton, ShortcutHint,
134
+ QueryBar, RangeTrack, RatioBar, READING_WIDTH, RemoteImage, Section, SectionHeading, SegmentedControl, SelectButton, ShortcutHint,
135
135
  Spinner, StatGrid, statGridColumns, statGridRows, Tabs, TextField, TextPromptDialog, unavailableText,
136
136
  useFieldRing, usePaneLinkMenuEntry, useQueryBarSearch,
137
137
  } from "./ui";
@@ -145,7 +145,7 @@ export type {
145
145
  MenuProps, MultiSelectDialogButtonHandle, MultiSelectPopoverAnchorPoint, MultiSelectRowAction,
146
146
  NoticeProps, NumberFieldProps, PaneStatusBodyProps, PopoverProps, ProseProps, QueryBarFilter,
147
147
  QueryBarMultiFilter, QueryBarProps, QueryBarSearch, QueryBarSearchFocus, QueryBarSelectFilter, QueryBarTextFilter,
148
- QueryBarToggleFilter, QueryBarView, SectionHeadingProps, SectionProps, SegmentedControlProps,
148
+ QueryBarToggleFilter, QueryBarView, RangeTrackProps, RatioBarProps, SectionHeadingProps, SectionProps, SegmentedControlProps,
149
149
  SelectButtonOption, SelectButtonProps, SelectControl, StatGridProps, StatItem, TabsProps,
150
150
  TextFieldProps, TextPromptDialogProps,
151
151
  } from "./ui";
@@ -31,6 +31,8 @@ interface FloatingPaneWrapperProps {
31
31
  onHeaderContextMenu?: (event: any) => void;
32
32
  onActionMouseDown?: (event: any) => void;
33
33
  onCloseMouseDown?: (event: any) => void;
34
+ onRestoreMouseDown?: (event: any) => void;
35
+ fullscreen?: boolean;
34
36
  onResizeMouseDown?: (event: any) => void;
35
37
  onResizeMouseDrag?: (event: any) => void;
36
38
  onResizeMouseDragEnd?: (event: any) => void;
@@ -80,6 +82,8 @@ export function FloatingPaneWrapper({
80
82
  onHeaderContextMenu,
81
83
  onActionMouseDown,
82
84
  onCloseMouseDown,
85
+ onRestoreMouseDown,
86
+ fullscreen = false,
83
87
  onResizeMouseDown,
84
88
  onResizeMouseDrag,
85
89
  onResizeMouseDragEnd,
@@ -137,6 +141,8 @@ export function FloatingPaneWrapper({
137
141
  onHeaderContextMenu={onHeaderContextMenu}
138
142
  onActionMouseDown={onActionMouseDown}
139
143
  onCloseMouseDown={onCloseMouseDown}
144
+ onRestoreMouseDown={onRestoreMouseDown}
145
+ fullscreen={fullscreen}
140
146
  />
141
147
 
142
148
  <PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={bg}>
@@ -239,14 +239,15 @@ function HeaderCommandPrompt({
239
239
  );
240
240
  }
241
241
 
242
- function UpdateStatus() {
242
+ function UpdateStatus({ onRestart }: { onRestart?: () => void }) {
243
243
  const colors = useThemeColors();
244
244
  const dispatch = useAppDispatch();
245
245
  const updateAvailable = useAppSelector((state) => state.updateAvailable);
246
246
  const updateProgress = useAppSelector((state) => state.updateProgress);
247
247
  const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
248
248
  const updateNotice = useAppSelector((state) => state.updateNotice);
249
- const retryKey = useActionShortcut("install-update");
249
+ // One key: Restart for a downloaded update, retry for a failed one.
250
+ const updateKey = useActionShortcut("install-update");
250
251
 
251
252
  useEffect(() => {
252
253
  if (!updateNotice || updateAvailable || updateProgress || updateCheckInProgress) return;
@@ -278,15 +279,28 @@ function UpdateStatus() {
278
279
  </Box>
279
280
  );
280
281
  }
282
+ if (updateProgress.phase === "ready") {
283
+ // A desktop update waits for the user: the app never relaunches by itself.
284
+ // The terminal binary is already swapped in and only needs a new launch.
285
+ if (updateProgress.canRestart && onRestart) {
286
+ return (
287
+ <Box flexDirection="row" gap={1} alignItems="center">
288
+ <Text fg={colors.headerText}>{t("Update ready")}</Text>
289
+ <Button compact label="Restart" shortcut={updateKey || undefined} onPress={onRestart} stopPropagation />
290
+ </Box>
291
+ );
292
+ }
293
+ return <Text fg={colors.headerText}>{t("Update ready, restart to apply")}</Text>;
294
+ }
281
295
  if (updateProgress.phase === "done") {
282
- return <Text fg={colors.headerText}>{t(updateProgress.message ?? "Update installed, restart to apply")}</Text>;
296
+ return <Text fg={colors.headerText}>{t(updateProgress.message ?? "Update ready, restart to apply")}</Text>;
283
297
  }
284
298
  if (updateProgress.phase === "error") {
285
- const retry = retryKey && canRetryUpdate({ updateAvailable, updateProgress, updateCheckInProgress });
299
+ const retry = updateKey && canRetryUpdate({ updateAvailable, updateProgress, updateCheckInProgress });
286
300
  return (
287
301
  <Box flexDirection="row">
288
302
  <Text fg={colors.headerText}>{tf("Update failed: {error}", { error: updateProgress.error ?? "Unknown error" })}</Text>
289
- {retry ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{` ${retryKey} ${t("retry")}`}</Text> : null}
303
+ {retry ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{` ${updateKey} ${t("retry")}`}</Text> : null}
290
304
  </Box>
291
305
  );
292
306
  }
@@ -378,8 +392,11 @@ function HeaderMarketSummary({ nativePaneChrome, width }: { nativePaneChrome: bo
378
392
 
379
393
  export function Header({
380
394
  onOpenHelp,
395
+ onRestartForUpdate,
381
396
  }: {
382
397
  onOpenHelp?: () => void;
398
+ /** Applies a downloaded desktop update and relaunches; the header offers Restart only when it is given. */
399
+ onRestartForUpdate?: () => void;
383
400
  }) {
384
401
  const colors = useThemeColors();
385
402
  const rendererHost = useRendererHost();
@@ -444,7 +461,7 @@ export function Header({
444
461
  <Box width={prompt.left} />
445
462
  {commandPrompt}
446
463
  <Box flexGrow={1} paddingLeft={2} paddingRight={2} minWidth={0}>
447
- <UpdateStatus />
464
+ <UpdateStatus onRestart={onRestartForUpdate} />
448
465
  </Box>
449
466
  {onOpenHelp ? (
450
467
  <Box flexShrink={0} data-gloom-role="header-help-action" style={{ marginRight: 8 }}>
@@ -470,7 +487,7 @@ export function Header({
470
487
  <Box width={prompt.left} />
471
488
  {commandPrompt}
472
489
  <Box flexGrow={1} minWidth={0} paddingLeft={2}>
473
- <UpdateStatus />
490
+ <UpdateStatus onRestart={onRestartForUpdate} />
474
491
  </Box>
475
492
  {marketSummary}
476
493
  {showWindowControls ? <WindowControls /> : null}
@@ -3,7 +3,7 @@ import { Button } from "../ui/button";
3
3
  import { confirmDialog } from "../ui/confirm-dialog";
4
4
  import { EmptyState } from "../ui/status";
5
5
  import { runsExternalPlugins } from "../../plugins/current-target";
6
- import { activateInstalledPlugin } from "../../plugins/builtin/plugin-marketplace/activation";
6
+ import { activateInstall } from "../../plugins/builtin/plugin-marketplace/activation";
7
7
  import { installConsent } from "../../plugins/builtin/plugin-marketplace/model";
8
8
  import { getMarketplaceHost, getPluginManager } from "../../plugins/builtin/plugin-marketplace/store";
9
9
  import type { PaneDef, PaneProps } from "../../types/plugin";
@@ -115,7 +115,7 @@ function MissingPanePlaceholder({ paneType, width }: PaneProps) {
115
115
  // Bring it into this session so the pane replaces the placeholder rather
116
116
  // than waiting for a restart; a plugin that cannot load live says so.
117
117
  const host = getMarketplaceHost();
118
- const activated = host ? await activateInstalledPlugin(result.directory, host, installer) : null;
118
+ const activated = host ? await activateInstall(result, host, installer) : null;
119
119
  if (activated && !activated.ok) setNotice({ text: `Installed but did not load: ${activated.error}`, error: true });
120
120
  else if (!activated || activated.restart) setNotice({ text: "Installed. Restart to finish.", error: false });
121
121
  } finally {
@@ -1,11 +1,11 @@
1
- import { Box, Text, useActionShortcut, useNativeRenderer, useUiCapabilities } from "../../../ui";
1
+ import { Box, Text, useActionShortcut, useNativeRenderer, useRendererHost, useUiCapabilities } from "../../../ui";
2
2
  import { useCallback, useRef, type ReactNode } from "react";
3
3
  import { blendHex, colors, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "../../../theme/colors";
4
4
  import { WEB_CELL_WIDTH } from "../../../theme/font-scale";
5
5
  import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
6
6
  import { capturePointerDrag } from "../../../ui/pointer-drag";
7
7
  import { Tabs } from "../../ui/tabs";
8
- import { Icon, IconButton } from "../../ui/icon";
8
+ import { ICON_GLYPHS, Icon, IconButton } from "../../ui/icon";
9
9
  import type { PaneHeaderTabsRegistration } from "./header-tabs";
10
10
  import { nativePaneHeaderRows } from "./sizing";
11
11
 
@@ -15,6 +15,8 @@ const HEADER_TAB_TOP_GAP = 2;
15
15
  const PANE_HEADER_GRIP = ":: ";
16
16
  export const PANE_HEADER_ACTION = " ... ";
17
17
  export const PANE_HEADER_CLOSE = " x ";
18
+ /** Same width as the close mark, so the header hit areas stay put. */
19
+ export const PANE_HEADER_RESTORE = " - ";
18
20
  const PANE_HEADER_LOCK = " 🔒 ";
19
21
 
20
22
  interface PaneHeaderProps {
@@ -45,6 +47,9 @@ interface PaneHeaderProps {
45
47
  onHeaderContextMenu?: (event: any) => void;
46
48
  onActionMouseDown?: (event: any) => void;
47
49
  onCloseMouseDown?: (event: any) => void;
50
+ /** Pane fullscreen. The corner mark leaves fullscreen instead of closing. */
51
+ fullscreen?: boolean;
52
+ onRestoreMouseDown?: (event: any) => void;
48
53
  }
49
54
 
50
55
  /**
@@ -63,9 +68,14 @@ interface PaneHeaderTitleBar {
63
68
  trailing?: ReactNode;
64
69
  }
65
70
 
71
+ /** A quick setting's terminal button: its glyph with a space either side. */
72
+ function quickSettingText(setting: { icon: PaneHeaderQuickSetting["icon"] }): string {
73
+ return ` ${ICON_GLYPHS[setting.icon]} `;
74
+ }
75
+
66
76
  export interface PaneHeaderQuickSetting {
67
77
  key: string;
68
- icon: "zap";
78
+ icon: "zap" | "sqrt";
69
79
  label: string;
70
80
  description?: string;
71
81
  active: boolean;
@@ -118,18 +128,23 @@ export function PaneHeader({
118
128
  onHeaderContextMenu,
119
129
  onActionMouseDown,
120
130
  onCloseMouseDown,
131
+ fullscreen = false,
132
+ onRestoreMouseDown,
121
133
  }: PaneHeaderProps) {
122
- const { nativePaneChrome } = useUiCapabilities();
134
+ const { nativePaneChrome, titleBarOverlay, nativeWindowChrome = titleBarOverlay } = useUiCapabilities();
135
+ const rendererHost = useRendererHost();
123
136
  const nativeRenderer = useNativeRenderer();
124
137
  const menuShortcut = useActionShortcut("pane-menu");
125
138
  const closeShortcut = useActionShortcut("pane-close");
139
+ const fullscreenShortcut = useActionShortcut("pane-fullscreen");
126
140
  const terminalHeaderRef = useRef<unknown>(null);
127
141
  const visuallyFocused = focused || windowModeSelected;
128
142
  const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
129
143
  const actionText = showActions ? PANE_HEADER_ACTION : " ";
130
- const closeText = floating ? PANE_HEADER_CLOSE : "";
144
+ const closeText = fullscreen ? PANE_HEADER_RESTORE : floating ? PANE_HEADER_CLOSE : "";
145
+ const onCornerMouseDown = fullscreen ? onRestoreMouseDown : onCloseMouseDown;
131
146
  const lockText = locked ? PANE_HEADER_LOCK : "";
132
- const terminalQuickSettingsWidth = quickSettings.reduce((total) => total + displayWidth(" ⚡ "), 0)
147
+ const terminalQuickSettingsWidth = quickSettings.reduce((total, setting) => total + displayWidth(quickSettingText(setting)), 0)
133
148
  + displayWidth(lockText);
134
149
  const textColor = paneTitleText(visuallyFocused, floating);
135
150
  const topInset = topRule ? 1 : 0;
@@ -137,6 +152,27 @@ export function PaneHeader({
137
152
  capturePointerDrag(nativeRenderer, terminalHeaderRef.current);
138
153
  onHeaderMouseDown?.(event);
139
154
  }, [nativeRenderer, onHeaderMouseDown]);
155
+ // The grip, the title and the empty bar move the pane. A fullscreen pane
156
+ // cannot move, so there they move the desktop window instead.
157
+ const windowDrag = Boolean(titleBar) || (fullscreen && titleBarOverlay === true && nativeWindowChrome === true);
158
+ const beginPaneDrag = (event: { stopPropagation?: () => void }) => {
159
+ event.stopPropagation?.();
160
+ onHeaderMouseDown?.(event);
161
+ };
162
+ const beginWindowDrag = (event?: { stopPropagation?: () => void }) => {
163
+ event?.stopPropagation?.();
164
+ if (titleBar) {
165
+ onHeaderMouseDown?.(event);
166
+ return;
167
+ }
168
+ void rendererHost.startWindowDrag?.();
169
+ };
170
+ const paneDragHandlers = {
171
+ onMouseDown: beginPaneDrag,
172
+ onMouseDrag: onHeaderMouseDrag,
173
+ onMouseDragEnd: onHeaderMouseDragEnd,
174
+ };
175
+ const chromeHandlers = windowDrag ? { onMouseDown: beginWindowDrag } : paneDragHandlers;
140
176
 
141
177
  if (nativePaneChrome) {
142
178
  const ruleColor = visuallyFocused ? colors.borderFocused : colors.border;
@@ -151,10 +187,7 @@ export function PaneHeader({
151
187
  data-floating={floating ? "true" : "false"}
152
188
  data-focused={focused ? "true" : "false"}
153
189
  data-window-mode-selected={windowModeSelected ? "true" : "false"}
154
- onMouseDown={onHeaderMouseDown}
155
190
  onMouseMove={onHeaderMouseMove}
156
- onMouseDrag={onHeaderMouseDrag}
157
- onMouseDragEnd={onHeaderMouseDragEnd}
158
191
  onContextMenu={onHeaderContextMenu}
159
192
  style={{
160
193
  // One 1px rule at the bottom, painted inside the header, mirrored by
@@ -173,15 +206,14 @@ export function PaneHeader({
173
206
  : `inset 0 -1px 0 ${ruleColor}`,
174
207
  }}
175
208
  >
176
- {/* Part of the header, so it clicks and drags like the rest of it. */}
177
209
  {!titleBar && (
178
- <Box data-gloom-role="pane-grip" flexShrink={0} flexDirection="row" alignItems="center" style={{ alignSelf: "stretch", marginRight: 6 }}>
210
+ <Box data-gloom-role="pane-grip" flexShrink={0} flexDirection="row" alignItems="center" style={{ alignSelf: "stretch", marginRight: 6 }} {...paneDragHandlers}>
179
211
  <Icon name="grip" size={12} color={visuallyFocused ? colors.borderFocused : colors.textMuted} />
180
212
  </Box>
181
213
  )}
182
214
  {/* Full header height, so trimming the title to its capitals never lets
183
215
  this clip cut descenders. */}
184
- <Box minWidth={0} flexShrink={tabs ? 0 : 1} overflow="hidden" flexDirection="row" alignItems="center" style={{ alignSelf: "stretch", ...(tabs ? { maxWidth: "40%" } : {}) }}>
216
+ <Box data-gloom-role="pane-header-chrome" minWidth={0} flexShrink={tabs ? 0 : 1} overflow="hidden" flexDirection="row" alignItems="center" style={{ alignSelf: "stretch", ...(tabs ? { maxWidth: "40%" } : {}) }} {...chromeHandlers}>
185
217
  <Text
186
218
  fg={textColor}
187
219
  selectable={false}
@@ -237,6 +269,7 @@ export function PaneHeader({
237
269
  onAdd={tabs.onAdd}
238
270
  addLabel={tabs.addLabel}
239
271
  onReorder={tabs.onReorder}
272
+ onDrag={windowDrag && !tabs.onReorder ? beginWindowDrag : undefined}
240
273
  closeMode={tabs.closeMode}
241
274
  paneMenu={tabs.paneMenu}
242
275
  />
@@ -245,14 +278,14 @@ export function PaneHeader({
245
278
  {quickSettings.map((setting) => (
246
279
  <Box key={setting.key} data-gloom-role="pane-quick-setting" data-setting-key={setting.key}>
247
280
  <IconButton
248
- icon="zap"
281
+ icon={setting.icon}
249
282
  label={`${setting.label}: ${setting.active ? "on" : "off"}`}
250
283
  pressed={setting.active}
251
284
  onPress={setting.onMouseDown ? (event) => setting.onMouseDown?.(event) : undefined}
252
285
  />
253
286
  </Box>
254
287
  ))}
255
- <Box flexGrow={1} minWidth={0} />
288
+ <Box data-gloom-role="pane-header-chrome" flexGrow={1} minWidth={0} {...chromeHandlers} />
256
289
  {locked && (
257
290
  <Box data-gloom-role="pane-lock">
258
291
  <IconButton icon="lock" label="Locked: the close shortcut leaves this pane open" color={colors.textMuted} />
@@ -269,13 +302,13 @@ export function PaneHeader({
269
302
  />
270
303
  ) : <Box width={2} />}
271
304
  </Box>
272
- {floating && !titleBar && (
273
- <Box data-gloom-role="pane-close" marginLeft={1}>
305
+ {(fullscreen || (floating && !titleBar)) && (
306
+ <Box data-gloom-role={fullscreen ? "pane-restore" : "pane-close"} marginLeft={1}>
274
307
  <IconButton
275
- icon="close"
276
- label="Close pane"
277
- shortcut={closeShortcut || undefined}
278
- onPress={onCloseMouseDown ? (event) => onCloseMouseDown(event) : undefined}
308
+ icon={fullscreen ? "minimize" : "close"}
309
+ label={fullscreen ? "Exit fullscreen" : "Close pane"}
310
+ shortcut={(fullscreen ? fullscreenShortcut : closeShortcut) || undefined}
311
+ onPress={onCornerMouseDown ? (event) => onCornerMouseDown(event) : undefined}
279
312
  />
280
313
  </Box>
281
314
  )}
@@ -284,9 +317,9 @@ export function PaneHeader({
284
317
  );
285
318
  }
286
319
 
287
- if (visuallyFocused || floating) {
320
+ if (visuallyFocused || floating || fullscreen) {
288
321
  // Build: ┌─:: Title ─────────── ... x─┐
289
- // Reserve 2 for corners, 1 for ─ after ┌, 1 for ─ before ┐
322
+ // Fullscreen draws "-" in that corner. Reserve 2 for the frame.
290
323
  const borderColor = visuallyFocused ? colors.borderFocused : colors.border;
291
324
  const innerWidth = Math.max(0, width - 4);
292
325
  const contentWidth = PANE_HEADER_GRIP.length + terminalQuickSettingsWidth + closeText.length + actionText.length;
@@ -312,7 +345,7 @@ export function PaneHeader({
312
345
  {quickSettings.map((setting) => (
313
346
  <TerminalPaneButton
314
347
  key={setting.key}
315
- text=" ⚡ "
348
+ text={quickSettingText(setting)}
316
349
  fg={setting.active ? colors.warning : colors.textDim}
317
350
  role="pane-quick-setting"
318
351
  onMouseDown={setting.onMouseDown}
@@ -328,12 +361,12 @@ export function PaneHeader({
328
361
  role="pane-action"
329
362
  onMouseDown={onActionMouseDown}
330
363
  />
331
- {floating && (
364
+ {(fullscreen || floating) && (
332
365
  <TerminalPaneButton
333
366
  text={closeText}
334
367
  fg={textColor}
335
- role="pane-close"
336
- onMouseDown={onCloseMouseDown}
368
+ role={fullscreen ? "pane-restore" : "pane-close"}
369
+ onMouseDown={onCornerMouseDown}
337
370
  />
338
371
  )}
339
372
  <Text fg={borderColor} selectable={false}>{"─┐"}</Text>
@@ -363,7 +396,7 @@ export function PaneHeader({
363
396
  {quickSettings.map((setting) => (
364
397
  <TerminalPaneButton
365
398
  key={setting.key}
366
- text=" ⚡ "
399
+ text={quickSettingText(setting)}
367
400
  fg={setting.active ? colors.warning : colors.textDim}
368
401
  role="pane-quick-setting"
369
402
  onMouseDown={setting.onMouseDown}
@@ -378,12 +411,12 @@ export function PaneHeader({
378
411
  role="pane-action"
379
412
  onMouseDown={onActionMouseDown}
380
413
  />
381
- {floating && (
414
+ {(fullscreen || floating) && (
382
415
  <TerminalPaneButton
383
416
  text={closeText}
384
417
  fg={textColor}
385
- role="pane-close"
386
- onMouseDown={onCloseMouseDown}
418
+ role={fullscreen ? "pane-restore" : "pane-close"}
419
+ onMouseDown={onCornerMouseDown}
387
420
  />
388
421
  )}
389
422
  </Box>
@@ -28,6 +28,8 @@ interface PaneWrapperProps {
28
28
  onHeaderMouseDragEnd?: (event: any) => void;
29
29
  onHeaderContextMenu?: (event: any) => void;
30
30
  onActionMouseDown?: (event: any) => void;
31
+ onRestoreMouseDown?: (event: any) => void;
32
+ fullscreen?: boolean;
31
33
  footer?: CombinedPaneFooter | null;
32
34
  children: ReactNode;
33
35
  }
@@ -52,6 +54,8 @@ export function PaneWrapper({
52
54
  onHeaderMouseDragEnd,
53
55
  onHeaderContextMenu,
54
56
  onActionMouseDown,
57
+ onRestoreMouseDown,
58
+ fullscreen = false,
55
59
  footer,
56
60
  children,
57
61
  }: PaneWrapperProps) {
@@ -107,6 +111,8 @@ export function PaneWrapper({
107
111
  onHeaderMouseDragEnd={onHeaderMouseDragEnd}
108
112
  onHeaderContextMenu={onHeaderContextMenu}
109
113
  onActionMouseDown={onActionMouseDown}
114
+ fullscreen={fullscreen}
115
+ onRestoreMouseDown={onRestoreMouseDown}
110
116
  />
111
117
  )}
112
118
  <PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={bg}>
@@ -7,13 +7,13 @@ import {
7
7
  } from "../../../types/config";
8
8
  import type { PaneDef } from "../../../types/plugin";
9
9
  import { paneTitleMnemonic } from "../../../layout/pane-follow";
10
- import { canFollowTickerSource } from "../../../layout/ticker-navigation";
10
+ import { canFollowTickerSource, isTickerLinkPeer } from "../../../layout/ticker-navigation";
11
11
 
12
12
  /** Single-cell link glyph: emoji link icons render double-width in terminals. */
13
13
  const LINK_GLYPH = "\u29c9";
14
14
 
15
- /** The title without the link suffix; a menu row naming a source uses it as is. */
16
- export function getBasePaneDisplayTitle(
15
+ /** The title without the link suffix. */
16
+ function getBasePaneDisplayTitle(
17
17
  state: Pick<AppState, "config" | "paneState">,
18
18
  instance: PaneInstanceConfig,
19
19
  paneDef: PaneDef,
@@ -79,9 +79,33 @@ export function getPaneDisplayTitle(
79
79
  const source = findPaneInstance(state.config.layout, instance.binding.sourceInstanceId);
80
80
  const sourceDef = source ? panes.get(source.paneId) : null;
81
81
  if (!source || !sourceDef) return title;
82
- // Other panes name only the list or scanner they follow; one chained to a fixed pane (a desk's
83
- // OMON following OVDV) reads as it always has.
84
- if (!research && !sourceDef.tickerSource) return title;
85
- const sourceTitle = getBasePaneDisplayTitle(state, source, sourceDef);
82
+ // Name the list, scanner, or single-ticker pane this one follows. A follow pointed at anything
83
+ // else (a desk's OMON following a comparison chart) reads as it always has.
84
+ if (!sourceDef.tickerSource && !isTickerLinkPeer(source, panes)) return title;
85
+ const sourceTitle = getTickerLinkSourceTitle(state, source, sourceDef, panes, "title");
86
86
  return `${title} ${LINK_GLYPH} ${tf("Linked to {source}", { source: sourceTitle })}`;
87
87
  }
88
+
89
+ /**
90
+ * How a follower names the pane it follows. A list or scanner goes by its title. A single-ticker
91
+ * pane shows the follower's own symbol, so a title names only its command ("OMON SPY ⧉ Linked to
92
+ * OVDV"), and a menu row keeps the symbol to tell two of them apart ("Link to OVDV SPY"). Ticker
93
+ * Research's title is only its symbol, which would read as a ticker, so it goes by its name
94
+ * ("Link to Ticker Research AAPL").
95
+ */
96
+ export function getTickerLinkSourceTitle(
97
+ state: Pick<AppState, "config" | "paneState">,
98
+ source: PaneInstanceConfig,
99
+ sourceDef: PaneDef,
100
+ panes: ReadonlyMap<string, PaneDef>,
101
+ place: "menu" | "title",
102
+ ): string {
103
+ const title = getBasePaneDisplayTitle(state, source, sourceDef, panes);
104
+ if (sourceDef.tickerSource) return title;
105
+ const research = source.paneId === TICKER_RESEARCH_PANE_ID;
106
+ const kind = research ? t("Ticker Research") : paneTitleMnemonic(title);
107
+ if (!kind) return title;
108
+ if (place === "title") return kind;
109
+ const symbol = research ? resolveTickerForPane(state as AppState, source.instanceId) : null;
110
+ return symbol ? `${kind} ${symbol}` : title;
111
+ }
@@ -10,7 +10,7 @@ import {
10
10
  } from "../../../../layout/pane-manager";
11
11
  import type { DesktopDockPreviewState } from "../../../../types/desktop-window";
12
12
  import type { LayoutConfig } from "../../../../types/config";
13
- import { PANE_HEADER_ACTION, PANE_HEADER_CLOSE } from "../../pane/header";
13
+ import { PANE_HEADER_ACTION, PANE_HEADER_CLOSE, PANE_HEADER_RESTORE } from "../../pane/header";
14
14
 
15
15
  export interface HoverOverlay {
16
16
  targetId: string;
@@ -355,18 +355,19 @@ export function resolveSnapGuide(x: number, y: number, guides: SnapGuide[]): Sna
355
355
 
356
356
  export function resolveHeaderHitAreas(
357
357
  width: number,
358
- options: { floating: boolean; focused: boolean },
358
+ options: { floating: boolean; focused: boolean; fullscreen?: boolean },
359
359
  ): {
360
360
  actionStart: number | null;
361
361
  closeStart: number | null;
362
362
  } {
363
363
  // Focused panes and all floating panes render corner chrome around the header.
364
- let rightEdge = options.focused || options.floating ? width - 2 : width;
364
+ let rightEdge = options.focused || options.floating || options.fullscreen ? width - 2 : width;
365
365
  let closeStart: number | null = null;
366
366
  let actionStart: number | null = null;
367
367
 
368
- if (options.floating) {
369
- closeStart = Math.max(0, rightEdge - PANE_HEADER_CLOSE.length);
368
+ if (options.floating || options.fullscreen) {
369
+ const corner = options.fullscreen ? PANE_HEADER_RESTORE : PANE_HEADER_CLOSE;
370
+ closeStart = Math.max(0, rightEdge - corner.length);
370
371
  rightEdge = closeStart;
371
372
  }
372
373
 
@@ -171,6 +171,7 @@ interface UseShellPointerRuntimeOptions {
171
171
  focusPane: (paneId: string) => void;
172
172
  focusedPaneId: string | null;
173
173
  handleFloatingClose: (paneId: string) => void;
174
+ restoreFullscreen: () => void;
174
175
  menuState: ActionMenuState | null;
175
176
  nativePaneChrome: boolean;
176
177
  openPaneMenu: (
@@ -206,6 +207,7 @@ export function useShellPointerRuntime({
206
207
  focusPane,
207
208
  focusedPaneId,
208
209
  handleFloatingClose,
210
+ restoreFullscreen,
209
211
  menuState,
210
212
  nativePaneChrome,
211
213
  openPaneMenu,
@@ -254,6 +256,7 @@ export function useShellPointerRuntime({
254
256
  focusedPaneId,
255
257
  handleActiveDrag,
256
258
  handleFloatingClose,
259
+ restoreFullscreen,
257
260
  menuState,
258
261
  openPaneMenu,
259
262
  paneMap,
@@ -754,6 +754,7 @@ export function Shell({
754
754
  focusPane,
755
755
  focusedPaneId,
756
756
  handleFloatingClose,
757
+ restoreFullscreen: exitTransientFocusLayout,
757
758
  menuState,
758
759
  nativePaneChrome,
759
760
  openPaneMenu,
@@ -834,6 +835,11 @@ export function Shell({
834
835
  getPaneQuickSettings={getPaneQuickSettings}
835
836
  handleFloatingClose={handleFloatingClose}
836
837
  handleFloatingCloseMouseDown={handleFloatingCloseMouseDown}
838
+ handleRestoreFullscreen={(event) => {
839
+ event?.preventDefault?.();
840
+ event?.stopPropagation?.();
841
+ exitTransientFocusLayout();
842
+ }}
837
843
  handleNativeDrag={handleNativeDrag}
838
844
  handleNativePaneContextMenu={handleNativePaneContextMenu}
839
845
  handleNativePaneMouseDown={handleNativePaneMouseDown}