gloomberb 0.18.2 → 0.18.3

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 (157) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/data.ts +15 -0
  4. package/src/api-client/index.ts +2 -0
  5. package/src/api-client/request.ts +5 -3
  6. package/src/api-client/telemetry.ts +17 -0
  7. package/src/app/global-shortcuts.ts +1 -16
  8. package/src/app/runtime/update.ts +28 -8
  9. package/src/architecture/pane-tabs-allowlist.json +65 -0
  10. package/src/cli/commands/market.ts +73 -33
  11. package/src/cli/commands/ticker.ts +82 -25
  12. package/src/cli/help.ts +1 -1
  13. package/src/cli/index.ts +8 -0
  14. package/src/cli/issuer-membership.ts +103 -0
  15. package/src/cli/listing-arg.ts +15 -2
  16. package/src/cli/name-entries.ts +51 -12
  17. package/src/cli/pane-functions/capabilities.ts +2 -0
  18. package/src/cli/pane-functions/catalog.ts +16 -0
  19. package/src/cli/pane-functions/dom.ts +2 -1
  20. package/src/cli/pane-functions/headless.ts +25 -6
  21. package/src/cli/pane-functions/index.ts +64 -5
  22. package/src/cli/pane-functions/resolver.ts +19 -5
  23. package/src/cli/report-tables.ts +136 -18
  24. package/src/cli/result.ts +12 -4
  25. package/src/cli/symbol-list.ts +47 -11
  26. package/src/components/chart/composite/composite-chart.tsx +14 -5
  27. package/src/components/chart/composite/drawing-store.ts +26 -6
  28. package/src/components/chart/composite/interactions.ts +2 -1
  29. package/src/components/chart/composite/panel-surface.tsx +11 -0
  30. package/src/components/chart/composite/types.ts +6 -0
  31. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  32. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  33. package/src/components/data-table/view.tsx +23 -3
  34. package/src/components/index.ts +1 -0
  35. package/src/components/layout/detached-pane-shell.tsx +1 -0
  36. package/src/components/layout/floating-pane.tsx +1 -0
  37. package/src/components/layout/header.tsx +228 -55
  38. package/src/components/layout/pane/header.tsx +10 -3
  39. package/src/components/layout/pane/index.tsx +1 -0
  40. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  41. package/src/components/ui/stat-grid.tsx +28 -3
  42. package/src/components/ui/status.tsx +3 -1
  43. package/src/core/app-services.ts +4 -0
  44. package/src/i18n/es.ts +22 -0
  45. package/src/i18n/ja.ts +22 -0
  46. package/src/i18n/ko.ts +22 -0
  47. package/src/i18n/zh-cn.ts +22 -0
  48. package/src/i18n/zh-tw.ts +22 -0
  49. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  50. package/src/plugins/builtin/alerts/events.ts +1 -0
  51. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  52. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  53. package/src/plugins/builtin/attention/pane.tsx +16 -14
  54. package/src/plugins/builtin/awards/pane.tsx +6 -6
  55. package/src/plugins/builtin/backtest/export.ts +1 -1
  56. package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
  57. package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
  58. package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
  59. package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
  60. package/src/plugins/builtin/catalysts/client.ts +2 -2
  61. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  62. package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
  63. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
  64. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  65. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  66. package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
  67. package/src/plugins/builtin/correlation/headless.ts +2 -1
  68. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  69. package/src/plugins/builtin/correlation/settings.ts +1 -0
  70. package/src/plugins/builtin/cot/pane.tsx +4 -3
  71. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  72. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  73. package/src/plugins/builtin/doe/pane.tsx +1 -1
  74. package/src/plugins/builtin/equity-screener/headless.ts +10 -0
  75. package/src/plugins/builtin/equity-screener/model.ts +28 -0
  76. package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
  77. package/src/plugins/builtin/exposure/pane.tsx +10 -9
  78. package/src/plugins/builtin/gpu/board.tsx +4 -1
  79. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  80. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  81. package/src/plugins/builtin/gpu/history.tsx +4 -2
  82. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  83. package/src/plugins/builtin/holders/client.ts +2 -1
  84. package/src/plugins/builtin/holders/headless.ts +2 -1
  85. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  86. package/src/plugins/builtin/insider/base-values.ts +11 -7
  87. package/src/plugins/builtin/insider/client.ts +3 -0
  88. package/src/plugins/builtin/insider/headless.ts +19 -6
  89. package/src/plugins/builtin/insider/index.tsx +17 -8
  90. package/src/plugins/builtin/insider/issuer.ts +34 -4
  91. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  92. package/src/plugins/builtin/insider/table-model.ts +35 -13
  93. package/src/plugins/builtin/insider/table.tsx +5 -2
  94. package/src/plugins/builtin/iv-history/events.ts +145 -0
  95. package/src/plugins/builtin/iv-history/format.ts +3 -0
  96. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  97. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  98. package/src/plugins/builtin/market-movers/client.ts +4 -3
  99. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  100. package/src/plugins/builtin/notes/cli.ts +1 -1
  101. package/src/plugins/builtin/notes/export.ts +17 -6
  102. package/src/plugins/builtin/notes/files.ts +16 -5
  103. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  104. package/src/plugins/builtin/options/model-spot.ts +70 -4
  105. package/src/plugins/builtin/options/view.tsx +41 -13
  106. package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
  107. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  108. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  109. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  110. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  111. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  112. package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
  113. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  114. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  115. package/src/plugins/builtin/power/client.ts +2 -2
  116. package/src/plugins/builtin/power/pane.tsx +36 -31
  117. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  118. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  119. package/src/plugins/builtin/seasonality/client.ts +2 -1
  120. package/src/plugins/builtin/sectors/client.ts +4 -3
  121. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  122. package/src/plugins/builtin/shared/research-trading.tsx +2 -0
  123. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  124. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  125. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  126. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  127. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  128. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  129. package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
  130. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  131. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  132. package/src/renderers/dom/desktop/controls.tsx +1 -1
  133. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  134. package/src/renderers/dom/host/tabs.tsx +16 -5
  135. package/src/renderers/dom/host/top-surface.ts +127 -0
  136. package/src/renderers/dom/styles.css +39 -0
  137. package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
  138. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  139. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  140. package/src/renderers/electrobun/view/news-source.ts +4 -2
  141. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  142. package/src/renderers/opentui/start.tsx +7 -0
  143. package/src/sources/gloomberb-cloud/index.ts +7 -0
  144. package/src/sources/provider-router/index.ts +16 -0
  145. package/src/telemetry/crash-reports.ts +4 -0
  146. package/src/telemetry/update-failures-node.ts +59 -0
  147. package/src/telemetry/update-failures.ts +307 -0
  148. package/src/tickers/company-name-listing.ts +9 -7
  149. package/src/tickers/company-name.ts +43 -0
  150. package/src/tickers/listing-fate.ts +30 -0
  151. package/src/types/data-provider.ts +6 -0
  152. package/src/types/headless.ts +7 -0
  153. package/src/types/issuer-listings.ts +28 -0
  154. package/src/updater/failure.ts +163 -0
  155. package/src/updater/index.ts +87 -23
  156. package/src/utils/currency-pair.ts +26 -0
  157. package/src/utils/sec.ts +68 -0
