gloomberb 0.17.2 → 0.17.4

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 (200) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat.ts +28 -0
  3. package/src/api-client/errors.ts +11 -1
  4. package/src/api-client/futures-curve.ts +16 -0
  5. package/src/api-client/index.ts +22 -9
  6. package/src/api-client/normalizers.ts +21 -0
  7. package/src/api-client/socket.ts +27 -0
  8. package/src/api-client/types.ts +3 -1
  9. package/src/app/runtime/startup.ts +4 -0
  10. package/src/cli/commands/market.ts +60 -9
  11. package/src/cli/commands/search.ts +43 -2
  12. package/src/cli/commands/ticker.ts +100 -21
  13. package/src/cli/desktop-pane-shot.ts +34 -9
  14. package/src/cli/fx-pair.ts +17 -0
  15. package/src/cli/glossary.ts +134 -6
  16. package/src/cli/history-rows.ts +16 -4
  17. package/src/cli/index.ts +44 -12
  18. package/src/cli/known-gaps.ts +21 -2
  19. package/src/cli/listing-arg.ts +19 -4
  20. package/src/cli/listing-suggestion.ts +33 -2
  21. package/src/cli/pane-functions/dom.ts +39 -6
  22. package/src/cli/pane-functions/freshness.ts +48 -5
  23. package/src/cli/pane-functions/function-help.ts +1 -1
  24. package/src/cli/pane-functions/headless.ts +85 -19
  25. package/src/cli/pane-functions/index.ts +60 -4
  26. package/src/cli/pane-functions/market-session.ts +69 -6
  27. package/src/cli/pane-functions/resolver.ts +4 -2
  28. package/src/cli/pane-functions/screenshot.ts +14 -1
  29. package/src/cli/scroll-truncation.ts +5 -1
  30. package/src/cli/symbol-args.ts +41 -3
  31. package/src/cli/symbol-list.ts +19 -7
  32. package/src/cli/unknown-command.ts +70 -0
  33. package/src/components/chart/native/surface/visibility.ts +37 -7
  34. package/src/components/index.ts +1 -1
  35. package/src/components/layout/header.tsx +348 -102
  36. package/src/components/markdown-text.tsx +40 -66
  37. package/src/components/market-board/index.tsx +12 -6
  38. package/src/components/metric-treemap/heat-surface.tsx +76 -14
  39. package/src/components/metric-treemap/heat-tiles.ts +5 -3
  40. package/src/components/metric-treemap/index.tsx +1 -1
  41. package/src/components/reader/detail-reader.tsx +28 -6
  42. package/src/components/ticker/badge/index.tsx +2 -0
  43. package/src/components/ui/button.tsx +24 -9
  44. package/src/components/ui/display.tsx +6 -1
  45. package/src/components/ui/external-link.tsx +10 -2
  46. package/src/components/ui/index.ts +1 -1
  47. package/src/components/ui/status.tsx +148 -5
  48. package/src/data/memory-resource-store.ts +1 -1
  49. package/src/data/resource-store.ts +13 -1
  50. package/src/docs/snapshot.json +13 -13
  51. package/src/i18n/es.ts +1 -0
  52. package/src/i18n/ja.ts +1 -0
  53. package/src/i18n/ko.ts +1 -0
  54. package/src/i18n/zh-cn.ts +1 -0
  55. package/src/i18n/zh-tw.ts +1 -0
  56. package/src/layout-marketplace/gallery-desktop.tsx +2 -2
  57. package/src/market-data/futures-board.ts +44 -1
  58. package/src/market-data/futures-contract.ts +10 -0
  59. package/src/market-data/market/format.ts +7 -2
  60. package/src/market-data/market/freshness.ts +25 -1
  61. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  62. package/src/market-data/quotes/resolution.ts +2 -0
  63. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  64. package/src/plugins/builtin/account-management/pane.tsx +17 -13
  65. package/src/plugins/builtin/alerts/cli.ts +72 -11
  66. package/src/plugins/builtin/attention/headless.ts +19 -10
  67. package/src/plugins/builtin/attention/model.ts +22 -0
  68. package/src/plugins/builtin/awards/headless.ts +5 -1
  69. package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
  70. package/src/plugins/builtin/catalysts/headless.ts +5 -1
  71. package/src/plugins/builtin/changelog/index.tsx +11 -8
  72. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
  73. package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
  74. package/src/plugins/builtin/chat/attachments/model.ts +5 -3
  75. package/src/plugins/builtin/chat/content/footer.tsx +6 -0
  76. package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
  77. package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
  78. package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
  79. package/src/plugins/builtin/chat/controller/connections.ts +18 -0
  80. package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
  81. package/src/plugins/builtin/chat/controller/index.ts +49 -21
  82. package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
  83. package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
  84. package/src/plugins/builtin/chat/layout.ts +91 -31
  85. package/src/plugins/builtin/chat/markdown.ts +94 -0
  86. package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
  87. package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
  88. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  89. package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
  90. package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
  91. package/src/plugins/builtin/company-attention/headless.ts +8 -5
  92. package/src/plugins/builtin/company-attention/model.ts +53 -2
  93. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  94. package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
  95. package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
  96. package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
  97. package/src/plugins/builtin/cot/client.ts +12 -4
  98. package/src/plugins/builtin/cot/model.ts +20 -0
  99. package/src/plugins/builtin/cot/pane.tsx +2 -2
  100. package/src/plugins/builtin/crypto-board/model.ts +7 -3
  101. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  102. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
  103. package/src/plugins/builtin/debug/index.tsx +15 -15
  104. package/src/plugins/builtin/dividend-yield/client.ts +8 -1
  105. package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
  106. package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
  107. package/src/plugins/builtin/docs/index.tsx +5 -3
  108. package/src/plugins/builtin/docs/pane.tsx +19 -2
  109. package/src/plugins/builtin/docs/targets.ts +5 -3
  110. package/src/plugins/builtin/earnings-calls/data.ts +22 -0
  111. package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
  112. package/src/plugins/builtin/equity-screener/headless.ts +4 -3
  113. package/src/plugins/builtin/equity-screener/live.ts +27 -1
  114. package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
  115. package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
  116. package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
  117. package/src/plugins/builtin/futures/index.tsx +33 -6
  118. package/src/plugins/builtin/futures/model.ts +69 -5
  119. package/src/plugins/builtin/futures/series.ts +3 -3
  120. package/src/plugins/builtin/futures/table.tsx +31 -11
  121. package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
  122. package/src/plugins/builtin/holders/pane.tsx +6 -1
  123. package/src/plugins/builtin/jobs/pane.tsx +12 -9
  124. package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
  125. package/src/plugins/builtin/mna/client.ts +12 -2
  126. package/src/plugins/builtin/mna/detail.tsx +2 -2
  127. package/src/plugins/builtin/mna/pane.tsx +2 -2
  128. package/src/plugins/builtin/news/headless.ts +4 -2
  129. package/src/plugins/builtin/notes/owner.tsx +5 -4
  130. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
  131. package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
  132. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  133. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  134. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
  135. package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
  136. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
  137. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
  138. package/src/plugins/builtin/research/event-model.ts +14 -0
  139. package/src/plugins/builtin/research/headless-schema.ts +31 -1
  140. package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
  141. package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
  142. package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
  143. package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
  144. package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
  145. package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
  146. package/src/plugins/builtin/research-search/data.ts +11 -0
  147. package/src/plugins/builtin/research-search/pane.tsx +13 -2
  148. package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
  149. package/src/plugins/builtin/seasonality/client.ts +23 -0
  150. package/src/plugins/builtin/seasonality/pane.tsx +4 -2
  151. package/src/plugins/builtin/sectors/index.tsx +7 -3
  152. package/src/plugins/builtin/shared/report-freshness.ts +6 -2
  153. package/src/plugins/builtin/short-interest/headless.ts +13 -3
  154. package/src/plugins/builtin/short-interest/index.tsx +1 -1
  155. package/src/plugins/builtin/short-interest/model.ts +62 -3
  156. package/src/plugins/builtin/short-interest/pane.tsx +2 -2
  157. package/src/plugins/builtin/short-interest/schedule.ts +107 -0
  158. package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
  159. package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
  160. package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
  161. package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
  162. package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
  163. package/src/plugins/builtin/themes/headless.ts +1 -1
  164. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
  165. package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
  166. package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
  167. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
  168. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
  169. package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
  170. package/src/plugins/builtin/volatility/chart-model.ts +1 -1
  171. package/src/plugins/builtin/volatility/charts.tsx +1 -1
  172. package/src/renderers/dom/build-assets.ts +17 -4
  173. package/src/renderers/dom/desktop/controls.tsx +2 -0
  174. package/src/renderers/dom/styles.css +6 -0
  175. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
  176. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
  177. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
  178. package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
  179. package/src/sources/provider-router/batches.ts +2 -1
  180. package/src/sources/provider-router/cache.ts +3 -0
  181. package/src/sync/baseline.ts +19 -14
  182. package/src/sync/controller.ts +140 -40
  183. package/src/sync/core-contributors.ts +256 -32
  184. package/src/sync/types.ts +44 -0
  185. package/src/time-series/resolve.ts +24 -5
  186. package/src/types/data-provider.ts +8 -0
  187. package/src/types/financials.ts +40 -4
  188. package/src/types/headless.ts +43 -0
  189. package/src/types/plugin.ts +7 -0
  190. package/src/ui/resize-settle.ts +128 -0
  191. package/src/utils/calendar-date.ts +56 -0
  192. package/src/utils/decimal-comma.ts +31 -0
  193. package/src/utils/declared-dividends.ts +39 -0
  194. package/src/utils/forward-dividend.ts +34 -11
  195. package/src/utils/fundamentals.ts +13 -2
  196. package/src/utils/futures-generic.ts +25 -4
  197. package/src/utils/markdown.ts +180 -0
  198. package/src/utils/market-capitalization.ts +22 -0
  199. package/src/utils/plugin-proxy-hosts.json +5 -0
  200. package/src/utils/utc-time.ts +14 -3
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { DataTableStackView, DataTableView, KeyValueRow, QueryBar, usePaneNoticeFooter, useQueryBarSearch, useTableLoadMore, type DataTableColumn } from "../../../components";
2
+ import { DataTableStackView, DataTableView, KeyValueRow, QueryBar, usePagedRows, usePaneNoticeFooter, useQueryBarSearch, useTableLoadMore, type DataTableColumn, type PageRequest } from "../../../components";
3
3
  import { useShortcut } from "../../../react/input";
