gloomberb 0.17.1 → 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 (101) 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/earnings.ts +4 -2
  4. package/src/app/global-shortcuts.ts +10 -0
  5. package/src/app/keybindings/actions.ts +16 -0
  6. package/src/app/runtime/desktop-deeplink.ts +1 -1
  7. package/src/app.tsx +1 -1
  8. package/src/architecture/core-builtin-allowlist.json +4 -9
  9. package/src/cli/commands/market.ts +34 -16
  10. package/src/cli/commands/options-chain.ts +29 -33
  11. package/src/cli/commands/options-leaps.ts +462 -63
  12. package/src/cli/commands/search.ts +10 -7
  13. package/src/cli/commands/ticker.ts +1 -1
  14. package/src/cli/known-gaps.ts +22 -0
  15. package/src/cli/pane-functions/catalog.ts +6 -3
  16. package/src/cli/pane-functions/dom.ts +17 -2
  17. package/src/cli/pane-functions/function-help.ts +1 -1
  18. package/src/cli/pane-functions/index.ts +5 -3
  19. package/src/cli/pane-functions/report-notices.ts +15 -0
  20. package/src/cli/symbol-args.ts +19 -0
  21. package/src/components/command-bar/workflow/builtin.ts +2 -2
  22. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  23. package/src/components/feedback-dialog.tsx +1 -1
  24. package/src/components/layout/shell/index.tsx +1 -1
  25. package/src/components/layout/status-bar-groups.ts +1 -1
  26. package/src/components/layout/status-bar.tsx +2 -2
  27. package/src/components/markdown-text.tsx +14 -2
  28. package/src/components/reader/article-reader.tsx +1 -1
  29. package/src/components/reader/detail-reader.tsx +10 -7
  30. package/src/components/reader/reader-text.tsx +32 -17
  31. package/src/layout-marketplace/gallery.tsx +2 -2
  32. package/src/layout-marketplace/linked-sync.ts +1 -1
  33. package/src/market-data/market/freshness.ts +3 -0
  34. package/src/market-data/published-apac-sessions.ts +12 -2
  35. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  36. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  37. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  38. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  39. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  40. package/src/plugins/builtin/chat/conversations.ts +1 -1
  41. package/src/plugins/builtin/chat/index.tsx +2 -2
  42. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  43. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  44. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  45. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  46. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  47. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  48. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  49. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  50. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  51. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  52. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  53. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  54. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  55. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  56. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  57. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  58. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  59. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  60. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  61. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  62. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  63. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  64. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  65. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  66. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  67. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  68. package/src/plugins/builtin/docs/pane.tsx +6 -5
  69. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  70. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  71. package/src/plugins/builtin/earnings/headless.ts +13 -2
  72. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  73. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  74. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  75. package/src/plugins/builtin/notes/index.tsx +1 -1
  76. package/src/plugins/builtin/notes/owner.tsx +2 -2
  77. package/src/plugins/builtin/options/carry.ts +36 -0
  78. package/src/plugins/builtin/options/open-interest-as-of.ts +16 -5
  79. package/src/plugins/builtin/options/view.tsx +19 -7
  80. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  81. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  82. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  83. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  84. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  85. package/src/plugins/builtin/research/event-model.ts +7 -4
  86. package/src/plugins/builtin/series-inventory.ts +1 -0
  87. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  88. package/src/renderers/dom/styles.css +4 -2
  89. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  90. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  91. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  92. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  93. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  94. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  95. package/src/types/data-provider.ts +3 -0
  96. package/src/types/financials.ts +6 -0
  97. package/src/utils/decimal-comma.ts +18 -0
  98. package/src/utils/exchanges.ts +7 -0
  99. package/src/utils/forward-dividend.ts +39 -9
  100. package/src/utils/option-expiry.ts +16 -0
  101. package/src/utils/sec.ts +2 -1