@@ -26,7 +26,8 @@ import {
26
26
  } from "../command-bar/panel/prompt-binding";
27
27
  import { useAppDispatch, useAppSelector, type AppState } from "../../state/app/context";
28
28
  import { useViewport } from "../../react/input";
29
- import { canRetryUpdate } from "../../app/global-shortcuts";
29
+ import { canRetryUpdate } from "../../updater";
30
+ import { updateFailureReason } from "../../updater/failure";
30
31
  import { t, tf } from "../../i18n";
31
32
  import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
32
33
  import { ClippedText, NATIVE_CLIP_STYLE } from "../ui/display";
@@ -43,8 +44,12 @@ const UPDATE_NOTICE_DURATION_MS = 5_000;
43
44
  * The retry hint gives way before the message goes under it.
44
45
  */
45
46
  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;
47
+ /** Kept clear after the terminal update status, so its last character never touches the Help pill. */
48
+ const UPDATE_STATUS_GAP_COLUMNS = 1;
49
+ /** Below this many characters of the reason, the terminal banner says "Update failed" and the retry key instead. */
50
+ const UPDATE_REASON_MIN_COLUMNS = 8;
51
+ /** A terminal line about an update narrower than this is left out rather than shown as a stub. */
52
+ const UPDATE_TEXT_MIN_COLUMNS = 8;
48
53
  /** Left padding of the update status's slot, in cells; the desktop's title-bar layout also pads its right. */
49
54
  const UPDATE_SLOT_PADDING_COLUMNS = 2;
50
55
  /** The Help pill's own props, shared by its button and the width the update status leaves it. */
@@ -315,6 +320,9 @@ type UpdateStatusView =
315
320
  | { kind: "restart"; text: string; version: string }
316
321
  | { kind: "failed"; message: string; parts: StatusPart[]; retry: boolean };
317
322
 
323
+ /** The failed banner's words without the reason, for a terminal slot too narrow to read one. */
324
+ const UPDATE_FAILED_SHORT = "Update failed";
325
+
318
326
  /**
319
327
  * A line of the status in the pieces the desktop may treat apart: a `cut`
320
328
  * piece ends in an ellipsis and gives way, the rest stays whole. The download
@@ -372,8 +380,13 @@ function resolveUpdateStatusView({
372
380
  return { kind: "text", text: t(updateProgress.message ?? "Update ready, restart to apply") };
373
381
  }
374
382
  if (updateProgress.phase === "error") {
375
- // One line: an error that carries a stack or a command's output must not grow the header.
376
- const error = (updateProgress.error ?? "Unknown error").replace(/\s+/g, " ").trim();
383
+ // The reason in a few words; the whole error is in the command bar's Check
384
+ // for Updates and the log. An error without one (from an older desktop
385
+ // process) is shown on one line: a stack or a command's output must not
386
+ // grow the header.
387
+ const error = updateProgress.failure
388
+ ? t(updateFailureReason(updateProgress.failure))
389
+ : (updateProgress.error ?? "Unknown error").replace(/\s+/g, " ").trim();
377
390
  return {
378
391
  kind: "failed",
379
392
  message: tf("Update failed: {error}", { error }),
@@ -411,24 +424,16 @@ function resolveUpdateStatusView({
411
424
  }
412
425
 
413
426
  /**
414
- * What the header says about an update. The terminal cuts it to `columns`,
415
- * the room its slot is guaranteed; the desktop and the web let the browser fit
416
- * it to the room the bar leaves (NativeUpdateStatus). Either way it never runs
417
- * into the Help pill or the market cluster beside it.
427
+ * What the header says about an update, and the key that acts on it (Restart
428
+ * for a downloaded update, retry for a failed one). A passing notice goes
429
+ * after its time.
418
430
  */