4
4
  import { colors } from "../../../theme/colors";
5
5
  import { Box, type ScrollBoxRenderable } from "../../../ui";
@@ -28,47 +28,42 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
28
28
  const [target, setTarget] = usePluginPaneState<ThirteenFFund | null>("overlap:target", null);
29
29
  const [selectedId, setSelectedId] = usePluginPaneState<string | null>("overlap:selected", null);
30
30
  const [selectedPositionId, setSelectedPositionId] = usePluginPaneState<string | null>("overlap:position", null);
31
- const [funds, setFunds] = useState<ThirteenFFund[]>([]);
32
31
  const [peer, setPeer] = useState<FundDetailData | null>(null);
33
32
  const [loading, setLoading] = useState(false);
34
33
  const [error, setError] = useState<string | null>(null);
35
34
  const { active: searchFocused, focus: focusSearch, blur: blurSearch, searchProps } = useQueryBarSearch();
36
35
  const [fundSort, setFundSort] = useState<{ columnId: "name" | "cik"; direction: SortDirection }>({ columnId: "name", direction: "asc" });
37
- const [hasMore, setHasMore] = useState(false);
38
- const [offset, setOffset] = useState(0);
39
- const moreController = useRef<AbortController | null>(null);
40
36
  const scrollRef = useRef<ScrollBoxRenderable | null>(null);