@@ -7,13 +7,17 @@ import {
7
7
  import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
8
8
  import { MAX_CHART_COMPOSER_SERIES } from "./chart-spec";
9
9
  import { buildCustomSeries, buildSeriesSpec, panelsForSeries } from "./chart-spec-edit";
10
+ import { currencyPairSpelling } from "../../../utils/currency-pair";
11
+ import { dashedCoinPair } from "../../../utils/crypto-pair";
10
12
  import {
11
13
  CHART_FIELD_IDS,
12
14
  normalizeInstrument,
13
15
  parseChartExpression,
16
+ parseSeriesExpression,
14
17
  resolveChartFieldAlias,
18
+ type ParsedSeriesExpression,
15
19
  } from "./series-expression";
16
- import { setBuiltinStudies } from "./studies";
20
+ import { setBuiltinStudies, setPairStudies, type PairStudySelection } from "./studies";
17
21
 
18
22
  function chartSpec(
19
23
  series: ChartSeriesSpec[],
@@ -33,7 +37,35 @@ export function buildEmptyChartPreset(): ChartSpec {
33
37
  return chartSpec([]);
34
38
  }
35
39
 
40
+ /**
41
+ * `GAA/VOO`, `GAA / VOO`, and `CL=F - ES=F` are a ratio or a spread of two series.
42
+ * A slash that is the symbol stays one series (`EUR/USD`, `BTC/USD`), and so does
43
+ * a field written with a slash (`AAPL:EV/EBITDA`). A comma-separated list stays a list.
44
+ */
45
+ function derivedPair(expression: string): { series: ParsedSeriesExpression[]; formula: PairStudySelection } | null {
46
+ const trimmed = expression.trim();
47
+ if (!trimmed || /[;,\n]/.test(trimmed)) return null;
48
+ const spaced = /^(.*?)\s+(\/|-)\s+(.*)$/.exec(trimmed);
49
+ if (spaced) {
50
+ const left = parseSeriesExpression(spaced[1]!);
51
+ const right = parseSeriesExpression(spaced[3]!);
52
+ if (!left || !right) return null;
53
+ return { series: [left, right], formula: spaced[2] === "/" ? "ratio" : "spread" };
54
+ }
55
+ const tight = /^([^/\s]+)\/([^/\s]+)$/.exec(trimmed);
56
+ if (!tight) return null;
57
+ const [leftToken, rightToken] = [tight[1]!, tight[2]!];
58
+ const quote = rightToken.split(":")[0]!;
59
+ if (currencyPairSpelling(`${leftToken}/${quote}`) || dashedCoinPair(`${leftToken}/${quote}`)) return null;
60
+ const left = parseSeriesExpression(leftToken);
61
+ const right = parseSeriesExpression(rightToken);
62
+ if (!left || !right) return null;
63
+ return { series: [left, right], formula: "ratio" };
64
+ }
65
+
36
66
  export function buildCustomChartPreset(expression: string, fallbackSymbol?: string | null): ChartSpec {
67
+ const derived = derivedPair(expression);
68
+ if (derived) return setPairStudies(chartSpec(buildCustomSeries(derived.series)), [derived.formula]);
37
69
  const parsed = parseChartExpression(expression);
38
70
  if (parsed.length === 0) return fallbackSymbol ? buildPriceChartPreset(fallbackSymbol) : buildEmptyChartPreset();
39
71
  return chartSpec(buildCustomSeries(parsed));
@@ -1,8 +1,8 @@
1
1
  import type { ChatChannel } from "../../../api-client";
2
2
  import { t } from "../../../i18n";
3
3
  import { normalizeChannelId } from "./controller/state";
4
- import { teamPrefix } from "../cloud/team/model";
5
- import { teamStore } from "../cloud/team/store";
4
+ import { teamPrefix } from "../../../team/model";
5
+ import { teamStore } from "../../../team/store";
6
6
 
7
7
  /** `MD·` for a team channel, empty otherwise. Team markers never depend on color. */
8
8
  function teamChannelPrefix(channel: ChatChannel | undefined, fallbackId: string): string {
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore
2
2
  import type { MutableRefObject } from "react";
3
3
  import type { ChatChannel } from "../../../../api-client";
4
4
  import { useThrottledCommitValue } from "../../../../react/use-throttled-commit-value";
5
- import { teamStore } from "../../cloud/team/store";
5
+ import { teamStore } from "../../../../team/store";
6
6
  import { chatSidebarStore } from "../sidebar-store";
7
7
  import {
8
8
  buildChatSidebarRows,
@@ -55,10 +55,10 @@ import {
55
55
  import type { ChatUserAnchor, ChatUserContextMenuEvent } from "../message/types";
56
56
  import type { ContextMenuItem } from "../../../../types/context-menu";
57
57
  import { usePluginAppActions } from "../../../runtime";
58
- import { openTeamPane } from "../../cloud/team/pane-request";
58
+ import { openTeamPane } from "../../../../team/pane-request";
59
59
  import { openChatPaneLink } from "../pane-links/open";
60
60
  import type { ChatMessagePaneLink } from "../pane-links/model";
61
- import { teamStore } from "../../cloud/team/store";
61
+ import { teamStore } from "../../../../team/store";
62
62
  import { requestAccountManagementTab } from "../../account-management/navigation";
63
63
  import {
64
64
  CHAT_MESSAGE_EDIT_WINDOW_MS,
@@ -1,5 +1,5 @@
1
1
  import type { ChatChannel, ChatUserSummary } from "../../../api-client";
2
- import { teamStore } from "../cloud/team/store";
2
+ import { teamStore } from "../../../team/store";
3
3
  import { formatChatPaneTitle } from "./channel-labels";
4
4
 
5
5
  /** What the chat controller holds about one conversation, read without a request. */
@@ -1,6 +1,6 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
- import { teamChannelId } from "../cloud/team/model";
3
- import { teamStore } from "../cloud/team/store";
2
+ import { teamChannelId } from "../../../team/model";
3
+ import { teamStore } from "../../../team/store";
4
4
  import { formatChatPaneTitle } from "./channel-labels";
5
5
  import {
6
6
  DEFAULT_CHAT_CHANNEL_ID,
@@ -2,7 +2,7 @@ import { apiClient } from "../../../../api-client";
2
2
  import { pngDataUrl } from "../../../../shares/pane-image";
3
3
  import { registerPaneChatSharing } from "../../../../shares/chat";
4
4
  import type { GloomPluginContext } from "../../../../types/plugin";
5
- import { requestAuthDialog } from "../../cloud/auth-dialog";
5
+ import { requestAuthDialog } from "../../../../account/auth-dialog";
6
6
  import { MAX_CHAT_IMAGE_BYTES } from "../attachments/model";
7
7
  import { LAST_VISITED_CHAT_CHANNEL_KEY } from "../channels";
8
8
  import { chatController } from "../controller";
@@ -1,5 +1,5 @@
1
1
  import type { ChatChannel, TeamSummary } from "../../../api-client";
2
- import { sortTeamChannels } from "../cloud/team/model";
2
+ import { sortTeamChannels } from "../../../team/model";
3
3
  import type { ChatSidebarSection } from "./sidebar-store";
4
4
 
5
5
  interface SidebarHeaderBase {
@@ -14,8 +14,8 @@ import { blendHex, colors } from "../../../theme/colors";
14
14
  import { t } from "../../../i18n";
15
15
  import type { ChatChannel } from "../../../api-client";
16
16
  import { truncateWithEllipsis } from "../../../utils/text-wrap";
17
- import { teamAccentHex, teamPrefix } from "../cloud/team/model";
18
- import { teamStore } from "../cloud/team/store";
17
+ import { teamAccentHex, teamPrefix } from "../../../team/model";
18
+ import { teamStore } from "../../../team/store";
19
19
  import type { ChatController } from "./controller";
20
20
  import { chatSidebarStore } from "./sidebar-store";
21
21
  import { buildChatSidebarRows } from "./sidebar-rows";
@@ -29,8 +29,8 @@ import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
29
29
  import { usePluginAppActions, usePluginPaneState } from "../../runtime";
30
30
  import { requestPriceAlertFocus } from "../alerts/focus";
31
31
  import { SignInWall } from "../cloud/auth-actions";
32
- import { openTeamPane } from "../cloud/team/pane-request";
33
- import { teamStore } from "../cloud/team/store";
32
+ import { openTeamPane } from "../../../team/pane-request";
33
+ import { teamStore } from "../../../team/store";
34
34
  import { chatController, type ChatController } from "./controller";
35
35
  import { applyUnreadInboxItemToConfig } from "./pane-state";
36
36
  import { buildUnreadListRows, type UnreadInboxItem, type UnreadListRow } from "./unread-inbox";
@@ -16,7 +16,7 @@ import { usePluginAppActions } from "../../runtime";
16
16
  import { colors } from "../../../theme/colors";
17
17
  import { t, tf } from "../../../i18n";
18
18
  import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade";
19
- import { requestAuthDialog } from "./auth-dialog";
19
+ import { requestAuthDialog } from "../../../account/auth-dialog";
20
20
  import type { AccountMode } from "../../../account/auth-model";
21
21
  import { recordWallCtaClicked } from "../../../api-client/research-activity";
22
22
  import { WallTeaser } from "../shared/wall-teaser";
@@ -1,7 +1,7 @@
1
1
  import type { GloomPluginContext } from "../../../types/plugin";
2
2
  import { apiClient } from "../../../api-client";
3
3
  import { chatController } from "../chat/controller";
4
- import { requestAuthDialog } from "./auth-dialog";
4
+ import { requestAuthDialog } from "../../../account/auth-dialog";
5
5
  import { requestDeviceSignInDialog } from "../../../account/device-signin-dialog";
6
6
 
7
7
  export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
@@ -27,7 +27,7 @@ import { Box, Text, TextAttributes, useRendererHost, useUiCapabilities } from ".
27
27
  import { useDialog, useDialogKeyboard, type DialogApi, type PromptContext } from "../../../../ui/dialog";
28
28
  import { isPlainKey } from "../../../../utils/keyboard";
29
29
  import { useCloudUpgradeAction } from "../../shared/cloud-upgrade";
30
- import { requestAuthDialog } from "../auth-dialog";
30
+ import { requestAuthDialog } from "../../../../account/auth-dialog";
31
31
  import {
32
32
  currentMcpEndpointUrl,
33
33
  MCP_CLIENTS,
@@ -6,9 +6,9 @@ import { colors } from "../../../../theme/colors";
6
6
  import { Box, Text } from "../../../../ui";
7
7
  import { isPlainKey } from "../../../../utils/keyboard";
8
8
  import { usePluginAppActions } from "../../../runtime";
9
- import { describeTeam, teamAccentHex, teamLabel, userHandle } from "./model";
10
- import { openTeamPane } from "./pane-request";
11
- import { teamStore } from "./store";
9
+ import { describeTeam, teamAccentHex, teamLabel, userHandle } from "../../../../team/model";
10
+ import { openTeamPane } from "../../../../team/pane-request";
11
+ import { teamStore } from "../../../../team/store";
12
12
 
13
13
  const NEW_TEAM_ID = "__new__";
14
14
 
@@ -1,6 +1,6 @@
1
1
  import type { PluginCapability } from "../../../../capabilities";
2
- import { teamAccentHex } from "./model";
3
- import { teamStore, type TeamStoreSnapshot } from "./store";
2
+ import { teamAccentHex } from "../../../../team/model";
3
+ import { teamStore, type TeamStoreSnapshot } from "../../../../team/store";
4
4
 
5
5
  const CLOUD_TEAM_CAPABILITY_ID = "cloud.team";
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { apiClient } from "../../../../api-client";
2
2
  import type { CommandResultDef, GloomPluginContext } from "../../../../types/plugin";
3
- import { requestAuthDialog } from "../auth-dialog";
3
+ import { requestAuthDialog } from "../../../../account/auth-dialog";
4
4
  import {
5
5
  canInviteToTeam,
6
6
  canManageTeam,
@@ -9,9 +9,9 @@ import {
9
9
  teamChannelId,
10
10
  teamLabel,
11
11
  userHandle,
12
- } from "./model";
13
- import { openTeamPane, type TeamPaneSection } from "./pane-request";
14
- import { teamStore } from "./store";
12
+ } from "../../../../team/model";
13
+ import { openTeamPane, type TeamPaneSection } from "../../../../team/pane-request";
14
+ import { teamStore } from "../../../../team/store";
15
15
 
16
16
  const TEAM_SUBCOMMAND = /^(invite|new|settings|members|channels|leave|focus|chat)\b\s*(.*)$/i;
17
17
 
@@ -2,11 +2,11 @@ import type { PluginModule } from "../../plugin-module";
2
2
  import { chatController } from "../../chat/controller";
3
3
  import { createCloudTeamCapability } from "./capability";
4
4
  import { registerTeamCommands } from "./command";
5
- import { teamChannelId } from "./model";
5
+ import { teamChannelId } from "../../../../team/model";
6
6
  import { TeamPane } from "./pane";
7
- import { TEAM_PANE_ID, TEAM_PANE_TEMPLATE_ID, openTeamPane } from "./pane-request";
7
+ import { TEAM_PANE_ID, TEAM_PANE_TEMPLATE_ID, openTeamPane } from "../../../../team/pane-request";
8
8
  import { TeamStatusWidget } from "./status-widget";
9
- import { teamStore } from "./store";
9
+ import { teamStore } from "../../../../team/store";
10
10
  import { installTeamStateHost } from "./team-state-host";
11
11
  import { createCloudViewsCapability, teamViewsStore } from "./views";
12
12
 
@@ -10,8 +10,8 @@ import {
10
10
  deriveTeamShortName,
11
11
  normalizeTeamShortName,
12
12
  teamChannelId,
13
- } from "./model";
14
- import type { TeamPaneSection } from "./pane-request";
13
+ } from "../../../../team/model";
14
+ import type { TeamPaneSection } from "../../../../team/pane-request";
15
15
 
16
16
  export const TEAM_PANE_SECTIONS: readonly { value: TeamPaneSection; label: string }[] = [
17
17
  { value: "members", label: "Members" },
@@ -19,7 +19,7 @@ import {
19
19
  teamChannelId,
20
20
  teamPrefix,
21
21
  userHandle,
22
- } from "./model";
22
+ } from "../../../../team/model";
23
23
  import {
24
24
  draftProblem,
25
25
  setDraftName,
@@ -3,7 +3,7 @@ import type { TeamAccentColor } from "../../../../api-client";
3
3
  import { Badge, Button, Checkbox, FieldLabel, SectionHeading, TextField, type ButtonVariant } from "../../../../components";
4
4
  import { colors } from "../../../../theme/colors";
5
5
  import { Box, Span, Text, TextAttributes, type BoxRenderable } from "../../../../ui";
6
- import { TEAM_ACCENT_COLORS, teamAccentHex, teamPrefix } from "./model";
6
+ import { TEAM_ACCENT_COLORS, teamAccentHex, teamPrefix } from "../../../../team/model";
7
7
 
8
8
  /**
9
9
  * Every control in the pane registers under a field id. The pane keeps one
@@ -41,7 +41,7 @@ import {
41
41
  teamIdFromChannelId,
42
42
  teamPrefix,
43
43
  userHandle,
44
- } from "./model";
44
+ } from "../../../../team/model";
45
45
  import {
46
46
  TEAM_PANE_SECTIONS,
47
47
  cycleAccent,
@@ -61,7 +61,7 @@ import {
61
61
  subscribeRequestedTeamPaneView,
62
62
  type TeamPaneSection,
63
63
  type TeamPaneView,
64
- } from "./pane-request";
64
+ } from "../../../../team/pane-request";
65
65
  import {
66
66
  ChannelsSection,
67
67
  CreateTeamForm,
@@ -70,7 +70,7 @@ import {
70
70
  SettingsSection,
71
71
  } from "./pane-sections";
72
72
  import { Muted, PaneButton, TeamPaneFocusContext, type TeamPaneFocus } from "./pane-ui";
73
- import { teamStore } from "./store";
73
+ import { teamStore } from "../../../../team/store";
74
74
 
75
75
  type Message = { tone: "info" | "success" | "error"; text: string } | null;
76
76
 
@@ -4,9 +4,9 @@ import { colors } from "../../../../theme/colors";
4
4
  import { Box, Span, Text, TextAttributes, useCommandBarShortcut } from "../../../../ui";
5
5
  import { usePluginAppActions } from "../../../runtime";
6
6
  import { chatController } from "../../chat/controller";
7
- import { countTeamUpdates, teamAccentHex, teamIdFromChannelId } from "./model";
8
- import { openTeamPane } from "./pane-request";
9
- import { teamStore } from "./store";
7
+ import { countTeamUpdates, teamAccentHex, teamIdFromChannelId } from "../../../../team/model";
8
+ import { openTeamPane } from "../../../../team/pane-request";
9
+ import { teamStore } from "../../../../team/store";
10
10
 
11
11
  /**
12
12
  * One chip per team in the status bar, in the team's accent, with the unread
@@ -1,6 +1,6 @@
1
1
  import { apiClient, type TeamPluginStateEntry, type TeamPluginStateEvent } from "../../../../api-client";
2
2
  import { setTeamStateHost, type TeamStateHost } from "../../../team-state";
3
- import { teamStore } from "./store";
3
+ import { teamStore } from "../../../../team/store";
4
4
 
5
5
  type Listener = (entry: { value: unknown; revision: number; updatedBy: string; updatedAt: string } | null) => void
6
6
 
@@ -8,8 +8,8 @@ import {
8
8
  setViewRefResolver,
9
9
  type ViewSpec,
10
10
  } from "../../custom-view";
11
- import { teamPrefix } from "./model";
12
- import { teamStore } from "./store";
11
+ import { teamPrefix } from "../../../../team/model";
12
+ import { teamStore } from "../../../../team/store";
13
13
 
14
14
  const CLOUD_VIEWS_CAPABILITY_ID = "cloud.views";
15
15
 
@@ -24,7 +24,7 @@ import { usePluginAppActions } from "../../../runtime";
24
24
  import { SignInWall } from "../auth-actions";
25
25
  import { useCloudUpgradeAction } from "../../shared/cloud-upgrade";
26
26
  import { usePlanAccess } from "../../../../api-client/plan-access";
27
- import { teamStore } from "../team/store";
27
+ import { teamStore } from "../../../../team/store";
28
28
  import { ThesisDetail } from "./detail";
29
29
  import { useBookExposure } from "./exposure";
30
30
  import * as flows from "./flows";
@@ -10,7 +10,7 @@ import { isPlainKey } from "../../../../utils/keyboard";
10
10
  import { usePluginAppActions } from "../../../runtime";
11
11
  import { useCloudUpgradeAction } from "../../shared/cloud-upgrade";
12
12
  import { usePlanAccess } from "../../../../api-client/plan-access";
13
- import { teamStore } from "../team/store";
13
+ import { teamStore } from "../../../../team/store";
14
14
  import * as flows from "./flows";
15
15
  import {
16
16
  catalystState,
@@ -10,7 +10,7 @@ import { usePluginAppActions, usePluginPaneState } from "../../../runtime";
10
10
  import { SignInWall } from "../auth-actions";
11
11
  import { useCloudUpgradeAction } from "../../shared/cloud-upgrade";
12
12
  import { usePlanAccess } from "../../../../api-client/plan-access";
13
- import { teamStore } from "../team/store";
13
+ import { teamStore } from "../../../../team/store";
14
14
  import { ThesisDetail } from "./detail";
15
15
  import * as flows from "./flows";
16
16
  import { thesesCovering } from "./model";
@@ -3,7 +3,7 @@ import type { GloomPluginContext } from "../../../types/plugin";
3
3
  import { requestAccountManagementTab } from "../account-management/navigation";
4
4
  import { openCloudUpgradeUrl } from "../shared/cloud-upgrade";
5
5
  import { resolvePlanAccess } from "../../../api-client/plan-access";
6
- import { requestAuthDialog } from "./auth-dialog";
6
+ import { requestAuthDialog } from "../../../account/auth-dialog";
7
7
 
8
8
  export function registerCloudUpgradeCommand(ctx: GloomPluginContext): void {
9
9
  ctx.registerCommand({
@@ -4,8 +4,8 @@ import { DataTableView } from "../../../components/data-table/view";
4
4
  import { ChoiceDialog } from "../../../components/ui/choice-dialog";
5
5
  import { useDialog, type PromptContext } from "../../../ui/dialog";
6
6
  import { getSharedRegistry } from "../../registry";
7
- import { teamPrefix } from "../cloud/team/model";
8
- import { teamStore } from "../cloud/team/store";
7
+ import { teamPrefix } from "../../../team/model";
8
+ import { teamStore } from "../../../team/store";
9
9
  import { teamViewsStore } from "../cloud/team/views";
10
10
  import { setPaneSettings } from "../../../pane-settings";
11
11
  import { customViewInstanceSettings } from "./index";
@@ -55,6 +55,12 @@ function dateEntry(label: string, date: Date | null) {
55
55
  return { label, value: date, formatted: date ? date.toISOString().slice(0, 10) : "—" };
56
56
  }
57
57
 
58
+ /** The next pay date, when it is the one stated beside the ex-date the forward rate is dated by. */
59
+ function forwardPayDate(metrics: DividendData["metrics"]): Date | null {
60
+ const exDate = metrics.nextExDividendDate?.toISOString().slice(0, 10);
61
+ return exDate != null && exDate === metrics.forwardRateAsOf ? metrics.nextPayDate : null;
62
+ }
63
+
58
64
  function matchesType(payment: DividendPayment, type: string): boolean {
59
65
  return type === "all" || payment.type === type;
60
66
  }
@@ -95,7 +101,7 @@ export function projectDividendYieldHeadless(
95
101
  : "Reference price time unavailable; cash yield may be out of date." }] : []),
96
102
  { label: "Trailing yield", value: metrics.trailingYield, formatted: formatDividendYield(metrics.trailingYield) },
97
103
  { label: "Forward yield", value: metrics.forwardYield, formatted: metrics.forwardYield == null ? formatDividendYield(null)
98
- : `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf)})` },
104
+ : `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf, { payDate: forwardPayDate(metrics) })})` },
99
105
  { label: "Trailing rate", value: metrics.trailingRate, formatted: currency ? formatDistributionAmount(metrics.trailingRate ?? undefined, currency) : "—" },
100
106
  { label: "Forward rate", value: metrics.forwardRate, formatted: currency ? formatDistributionAmount(metrics.forwardRate ?? undefined, currency) : "—" },
101
107
  { label: "Earnings Payout", value: metrics.payoutRatio, formatted: formatDividendYield(metrics.payoutRatio) },
@@ -132,7 +138,7 @@ export function projectDividendYieldHeadless(
132
138
  limitations: data.notes ?? [],
133
139
  yieldMethod: "Cash distributions with ex-dates in the preceding 12 months, one payment per period for a regular cadence, divided by the reference share price; excludes reinvestment and is not SEC yield or total return.",
134
140
  forwardRateAsOf: metrics.forwardRateAsOf,
135
- forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, dated by its ex-date (forwardRateAsOf). Only when its currency units are comparable with the share price.",
141
+ forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, or once the next one is declared that one, dated by its ex-date (forwardRateAsOf; a date still ahead is the declared distribution's). Only when its currency units are comparable with the share price.",
136
142
  paymentFrequencyMethod: "Cadence inferred from ex-dates in the last two years, unavailable when no cash was reported in the last year; not an announced payment schedule.",
137
143
  },
138
144
  };
@@ -22,6 +22,7 @@ import { colors, priceColor } from "../../../theme/colors";
22
22
  import { TextAttributes, useUiCapabilities } from "../../../ui";
23
23
  import { displayWidth, formatCurrency, formatDistributionAmount, formatPercentRaw } from "../../../utils/format";
24
24
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
25
+ import { forwardDividendNote } from "../../../utils/forward-dividend";
25
26
  import { nextHeaderSort } from "../../../utils/sort-values";
26
27
  import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
27
28
  import { SignInWall } from "../cloud/auth-actions";
@@ -76,11 +77,12 @@ function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory
76
77
  const exDate: StatItem[] = metrics.nextExDividendDate
77
78
  ? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
78
79
  : hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
80
+ const forwardNote = metrics.forwardYield != null ? forwardDividendNote(metrics.forwardRateAsOf) : null;
79
81
  return [
80
82
  { id: "ttm-yield", label: short ? "TTM yld" : "TTM yield", value: formatDividendYield(metrics.trailingYield), color: priceColor(metrics.trailingYield ?? 0) },
81
- // Dated by the distribution it annualizes; docs/research-data.md says how it is built.
83
+ // Dated by the distribution it annualizes, or marked declared beside the next ex-date; docs/research-data.md says how it is built.
82
84
  { id: "forward-yield", label: short ? "Fwd yld" : "Fwd yield", value: formatDividendYield(metrics.forwardYield), color: priceColor(metrics.forwardYield ?? 0),
83
- ...(metrics.forwardYield != null && metrics.forwardRateAsOf ? { detail: `as of ${metrics.forwardRateAsOf}` } : {}) },
85
+ ...(forwardNote ? { detail: forwardNote } : {}) },
84
86
  { id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency) },
85
87
  { id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency) },
86
88
  { id: "growth-1y", label: short ? "1Y chg" : "1Y growth", value: formatGrowth(metrics.growth1Y), color: priceColor(metrics.growth1Y ?? 0) },
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { useState } from "react";
8
8
  import { ActionRow, MarkdownText, RemoteImage, usePaneLinkMenuEntry } from "../../../components";
9
+ import { READER_HEADING_STYLE } from "../../../components/reader/reader-text";
9
10
  import type { DocsBlock } from "../../../docs/content";
10
11
  import { tf } from "../../../i18n";
11
12
  import { isDarkTheme } from "../../../theme/themes";
@@ -25,6 +26,9 @@ const CALLOUT_LABELS: Record<string, string> = {
25
26
  example: "Example",
26
27
  };
27
28
 
29
+ /** The reader's section heading size; the space around it is the blocks' own. */
30
+ const SECTION_HEADING_STYLE = { fontSize: READER_HEADING_STYLE.fontSize, lineHeight: READER_HEADING_STYLE.lineHeight } as const;
31
+
28
32
  const LINK_PATTERN = /\[([^\]]+)\]\(([^)\s]+)\)/g;
29
33
 
30
34
  /** Inline links of a block, for the pane menu: the terminal cannot press a word. */
@@ -122,9 +126,16 @@ function Block({ block, width, onOpenLink }: { block: DocsBlock; width: number;
122
126
  case "paragraph":
123
127
  return <MarkdownText text={block.text.replace(/\n/g, " ")} lineWidth={width} onOpenLink={onOpenLink} />;
124
128
  case "heading":
125
- // A section reads bright; a step inside it, in the text colour.
129
+ // A section reads bright and, on the desktop and the web, a step larger, as in every reader; a step inside it, in the text colour.
126
130
  return (
127
- <Text fg={block.level === 2 ? colors.textBright : colors.text} attributes={TextAttributes.BOLD} wrapText selectable width={nativePaneChrome ? "100%" : width}>
131
+ <Text
132
+ fg={block.level === 2 ? colors.textBright : colors.text}
133
+ attributes={TextAttributes.BOLD}
134
+ wrapText
135
+ selectable
136
+ width={nativePaneChrome ? "100%" : width}
137
+ style={nativePaneChrome && block.level === 2 ? SECTION_HEADING_STYLE : undefined}
138
+ >
128
139
  {block.text.replace(/[*`]/g, "")}
129
140
  </Text>
130
141
  );
@@ -54,6 +54,7 @@ import type { PaneProps } from "../../../types/plugin";
54
54
  import { Box, Text, TextAttributes, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
55
55
  import { usePluginAppActions, usePluginPaneState } from "../../runtime";
56
56
  import { badgeCell } from "../help/table-cells";
57
+ import { READER_COLUMN_FONT } from "../../../components/reader/reader-text";
57
58
  import { DocsBlocks } from "./blocks";
58
59
  import { useDocsFeed, useDocsLesson } from "./use-docs";
59
60
 
@@ -73,12 +74,12 @@ const STACK_LIMIT = 30;
73
74
  /** Prose reads best at about seventy columns, however wide the pane. */
74
75
  const MEASURE = 72;
75
76
  /**
76
- * On the desktop and the web the text reads a size up from the table text,
77
- * with more air between lines, at most 69 characters of it to the line (as
78
- * wide as a screenshot of the measure): the column fills a narrower pane and
79
- * stops there in a wider one.
77
+ * On the desktop and the web the text reads at the reader's body size, a step
78
+ * up from the table text, with more air between lines, at most 69 characters
79
+ * of it to the line (as wide as a screenshot of the measure): the column fills
80
+ * a narrower pane and stops there in a wider one.
80
81
  */
81
- const READING_STYLE = { fontSize: "1.1667em", "--cell-h": "1.7em", maxWidth: "69ch" } as const;
82
+ const READING_STYLE = { fontSize: READER_COLUMN_FONT, "--cell-h": "1.7em", maxWidth: "69ch" } as const;
82
83
  /** The terminal wraps at `width` columns; the desktop and the web at the reading column's own width. */
83
84
  function useTextWidth(width: number): number | "100%" {
84
85
  return useUiCapabilities().nativePaneChrome ? "100%" : width;
@@ -1,7 +1,9 @@
1
1
  import type { EarningsCalendarReport, EarningsTiming } from "../../../api-client/earnings";
2
2
  import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
3
3
  import type { EarningsEvent } from "../../../types/data-provider";
4
+ import type { EarningsDateStatus, EarningsEpsBasis } from "../../../types/financials";
4
5
  import type { TickerRecord } from "../../../types/ticker";
6
+ import { dateStatusFromEstimateFlag, earningsDateStatus, earningsEpsBasis } from "../shared/earnings-report";
5
7
  import { coherentEarningsValue } from "./estimate-basis";
6
8
  import { shortDate, weekday } from "./format";
7
9
 
@@ -16,9 +18,15 @@ export interface BoardReport {
16
18
  timing: EarningsTiming | null;
17
19
  /** Timing taken from the company's last reports, not announced yet. */
18
20
  expectedTiming: boolean;
21
+ /** Whether the date is out, announced or projected; null when the source does not say. */
22
+ dateStatus: EarningsDateStatus | null;
19
23
  marketCap: number | null;
20
24
  epsEstimate: number | null;
25
+ /** Analysts behind `epsEstimate`. */
26
+ epsAnalysts: number | null;
21
27
  epsActual: number | null;
28
+ /** Null when the reported figure's basis cannot be determined. */
29
+ epsBasis: EarningsEpsBasis | null;
22
30
  revenueEstimate: number | null;
23
31
  impliedMove: number | null;
24
32
  averageMove: number | null;
@@ -120,9 +128,12 @@ export function boardReport(report: EarningsCalendarReport, owned: ReadonlyMap<s
120
128
  date: report.date,
121
129
  timing: report.timing,
122
130
  expectedTiming: report.timingSource === "history",
131
+ dateStatus: earningsDateStatus(report.dateStatus),
123
132
  marketCap: report.marketCap,
124
133
  epsEstimate: report.epsEstimate,
134
+ epsAnalysts: report.epsAnalysts,
125
135
  epsActual: report.epsActual,
136
+ epsBasis: earningsEpsBasis(report.epsBasis),
126
137
  revenueEstimate: report.revenueEstimate,
127
138
  impliedMove: report.implied?.move ?? null,
128
139
  averageMove: report.averageReports >= MIN_AVERAGE_REPORTS ? report.averageMove : null,
@@ -139,6 +150,7 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
139
150
  const currency = event.estimateBasis?.[field]?.currency;
140
151
  return currency && currency !== "USD" ? null : coherentEarningsValue(event, field);
141
152
  };
153
+ const epsEstimate = dollars("epsEstimate");
142
154
  return {
143
155
  key: `${event.symbol}:${date}`,
144
156
  symbol: event.symbol,
@@ -146,9 +158,14 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
146
158
  date,
147
159
  timing,
148
160
  expectedTiming: false,
161
+ dateStatus: dateStatusFromEstimateFlag(event.isDateEstimate),
149
162
  marketCap: null,
150
- epsEstimate: dollars("epsEstimate"),
163
+ epsEstimate,
164
+ // The count belongs to the estimate beside it.
165
+ epsAnalysts: epsEstimate == null ? null : coherentEarningsValue(event, "epsAnalysts"),
151
166
  epsActual: null,
167
+ // The estimate's basis goes with it.
168
+ epsBasis: epsEstimate == null ? null : earningsEpsBasis(event.epsBasis),
152
169
  revenueEstimate: dollars("revenueEstimate"),
153
170
  impliedMove: null,
154
171
  averageMove: null,