419
- function UpdateStatus({ columns, onRestart, trailingGap = false }: {
420
- columns: number;
421
- onRestart?: () => void;
422
- /** The desktop title bar keeps a gap after the status as well as before it. */
423
- trailingGap?: boolean;
424
- }) {
425
- const { nativePaneChrome = false } = useUiCapabilities();
431
+ function useUpdateStatus(canRestart: boolean): { view: UpdateStatusView | null; updateKey: string } {
426
432
  const dispatch = useAppDispatch();
427
433
  const updateAvailable = useAppSelector((state) => state.updateAvailable);
428
434
  const updateProgress = useAppSelector((state) => state.updateProgress);
429
435
  const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
430
436
  const updateNotice = useAppSelector((state) => state.updateNotice);
431
- // One key: Restart for a downloaded update, retry for a failed one.
432
437
  const updateKey = useActionShortcut("install-update");
433
438
 
434
439
  useEffect(() => {
@@ -440,12 +445,65 @@ function UpdateStatus({ columns, onRestart, trailingGap = false }: {
440
445
  }, [dispatch, updateAvailable, updateCheckInProgress, updateNotice, updateProgress]);
441
446
 
442
447
  const view = resolveUpdateStatusView({
443
- canRestart: !!onRestart,
448
+ canRestart,
444
449
  updateAvailable,
445
450
  updateCheckInProgress,
446
451
  updateNotice,
447
452
  updateProgress,
448
453
  });
454
+ return { view, updateKey };
455
+ }
456
+
457
+ /** The retry key after a failed update's message, when there is something to retry. */
458
+ function retryHint(view: Extract<UpdateStatusView, { kind: "failed" }>, updateKey: string): string {
459
+ return updateKey && view.retry ? ` ${updateKey} ${t("retry")}` : "";
460
+ }
461
+
462
+ /**
463
+ * Columns the terminal status takes when nothing is cut: the header gives the
464
+ * slot that much from the closed prompt's empty room before it cuts anything.
465
+ */
466
+ function terminalUpdateStatusColumns(view: UpdateStatusView, updateKey: string): number {
467
+ return UPDATE_STATUS_GAP_COLUMNS + terminalUpdateStatusContentColumns(view, updateKey);
468
+ }
469
+
470
+ function terminalUpdateStatusContentColumns(view: UpdateStatusView, updateKey: string): number {
471
+ switch (view.kind) {
472
+ case "busy":
473
+ return 2 + displayWidth(view.text);
474
+ case "restart":
475
+ return displayWidth(view.text) + 1 + terminalButtonColumns({ label: "Restart", shortcut: updateKey || undefined, compact: true });
476
+ case "failed":
477
+ return displayWidth(view.message) + displayWidth(retryHint(view, updateKey));
478
+ case "text":
479
+ return displayWidth(view.text);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * The narrowest the closed terminal prompt goes to make room for an update's
485
+ * status: its caret and the whole placeholder, which sells the command bar to
486
+ * a first-time user, without the key hint (resolveHeaderPromptContent).
487
+ */
488
+ function terminalClosedPromptFloor(): number {
489
+ return 2 + PROMPT_CARET.length + t("Search or run a command").length;
490
+ }
491
+
492
+ /**
493
+ * What the header says about an update. The terminal cuts it to `columns`,
494
+ * the room its slot is guaranteed; the desktop and the web let the browser fit
495
+ * it to the room the bar leaves (NativeUpdateStatus). Either way it never runs
496
+ * into the Help pill or the market cluster beside it.
497
+ */
498
+ function UpdateStatus({ columns, onRestart, status, trailingGap = false }: {
499
+ columns: number;
500
+ onRestart?: () => void;
501
+ status: { view: UpdateStatusView | null; updateKey: string };
502
+ /** The desktop title bar keeps a gap after the status as well as before it. */
503
+ trailingGap?: boolean;
504
+ }) {
505
+ const { nativePaneChrome = false } = useUiCapabilities();
506
+ const { view, updateKey } = status;
449
507
  if (!view) return null;
450
508
  return nativePaneChrome
451
509
  ? <NativeUpdateStatus view={view} onRestart={onRestart} trailingGap={trailingGap} updateKey={updateKey} />
@@ -458,39 +516,115 @@ function TerminalUpdateStatus({ columns, onRestart, updateKey, view }: {
458
516
  updateKey: string;
459
517
  view: UpdateStatusView;
460
518
  }) {
461
- const colors = useThemeColors();
519
+ const content = terminalUpdateStatusContent({ room: columns - UPDATE_STATUS_GAP_COLUMNS, onRestart, updateKey, view });
520
+ if (!content) return null;
521
+ return (
522
+ <Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_STATUS_GAP_COLUMNS}>
523
+ {content}
524
+ </Box>
525
+ );
526
+ }
527
+
528
+ /** The status cut to `room` columns, or null when nothing of it would read. */
529
+ function terminalUpdateStatusContent({ onRestart, room, updateKey, view }: {
530
+ onRestart?: () => void;
531
+ room: number;
532
+ updateKey: string;
533
+ view: UpdateStatusView;
534
+ }): ReactNode {
462
535
  if (view.kind === "busy") {
463
- return (
464
- <Box flexDirection="row" gap={1}>
465
- <SpinnerMark name="dots" color={colors.headerText} />
466
- <Text fg={colors.headerText}>{view.text}</Text>
467
- </Box>
468
- );
536
+ // The spinner and a cell after it, then the words, cut to what is left.
537
+ if (room < 2 + UPDATE_TEXT_MIN_COLUMNS) return null;
538
+ return <TerminalBusyStatus text={fitStatusParts(view.parts, room - 2)} />;
469
539
  }
470
540
  if (view.kind === "restart") {
471
- return (
472
- <Box flexDirection="row" gap={1} alignItems="center">
473
- <Text fg={colors.headerText}>{view.text}</Text>
474
- <Button compact label="Restart" shortcut={updateKey || undefined} onPress={onRestart} stopPropagation />
475
- </Box>
476
- );
541
+ // The button stays when the words do not fit beside it.
542
+ const buttonColumns = terminalButtonColumns({ label: "Restart", shortcut: updateKey || undefined, compact: true });
543
+ if (room < buttonColumns) return null;
544
+ const showText = displayWidth(view.text) + 1 + buttonColumns <= room;
545
+ return <TerminalRestartStatus text={showText ? view.text : null} onRestart={onRestart} updateKey={updateKey} />;
477
546
  }
478
547
  if (view.kind === "failed") {
479
- const hint = updateKey && view.retry ? ` ${updateKey} ${t("retry")}` : "";
548
+ const hint = retryHint(view, updateKey);
480
549
  const hintColumns = displayWidth(hint);
481
- // The message goes first, and keeps its room before the hint takes any.
482
- const room = columns - UPDATE_ERROR_GAP_COLUMNS;
483
- const showHint = hintColumns > 0 && room - hintColumns >= UPDATE_ERROR_MIN_COLUMNS;
484
- const messageColumns = room - (showHint ? hintColumns : 0);
485
- if (messageColumns < UPDATE_ERROR_MIN_COLUMNS) return null;
486
- return (
487
- <Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_ERROR_GAP_COLUMNS}>
488
- <ClippedText fg={colors.headerText} text={view.message} width={messageColumns} />
489
- {showHint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
490
- </Box>
491
- );
550
+ const messageColumns = displayWidth(view.message);
551
+ const short = t(UPDATE_FAILED_SHORT);
552
+ const reasonRoom = room - displayWidth(view.parts[0]?.cut === false ? view.parts[0].text : "");
553
+ // The whole message with its key, then the message alone, then as much of
554
+ // the reason as reads, then "Update failed" with its key; never less.
555
+ let text: string;
556
+ let showHint = false;
557
+ if (messageColumns + hintColumns <= room) {
558
+ text = view.message;
559
+ showHint = hintColumns > 0;
560
+ } else if (messageColumns <= room) {
561
+ text = view.message;
562
+ } else if (reasonRoom >= UPDATE_REASON_MIN_COLUMNS) {
563
+ text = truncateToDisplayWidth(view.message, room);
564
+ } else if (displayWidth(short) + hintColumns <= room) {
565
+ text = short;
566
+ showHint = hintColumns > 0;
567
+ } else if (room >= UPDATE_ERROR_MIN_COLUMNS) {
568
+ text = truncateToDisplayWidth(short, room);
569
+ } else {
570
+ return null;
571
+ }
572
+ return <TerminalFailedStatus text={text} hint={showHint ? hint : ""} />;
492
573
  }
493
- return <Text fg={colors.headerText}>{view.text}</Text>;
574
+ // A notice or a line about the update: cut to the slot, never past it.
575
+ if (room < UPDATE_TEXT_MIN_COLUMNS) return null;
576
+ return <TerminalTextStatus text={view.text} width={room} />;
577
+ }
578
+
579
+ /**
580
+ * A status line in `width` columns: the pieces that stay whole (a download's
581
+ * percent) keep their room, and the rest is cut to what they leave.
582
+ */
583
+ function fitStatusParts(parts: StatusPart[], width: number): string {
584
+ const whole = parts.reduce((total, part) => total + (part.cut ? 0 : displayWidth(part.text)), 0);
585
+ const cutRoom = Math.max(0, width - whole);
586
+ const cutTotal = parts.reduce((total, part) => total + (part.cut ? displayWidth(part.text) : 0), 0);
587
+ if (cutTotal <= cutRoom) return parts.map((part) => part.text).join("");
588
+ // A cut piece keeps a space before the whole one after it: "Downloading v... 42%".
589
+ const cut = (part: StatusPart, index: number) => index < parts.length - 1
590
+ ? `${truncateToDisplayWidth(part.text.trimEnd(), Math.max(0, cutRoom - 1))} `
591
+ : truncateToDisplayWidth(part.text, cutRoom);
592
+ return truncateToDisplayWidth(parts.map((part, index) => (part.cut ? cut(part, index) : part.text)).join(""), width);
593
+ }
594
+
595
+ function TerminalBusyStatus({ text }: { text: string }) {
596
+ const colors = useThemeColors();
597
+ return (
598
+ <Box flexDirection="row" gap={1}>
599
+ <SpinnerMark name="dots" color={colors.headerText} />
600
+ <Text fg={colors.headerText}>{text}</Text>
601
+ </Box>
602
+ );
603
+ }
604
+
605
+ function TerminalRestartStatus({ onRestart, text, updateKey }: { onRestart?: () => void; text: string | null; updateKey: string }) {
606
+ const colors = useThemeColors();
607
+ return (
608
+ <Box flexDirection="row" gap={1} alignItems="center">
609
+ {text ? <Text fg={colors.headerText}>{text}</Text> : null}
610
+ <Button compact label="Restart" shortcut={updateKey || undefined} onPress={onRestart} stopPropagation />
611
+ </Box>
612
+ );
613
+ }
614
+
615
+ function TerminalFailedStatus({ hint, text }: { hint: string; text: string }) {
616
+ const colors = useThemeColors();
617
+ return (
618
+ <>
619
+ <Text fg={colors.headerText}>{text}</Text>
620
+ {hint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
621
+ </>
622
+ );
623
+ }
624
+
625
+ function TerminalTextStatus({ text, width }: { text: string; width: number }) {
626
+ const colors = useThemeColors();
627
+ return <ClippedText fg={colors.headerText} text={text} width={width} />;
494
628
  }
495
629
 
496
630
  /**
@@ -510,6 +644,23 @@ function nativeCutTextStyle(alone: boolean) {
510
644
  maxWidth: "max-content",
511
645
  } as const;
512
646
  }
647
+ /**
648
+ * A failed update's reason on the desktop: whole when the line has room for it,
649
+ * so the retry key after it moves to the hidden line first; never wider than
650
+ * what the words before it leave, so when even that is too little it stays on
651
+ * the line and ends in an ellipsis instead of leaving with the key.
652
+ */
653
+ function nativeReasonStyle(leadColumns: number) {
654
+ return {
655
+ ...NATIVE_CLIP_STYLE,
656
+ flexGrow: 0,
657
+ flexShrink: 1,
658
+ flexBasis: "auto",
659
+ minWidth: `${NATIVE_STATUS_PIECE_MIN_CH}ch`,
660
+ maxWidth: `calc(100% - ${leadColumns}ch)`,
661
+ } as const;
662
+ }
663
+
513
664
  /** One cell between the parts of the desktop status, as `gap={1}` puts it in the terminal. */
514
665
  const NATIVE_STATUS_GAP = "var(--cell-w)";
515
666
 
@@ -597,12 +748,16 @@ function NativeUpdateStatus({ onRestart, trailingGap, updateKey, view }: {
597
748
  );
598
749
  }
599
750
  if (view.kind === "failed") {
600
- // The words that say it failed, then as much of the error as reads, then the retry key.
601
- const hint = updateKey && view.retry ? ` ${updateKey} ${t("retry")}` : "";
602
- const alone = view.parts.every((part) => part.cut);
751
+ // The words that say it failed, then the reason, then the retry key, which
752
+ // gives way first, as in the terminal: the reason is cut only once the key
753
+ // is gone and it still does not fit (nativeReasonStyle).
754
+ const hint = retryHint(view, updateKey);
755
+ const lead = view.parts.reduce((total, part) => total + (part.cut ? 0 : displayWidth(part.text)), 0);
603
756
  return (
604
757
  <NativeStatusRow trailingGap={trailingGap}>
605
- {view.parts.map((part, index) => nativeStatusPiece(part, index, { alone, color, title: view.message }))}
758
+ {view.parts.map((part, index) => part.cut
759
+ ? <Text key={index} fg={color} title={view.message} style={nativeReasonStyle(lead)}>{part.text}</Text>
760
+ : nativeStatusPiece(part, index, { alone: false, color, title: view.message }))}
606
761
  {hint ? <Text fg={colors.textMuted}>{hint}</Text> : null}
607
762
  </NativeStatusRow>
608
763
  );
@@ -698,14 +853,32 @@ export function Header({
698
853
  windowFullscreen,
699
854
  });
700
855
  const shortcutLabel = useCommandBarShortcut();
856
+ const updateStatus = useUpdateStatus(!!onRestartForUpdate);
701
857
  // The terminal lays the header out by cell counts, so it works out what is left
702
858
  // for the update status once the Help pill and the slot's padding are paid for.
703
- const updateStatusColumns = Math.max(
859
+ let updateStatusColumns = Math.max(
704
860
  0,
705
861
  prompt.updateColumns
706
862
  - (onOpenHelp ? terminalButtonColumns(helpButtonProps()) + TERMINAL_HELP_PADDING_COLUMNS : 0)
707
863
  - UPDATE_SLOT_PADDING_COLUMNS * (titleBarOverlay ? 2 : 1),
708
864
  );
865
+ // Short of that, the closed prompt gives the status its empty room, down to
866
+ // its placeholder, as the desktop's does: at 80 to 160 columns the slot alone
867
+ // is too narrow to say anything. Open, the prompt is the command bar's input
868
+ // and keeps its width.
869
+ // The prompt's own right padding then stands for one of the slot's.
870
+ let promptWidth = prompt.width;
871
+ let terminalSlotPadding = UPDATE_SLOT_PADDING_COLUMNS;
872
+ if (!nativePaneChrome && !commandBarOpen && updateStatus.view) {
873
+ const need = terminalUpdateStatusColumns(updateStatus.view, updateStatus.updateKey);
874
+ if (need > updateStatusColumns) {
875
+ const spare = Math.max(0, prompt.width - terminalClosedPromptFloor());
876
+ const given = Math.min(need - updateStatusColumns - 1, spare);
877
+ promptWidth -= Math.max(0, given);
878
+ terminalSlotPadding -= 1;
879
+ updateStatusColumns += 1 + Math.max(0, given);
880
+ }
881
+ }
709
882
  // Desktop has its own window chrome to sit on; the terminal has none, so the
710
883
  // bar itself wears the prompt's fill and reads as a single strip.
711
884
  const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
@@ -727,7 +900,7 @@ export function Header({
727
900
  onOpen={openCommandBar}
728
901
  open={commandBarOpen}
729
902
  shortcutLabel={shortcutLabel}
730
- width={prompt.width}
903
+ width={promptWidth}
731
904
  />
732
905
  );
733
906
  const marketSummary = prompt.marketColumns > 0
@@ -748,7 +921,7 @@ export function Header({
748
921
  data-gloom-role="header-update-status"
749
922
  style={{ alignSelf: "stretch" }}
750
923
  >
751
- <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} trailingGap={!!titleBarOverlay} />
924
+ <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} status={updateStatus} trailingGap={!!titleBarOverlay} />
752
925
  </Box>
753
926
  ) : null;
754
927
 
@@ -771,8 +944,8 @@ export function Header({
771
944
  <Box width={prompt.left} />
772
945
  {commandPrompt}
773
946
  {nativeUpdateSlot ?? (
774
- <Box flexGrow={1} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS} paddingRight={UPDATE_SLOT_PADDING_COLUMNS} minWidth={0}>
775
- <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
947
+ <Box flexGrow={1} paddingLeft={terminalSlotPadding} paddingRight={UPDATE_SLOT_PADDING_COLUMNS} minWidth={0}>
948
+ <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} status={updateStatus} />
776
949
  </Box>
777
950
  )}
778
951
  {onOpenHelp ? <HeaderHelpButton nativePaneChrome onPress={onOpenHelp} /> : null}
@@ -795,8 +968,8 @@ export function Header({
795
968
  <Box width={prompt.left} />
796
969
  {commandPrompt}
797
970
  {nativeUpdateSlot ?? (
798
- <Box flexGrow={1} minWidth={0} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS}>
799
- <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
971
+ <Box flexGrow={1} minWidth={0} paddingLeft={terminalSlotPadding}>
972
+ <UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} status={updateStatus} />
800
973
  </Box>
801
974
  )}
802
975
  {onOpenHelp ? <HeaderHelpButton nativePaneChrome={nativePaneChrome} onPress={onOpenHelp} /> : null}
@@ -10,7 +10,6 @@ import type { PaneHeaderTabsRegistration } from "./header-tabs";
10
10
  import { nativePaneHeaderRows } from "./sizing";
11
11
 
12
12
  const PANE_HEADER_HEIGHT = 1;
13
- /** Space above desktop title-bar tabs, in px. */
14
13
  const PANE_HEADER_GRIP = ":: ";
15
14
  export const PANE_HEADER_ACTION = " ... ";
16
15
  /** The pane's docs: the same width shown or blank, so the title never shifts. */
@@ -38,6 +37,8 @@ interface PaneHeaderProps {
38
37
  quickSettings?: PaneHeaderQuickSetting[];
39
38
  /** Desktop only: the pane's primary tab strip, drawn as title-bar tabs. */
40
39
  tabs?: PaneHeaderTabsRegistration | null;
40
+ /** The body colour the active title-bar tab opens into. */
41
+ bodyBackground?: string;
41
42
  /**
42
43
  * Desktop: a 1px line is drawn over the header's first row (a dock divider
43
44
  * above it, or a floating pane's border). The header mirrors it with padding
@@ -131,6 +132,7 @@ export function PaneHeader({
131
132
  showActions = false,
132
133
  quickSettings = [],
133
134
  tabs = null,
135
+ bodyBackground,
134
136
  topRule = false,
135
137
  titleBar = null,
136
138
  onHeaderMouseMove,
@@ -262,9 +264,14 @@ export function PaneHeader({
262
264
  marginTop: -topInset,
263
265
  marginBottom: -1,
264
266
  marginLeft: "var(--gloom-space-sm)",
265
- // Undoes the negative top margin for controls centred in the
266
- // strip (the overflow chevron).
267
+ // Undoes the negative top margin for what is centred in the strip:
268
+ // the overflow chevron, and the tab labels (styles.css).
267
269
  "--pane-tab-top-inset": `${topInset}px`,
270
+ // The active tab's box opens into the pane over the bottom rule
271
+ // (styles.css), drawn in the rule's colour so the two are one line.
272
+ "--pane-tab-border": ruleColor,
273
+ // Until the strip has measured the surface under the tab (top-surface.ts).
274
+ "--pane-tab-active-bg": bodyBackground ?? colors.bg,
268
275
  // The active tab reads as active whenever the pane is focused, even
269
276
  // while a detail or field inside the pane owns the keyboard.
270
277
  "--pane-tab-active-fg": visuallyFocused ? colors.textBright : colors.text,
@@ -119,6 +119,7 @@ export function PaneWrapper({
119
119
  showActions={showActions}
120
120
  quickSettings={quickSettings}
121
121
  tabs={headerTabs}
122
+ bodyBackground={bg}
122
123
  topRule={topRule}
123
124
  onHeaderMouseMove={onHeaderMouseMove}
124
125
  onHeaderMouseDown={onHeaderMouseDown}
@@ -25,7 +25,15 @@ export interface PaneTabs {
25
25
  * A pane's primary tab strip in one call. It registers the strip with the
26
26
  * chrome, which draws it in the desktop title bar, and otherwise returns the
27
27
  * row the body draws first, so the tabs are described once. Call it above any
28
- * early return, and pass null while the pane has no strip (a sign-in wall).
28
+ * early return, and draw `strip` above every body state, loading and error
29
+ * included.
30
+ *
31
+ * The strip is chrome: it is there from the first frame and stays while a tab,
32
+ * a filter or a refresh loads, fails or comes back empty, so switching never
33
+ * takes it away. Pass null only while there is nothing to choose between: a
34
+ * sign-in or plan wall, no ticker or portfolio yet, a detail that replaces the
35
+ * tabs by design. A conditional call is listed with its reason in
36
+ * `src/architecture/pane-tabs-allowlist.json`.
29
37
  */
30
38
  export function usePaneTabs(options: PaneTabsOptions | null): PaneTabs {
31
39
  const inHeader = usePaneHeaderTabs(options);
@@ -28,6 +28,13 @@ export interface StatItem {
28
28
  label: string;
29
29
  value: string;
30
30
  detail?: string;
31
+ /**
32
+ * Shorter forms of `detail`, longest first (an empty one shows none). The
33
+ * grid sizes its columns by the shortest, so a detail that grows with the
34
+ * data (a stamp outside regular hours) never takes a column from the band,
35
+ * and shows the longest form that fits the cell.
36
+ */
37
+ detailVariants?: readonly string[];
31
38
  tone?: StatTone;
32
39
  /** A domain colour (a price change, a series). Wins over `tone`. */
33
40
  color?: string;
@@ -55,9 +62,22 @@ function labelChars(items: StatItem[]): number {
55
62
  return Math.min(MAX_LABEL_CHARS, Math.max(4, ...items.map((item) => displayWidth(t(item.label)))));
56
63
  }
57
64
 
65
+ /** The form of the detail the columns are sized by: the shortest. */
66
+ function sizingDetail(item: StatItem): string | undefined {
67
+ return item.detailVariants?.length ? item.detailVariants[item.detailVariants.length - 1] : item.detail;
68
+ }
69
+
70
+ /** The longest form of a cell's detail that fits `room` cells, else its shortest. */
71
+ function fittingStatDetail(item: Pick<StatItem, "detail" | "detailVariants">, room: number): string | undefined {
72
+ if (!item.detail || !item.detailVariants?.length) return item.detail;
73
+ const forms = [item.detail, ...item.detailVariants];
74
+ return forms.find((form) => displayWidth(form) <= room) ?? forms[forms.length - 1];
75
+ }
76
+
58
77
  function naturalWidth(item: StatItem, labels: number): number {
59
78
  // A split bar takes the room the cell has left and is not counted here.
60
- const detail = item.detail ? displayWidth(item.detail) + 2 : 0;
79
+ const sized = sizingDetail(item);
80
+ const detail = sized ? displayWidth(sized) + 2 : 0;
61
81
  return labels + 1 + displayWidth(item.value) + detail + CELL_GAP;
62
82
  }
63
83
 
@@ -132,9 +152,14 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
132
152
  const barRoom = valueWidth - displayWidth(value) - 1;
133
153
  const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : barRoom >= 4 ? Math.min(SPLIT_BAR_CELLS, barRoom) : 0;
134
154
  const detailWidth = valueWidth - displayWidth(value) - (barCells ? barCells + 1 : 0) - 2;
135
- const detail = !item.detail
155
+ // The desktop cell has no gap between cells: its label column, inset and gaps take the room
156
+ // (measured short, as a cell is wider than a character, so a form that fits is never clipped).
157
+ const shownDetail = fittingStatDetail(item, nativePaneChrome
158
+ ? width + (item.wide ? 0 : CELL_GAP) - labels - displayWidth(value) - (barCells ? barCells + 1 : 0) - 4
159
+ : detailWidth);
160
+ const detail = !shownDetail
136
161
  ? ""
137
- : nativePaneChrome ? item.detail : detailWidth > 1 ? truncateToDisplayWidth(item.detail, detailWidth) : "";
162
+ : nativePaneChrome ? shownDetail : detailWidth > 1 ? truncateToDisplayWidth(shownDetail, detailWidth) : "";
138
163
  return (
139
164
  <Box
140
165
  key={item.id ?? `${rowIndex}:${index}`}
@@ -334,7 +334,9 @@ export function PaneStatusBody({
334
334
  <EmptyStateBody
335
335
  status={status}
336
336
  title={status === "error" ? errorTitle ?? (subject ? unavailableText(subject) : error!) : emptyTitle ?? t("Nothing to show yet.")}
337
- message={status === "error" ? (errorTitle || subject ? error! : undefined) : emptyMessage}
337
+ message={status === "error"
338
+ ? ((errorTitle || subject) && error && error !== (errorTitle ?? unavailableText(subject!)) ? error : undefined)
339
+ : emptyMessage}
338
340
  actions={actions}
339
341
  retry={retry}
340
342
  feedback={feedback}
@@ -26,6 +26,10 @@ export function createAppServices({ config, plugins, externalPlugins }: AppServi
26
26
  name: "News",
27
27
  provider: {
28
28
  fetchNews: async (query) => (await newsService.load(query)).articles,
29
+ fetchNewsPage: async (query) => {
30
+ const { articles, error, nextCursor } = await newsService.load(query);
31
+ return { articles, error, nextCursor };
32
+ },
29
33
  getCachedNews: (query) => newsService.getQueryState(query).articles,
30
34
  },
31
35
  }),
package/src/i18n/es.ts CHANGED
@@ -1006,6 +1006,28 @@ export const es: Record<string, string> = {
1006
1006
  "Checking for updates...": "Buscando actualizaciones...",
1007
1007
  "Downloading v{version}: {percent}%": "Descargando v{version}: {percent}%",
1008
1008
  "Update failed: {error}": "Error al actualizar: {error}",
1009
+ "Update failed": "Error al actualizar",
1010
+ "Update check failed: {reason}": "Error al buscar actualizaciones: {reason}",
1011
+ "no connection": "sin conexión",
1012
+ "connection timed out": "se agotó el tiempo de conexión",
1013
+ "GitHub rate limit": "límite de GitHub",
1014
+ "release server error": "error del servidor de versiones",
1015
+ "unreadable release data": "datos de versión ilegibles",
1016
+ "no build for this system": "no hay versión para este sistema",
1017
+ "download interrupted": "descarga interrumpida",
1018
+ "download corrupted": "descarga dañada",
1019
+ "no permission to replace the app": "sin permiso para reemplazar la app",
1020
+ "move the app to Applications": "mueve la app a Aplicaciones",
1021
+ "app and update on different disks": "la app y la actualización están en discos distintos",
1022
+ "install folder is read-only": "la carpeta de instalación es de solo lectura",
1023
+ "disk full": "disco lleno",
1024
+ "app files in use": "archivos de la app en uso",
1025
+ "downloaded update missing": "falta la actualización descargada",
1026
+ "check failed": "falló la búsqueda",
1027
+ "download failed": "falló la descarga",
1028
+ "could not verify the download": "no se pudo verificar la descarga",
1029
+ "could not install": "no se pudo instalar",
1030
+ "could not restart": "no se pudo reiniciar",
1009
1031
  "v{version} available — run {command}": "v{version} disponible — ejecuta {command}",
1010
1032
  "v{version} available, update through the package manager that installed Gloomberb": "v{version} disponible, actualiza con el gestor de paquetes que instaló Gloomberb",
1011
1033
  "v{version} available — starting download...": "v{version} disponible — iniciando descarga...",
package/src/i18n/ja.ts CHANGED
@@ -1020,6 +1020,28 @@ export const ja: Record<string, string> = {
1020
1020
  "Checking for updates...": "アップデートを確認中...",
1021
1021
  "Downloading v{version}: {percent}%": "v{version} をダウンロード中: {percent}%",
1022
1022
  "Update failed: {error}": "更新に失敗しました: {error}",
1023
+ "Update failed": "更新に失敗しました",
1024
+ "Update check failed: {reason}": "アップデートの確認に失敗しました: {reason}",
1025
+ "no connection": "接続がありません",
1026
+ "connection timed out": "接続がタイムアウトしました",
1027
+ "GitHub rate limit": "GitHub のレート制限",
1028
+ "release server error": "リリースサーバーのエラー",
1029
+ "unreadable release data": "リリース情報を読み取れません",
1030
+ "no build for this system": "このシステム向けのビルドがありません",
1031
+ "download interrupted": "ダウンロードが中断されました",
1032
+ "download corrupted": "ダウンロードが破損しています",
1033
+ "no permission to replace the app": "アプリを置き換える権限がありません",
1034
+ "move the app to Applications": "アプリをアプリケーションフォルダに移動してください",
1035
+ "app and update on different disks": "アプリとアップデートが別のディスクにあります",
1036
+ "install folder is read-only": "インストール先が読み取り専用です",
1037
+ "disk full": "ディスクがいっぱいです",
1038
+ "app files in use": "アプリのファイルが使用中です",
1039
+ "downloaded update missing": "ダウンロードした更新が見つかりません",
1040
+ "check failed": "確認に失敗しました",
1041
+ "download failed": "ダウンロードに失敗しました",
1042
+ "could not verify the download": "ダウンロードを検証できません",
1043
+ "could not install": "インストールできません",
1044
+ "could not restart": "再起動できません",
1023
1045
  "v{version} available — run {command}": "v{version} が利用可能 — {command} を実行",
1024
1046
  "v{version} available, update through the package manager that installed Gloomberb": "v{version} が利用可能です。Gloomberb をインストールしたパッケージマネージャーで更新してください",
1025
1047
  "v{version} available — starting download...": "v{version} が利用可能 — ダウンロードを開始しています...",
package/src/i18n/ko.ts CHANGED
@@ -1018,6 +1018,28 @@ export const ko: Record<string, string> = {
1018
1018
  "Checking for updates...": "업데이트 확인 중...",
1019
1019
  "Downloading v{version}: {percent}%": "v{version} 다운로드 중: {percent}%",
1020
1020
  "Update failed: {error}": "업데이트 실패: {error}",
1021
+ "Update failed": "업데이트 실패",
1022
+ "Update check failed: {reason}": "업데이트 확인 실패: {reason}",
1023
+ "no connection": "연결 없음",
1024
+ "connection timed out": "연결 시간 초과",
1025
+ "GitHub rate limit": "GitHub 요청 한도",
1026
+ "release server error": "릴리스 서버 오류",
1027
+ "unreadable release data": "릴리스 정보를 읽을 수 없음",
1028
+ "no build for this system": "이 시스템용 빌드 없음",
1029
+ "download interrupted": "다운로드 중단됨",
1030
+ "download corrupted": "다운로드 손상됨",
1031
+ "no permission to replace the app": "앱을 교체할 권한 없음",
1032
+ "move the app to Applications": "앱을 응용 프로그램 폴더로 옮기세요",
1033
+ "app and update on different disks": "앱과 업데이트가 서로 다른 디스크에 있습니다",
1034
+ "install folder is read-only": "설치 폴더가 읽기 전용임",
1035
+ "disk full": "디스크 공간 부족",
1036
+ "app files in use": "앱 파일이 사용 중임",
1037
+ "downloaded update missing": "다운로드한 업데이트 없음",
1038
+ "check failed": "확인 실패",
1039
+ "download failed": "다운로드 실패",
1040
+ "could not verify the download": "다운로드를 확인할 수 없음",
1041
+ "could not install": "설치할 수 없음",
1042
+ "could not restart": "다시 시작할 수 없음",
1021
1043
  "v{version} available — run {command}": "v{version} 사용 가능 — {command} 실행",
1022
1044
  "v{version} available, update through the package manager that installed Gloomberb": "v{version} 사용 가능, Gloomberb를 설치한 패키지 관리자로 업데이트하세요",
1023
1045
  "v{version} available — starting download...": "v{version} 사용 가능 — 다운로드 시작 중...",