41
- const [loadingMore, setLoadingMore] = useState(false);
42
37
  const [mineOnly, setMineOnly] = usePluginPaneState<boolean>("overlap:mine", false);
43
38
  const [sort, setSort] = usePluginPaneState<{ id: "weight" | "comparedWeight"; desc: boolean }>("overlap:sort", { id: "weight", desc: true });
44
39
  const mine = useMineTickers();
45
40
  const { pinTicker } = usePluginTickerActions();
46
41
  const [refresh, setRefresh] = useState(0);
47
42
  const lastRefresh = useRef(0);
43
+ const searchPage = useCallback(async ({ offset, signal }: PageRequest) => {
44
+ const result = await searchThirteenFFunds(query, 50, signal, { offset });
45
+ const rows = result.filter(fund => fund.cik !== data.cik);
46
+ return { rows, hasMore: result.length === 50, nextOffset: offset + result.length };
47
+ }, [query, data.cik]);
48
+ const paged = usePagedRows(target || !query || isCikQuery(query) ? null : searchPage, { getId: (fund) => fund.cik });
49
+ const cikFunds = useMemo(() => isCikQuery(query) && !target ? [{ cik: normalizeCik(query), name: query }] : [], [query, target]);
50
+ const funds = target ? [] : cikFunds.length ? cikFunds : paged.rows;
48
51
  useEffect(() => {
49
52
  const controller = new AbortController();
50
- setError(null); setPeer(null); setFunds([]); setHasMore(false); setOffset(0); setLoadingMore(false); moreController.current?.abort();
51
- if (!target && !query) { setLoading(false); return; }
53
+ setError(null); setPeer(null);
54
+ if (!target) { setLoading(false); return; }
52
55
  setLoading(true);
53
56
  const forceRefresh = refresh !== lastRefresh.current;
54
57
  lastRefresh.current = refresh;
55
- const work = target ? loadFundDetail(target.cik, target.name, controller.signal, { forceRefresh }).then(result => { if (!controller.signal.aborted) setPeer(result); })
56
- : isCikQuery(query) ? Promise.resolve().then(() => setFunds([{ cik: normalizeCik(query), name: query }]))
57
- : searchThirteenFFunds(query, 50, controller.signal).then(result => { if (!controller.signal.aborted) { setFunds(result.filter(fund => fund.cik !== data.cik)); setHasMore(result.length === 50); setOffset(result.length); } });
58
- void work.catch(cause => { if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause)); }).finally(() => { if (!controller.signal.aborted) setLoading(false); });
59
- return () => { controller.abort(); moreController.current?.abort(); };
60
- }, [query, target, refresh, data.cik]);
61
- const loadMore = useCallback(() => {
62
- if (!hasMore || loadingMore || loading || target) return;
63
- moreController.current?.abort(); const request = new AbortController(); moreController.current = request; setLoadingMore(true);
64
- void searchThirteenFFunds(query, 50, request.signal, { offset }).then(result => {
65
- if (request.signal.aborted) return;
66
- setFunds(current => [...new Map([...current, ...result.filter(fund => fund.cik !== data.cik)].map(fund => [fund.cik, fund])).values()]);
67
- setHasMore(result.length === 50); setOffset(offset + result.length);
68
- }).catch(cause => { if (!request.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause)); }).finally(() => { if (!request.signal.aborted) setLoadingMore(false); });
69
- }, [hasMore, loadingMore, loading, target, query, offset, data.cik]);
70
- const onScroll = useTableLoadMore(scrollRef, hasMore && !loadingMore && !loading && !target, loadMore);
71
- const refreshData = useCallback(() => setRefresh(value => value + 1), []);
58
+ void loadFundDetail(target.cik, target.name, controller.signal, { forceRefresh })
59
+ .then(result => { if (!controller.signal.aborted) setPeer(result); })
60
+ .catch(cause => { if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause)); })
61
+ .finally(() => { if (!controller.signal.aborted) setLoading(false); });
62
+ return () => { controller.abort(); };
63
+ }, [target, refresh]);
64
+ const loadingMore = paged.loadingMore;
65
+ const onScroll = useTableLoadMore(scrollRef, paged.hasMore && !loadingMore && !loading && !target, paged.loadMore);
66
+ const refreshData = useCallback(() => { setRefresh(value => value + 1); paged.reload(); }, [paged.reload]);
72
67
  useShortcut(event => {
73
68
  if (!focused || !target || event.targetEditable || !isDetailBackNavigationKey(event)) return;
74
69
  event.preventDefault?.(); event.stopPropagation?.(); setTarget(null);
@@ -80,7 +75,8 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
80
75
  });
81
76
  const period = peer ? overlapPeriod(data, peer) : null;
82
77
  const mismatch = peer && !period;
83
- usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore, error, hints: target ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
78
+ const listError = error ?? paged.error?.message ?? paged.moreError?.message ?? null;
79
+ usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading, error: listError, hints: target ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
84
80
  usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [...(peer?.warnings ?? []), ...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : [])] });
