gloomberb 0.16.3 → 0.16.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.
- package/package.json +4 -1
- package/src/api-client/askg.ts +43 -0
- package/src/api-client/auth.ts +15 -0
- package/src/api-client/beneficial-owners.ts +4 -0
- package/src/api-client/chat.ts +19 -0
- package/src/api-client/company-kpis.ts +14 -2
- package/src/api-client/data.ts +34 -16
- package/src/api-client/index.ts +12 -0
- package/src/api-client/normalizers.ts +27 -0
- package/src/api-client/paths.ts +88 -23
- package/src/api-client/request.ts +2 -1
- package/src/api-client/research-activity.ts +31 -0
- package/src/api-client/socket.ts +39 -8
- package/src/api-client/types.ts +57 -1
- package/src/api-client/upgrade-experiment.ts +90 -0
- package/src/app/global-shortcuts.ts +11 -1
- package/src/app/keybindings/actions.ts +10 -0
- package/src/app/pane-runtime/index.tsx +8 -0
- package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +65 -25
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
- package/src/app/runtime/desktop-deeplink.ts +5 -0
- package/src/app/star-prompt/model.ts +98 -0
- package/src/app/star-prompt/runtime.ts +41 -0
- package/src/app/star-prompt/status-line.tsx +190 -0
- package/src/app.tsx +13 -7
- package/src/architecture/core-builtin-allowlist.json +70 -0
- package/src/brokers/signed-in/adapter.ts +33 -2
- package/src/brokers/signed-in/client.ts +10 -0
- package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
- package/src/brokers/signed-in/sign-in.ts +62 -5
- package/src/capabilities/registry.ts +8 -1
- package/src/capabilities/types.ts +2 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +359 -99
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +164 -26
- package/src/cli/commands/search.ts +29 -8
- package/src/cli/commands/system.ts +99 -19
- package/src/cli/commands/themes.ts +64 -0
- package/src/cli/commands/ticker.ts +62 -30
- package/src/cli/desktop-pane-shot.ts +97 -16
- package/src/cli/errors.ts +25 -2
- package/src/cli/freshness.ts +7 -4
- package/src/cli/fx-pair.ts +57 -0
- package/src/cli/help.ts +14 -1
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +9 -6
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +50 -6
- package/src/cli/pane-functions/capabilities.ts +8 -2
- package/src/cli/pane-functions/catalog.ts +13 -2
- package/src/cli/pane-functions/data.ts +36 -5
- package/src/cli/pane-functions/dom.ts +21 -13
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +67 -22
- package/src/cli/pane-functions/headless.ts +5 -4
- package/src/cli/pane-functions/index.ts +3 -0
- package/src/cli/pane-functions/market-session.ts +133 -0
- package/src/cli/pane-functions/options.ts +15 -4
- package/src/cli/pane-functions/report-notices.ts +63 -0
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +4 -6
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +105 -23
- package/src/cli/registry.ts +2 -1
- package/src/cli/result.ts +50 -13
- package/src/cli/row-window.ts +76 -0
- package/src/cli/scroll-truncation.ts +34 -0
- package/src/components/chart/composite/composite-chart.tsx +43 -4
- package/src/components/chart/composite/legend.tsx +39 -6
- package/src/components/chart/composite/rasterizer.ts +64 -4
- package/src/components/chart/composite/series-cues.ts +71 -0
- package/src/components/chart/composite/text-renderer.ts +16 -8
- package/src/components/chart/composite/types.ts +3 -0
- package/src/components/command-bar/commands/direct/index.ts +9 -1
- package/src/components/command-bar/commands/plugin/items.ts +3 -1
- package/src/components/command-bar/commands/registry.ts +7 -0
- package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/routes/root/command-items.ts +2 -0
- package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
- package/src/components/command-bar/routes/root/results.ts +40 -10
- package/src/components/command-bar/routes/root/runtime.ts +8 -0
- package/src/components/command-bar/routes/root/search-providers.ts +97 -14
- package/src/components/command-bar/surface/index.tsx +18 -4
- package/src/components/command-bar/theme-picker.tsx +48 -13
- package/src/components/command-bar/workflow/ops.ts +5 -3
- package/src/components/data-table/view.tsx +28 -9
- package/src/components/layout/floating-pane.tsx +6 -2
- package/src/components/layout/market-summary.ts +23 -9
- package/src/components/layout/pane/content.tsx +46 -29
- package/src/components/layout/pane/cycle-order.ts +2 -11
- package/src/components/layout/pane/header.tsx +2 -1
- package/src/components/layout/pane/title.ts +1 -1
- package/src/components/layout/presentation.ts +22 -0
- package/src/components/layout/shell/active-drag.ts +1 -1
- package/src/components/layout/shell/drag/index.ts +4 -0
- package/src/components/layout/shell/drag/live.ts +76 -5
- package/src/components/layout/shell/drag/runtime.ts +8 -8
- package/src/components/layout/shell/index.tsx +47 -6
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +51 -10
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/pane/new-layout.ts +116 -0
- package/src/components/layout/shell/shortcuts.ts +5 -2
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-text.tsx +9 -5
- package/src/components/metric-treemap/heat-surface.tsx +27 -8
- package/src/components/metric-treemap/heat-tiles.ts +48 -17
- package/src/components/onboarding/onboarding-frame.tsx +33 -10
- package/src/components/onboarding/onboarding-wizard.tsx +49 -40
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
- package/src/components/onboarding/pro-step-layout.ts +86 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
- package/src/components/ui/button.tsx +10 -0
- package/src/components/ui/data-table/index.tsx +38 -0
- package/src/components/ui/data-table/types.ts +8 -1
- package/src/components/ui/data-table/visible-range.ts +2 -0
- package/src/components/ui/icon.tsx +11 -2
- package/src/components/ui/index.ts +1 -1
- package/src/core/connection-health.ts +1 -1
- package/src/core/state/app/layout-reducer.ts +89 -3
- package/src/core/state/app/state.ts +8 -5
- package/src/core/state/app/types.ts +13 -2
- package/src/data/config/layout.ts +55 -21
- package/src/data/config/store/fold-panes.ts +149 -0
- package/src/data/config/store/migrations.ts +7 -0
- package/src/data/config/store/node.ts +27 -6
- package/src/data/config/store/normalize.ts +37 -3
- package/src/i18n/es.ts +14 -1
- package/src/i18n/ja.ts +14 -1
- package/src/i18n/ko.ts +14 -1
- package/src/i18n/zh-cn.ts +14 -1
- package/src/i18n/zh-tw.ts +14 -1
- package/src/layout/desks.ts +1 -2
- package/src/layout/pane-layout-move.ts +264 -0
- package/src/layout/pane-manager/hidden-panes.ts +160 -0
- package/src/layout/pane-manager/index.ts +1 -0
- package/src/market-data/coordinator/entries.ts +7 -1
- package/src/market-data/coordinator/quotes.ts +2 -2
- package/src/market-data/market/freshness.ts +118 -18
- package/src/market-data/market/status.ts +40 -0
- package/src/market-data/options-alternatives.ts +69 -0
- package/src/market-data/quotes/contributions.ts +19 -2
- package/src/market-data/quotes/metadata.ts +16 -0
- package/src/market-data/quotes/screener-live-quotes.ts +56 -5
- package/src/plugins/builtin/account-management/keyboard.ts +3 -0
- package/src/plugins/builtin/account-management/model.ts +1 -0
- package/src/plugins/builtin/account-management/navigation.ts +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +31 -4
- package/src/plugins/builtin/alerts/quotes.ts +3 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
- package/src/plugins/builtin/analytics/risk-model.ts +37 -8
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/broker-manager/model.ts +8 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
- package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
- package/src/plugins/builtin/broker-manager/table.ts +5 -0
- package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +34 -36
- package/src/plugins/builtin/chat/command-bar.ts +138 -0
- package/src/plugins/builtin/chat/composer-commands.ts +21 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
- package/src/plugins/builtin/chat/content/footer.tsx +5 -3
- package/src/plugins/builtin/chat/content/index.tsx +21 -1
- package/src/plugins/builtin/chat/content/mentions.ts +2 -1
- package/src/plugins/builtin/chat/content/user-map.ts +3 -1
- package/src/plugins/builtin/chat/controller/index.ts +48 -0
- package/src/plugins/builtin/chat/conversations.ts +256 -0
- package/src/plugins/builtin/chat/discord-command.ts +91 -0
- package/src/plugins/builtin/chat/edit-window.ts +12 -6
- package/src/plugins/builtin/chat/ghost-user.ts +14 -0
- package/src/plugins/builtin/chat/index.tsx +40 -2
- package/src/plugins/builtin/chat/layout.ts +2 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/message/header.tsx +24 -15
- package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/pane-requests.ts +60 -0
- package/src/plugins/builtin/chat/pane-state.ts +32 -2
- package/src/plugins/builtin/chat/pane.tsx +8 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
- package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
- package/src/plugins/builtin/cloud/askg/model.ts +129 -70
- package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
- package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
- package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
- package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
- package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
- package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
- package/src/plugins/builtin/cloud/plugin.tsx +36 -1
- package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
- package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
- package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
- package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
- package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
- package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
- package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
- package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
- package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
- package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
- package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
- package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
- package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
- package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
- package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
- package/src/plugins/builtin/comment-letters/client.ts +1 -3
- package/src/plugins/builtin/company-kpis/client.ts +23 -8
- package/src/plugins/builtin/company-kpis/headless.ts +3 -1
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
- package/src/plugins/builtin/composite-plugins.ts +107 -64
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
- package/src/plugins/builtin/credit-boards/index.tsx +21 -21
- package/src/plugins/builtin/credit-boards/model.ts +0 -1
- package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
- package/src/plugins/builtin/debt-maturities/client.ts +37 -9
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/data.ts +28 -7
- package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
- package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +42 -17
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/index.tsx +68 -22
- package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +17 -0
- package/src/plugins/builtin/executives/data.ts +17 -6
- package/src/plugins/builtin/executives/pane.tsx +16 -7
- package/src/plugins/builtin/filing-events/pane.tsx +22 -5
- package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
- package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
- package/src/plugins/builtin/gpu/headless.ts +9 -3
- package/src/plugins/builtin/gpu/model.ts +14 -5
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/headless.ts +6 -1
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/insider/index.tsx +4 -4
- package/src/plugins/builtin/iv-history/charts.tsx +21 -7
- package/src/plugins/builtin/iv-history/client.ts +12 -2
- package/src/plugins/builtin/iv-history/format.ts +33 -2
- package/src/plugins/builtin/iv-history/headless.ts +22 -10
- package/src/plugins/builtin/iv-history/index.tsx +3 -3
- package/src/plugins/builtin/iv-history/model.ts +61 -13
- package/src/plugins/builtin/iv-history/pane.tsx +11 -3
- package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
- package/src/plugins/builtin/jobs/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
- package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
- package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
- package/src/plugins/builtin/layout-manager/index.tsx +9 -1
- package/src/plugins/builtin/macro-day/client.ts +24 -0
- package/src/plugins/builtin/macro-day/headless.ts +11 -8
- package/src/plugins/builtin/macro-day/index.tsx +3 -0
- package/src/plugins/builtin/macro-day/model.ts +11 -6
- package/src/plugins/builtin/macro-day/pane.tsx +12 -8
- package/src/plugins/builtin/macro-day/release-table.ts +40 -0
- package/src/plugins/builtin/macro-day/releases.ts +62 -44
- package/src/plugins/builtin/market-heatmap/data.ts +25 -5
- package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
- package/src/plugins/builtin/market-heatmap/model.ts +94 -3
- package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
- package/src/plugins/builtin/market-movers/headless.ts +31 -4
- package/src/plugins/builtin/market-movers/index.tsx +8 -1
- package/src/plugins/builtin/market-movers/model.ts +71 -17
- package/src/plugins/builtin/market-movers/table.ts +50 -23
- package/src/plugins/builtin/news/headless.ts +9 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
- package/src/plugins/builtin/openfda/client.ts +1 -1
- package/src/plugins/builtin/options/carry.ts +95 -0
- package/src/plugins/builtin/options/index.tsx +58 -5
- package/src/plugins/builtin/options/strike-window.ts +183 -0
- package/src/plugins/builtin/options/table.ts +55 -3
- package/src/plugins/builtin/options/types.ts +8 -1
- package/src/plugins/builtin/options/view.tsx +161 -12
- package/src/plugins/builtin/options-positioning/headless.ts +4 -2
- package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
- package/src/plugins/builtin/options-scenario/client.ts +73 -17
- package/src/plugins/builtin/options-scenario/headless.ts +71 -24
- package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
- package/src/plugins/builtin/options-scenario/index.tsx +9 -2
- package/src/plugins/builtin/options-scenario/model.ts +33 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
- package/src/plugins/builtin/pe-band/model.ts +2 -2
- package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
- package/src/plugins/builtin/perps/equity-row.tsx +8 -5
- package/src/plugins/builtin/perps/model.ts +16 -5
- package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
- package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
- package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
- package/src/plugins/builtin/plugin-module.ts +87 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
- package/src/plugins/builtin/realized-vol/headless.ts +3 -0
- package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
- package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
- package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
- package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
- package/src/plugins/builtin/risk-factors/data.ts +23 -9
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
- package/src/plugins/builtin/seasonality/pane.tsx +1 -1
- package/src/plugins/builtin/sec/index.tsx +5 -5
- package/src/plugins/builtin/sectors/client.ts +10 -2
- package/src/plugins/builtin/sectors/headless.ts +35 -6
- package/src/plugins/builtin/sectors/index.tsx +18 -1
- package/src/plugins/builtin/sectors/sector-model.ts +150 -41
- package/src/plugins/builtin/shared/command-tab.ts +51 -0
- package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
- package/src/plugins/builtin/shared/members-menu.tsx +4 -4
- package/src/plugins/builtin/shared/report-freshness.ts +50 -2
- package/src/plugins/builtin/shared/ticker-request.ts +25 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
- package/src/plugins/builtin/shared/volatility/options.ts +1 -46
- package/src/plugins/builtin/short-interest/index.tsx +19 -4
- package/src/plugins/builtin/short-interest/surface.tsx +4 -4
- package/src/plugins/builtin/short-volume/index.tsx +1 -10
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
- package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
- package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
- package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
- package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
- package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
- package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
- package/src/plugins/builtin/vol-surface/client.ts +6 -2
- package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
- package/src/plugins/builtin/vol-surface/headless.ts +124 -20
- package/src/plugins/builtin/vol-surface/model.ts +23 -11
- package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
- package/src/plugins/builtin/vol-surface/tables.ts +124 -0
- package/src/plugins/builtin/world-indices/client.ts +22 -4
- package/src/plugins/builtin/world-indices/headless.ts +54 -27
- package/src/plugins/builtin/world-indices/model.ts +4 -2
- package/src/plugins/builtin/world-indices/table.tsx +13 -2
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/builtin-editorial.ts +126 -0
- package/src/plugins/catalog-backend.ts +16 -9
- package/src/plugins/catalog-browser.ts +127 -63
- package/src/plugins/catalog-manifest.ts +73 -40
- package/src/plugins/catalog-ui.ts +26 -6
- package/src/plugins/extracted-plugins.ts +7 -1
- package/src/plugins/ownership.ts +150 -7
- package/src/plugins/plugin-functions.ts +54 -0
- package/src/plugins/plugin-off.ts +36 -0
- package/src/plugins/registry/host-actions.ts +16 -0
- package/src/plugins/registry/index.ts +84 -24
- package/src/plugins/registry/plugin-state.ts +25 -1
- package/src/plugins/runtime/context.tsx +29 -0
- package/src/react/input.ts +48 -1
- package/src/remote/app-host.tsx +5 -0
- package/src/remote/controller.ts +3 -53
- package/src/remote/matching.ts +69 -0
- package/src/remote/schema.ts +1 -1
- package/src/remote/semantic-tree.tsx +14 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/query-bar.tsx +59 -32
- package/src/renderers/dom/host/box.tsx +1 -1
- package/src/renderers/dom/host/drag-shield.ts +17 -6
- package/src/renderers/dom/host/live-frame.ts +10 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +13 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +11 -5
- package/src/sources/provider-router/financials.ts +42 -15
- package/src/sources/provider-router/history.ts +27 -11
- package/src/sources/provider-router/index.ts +1 -1
- package/src/sources/provider-router/primary.ts +23 -5
- package/src/sources/sec-edgar.ts +68 -8
- package/src/sync/core-contributors.ts +4 -3
- package/src/test-support/cli-context.ts +2 -1
- package/src/test-support/data-provider.ts +34 -0
- package/src/test-support/shell-pane-keys.tsx +1 -0
- package/src/theme/series-colors.ts +46 -0
- package/src/theme/themes.ts +149 -0
- package/src/tickers/search/index.ts +22 -2
- package/src/tickers/search/ranking.ts +45 -1
- package/src/tickers/search/types.ts +3 -1
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/headless.ts +11 -0
- package/src/types/instrument.ts +8 -0
- package/src/types/plugin.ts +49 -3
- package/src/ui/host.tsx +8 -4
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history.ts +39 -6
- package/src/utils/rpc-timeout-error.ts +6 -2
- package/src/utils/sec-eps-basis.ts +159 -30
- package/src/utils/sec.ts +38 -34
- package/src/utils/throttled-fetch.ts +51 -22
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
} from "./notifications";
|
|
58
58
|
import {
|
|
59
59
|
CHAT_MESSAGE_EDIT_WINDOW_LABEL,
|
|
60
|
+
DISCORD_MESSAGE_EDIT_NOTICE,
|
|
61
|
+
isEditableOnGloom,
|
|
60
62
|
isWithinChatMessageEditWindow,
|
|
61
63
|
} from "../edit-window";
|
|
62
64
|
import { ChatControllerRealtime } from "./realtime";
|
|
@@ -65,6 +67,7 @@ import { ChatControllerView } from "./view";
|
|
|
65
67
|
import { ChatControllerMessageLoading } from "./message-loading";
|
|
66
68
|
import { ChatControllerStorage } from "./storage";
|
|
67
69
|
import { listUnreadInboxItems } from "../unread-inbox";
|
|
70
|
+
import type { ChatConversationList } from "../conversations";
|
|
68
71
|
import {
|
|
69
72
|
applySignedOutChatControllerSession,
|
|
70
73
|
createChatControllerSessionState,
|
|
@@ -75,6 +78,26 @@ import {
|
|
|
75
78
|
const chatLog = debugLog.createLogger("chat-controller");
|
|
76
79
|
const IMAGE_LINK_REFRESH_INTERVAL_MS = 60_000;
|
|
77
80
|
|
|
81
|
+
function timeOf(value: string | undefined): number {
|
|
82
|
+
const time = value ? Date.parse(value) : Number.NaN;
|
|
83
|
+
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The newest message held for a conversation, sent or received. A DM or group
|
|
88
|
+
* with nothing on this device yet counts from when it was started.
|
|
89
|
+
*/
|
|
90
|
+
function lastConversationActivity(channel: ChatChannel, state: ChannelRuntimeState | undefined): number {
|
|
91
|
+
const started = channel.kind === "direct" || channel.kind === "group"
|
|
92
|
+
? timeOf(channel.created_at)
|
|
93
|
+
: Number.NEGATIVE_INFINITY;
|
|
94
|
+
return Math.max(
|
|
95
|
+
started,
|
|
96
|
+
timeOf(state?.messages.at(-1)?.createdAt),
|
|
97
|
+
timeOf(state?.pendingMessages.at(-1)?.createdAt),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
export type { ChatControllerSnapshot } from "./state";
|
|
79
102
|
|
|
80
103
|
export class ChatController {
|
|
@@ -186,6 +209,27 @@ export class ChatController {
|
|
|
186
209
|
});
|
|
187
210
|
}
|
|
188
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Every conversation the Chat sidebar lists, with its unread count and last
|
|
214
|
+
* activity, from what this device already holds; no request. Null until a
|
|
215
|
+
* verified account is signed in.
|
|
216
|
+
*/
|
|
217
|
+
listConversations(): ChatConversationList | null {
|
|
218
|
+
const user = this.session.user;
|
|
219
|
+
if (!user?.emailVerified) return null;
|
|
220
|
+
return {
|
|
221
|
+
userId: user.id,
|
|
222
|
+
states: this.channelCatalog.getChannels().map((channel) => {
|
|
223
|
+
const state = this.storage.channelStates.get(channel.id);
|
|
224
|
+
return {
|
|
225
|
+
channel,
|
|
226
|
+
unreadCount: Math.max(0, state?.unreadCount ?? 0),
|
|
227
|
+
lastActivityAt: lastConversationActivity(channel, state),
|
|
228
|
+
};
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
189
233
|
getChannels(): ChatChannel[] {
|
|
190
234
|
return this.channelCatalog.getChannels();
|
|
191
235
|
}
|
|
@@ -558,6 +602,10 @@ export class ChatController {
|
|
|
558
602
|
this.notifyFn({ body: "Only your latest sent message can be edited.", type: "error" });
|
|
559
603
|
return false;
|
|
560
604
|
}
|
|
605
|
+
if (!isEditableOnGloom(latestOwnMessage)) {
|
|
606
|
+
this.notifyFn({ body: DISCORD_MESSAGE_EDIT_NOTICE, type: "error" });
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
561
609
|
if (!isWithinChatMessageEditWindow(latestOwnMessage)) {
|
|
562
610
|
this.notifyFn({ body: `Messages can only be edited within ${CHAT_MESSAGE_EDIT_WINDOW_LABEL}.`, type: "error" });
|
|
563
611
|
return false;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { ChatChannel, ChatUserSummary } from "../../../api-client";
|
|
2
|
+
import { teamStore } from "../cloud/team/store";
|
|
3
|
+
import { formatChatPaneTitle } from "./channel-labels";
|
|
4
|
+
|
|
5
|
+
/** What the chat controller holds about one conversation, read without a request. */
|
|
6
|
+
export interface ChatConversationState {
|
|
7
|
+
channel: ChatChannel;
|
|
8
|
+
unreadCount: number;
|
|
9
|
+
/** Newest message on this device, or when a DM started; -Infinity when unknown. */
|
|
10
|
+
lastActivityAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The signed-in account and its conversations, as the chat controller holds them. */
|
|
14
|
+
export interface ChatConversationList {
|
|
15
|
+
userId: string;
|
|
16
|
+
states: ChatConversationState[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ChatConversationKind = "channel" | "team" | "direct" | "group";
|
|
20
|
+
|
|
21
|
+
export interface ChatConversation {
|
|
22
|
+
channelId: string;
|
|
23
|
+
kind: ChatConversationKind;
|
|
24
|
+
/** As the Chat pane titles it: #general, MD·#general, @alice, a group's name. */
|
|
25
|
+
title: string;
|
|
26
|
+
/** Muted beside the title: a DM's full name, a group's members, a channel's team. */
|
|
27
|
+
name: string | null;
|
|
28
|
+
/** The other person's username in a DM, lower case. */
|
|
29
|
+
username: string | null;
|
|
30
|
+
unreadCount: number;
|
|
31
|
+
lastActivityAt: number;
|
|
32
|
+
/** Position in the Chat sidebar: channels, then team channels, then DMs. */
|
|
33
|
+
order: number;
|
|
34
|
+
/** What the conversation is called, lower case: channel, username, full name, group name. */
|
|
35
|
+
names: string[];
|
|
36
|
+
/** Every word a typed word may start, lower case. */
|
|
37
|
+
words: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The empty bar is a browse list of everything; chat gets a few rows of it. */
|
|
41
|
+
const EMPTY_QUERY_CONVERSATION_LIMIT = 4;
|
|
42
|
+
/** Typing "messages" or "chat" asks for the list itself, so it gets more. */
|
|
43
|
+
const LISTING_CONVERSATION_LIMIT = 15;
|
|
44
|
+
const SEARCH_CONVERSATION_LIMIT = 8;
|
|
45
|
+
/** Below this many characters a match says little, so unread conversations lead. */
|
|
46
|
+
const SHORT_QUERY_LENGTH = 3;
|
|
47
|
+
|
|
48
|
+
/** Words that, typed alone, ask for the conversations rather than one of them. */
|
|
49
|
+
const LISTING_WORDS = new Set([
|
|
50
|
+
"chat",
|
|
51
|
+
"chats",
|
|
52
|
+
"message",
|
|
53
|
+
"messages",
|
|
54
|
+
"conversation",
|
|
55
|
+
"conversations",
|
|
56
|
+
"dm",
|
|
57
|
+
"dms",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const WORD = /[\p{L}\p{N}]+/gu;
|
|
61
|
+
|
|
62
|
+
function wordsOf(value: string | null | undefined): string[] {
|
|
63
|
+
return value?.toLowerCase().match(WORD) ?? [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** "Alice Smith" and "alice smith" are the same name. */
|
|
67
|
+
function nameKey(value: string | null | undefined): string | null {
|
|
68
|
+
const words = wordsOf(value);
|
|
69
|
+
return words.length > 0 ? words.join(" ") : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function conversationKind(channel: ChatChannel): ChatConversationKind {
|
|
73
|
+
if (channel.kind === "direct" || channel.kind === "group" || channel.kind === "team") return channel.kind;
|
|
74
|
+
return "channel";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const KIND_ORDER: Record<ChatConversationKind, number> = { channel: 0, team: 1, direct: 2, group: 2 };
|
|
78
|
+
|
|
79
|
+
function memberLabel(member: ChatUserSummary): string | null {
|
|
80
|
+
return member.username ? `@${member.username}` : member.displayName?.trim() || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function groupMembers(channel: ChatChannel, userId: string | null): string | null {
|
|
84
|
+
const members = (channel.members ?? []).filter((member) => member.id !== userId);
|
|
85
|
+
const labels = members.map(memberLabel).filter((label): label is string => !!label);
|
|
86
|
+
if (labels.length === 0) return null;
|
|
87
|
+
const shown = labels.slice(0, 3);
|
|
88
|
+
return labels.length > shown.length ? `${shown.join(", ")} +${labels.length - shown.length}` : shown.join(", ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function describe(
|
|
92
|
+
channel: ChatChannel,
|
|
93
|
+
userId: string | null,
|
|
94
|
+
): Pick<ChatConversation, "name" | "names" | "words"> & { username?: string | null } {
|
|
95
|
+
switch (conversationKind(channel)) {
|
|
96
|
+
case "direct": {
|
|
97
|
+
const username = channel.dmUser?.username?.trim() || null;
|
|
98
|
+
const displayName = channel.dmUser?.displayName?.trim() || null;
|
|
99
|
+
return {
|
|
100
|
+
// The title is @username when there is one; the full name says who that is.
|
|
101
|
+
name: username && displayName && nameKey(displayName) !== nameKey(username) ? displayName : null,
|
|
102
|
+
username: username?.replace(/^@+/, "").toLowerCase() ?? null,
|
|
103
|
+
names: [nameKey(username), nameKey(displayName)].filter((name): name is string => !!name),
|
|
104
|
+
words: [...wordsOf(username), ...wordsOf(displayName)],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
case "group": {
|
|
108
|
+
const members = (channel.members ?? []).filter((member) => member.id !== userId);
|
|
109
|
+
return {
|
|
110
|
+
name: groupMembers(channel, userId),
|
|
111
|
+
names: [nameKey(channel.name)].filter((name): name is string => !!name),
|
|
112
|
+
words: [
|
|
113
|
+
...wordsOf(channel.name),
|
|
114
|
+
...members.flatMap((member) => [...wordsOf(member.username), ...wordsOf(member.displayName)]),
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
case "team": {
|
|
119
|
+
const team = teamStore.getTeam(channel.teamId ?? null);
|
|
120
|
+
return {
|
|
121
|
+
name: team?.name ?? null,
|
|
122
|
+
names: [nameKey(channel.name)].filter((name): name is string => !!name),
|
|
123
|
+
words: [
|
|
124
|
+
...wordsOf(channel.name),
|
|
125
|
+
...wordsOf(team?.name),
|
|
126
|
+
...wordsOf(team?.shortName),
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
return {
|
|
132
|
+
name: null,
|
|
133
|
+
names: [nameKey(channel.name)].filter((name): name is string => !!name),
|
|
134
|
+
words: wordsOf(channel.name),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The conversations the Chat sidebar lists, in its order, each named as the Chat pane names it. */
|
|
140
|
+
export function buildChatConversations(
|
|
141
|
+
states: readonly ChatConversationState[],
|
|
142
|
+
userId: string | null = null,
|
|
143
|
+
): ChatConversation[] {
|
|
144
|
+
return states
|
|
145
|
+
.map((state, index): ChatConversation => {
|
|
146
|
+
const kind = conversationKind(state.channel);
|
|
147
|
+
const { username = null, ...described } = describe(state.channel, userId);
|
|
148
|
+
return {
|
|
149
|
+
channelId: state.channel.id,
|
|
150
|
+
kind,
|
|
151
|
+
title: formatChatPaneTitle(state.channel, state.channel.id),
|
|
152
|
+
username,
|
|
153
|
+
unreadCount: state.unreadCount,
|
|
154
|
+
lastActivityAt: state.lastActivityAt,
|
|
155
|
+
order: KIND_ORDER[kind] * 100_000 + index,
|
|
156
|
+
...described,
|
|
157
|
+
};
|
|
158
|
+
})
|
|
159
|
+
.sort((left, right) => left.order - right.order);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isConversationWithPeople(conversation: ChatConversation): boolean {
|
|
163
|
+
return conversation.kind === "direct" || conversation.kind === "group";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function compareUnreadFirst(left: ChatConversation, right: ChatConversation): number {
|
|
167
|
+
return Number(right.unreadCount > 0) - Number(left.unreadCount > 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Newest activity first; two with nothing known tie. */
|
|
171
|
+
function compareActivity(left: ChatConversation, right: ChatConversation): number {
|
|
172
|
+
const activity = right.lastActivityAt - left.lastActivityAt;
|
|
173
|
+
return Number.isNaN(activity) ? 0 : activity;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Newest activity first; the sidebar's order breaks ties. */
|
|
177
|
+
function compareRecent(left: ChatConversation, right: ChatConversation): number {
|
|
178
|
+
return compareActivity(left, right) || left.order - right.order;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unread conversations first, newest then most unread; then DMs and groups by
|
|
183
|
+
* their last activity; then channels in the sidebar's order.
|
|
184
|
+
*/
|
|
185
|
+
function compareListing(left: ChatConversation, right: ChatConversation): number {
|
|
186
|
+
const unread = compareUnreadFirst(left, right);
|
|
187
|
+
if (unread !== 0) return unread;
|
|
188
|
+
if (left.unreadCount > 0) {
|
|
189
|
+
return compareActivity(left, right) || right.unreadCount - left.unreadCount || left.order - right.order;
|
|
190
|
+
}
|
|
191
|
+
const people = Number(isConversationWithPeople(right)) - Number(isConversationWithPeople(left));
|
|
192
|
+
if (people !== 0) return people;
|
|
193
|
+
return isConversationWithPeople(left) ? compareRecent(left, right) : left.order - right.order;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* How well typed words name a conversation: 3 when they are its name, 2 when
|
|
198
|
+
* its name starts with them, 1 when each starts a word of it, 0 otherwise.
|
|
199
|
+
*/
|
|
200
|
+
function rankChatConversation(conversation: ChatConversation, query: string): number {
|
|
201
|
+
const tokens = wordsOf(query);
|
|
202
|
+
if (tokens.length === 0) return 0;
|
|
203
|
+
const phrase = tokens.join(" ");
|
|
204
|
+
if (conversation.names.includes(phrase)) return 3;
|
|
205
|
+
if (conversation.names.some((name) => name.startsWith(phrase))) return 2;
|
|
206
|
+
return tokens.every((token) => conversation.words.some((word) => word.startsWith(token))) ? 1 : 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type ChatConversationQuery =
|
|
210
|
+
| { kind: "recent" }
|
|
211
|
+
| { kind: "listing" }
|
|
212
|
+
| { kind: "search"; text: string };
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* What the typed text asks for: nothing typed is the recent list, a word like
|
|
216
|
+
* "messages" or "chat" on its own is the whole list, and "chat gen" searches
|
|
217
|
+
* for "gen".
|
|
218
|
+
*/
|
|
219
|
+
export function parseChatConversationQuery(query: string): ChatConversationQuery {
|
|
220
|
+
const trimmed = query.trim();
|
|
221
|
+
if (!trimmed) return { kind: "recent" };
|
|
222
|
+
const [first = "", ...rest] = trimmed.split(/\s+/);
|
|
223
|
+
if (!LISTING_WORDS.has(first.toLowerCase())) return { kind: "search", text: trimmed };
|
|
224
|
+
return rest.length === 0 ? { kind: "listing" } : { kind: "search", text: rest.join(" ") };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The conversations that answer the text in the command bar, best first.
|
|
229
|
+
* With nothing typed: unread ones and the latest DMs, a few at most. Short
|
|
230
|
+
* text puts unread matches first; longer text the closest names.
|
|
231
|
+
*/
|
|
232
|
+
export function selectChatConversations(
|
|
233
|
+
conversations: readonly ChatConversation[],
|
|
234
|
+
query: ChatConversationQuery,
|
|
235
|
+
): ChatConversation[] {
|
|
236
|
+
if (query.kind === "recent") {
|
|
237
|
+
return conversations
|
|
238
|
+
.filter((conversation) => conversation.unreadCount > 0 || isConversationWithPeople(conversation))
|
|
239
|
+
.sort(compareListing)
|
|
240
|
+
.slice(0, EMPTY_QUERY_CONVERSATION_LIMIT);
|
|
241
|
+
}
|
|
242
|
+
if (query.kind === "listing") {
|
|
243
|
+
return [...conversations].sort(compareListing).slice(0, LISTING_CONVERSATION_LIMIT);
|
|
244
|
+
}
|
|
245
|
+
const shortQuery = query.text.replace(/[#@\s]/g, "").length < SHORT_QUERY_LENGTH;
|
|
246
|
+
return conversations
|
|
247
|
+
.map((conversation) => ({ conversation, rank: rankChatConversation(conversation, query.text) }))
|
|
248
|
+
.filter((entry) => entry.rank > 0)
|
|
249
|
+
.sort((left, right) => {
|
|
250
|
+
const unread = compareUnreadFirst(left.conversation, right.conversation);
|
|
251
|
+
if (shortQuery && unread !== 0) return unread;
|
|
252
|
+
return right.rank - left.rank || unread || compareRecent(left.conversation, right.conversation);
|
|
253
|
+
})
|
|
254
|
+
.slice(0, SEARCH_CONVERSATION_LIMIT)
|
|
255
|
+
.map((entry) => entry.conversation);
|
|
256
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { apiClient, type ChatDiscordLink } from "../../../api-client";
|
|
2
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
|
+
import type { AppNotificationRequest } from "../../../types/plugin";
|
|
4
|
+
import type { DiscordComposerCommand } from "./composer-commands";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where /discord sends someone to connect. The approval has to happen in a
|
|
8
|
+
* browser that is signed in to gloom.sh as the same account, which a terminal
|
|
9
|
+
* user's often is not, so the page handles the web sign-in and then starts the
|
|
10
|
+
* Discord authorization itself.
|
|
11
|
+
*/
|
|
12
|
+
export const DISCORD_LINK_URL = "https://gloom.sh/discord/link";
|
|
13
|
+
|
|
14
|
+
/** The Discord calls the composer's /discord command makes. */
|
|
15
|
+
export interface DiscordCommandApi {
|
|
16
|
+
getLink(): Promise<ChatDiscordLink>;
|
|
17
|
+
unlink(): Promise<void>;
|
|
18
|
+
setMirror(enabled: boolean): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const discordCommandApi: DiscordCommandApi = {
|
|
22
|
+
getLink: () => apiClient.getChatDiscordLink(),
|
|
23
|
+
unlink: () => apiClient.unlinkChatDiscord(),
|
|
24
|
+
setMirror: (enabled) => apiClient.setChatDiscordMirror(enabled),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const DISCORD_UNAVAILABLE = "Discord sync is not available yet";
|
|
28
|
+
const DISCORD_USAGE = "Use /discord to connect, /discord unlink, or /discord mirror on|off";
|
|
29
|
+
|
|
30
|
+
type DiscordCommandOutcome = Pick<AppNotificationRequest, "body" | "type">;
|
|
31
|
+
|
|
32
|
+
/** A server without Discord sync answers its routes with 404, or 501 where it knows them but has no bridge. */
|
|
33
|
+
function isUnavailable(error: unknown): boolean {
|
|
34
|
+
return error instanceof ApiRequestError && (error.status === 404 || error.status === 501);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function linkedStatus(link: ChatDiscordLink): string {
|
|
38
|
+
const name = link.discordUsername ? `Linked as ${link.discordUsername}` : "Linked to Discord";
|
|
39
|
+
const mirror = link.mirror ? "Mirroring is on" : "Mirroring is off";
|
|
40
|
+
return `${name}. ${mirror}. /discord mirror on|off, /discord unlink`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const UNAVAILABLE_OUTCOME: DiscordCommandOutcome = { body: DISCORD_UNAVAILABLE, type: "info" };
|
|
44
|
+
|
|
45
|
+
async function connect(api: DiscordCommandApi, openUrl: (url: string) => Promise<void>): Promise<DiscordCommandOutcome> {
|
|
46
|
+
const link = await api.getLink();
|
|
47
|
+
if (!link.available) return UNAVAILABLE_OUTCOME;
|
|
48
|
+
if (link.linked) return { body: linkedStatus(link), type: "info" };
|
|
49
|
+
await openUrl(DISCORD_LINK_URL);
|
|
50
|
+
return { body: "Opened gloom.sh in your browser to connect Discord", type: "info" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function unlink(api: DiscordCommandApi): Promise<DiscordCommandOutcome> {
|
|
54
|
+
// A 404 from the unlink route alone could mean "no Discord sync" or "nothing linked", so ask first.
|
|
55
|
+
const link = await api.getLink();
|
|
56
|
+
if (!link.available) return UNAVAILABLE_OUTCOME;
|
|
57
|
+
if (!link.linked) return { body: "Discord is not linked", type: "info" };
|
|
58
|
+
await api.unlink();
|
|
59
|
+
return { body: "Unlinked from Discord", type: "success" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Runs one /discord subcommand and says what to tell the user. A server that
|
|
64
|
+
* has no Discord sync answers 404 or 501, which reads as "not available yet"
|
|
65
|
+
* and nothing else.
|
|
66
|
+
*/
|
|
67
|
+
export async function runDiscordCommand(
|
|
68
|
+
command: DiscordComposerCommand,
|
|
69
|
+
deps: { api?: DiscordCommandApi; openUrl: (url: string) => Promise<void> },
|
|
70
|
+
): Promise<DiscordCommandOutcome> {
|
|
71
|
+
const api = deps.api ?? discordCommandApi;
|
|
72
|
+
try {
|
|
73
|
+
switch (command.action) {
|
|
74
|
+
case "connect":
|
|
75
|
+
return await connect(api, deps.openUrl);
|
|
76
|
+
case "unlink":
|
|
77
|
+
return await unlink(api);
|
|
78
|
+
case "mirror":
|
|
79
|
+
await api.setMirror(command.enabled);
|
|
80
|
+
return {
|
|
81
|
+
body: command.enabled ? "Your public messages are mirrored to Discord" : "Your public messages are no longer mirrored to Discord",
|
|
82
|
+
type: "success",
|
|
83
|
+
};
|
|
84
|
+
case "usage":
|
|
85
|
+
return { body: DISCORD_USAGE, type: "info" };
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isUnavailable(error)) return UNAVAILABLE_OUTCOME;
|
|
89
|
+
return { body: error instanceof Error && error.message ? error.message : "Discord request failed.", type: "error" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -8,17 +8,23 @@ export function isWithinChatMessageEditWindow(message: ChatMessage, nowMs = Date
|
|
|
8
8
|
return Number.isFinite(createdMs) && nowMs - createdMs <= CHAT_MESSAGE_EDIT_WINDOW_MS;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** What the server answers (409) to an edit of a message written on Discord. */
|
|
12
|
+
export const DISCORD_MESSAGE_EDIT_NOTICE = "This message was sent on Discord. Edit it there.";
|
|
13
|
+
|
|
14
|
+
/** A message written on Discord is edited there; the server refuses an edit of it. */
|
|
15
|
+
export function isEditableOnGloom(message: ChatMessage): boolean {
|
|
16
|
+
return message.origin !== "discord";
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
export function findLatestEditableChatMessage(
|
|
12
20
|
messages: ChatMessage[],
|
|
13
21
|
userId: string | null | undefined,
|
|
14
22
|
nowMs = Date.now(),
|
|
15
23
|
): ChatMessage | null {
|
|
16
24
|
if (!userId) return null;
|
|
17
|
-
|
|
25
|
+
// Only your latest message can be edited, so a Discord one in that place leaves nothing to edit.
|
|
26
|
+
const latest = [...messages]
|
|
18
27
|
.reverse()
|
|
19
|
-
.find((message) =>
|
|
20
|
-
|
|
21
|
-
&& !message.clientStatus
|
|
22
|
-
&& isWithinChatMessageEditWindow(message, nowMs)
|
|
23
|
-
)) ?? null;
|
|
28
|
+
.find((message) => message.user.id === userId && !message.clientStatus);
|
|
29
|
+
return latest && isEditableOnGloom(latest) && isWithinChatMessageEditWindow(latest, nowMs) ? latest : null;
|
|
24
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChatUserSummary } from "../../../api-client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A person on the Gloom Discord with no linked Gloom account. Their messages
|
|
5
|
+
* show in the channel, but there is no profile to open and no one to message.
|
|
6
|
+
*/
|
|
7
|
+
export function isDiscordGhost(user: Pick<ChatUserSummary, "accountType"> | null | undefined): boolean {
|
|
8
|
+
return user?.accountType === "discord";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The name a message is signed with: the username, or for a ghost that has none the Discord name it carries. */
|
|
12
|
+
export function chatAuthorName(user: Pick<ChatUserSummary, "username" | "displayName" | "accountType">): string {
|
|
13
|
+
return user.username ?? ((isDiscordGhost(user) && user.displayName?.trim()) || "anon");
|
|
14
|
+
}
|
|
@@ -10,13 +10,33 @@ import {
|
|
|
10
10
|
openDmTargetFromCommand,
|
|
11
11
|
parseDmUsernames,
|
|
12
12
|
} from "./channels";
|
|
13
|
+
import { CHAT_REUSE_PANE_VALUE, createChatSearchProvider } from "./command-bar";
|
|
13
14
|
import { chatController } from "./controller";
|
|
15
|
+
import { buildChatConversations, selectChatConversations } from "./conversations";
|
|
16
|
+
import { pickChatPaneToReuse } from "./pane-state";
|
|
14
17
|
import { ChatPane } from "./pane";
|
|
15
18
|
import { chatSidebarStore } from "./sidebar-store";
|
|
16
19
|
import { ChatStatusWidget } from "./status-widget";
|
|
17
20
|
import { UnreadInboxPane } from "./unread-inbox-pane";
|
|
18
21
|
import { UNREAD_INBOX_PANE_ID, UNREAD_INBOX_TEMPLATE_ID } from "./unread-inbox";
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `CHAT general` or `CHAT #help` by id; failing that, `CHAT gen` or `CHAT alice`
|
|
25
|
+
* opens the conversation the command bar lists first for that text.
|
|
26
|
+
*/
|
|
27
|
+
async function resolveTypedChannelId(rawArg: string): Promise<string> {
|
|
28
|
+
try {
|
|
29
|
+
return await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(rawArg));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const list = chatController.listConversations();
|
|
32
|
+
const [best] = list
|
|
33
|
+
? selectChatConversations(buildChatConversations(list.states, list.userId), { kind: "search", text: rawArg })
|
|
34
|
+
: [];
|
|
35
|
+
if (best) return best.channelId;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
export const chatModule: PluginModule = {
|
|
21
41
|
panes: [{
|
|
22
42
|
id: "chat",
|
|
@@ -47,21 +67,38 @@ export const chatModule: PluginModule = {
|
|
|
47
67
|
keywords: ["new", "chat", "pane", "message"],
|
|
48
68
|
shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" },
|
|
49
69
|
createInstance: async (context, options) => {
|
|
70
|
+
const reuse = options?.values?.[CHAT_REUSE_PANE_VALUE] === "1";
|
|
71
|
+
const rawArg = options?.arg?.trim() ?? "";
|
|
72
|
+
// The command bar's New DM keeps the open pane on its conversation.
|
|
73
|
+
if (reuse && !rawArg) {
|
|
74
|
+
const open = pickChatPaneToReuse(context.layout, context.focusedPaneId, null);
|
|
75
|
+
if (open) return { placement: "floating", instanceId: open.instanceId };
|
|
76
|
+
}
|
|
50
77
|
// `CHAT MD` or `CHAT "Macro Desk"` opens that team's #general. A raw
|
|
51
78
|
// channel id (team ids are mixed case) is kept as typed.
|
|
52
|
-
const rawArg = options?.arg?.trim() ?? "";
|
|
53
79
|
const team = rawArg ? teamStore.findTeam(rawArg) : null;
|
|
54
80
|
const channelId = team
|
|
55
81
|
? teamChannelId(team.id)
|
|
56
82
|
: rawArg && chatController.getChannels().some((entry) => entry.id === rawArg)
|
|
57
83
|
? rawArg
|
|
58
84
|
: rawArg
|
|
59
|
-
? await
|
|
85
|
+
? await resolveTypedChannelId(rawArg)
|
|
60
86
|
: await chatController.resolvePreferredChannelId(
|
|
61
87
|
getPreferredChatOpenChannelId(context.config, chatController.getSnapshot()),
|
|
62
88
|
);
|
|
63
89
|
const channel = chatController.getChannels().find((entry) => entry.id === channelId);
|
|
64
90
|
const targetMessageId = options?.values?.messageId?.trim() || null;
|
|
91
|
+
// A conversation picked in the command bar goes to the Chat pane that is
|
|
92
|
+
// open, which switches to it, rather than to a pane of its own.
|
|
93
|
+
const open = reuse ? pickChatPaneToReuse(context.layout, context.focusedPaneId, channelId) : null;
|
|
94
|
+
if (open) {
|
|
95
|
+
return {
|
|
96
|
+
placement: "floating",
|
|
97
|
+
instanceId: open.instanceId,
|
|
98
|
+
title: formatChatPaneTitle(channel, channelId),
|
|
99
|
+
settings: { channelId },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
65
102
|
return {
|
|
66
103
|
placement: "floating",
|
|
67
104
|
// One pane per channel: re-opening the same channel focuses the pane
|
|
@@ -93,6 +130,7 @@ export const chatModule: PluginModule = {
|
|
|
93
130
|
"status:widget": () => <ChatStatusWidget />,
|
|
94
131
|
},
|
|
95
132
|
setup(ctx) {
|
|
133
|
+
ctx.registerCommandBarSearchProvider(createChatSearchProvider(ctx));
|
|
96
134
|
chatController.attachPersistence(ctx.persistence, ctx.resume);
|
|
97
135
|
chatSidebarStore.attach(ctx.persistence);
|
|
98
136
|
chatController.setNotifier(ctx.notify, (channelId, messageId) => {
|
|
@@ -23,6 +23,8 @@ export function isGroupedWithPrevious(messages: ChatMessage[], index: number) {
|
|
|
23
23
|
const prev = messages[index - 1]!;
|
|
24
24
|
const curr = messages[index]!;
|
|
25
25
|
if (prev.user.id !== curr.user.id) return false;
|
|
26
|
+
// The Discord tag sits on a header, so a linked person's Discord and app messages do not share one.
|
|
27
|
+
if ((prev.origin === "discord") !== (curr.origin === "discord")) return false;
|
|
26
28
|
return new Date(curr.createdAt).getTime() - new Date(prev.createdAt).getTime() < MESSAGE_GROUP_THRESHOLD_MS;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -7,6 +7,7 @@ import { useAppLanguage } from "../../../../i18n/react";
|
|
|
7
7
|
import { normalizeInlinePreview } from "../layout";
|
|
8
8
|
import { DesktopChatMessageImages } from "../attachments/desktop";
|
|
9
9
|
import { chatReplyQuoteText } from "../attachments/model";
|
|
10
|
+
import { chatAuthorName } from "../ghost-user";
|
|
10
11
|
import { ChatMessageActions, ChatMessageHeader } from "./header";
|
|
11
12
|
import { ResponsiveTickerBadgeText } from "./inline-tokens";
|
|
12
13
|
import { useSlowPendingSend } from "./pending-send";
|
|
@@ -153,7 +154,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
153
154
|
<DesktopChatMessageImages
|
|
154
155
|
attachments={attachments}
|
|
155
156
|
caption={msg.content}
|
|
156
|
-
author={msg.user
|
|
157
|
+
author={chatAuthorName(msg.user)}
|
|
157
158
|
review={msg.attachmentReview}
|
|
158
159
|
dimmed={state.isSending || msg.clientStatus === "failed"}
|
|
159
160
|
onLoadError={onImageLoadError}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
2
|
import { Box, Text, useUiCapabilities, type BoxRenderable } from "../../../../ui";
|
|
3
3
|
import { t } from "../../../../i18n";
|
|
4
|
+
import { displayWidth } from "../../../../utils/format";
|
|
5
|
+
import { chatAuthorName, isDiscordGhost } from "../ghost-user";
|
|
4
6
|
import { Button } from "../../../../components/ui";
|
|
5
7
|
import { MESSAGE_ACTION_WIDTH } from "../layout";
|
|
6
8
|
import type { ChatMessageRenderState } from "./render-state";
|
|
@@ -82,7 +84,9 @@ export function ChatMessageHeader({
|
|
|
82
84
|
/** The terminal sizes the author cell to its label. */
|
|
83
85
|
fitAuthorWidth?: boolean;
|
|
84
86
|
}) {
|
|
85
|
-
const authorLabel = msg.user
|
|
87
|
+
const authorLabel = chatAuthorName(msg.user);
|
|
88
|
+
// A Discord ghost has no Gloom profile to open and no one to message.
|
|
89
|
+
const hasProfile = !isDiscordGhost(msg.user);
|
|
86
90
|
const { nativeContextMenu } = useUiCapabilities();
|
|
87
91
|
// The card opens beside the name the pointer is on.
|
|
88
92
|
const nameRef = useRef<BoxRenderable | null>(null);
|
|
@@ -90,26 +94,31 @@ export function ChatMessageHeader({
|
|
|
90
94
|
<Box {...rowProps} flexDirection="row" height={1} paddingLeft={1}>
|
|
91
95
|
<Box
|
|
92
96
|
ref={nameRef}
|
|
93
|
-
width={fitAuthorWidth ? authorLabel
|
|
97
|
+
width={fitAuthorWidth ? displayWidth(authorLabel) : undefined}
|
|
94
98
|
height={1}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
{...(hasProfile
|
|
100
|
+
? {
|
|
101
|
+
onMouseOver: () => onUserHover(msg.user, nameRef.current),
|
|
102
|
+
onMouseMove: () => onUserHover(msg.user, nameRef.current),
|
|
103
|
+
onMouseOut: onUserHoverEnd,
|
|
104
|
+
"data-gloom-context-menu-surface": "true",
|
|
105
|
+
onMouseDown: (event: { button?: number; preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
106
|
+
event.preventDefault?.();
|
|
107
|
+
event.stopPropagation?.();
|
|
108
|
+
// A native menu opens on the right-click's contextmenu event instead.
|
|
109
|
+
if (event.button === 2 && nativeContextMenu === true) return;
|
|
110
|
+
onUserActivate?.(msg.user, nameRef.current);
|
|
111
|
+
},
|
|
112
|
+
onContextMenu: (event: { preventDefault?: () => void; stopPropagation?: () => void }) => onUserContextMenu?.(msg.user, event, nameRef.current),
|
|
113
|
+
style: { cursor: "pointer" },
|
|
114
|
+
}
|
|
115
|
+
: {})}
|
|
108
116
|
>
|
|
109
117
|
<Text fg={state.authorColor} attributes={state.authorAttributes}>
|
|
110
118
|
{authorLabel}
|
|
111
119
|
</Text>
|
|
112
120
|
</Box>
|
|
121
|
+
{msg.origin === "discord" && <Text fg={state.originTagColor}> Discord</Text>}
|
|
113
122
|
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
114
123
|
{(state.showReplyAction || (state.showRetryAction && actionProps.retryMessage)) && <Text fg={state.headerStatusColor}> </Text>}
|
|
115
124
|
<ChatMessageActions state={state} {...actionProps} />
|
|
@@ -5,6 +5,7 @@ import { colors } from "../../../../theme/colors";
|
|
|
5
5
|
import type { ChatUserSummary, PublicPortfolioAnalytics } from "../../../../api-client";
|
|
6
6
|
import { displayWidth, formatNumber, truncateToDisplayWidth } from "../../../../utils/format";
|
|
7
7
|
import { truncateWithEllipsis } from "../../../../utils/text-wrap";
|
|
8
|
+
import { isDiscordGhost } from "../ghost-user";
|
|
8
9
|
import type { ChatUserAnchor } from "./types";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -24,7 +25,7 @@ function hasPortfolioAnalytics(analytics: PublicPortfolioAnalytics | null | unde
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function hasPublicChatProfileInfo(user: ChatUserSummary): boolean {
|
|
27
|
-
if (user.profilePublic === false) return false;
|
|
28
|
+
if (user.profilePublic === false || isDiscordGhost(user)) return false;
|
|
28
29
|
return Boolean(user.bio?.trim() || user.title?.trim() || user.company?.trim() || hasPortfolioAnalytics(user.portfolioAnalytics));
|
|
29
30
|
}
|
|
30
31
|
|