85
81
  const rows = useMemo(() => (peer ? buildFundOverlap(data, peer) : []).filter(row => !mineOnly || mine.has(row.ticker)).sort((a, b) => ((a[sort.id] ?? -1) - (b[sort.id] ?? -1)) * (sort.desc ? -1 : 1)), [peer, data, mineOnly, mine, sort]);
86
82
  const positionColumns: DataTableColumn[] = [{ id: "mine", label: "MINE", width: 5, align: "left" }, { id: "ticker", label: "TICKER", width: 10, align: "left" }, { id: "type", label: "TYPE", width: 6, align: "left" }, { id: "issuer", label: "ISSUER", width: Math.max(18, width - 58), align: "left" }, { id: "weight", label: "FIRST %", width: 12, align: "right" }, { id: "comparedWeight", label: "SECOND %", width: 12, align: "right" }];
@@ -106,6 +102,6 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
106
102
  onActivate={fund => { blurSearch(); setTarget(fund); }}
107
103
  rootBefore={<QueryBar width={width} search={{ value: query, onChange: setQuery, placeholder: "Second fund name or CIK", focused, ...searchProps, debounceMs: 250 }} />}
108
104
  renderCell={(row, column, _index, state) => ({ text: column.id === "name" ? row.name : row.cik, color: state.selected ? colors.selectedText : colors.text })}
109
- emptyStateTitle={loading ? "Searching funds..." : query ? "No matching funds." : "Search for a second fund."}
105
+ emptyStateTitle={loading || paged.loading ? "Searching funds..." : query ? "No matching funds." : "Search for a second fund."}
110
106
  />;
111
107
  }
@@ -115,7 +115,7 @@ export const tickerDetailModule: PluginModule = {
115
115
  label: "Ticker Research",
116
116
  description: "Open another research pane for the selected ticker or current collection",
117
117
  keywords: ["new", "ticker", "research", "detail", "pane", "inspector"],
118
- shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
118
+ shortcut: { prefix: "T", aliases: ["DES"], argPlaceholder: "ticker", argKind: "ticker" },
119
119
  canCreate: (context) => context.activeTicker !== null || context.activeCollectionId !== null,
120
120
  createInstance: (context) => (
121
121
  context.activeTicker
@@ -183,13 +183,14 @@ export function buildOverviewStats({
183
183
  valueColor: priceColor(fundamentals.revenueGrowth),
184
184
  });
185
185
  }
186
- // At the regular session price, as the market cap above it is.
187
- const enterpriseValue = reportedEnterpriseValue(atRegularSessionPrice({ quote, fundamentals }).fundamentals);
186
+ // At the regular session price, as the market cap above it is, with the check moved beside it.
187
+ const priced = atRegularSessionPrice({ quote, fundamentals }).fundamentals;
188
+ const enterpriseValue = reportedEnterpriseValue(priced);
188
189
  if (fundamentals?.unavailableFields?.includes("enterpriseValue")) {
189
190
  stats.push({ label: "EV", value: "—" });
190
191
  } else if (enterpriseValue != null) {
191
192
  // More than 5% from market cap + debt - cash: the provider's, with the computed figure where it fits.
192
- const check = enterpriseValueCheck(fundamentals);
193
+ const check = enterpriseValueCheck(priced);
193
194
  const computed = check?.status === "differs" && check.computed != null ? formatCompact(check.computed) : undefined;
194
195
  stats.push({
195
196
  label: "EV",
@@ -28,6 +28,7 @@ import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
28
28
  import { publicTickerKey, resolveExchangeTimeZone } from "../../../utils/exchanges";
29
29
  import { convertCurrency, displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
30
30
  import { enterpriseValueReconciliationNote } from "../../../utils/fundamentals";
31
+ import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
31
32
  import { zonedDateKey } from "../../../utils/zoned-date-time";
32
33
  import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
33
34
  import {
@@ -144,10 +145,9 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
144
145
  panelId: "price",
145
146
  timeBasis: chartTimeZone ? { kind: "market", timeZone: chartTimeZone } : undefined,
146
147
  });
147
- // A provider EV off market cap + debt - cash, or why a withdrawn one is unavailable.
148
- const enterpriseValueNote = enterpriseValueReconciliationNote(fundamentals)
149
- ?? (fundamentals?.unavailableFields?.includes("enterpriseValue")
150
- ? t("Enterprise value unavailable: the source observation failed validation.") : undefined);
148
+ // A provider EV off market cap + debt - cash, or why a withdrawn one is unavailable, at the
149
+ // regular session price the EV beside it is shown at.
150
+ const enterpriseValueNote = enterpriseValueReconciliationNote(atRegularSessionPrice({ quote, fundamentals }).fundamentals);
151
151
  usePaneNoticeFooter({
152
152
  registrationId: "overview-notices",
153
153
  notices: [
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { useMemo } from "react";
2
+ import { useMemo, useRef } from "react";
3
3
  import { Box, Text, TextAttributes, useUiCapabilities } from "../../../../ui";
4
4
  import type { PricePoint, Quote, TickerFinancials } from "../../../../types/financials";
5
5
  import type { TickerRecord } from "../../../../types/ticker";
@@ -22,7 +22,7 @@ import { t } from "../../../../i18n";
22
22
  import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
23
23
  import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
24
24
  import { useQuoteFlashDirection } from "../../../../components/quote-flash";
25
- import { appendLiveQuotePoint } from "../../../../time-series/chart-data";
25
+ import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
26
26
  import {
27
27
  PriceAreaSparklineBackground,
28
28
  PriceSparkline,
@@ -145,8 +145,13 @@ export function QuoteMonitorCard({
145
145
  : cachedFinancials?.priceHistory;
146
146
  const assetCategory = quote?.instrumentType ?? ticker?.metadata.assetCategory;
147
147
  // Completed bars end at the prior close; the live price closes the line and the stated range.
148
- const priceHistory = useMemo(() => barHistory && appendLiveQuotePoint(barHistory, quote, { assetCategory }),
149
- [assetCategory, barHistory, quote]);
148
+ const sparklineRef = useRef<PricePoint[] | undefined>(undefined);
149
+ const priceHistory = useMemo(() => {
150
+ if (!barHistory) return undefined;
151
+ const next = followLiveSparklinePrice(barHistory, quote, { assetCategory, period: chartPeriod, previous: sparklineRef.current });
152
+ sparklineRef.current = next;
153
+ return next;
154
+ }, [assetCategory, barHistory, chartPeriod, quote]);
150
155
  const flashFinancials = useMemo<TickerFinancials | null>(
151
156
  () => quote ? { quote, annualStatements: [], quarterlyStatements: [], priceHistory: [] } : null,
152
157
  [quote],
@@ -21,6 +21,8 @@ import { useAutoRefresh } from "../../../react/auto-refresh";
21
21
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
22
22
  import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
23
23
  import { useLiveSessionRefresh } from "../shared/volatility/live-session";
24
+ import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
25
+ import { hasFreshReadyEntry } from "../../../market-data/coordinator/entries";
24
26
  import { createSurfaceDependencies, loadVolatilitySurface, withReusedYieldCurve, type SurfaceLoadRequest } from "./client";
25
27
  import { stableSurfaceSheet, SURFACE_LIVE_RELOAD_MS, surfaceFreshnessLabel, type SurfaceSheetAxes } from "./live";
26
28
  import { loadStoredSurface, loadSurfaceDates } from "../iv-history/client";
@@ -120,8 +122,20 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
120
122
  // never collapses to the first few expiries while the rest reload.
121
123
  const [live, setLive] = useState<{ key: string; snapshot: SurfaceSnapshot; at: number } | null>(null);
122
124
  const liveController = useRef<AbortController | null>(null);
125
+ const settledRef = useRef<SurfaceSnapshot | null | undefined>(null);
123
126
  const liveDependencies = useMemo(() => withReusedYieldCurve(createSurfaceDependencies()), []);
124
127
  const reloadLive = useCallback(async () => {
128
+ const coordinator = getSharedMarketDataCoordinator();
129
+ const instrument = target?.instrument;
130
+ const chainInstrument = { symbol: target?.effectiveTicker ?? symbol!, exchange: target?.effectiveExchange ?? "",
131
+ brokerId: instrument?.brokerId, brokerInstanceId: instrument?.brokerInstanceId, instrument };
132
+ const shown = settledRef.current;
133
+ const expiries = shown?.expiries.map((entry) => entry.expiration) ?? [];
134
+ const freshChain = coordinator != null && expiries.length > 0 && expiries.every((expiration) => {
135
+ const entry = coordinator.getOptionsEntry({ instrument: chainInstrument, expirationDate: expiration });
136
+ return hasFreshReadyEntry(entry, SURFACE_LIVE_RELOAD_MS);
137
+ });
138
+ if (freshChain) return;
125
139
  liveController.current?.abort();
126
140
  const abort = new AbortController();
127
141
  liveController.current = abort;
@@ -129,10 +143,11 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
129
143
  // Each expiry is fitted as its chain arrives, never all together at the end.
130
144
  onSnapshot: () => {} }, liveDependencies);
131
145
  if (!abort.signal.aborted) setLive({ key: requestKey, snapshot, at: Date.now() });
132
- }, [surfaceRequest, liveDependencies]);
146
+ }, [surfaceRequest, liveDependencies, symbol, target]);
133
147
  useEffect(() => () => liveController.current?.abort(), [reloadLive]);
134
148
  const liveReload = live?.key === requestKey && (resource.updatedAt == null || live.at > resource.updatedAt) ? live.snapshot : null;
135
149
  const settledSnapshot = liveReload ?? resource.data;
150
+ settledRef.current = settledSnapshot;
136
151
  // Stored close surfaces and IV rank come from the Cloud IV history of the underlying.
137
152
  const underlying = (target?.effectiveTicker ?? symbol ?? "").toUpperCase();
138
153
  const datesLoader = useCallback(async () => (await loadSurfaceDates(underlying)).dates, [underlying]);
@@ -141,7 +141,7 @@ export function volatilityHistorySeries(fred: FredChartInput, colors: Volatility
141
141
  const value = values.get(date);
142
142
  return { date: new Date(date), observedAt: new Date(date), value: value != null && Number.isFinite(value) ? value : null };
143
143
  }), { id, label, color, calendarSpaced: true }),
144
- unit: unitGroup === "volatility" ? "%" : "", unitGroup, panelId,
144
+ unit: unitGroup === "volatility" ? "index" : "", unitGroup, panelId,
145
145
  });
146
146
  const first = dates[0], last = dates.at(-1);
147
147
  return [
@@ -26,7 +26,7 @@ export function VolatilityHistoryChart({ fred, width, height, focused = false }:
26
26
  return <CompositeChart series={series} legendSeries={legendSeries} panels={HISTORY_PANELS} width={width} height={height}
27
27
  focused={focused} navigable={false} showLegend showTimeAxis remoteKind="vix-history"
28
28
  formatValue={(value, entry) => entry.unitGroup === "ratio" ? value.toFixed(2) : formatCompositeSeriesValue(value, entry)}
29
- formatAxisValue={(value, domain) => domain.unitGroup === "ratio" ? value.toFixed(2) : `${value.toFixed(0)}%`}
29
+ formatAxisValue={(value, domain) => domain.unitGroup === "ratio" ? value.toFixed(2) : value.toFixed(0)}
30
30
  colors={{ background: palette.bgColor, grid: palette.gridColor, crosshair: palette.crosshairColor, text: colors.text,
31
31
  textDim: palette.axisColor, negative: colors.negative }} />;
32
32
  }
@@ -25,6 +25,18 @@ interface ViewBundleOptions {
25
25
  aliasRules?: AliasRule[];
26
26
  failureMessage: string;
27
27
  missingEntryMessage: string;
28
+ /**
29
+ * Leave identifiers unminified (whitespace and syntax still are), so the
30
+ * components keep their names and a page audit can say which one drew an
31
+ * element (`scripts/pane-chrome/ui-audit.ts`). Bun's `keepNames` alone did
32
+ * not keep them.
33
+ */
34
+ keepNames?: boolean;
35
+ }
36
+
37
+ /** Bun's minify setting: everything, or everything but identifiers when names are kept. */
38
+ function minifyOption(keepNames: boolean | undefined) {
39
+ return keepNames ? { whitespace: true, syntax: true, identifiers: false } : true;
28
40
  }
29
41
 
30
42
  // From this file rather than the working directory, so a build started from
@@ -60,6 +72,7 @@ export async function buildViewBundle({
60
72
  aliasRules = [],
61
73
  failureMessage,
62
74
  missingEntryMessage,
75
+ keepNames,
63
76
  }: ViewBundleOptions): Promise<{ script: string; stylesheet: string | null }> {
64
77
  const chunks = new Map<string, string>();
65
78
  const result = await Bun.build({
@@ -69,7 +82,7 @@ export async function buildViewBundle({
69
82
  format: "esm",
70
83
  splitting: false,
71
84
  sourcemap,
72
- minify: true,
85
+ minify: minifyOption(keepNames),
73
86
  define: {
74
87
  "process.env.NODE_ENV": "\"production\"",
75
88
  __GLOOM_WORLD_MAP_DATA__: JSON.stringify(WORLD_MAP_DATA_SPECIFIER),
@@ -88,7 +101,7 @@ export async function buildViewBundle({
88
101
 
89
102
  const script = result.outputs.find((output) => output.kind === "entry-point" && output.path.endsWith(".js"));
90
103
  if (!script) throw new Error(missingEntryMessage);
91
- await buildLazyChunks(outdir, chunks, failureMessage);
104
+ await buildLazyChunks(outdir, chunks, failureMessage, keepNames);
92
105
 
93
106
  const stylesheet = result.outputs.find((output) => output.path.endsWith(".css"))?.path ?? null;
94
107
  if (stylesheet) await writeFile(stylesheet, withTitlebarOverlayHeight(await readFile(stylesheet, "utf8")));
@@ -181,7 +194,7 @@ function lazyChunkPlugin(rootOutdir: string, emitDir: string, chunks: Map<string
181
194
  }
182
195
 
183
196
  /** Builds each lazy chunk on its own, and the chunks those load in turn. */
184
- async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>, failureMessage: string): Promise<void> {
197
+ async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>, failureMessage: string, keepNames?: boolean): Promise<void> {
185
198
  const built = new Set<string>();
186
199
  let pending = [...chunks.entries()];
187
200
  while (pending.length) {
@@ -197,7 +210,7 @@ async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>,
197
210
  format: "esm",
198
211
  splitting: false,
199
212
  sourcemap: "none",
200
- minify: true,
213
+ minify: minifyOption(keepNames),
201
214
  plugins: [lazyChunkPlugin(rootOutdir, dirname(output), nested)],
202
215
  });
203
216
  if (!result.success) {
@@ -41,6 +41,7 @@ export function WebButton({
41
41
  onPress,
42
42
  variant = "secondary",
43
43
  disabled = false,
44
+ pending = false,
44
45
  active = false,
45
46
  shortcut,
46
47
  width,
@@ -61,6 +62,7 @@ export function WebButton({
61
62
  aria-keyshortcuts={ariaKeyShortcut(shortcut)}
62
63
  title={title}
63
64
  aria-expanded={expanded}
65
+ aria-busy={pending || undefined}
64
66
  disabled={disabled}
65
67
  onMouseDown={(event) => {
66
68
  if (stopPropagation) event.stopPropagation();
@@ -81,6 +81,12 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
81
81
  [data-titlebar-overlay="true"] * {
82
82
  cursor: inherit;
83
83
  }
84
+ /* A row of text grows with the text size and the title bar does not: at the
85
+ largest sizes the top bar's buttons and prompt would stand out of it. */
86
+ [data-gloom-role="app-header"][data-titlebar-overlay="true"] button,
87
+ [data-gloom-role="app-header"][data-titlebar-overlay="true"] [role="button"] {
88
+ max-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
89
+ }
84
90
  [data-titlebar-overlay="true"] button,
85
91
  [data-titlebar-overlay="true"] input,
86
92
  [data-titlebar-overlay="true"] textarea,
@@ -15,14 +15,15 @@ export function setMacosDockBadge(label: string): void {
15
15
  const dockTile = objc.send(app, objc.selector("dockTile"));
16
16
  if (!dockTile) return;
17
17
  const text = label
18
- ? objc.sendObject(
18
+ ? objc.sendCString(
19
19
  objc.send(objc.getClass("NSString"), objc.selector("alloc")),
20
20
  objc.selector("initWithUTF8String:"),
21
21
  cString(label),
22
22
  )
23
- : null;
23
+ : 0n;
24
24
  // AppKit only takes this on the main thread, and Bun runs on another one.
25
- // The call retains the string until it has run; a null label is nil.
25
+ // The call retains the string until it has run; no label is nil. A label as
26
+ // short as a count is a tagged pointer, which is why objects travel as ids.
26
27
  objc.sendOnMainThread(
27
28
  dockTile,
28
29
  objc.selector("performSelectorOnMainThread:withObject:waitUntilDone:"),
@@ -1,11 +1,10 @@
1
- import type { Pointer } from "bun:ffi";
2
1
  import { appIconColors, appIconSvg } from "./app-icon";
3
2
  import { getTheme } from "../../../../theme/themes";
4
- import { objcOrNull, type Objc } from "./macos-objc";
3
+ import { objcOrNull, type Objc, type ObjcId } from "./macos-objc";
5
4
 
6
5
  let appliedThemeId: string | null = null;
7
6
 
8
- function createImage(objc: Objc, svg: string): Pointer | null {
7
+ function createImage(objc: Objc, svg: string): ObjcId | null {
9
8
  const bytes = Buffer.from(svg);
10
9
  const data = objc.sendBytes(
11
10
  objc.send(objc.getClass("NSData"), objc.selector("alloc")),
@@ -4,28 +4,39 @@ const LIBOBJC = "/usr/lib/libobjc.A.dylib";
4
4
 
5
5
  export type Objc = ReturnType<typeof loadObjc>;
6
6
 
7
+ /** An Objective-C object or class as its address, `0n` for nil. */
8
+ export type ObjcId = bigint;
9
+
7
10
  let cachedObjc: Objc | null | undefined;
8
11
 
9
12
  export const cString = (value: string) => Buffer.from(`${value}\0`);
10
13
 
14
+ // Objects cross the FFI as u64, never as ptr. Bun hands a ptr result to
15
+ // JavaScript as a double, exact only up to 2**53, and Foundation keeps short
16
+ // strings and small numbers (a Dock badge label such as "3") in tagged
17
+ // pointers whose data sits in the top bits. Through a ptr they come back
18
+ // rounded, and the next message to one crashes the process.
19
+ const ID = FFIType.u64;
20
+
11
21
  // objc_msgSend has to be called with each method's exact C signature, so it is
12
22
  // bound once per call shape.
13
23
  function loadObjc() {
14
24
  const runtime = dlopen(LIBOBJC, {
15
- objc_getClass: { args: [FFIType.ptr], returns: FFIType.ptr },
25
+ objc_getClass: { args: [FFIType.ptr], returns: ID },
16
26
  sel_registerName: { args: [FFIType.ptr], returns: FFIType.ptr },
17
27
  }).symbols;
18
28
  const msgSend = <const Args extends FFIType[], Returns extends FFIType>(args: Args, returns: Returns) =>
19
- dlopen(LIBOBJC, { objc_msgSend: { args: [FFIType.ptr, FFIType.ptr, ...args], returns } }).symbols.objc_msgSend;
29
+ dlopen(LIBOBJC, { objc_msgSend: { args: [ID, FFIType.ptr, ...args], returns } }).symbols.objc_msgSend;
20
30
 
21
31
  return {
22
32
  getClass: (name: string) => runtime.objc_getClass(cString(name)),
23
33
  selector: (name: string) => runtime.sel_registerName(cString(name)),
24
- send: msgSend([], FFIType.ptr),
34
+ send: msgSend([], ID),
25
35
  sendVoid: msgSend([], FFIType.void),
26
- sendObject: msgSend([FFIType.ptr], FFIType.ptr),
27
- sendBytes: msgSend([FFIType.ptr, FFIType.u64], FFIType.ptr),
28
- sendOnMainThread: msgSend([FFIType.ptr, FFIType.ptr, FFIType.bool], FFIType.void),
36
+ sendObject: msgSend([ID], ID),
37
+ sendCString: msgSend([FFIType.ptr], ID),
38
+ sendBytes: msgSend([FFIType.ptr, FFIType.u64], ID),
39
+ sendOnMainThread: msgSend([FFIType.ptr, ID, FFIType.bool], FFIType.void),
29
40
  };
30
41
  }
31
42
 
@@ -1,4 +1,4 @@
1
- import { futuresGenericListing, futuresGenericPriceBasis } from "../../utils/futures-generic";
1
+ import { futuresListingPriceBasis } from "../../utils/futures-generic";
2
2
  import type { TimeRange } from "../../time-series/range";
3
3
  import { RANGE_HISTORY_RESOLUTION } from "../../time-series/resolution";
4
4
  import type {
@@ -55,6 +55,12 @@ function isImpossiblePreviousClose(
55
55
  && previousClose * IMPOSSIBLE_CLOSE_FACTOR < low52w;
56
56
  }
57
57
 
58
+ /** A quote that says it is something other than a future (an equity, a fund) keeps its own price basis. */
59
+ function isNonFutureType(instrumentType: string | undefined): boolean {
60
+ const type = instrumentType?.trim().toUpperCase();
61
+ return !!type && type !== "FUTURE" && type !== "FUTURES" && type !== "FUT";
62
+ }
63
+
58
64
  export function mapQuote(
59
65
  quote: CloudQuotePayload,
60
66
  providerMeta?: CloudProviderMeta,
@@ -76,12 +82,14 @@ export function mapQuote(
76
82
  const changePercent = !impossibleClose && typeof quote.changePercent === "number" && Number.isFinite(quote.changePercent)
77
83
  ? quote.changePercent
78
84
  : Number.NaN;
79
- // A generic future (VX1, TY1) reads in points or 32nds, not dollars.
80
- const generic = futuresGenericListing(quote.symbol, quote.listingExchangeName ?? quote.exchangeName);
85
+ // A generic future (VX1, TY1) reads in points or 32nds, and an index future's
86
+ // continuous symbol or contract (ES=F, SXOZ26.CME) in points, not dollars.
87
+ const futuresBasis = isNonFutureType(quote.instrumentType) ? null
88
+ : futuresListingPriceBasis(quote.symbol, quote.listingExchangeName ?? quote.exchangeName);
81
89
  const priceObservation = quote.priceBasis === "nav" ? "nav" : undefined;
82
90
  const priceBasis = quote.priceBasis === "nav"
83
91
  ? "per-unit"
84
- : (generic ? futuresGenericPriceBasis(generic) : null) ?? quote.priceBasis;
92
+ : futuresBasis ?? quote.priceBasis;
85
93
  return withoutUndefinedFields(reconcileQuoteDayRange({
86
94
  ...quote,
87
95
  priceBasis,
@@ -222,6 +230,7 @@ export function mapPricePoint(
222
230
  volume: point.volume,
223
231
  ...(point.partial === true ? { partial: true } : {}),
224
232
  ...(point.filled === true ? { filled: true } : {}),
233
+ ...(point.unsettled === true ? { unsettled: true } : {}),
225
234
  };
226
235
  }
227
236
 
@@ -73,7 +73,7 @@ export class ProviderRouterBatchRoutes {
73
73
  : isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol, { recentAnswer: !rawCached.stale }))
74
74
  ? rawCached
75
75
  : null;
76
- if (cached && !forceRefresh && !cached.stale) {
76
+ if (cached && !forceRefresh && !cached.stale && !options.revalidate) {
77
77
  if (brokerSourceKeys.includes(cached.sourceKey)) {
78
78
  misses.push({ index, target });
79
79
  return;
@@ -81,6 +81,7 @@ export class ProviderRouterBatchRoutes {
81
81
  results[index] = { target, quote: cached.value };
82
82
  return;
83
83
  }
84
+ // Revalidated, a fresh copy is still what the quote route answers when no source does.
84
85
  misses.push({ index, target });
85
86
  });
86
87
 
@@ -9,6 +9,8 @@ import { brokerContractIdentityKey } from "../../utils/instrument-identity";
9
9
  import { providerFinancialsMatchTarget, providerQuoteMatchesTarget } from "./financials";
10
10
 
11
11
  const MARKET_NAMESPACE = "market";
12
+ /** Quotes and price histories are read on every tick; their eviction order is refreshed at most this often. */
13
+ const HOT_READ_TOUCH_MS = 10 * 60_000;
12
14
  // A record written under another version is a miss: bump these instead of
13
15
  // repairing older records on read.
14
16
  export const FINANCIALS_SCHEMA_VERSION = 13;
@@ -174,6 +176,7 @@ export function listCachedResources<T>(
174
176
  sourceKeys,
175
177
  allowExpired,
176
178
  ...(SCHEMA_VERSIONS[kind] ? { schemaVersion: SCHEMA_VERSIONS[kind] } : {}),
179
+ ...(kind === "quote" || kind === "price-history" || kind === "price-history-recheck" ? { touchIntervalMs: HOT_READ_TOUCH_MS } : {}),
177
180
  }).filter((record) => {
178
181
  if (!record.sourceKey.startsWith("provider:") || entityKey.startsWith("contract:")
179
182
  || (kind !== "financials" && kind !== "quote")) return true;
@@ -3,6 +3,7 @@ import type { SyncBaselineStore } from "./types";
3
3
 
4
4
  const BASELINE_PLUGIN_ID = "core.sync";
5
5
  const BASELINE_KEY = "last-synced-payloads";
6
+ const MEMOS_KEY = "contributor-memos";
6
7
 
7
8
  /**
8
9
  * Persists what this device last pushed so the next pull can tell "the cloud is
@@ -13,20 +14,24 @@ const BASELINE_KEY = "last-synced-payloads";
13
14
  * local state, never to discarding it.
14
15
  */
15
16
  export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): SyncBaselineStore {
17
+ const load = (key: string) => {
18
+ try {
19
+ return pluginState.get<Record<string, unknown>>(BASELINE_PLUGIN_ID, key)?.value ?? null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+ const save = (key: string, value: Record<string, unknown>) => {
25
+ try {
26
+ pluginState.set(BASELINE_PLUGIN_ID, key, value);
27
+ } catch {
28
+ // Ignored: a stale baseline or memo only makes the next pull more conservative.
29
+ }
30
+ };
16
31
  return {
17
- load: () => {
18
- try {
19
- return pluginState.get<Record<string, unknown>>(BASELINE_PLUGIN_ID, BASELINE_KEY)?.value ?? null;
20
- } catch {
21
- return null;
22
- }
23
- },
24
- save: (payloads) => {
25
- try {
26
- pluginState.set(BASELINE_PLUGIN_ID, BASELINE_KEY, payloads);
27
- } catch {
28
- // Ignored: a stale baseline only makes the next pull more conservative.
29
- }
30
- },
32
+ load: () => load(BASELINE_KEY),
33
+ save: (payloads) => save(BASELINE_KEY, payloads),
34
+ loadMemos: () => load(MEMOS_KEY),
35
+ saveMemos: (memos) => save(MEMOS_KEY, memos),
31
36
  };
32
37
  }