gloomberb 0.16.2 → 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/account-cache.ts +45 -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/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +524 -148
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +223 -42
- 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 +200 -73
- package/src/cli/desktop-pane-shot.ts +100 -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 +40 -7
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +21 -13
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +55 -7
- package/src/cli/pane-functions/capabilities.ts +13 -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 +89 -28
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +71 -24
- package/src/cli/pane-functions/headless.ts +168 -35
- package/src/cli/pane-functions/index.ts +12 -2
- 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 +12 -1
- package/src/cli/pane-functions/resolver.ts +32 -8
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +109 -23
- package/src/cli/registry.ts +8 -4
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +63 -16
- 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/export.ts +6 -51
- package/src/components/data-table/view.tsx +31 -10
- 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 +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +16 -7
- package/src/components/layout/shell/drag/live.ts +193 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +84 -57
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +461 -180
- 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/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- 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/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- 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 +156 -11
- 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/freshness.ts +5 -0
- 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/pane-model.ts +10 -6
- 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/attention/pane.tsx +1 -1
- 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/catalysts/detail.tsx +1 -1
- 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-attention/pane.tsx +1 -1
- 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/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +32 -6
- package/src/plugins/builtin/holders/pane.tsx +25 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- 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 +121 -45
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- 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/members/pane.tsx +10 -0
- 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/perps/pane.tsx +1 -1
- 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/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- 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/research/relative-valuation-model.ts +7 -3
- 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/model.ts +2 -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 +260 -0
- 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/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- 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/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -6
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- 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/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -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 +113 -17
- package/src/renderers/dom/host/drag-shield.ts +35 -0
- package/src/renderers/dom/host/live-frame.ts +62 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +26 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
- package/src/renderers/electrobun/shared/protocol.ts +12 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +13 -7
- package/src/sources/provider-router/financials.ts +106 -16
- 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 +24 -6
- package/src/sources/sec-edgar.ts +68 -8
- package/src/state/pane-scroll-registry.tsx +67 -35
- 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/margin-account.ts +108 -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/resolution.ts +15 -0
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/financials.ts +32 -3
- 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 +16 -0
- package/src/ui/index.tsx +10 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history-integrity.ts +19 -6
- 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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { CLOUD_PLAN_KEY } from "../../shared/cloud-upgrade";
|
|
2
|
-
import { ActionRow } from "../../../../components/ui/action-row";
|
|
3
2
|
import { getCurrentPluginTarget } from "../../../current-target";
|
|
4
3
|
import {
|
|
5
4
|
useCallback,
|
|
6
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
useSyncExternalStore,
|
|
11
|
+
type ReactNode,
|
|
11
12
|
} from "react";
|
|
12
13
|
import { apiClient } from "../../../../api-client";
|
|
13
14
|
import {
|
|
@@ -23,17 +24,15 @@ import {
|
|
|
23
24
|
import {
|
|
24
25
|
Button,
|
|
25
26
|
ChoiceDialog,
|
|
26
|
-
|
|
27
|
+
Divider,
|
|
27
28
|
EmptyState,
|
|
28
29
|
getPaneSidebarWidth,
|
|
30
|
+
IconButton,
|
|
29
31
|
MessageComposer,
|
|
30
32
|
Prose,
|
|
31
|
-
QueryBar,
|
|
32
33
|
shouldShowPaneSidebar,
|
|
33
34
|
Spinner,
|
|
34
35
|
usePaneFooter,
|
|
35
|
-
type DataTableCell,
|
|
36
|
-
type DataTableColumn,
|
|
37
36
|
type PaneHint,
|
|
38
37
|
} from "../../../../components";
|
|
39
38
|
import { MarkdownText } from "../../../../components/markdown-text";
|
|
@@ -46,16 +45,20 @@ import {
|
|
|
46
45
|
} from "../../../../state/app/context";
|
|
47
46
|
import { useInlineTickers } from "../../../../state/hooks/inline-tickers";
|
|
48
47
|
import { useRemoteControlHandler } from "../../../../remote/app-host";
|
|
49
|
-
import {
|
|
48
|
+
import { useThemeColors } from "../../../../theme/theme-context";
|
|
50
49
|
import type { PaneProps } from "../../../../types/plugin";
|
|
50
|
+
import type { ContextMenuItem } from "../../../../types/context-menu";
|
|
51
|
+
import type { AppConfig } from "../../../../types/config";
|
|
52
|
+
import type { TickerRecord } from "../../../../types/ticker";
|
|
51
53
|
import { collectUniqueTickerSymbols } from "../../../../tickers/tokenizer";
|
|
52
54
|
import { countEscapeTowardClose } from "../../../../utils/double-escape-close";
|
|
53
55
|
import { isPlainKey } from "../../../../utils/keyboard";
|
|
54
|
-
import { truncateWithEllipsis } from "../../../../utils/text-wrap";
|
|
56
|
+
import { truncateWithEllipsis, wrapTextLines } from "../../../../utils/text-wrap";
|
|
55
57
|
import { usePluginAppActions, usePluginTickerActions } from "../../../runtime";
|
|
56
58
|
import { usePlanAccess } from "../../../../api-client/plan-access";
|
|
57
59
|
import { SignInWall } from "../auth-actions";
|
|
58
60
|
import { afterLayout, revealInScrollBox } from "../../../../components/ui/reveal-in-scroll-box";
|
|
61
|
+
import { describePortfolioTab } from "../../analytics/portfolio-selection";
|
|
59
62
|
import { ASKGSessionController, type ASKGControllerManifest } from "./controller";
|
|
60
63
|
import {
|
|
61
64
|
createASKGRendererToolExecutor,
|
|
@@ -74,26 +77,37 @@ import { ASKGConversationSidebar } from "./sidebar";
|
|
|
74
77
|
import { ASKGUndoManager } from "./undo";
|
|
75
78
|
import {
|
|
76
79
|
activeTurn,
|
|
80
|
+
canRateTurn,
|
|
77
81
|
canRetryASKGError,
|
|
78
82
|
describeASKGError,
|
|
79
|
-
describeToolStatus,
|
|
80
83
|
formatCellValue,
|
|
81
84
|
isTurnRunning,
|
|
82
85
|
pendingConfirmation,
|
|
83
|
-
rowSymbol,
|
|
84
86
|
toolResultTables,
|
|
85
|
-
toolRowHeadline,
|
|
86
87
|
type ASKGConversationState,
|
|
87
|
-
type ASKGResultTable,
|
|
88
88
|
type ASKGToolRow,
|
|
89
89
|
type ASKGTurn,
|
|
90
|
+
type ASKGTurnFeedback,
|
|
90
91
|
} from "./model";
|
|
91
|
-
import type { JsonValue } from "./protocol";
|
|
92
|
+
import type { ASKGFeedbackRating, ASKGFeedbackReason, JsonValue } from "./protocol";
|
|
93
|
+
import { describeToolRow, toolTitle, type ToolDisplayContext } from "./tool-display";
|
|
94
|
+
import {
|
|
95
|
+
canUndo,
|
|
96
|
+
ToolResultDetail,
|
|
97
|
+
toolGroupId,
|
|
98
|
+
TurnTools,
|
|
99
|
+
turnTimelineIds,
|
|
100
|
+
UNDO_KEY,
|
|
101
|
+
} from "./tool-timeline";
|
|
92
102
|
|
|
93
103
|
export const ASKG_PANE_ID = "askg";
|
|
94
104
|
|
|
95
105
|
const CLIENT_VERSION = "1";
|
|
96
106
|
const MIN_DETAIL_HEIGHT = 8;
|
|
107
|
+
/** The composer grows with the question up to this many lines, then scrolls. */
|
|
108
|
+
const MAX_COMPOSER_LINES = 6;
|
|
109
|
+
/** The daily allowance is news only once it runs low. */
|
|
110
|
+
const LOW_QUOTA = 20;
|
|
97
111
|
|
|
98
112
|
function clientKind(): "tui" | "desktop" | "web" {
|
|
99
113
|
const target = getCurrentPluginTarget();
|
|
@@ -107,24 +121,6 @@ function tierLabel(row: ASKGToolRow): string | null {
|
|
|
107
121
|
return "broker";
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
function statusColor(row: ASKGToolRow): string {
|
|
111
|
-
switch (row.status) {
|
|
112
|
-
case "ok":
|
|
113
|
-
return colors.textDim;
|
|
114
|
-
case "partial":
|
|
115
|
-
case "awaiting-confirmation":
|
|
116
|
-
return colors.warning;
|
|
117
|
-
case "error":
|
|
118
|
-
case "timeout":
|
|
119
|
-
return colors.negative;
|
|
120
|
-
case "denied":
|
|
121
|
-
case "cancelled":
|
|
122
|
-
return colors.textMuted;
|
|
123
|
-
default:
|
|
124
|
-
return colors.textDim;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
124
|
function previewLines(preview: JsonValue | undefined): string[] {
|
|
129
125
|
if (preview == null) return [];
|
|
130
126
|
if (typeof preview === "string") return preview.split("\n");
|
|
@@ -133,97 +129,162 @@ function previewLines(preview: JsonValue | undefined): string[] {
|
|
|
133
129
|
return Object.entries(preview).map(([key, value]) => `${key}: ${formatCellValue(value)}`);
|
|
134
130
|
}
|
|
135
131
|
|
|
136
|
-
/** Undo a write; not `u`, which installs an app update whenever one is waiting. */
|
|
137
|
-
const UNDO_KEY = "z";
|
|
138
|
-
|
|
139
132
|
/** Upgrade from a Pro-only answer, on the key every Pro prompt uses. */
|
|
140
133
|
const UPGRADE_KEY = CLOUD_PLAN_KEY;
|
|
141
134
|
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Three questions a first look can send with one click, worded for what the
|
|
137
|
+
* profile holds: a portfolio, a ticker on screen, a watchlist.
|
|
138
|
+
*/
|
|
139
|
+
function askgExampleQuestions({
|
|
140
|
+
config,
|
|
141
|
+
tickers,
|
|
142
|
+
activeSymbol,
|
|
143
|
+
}: {
|
|
144
|
+
config: Pick<AppConfig, "watchlists">;
|
|
145
|
+
tickers: Iterable<TickerRecord>;
|
|
146
|
+
activeSymbol: string | null;
|
|
147
|
+
}): string[] {
|
|
148
|
+
let holding: string | null = null;
|
|
149
|
+
let watched = false;
|
|
150
|
+
for (const ticker of tickers) {
|
|
151
|
+
if (!holding && ticker.metadata.positions.some((position) => position.shares !== 0)) holding = ticker.metadata.ticker;
|
|
152
|
+
if (ticker.metadata.watchlists.length > 0) watched = true;
|
|
153
|
+
}
|
|
154
|
+
const symbol = activeSymbol ?? holding ?? "NVDA";
|
|
155
|
+
// Short enough to read whole in a phone-wide pane.
|
|
156
|
+
return [
|
|
157
|
+
holding ? "How is my portfolio doing today?" : "What moved the market today?",
|
|
158
|
+
`What moved ${symbol} today?`,
|
|
159
|
+
holding
|
|
160
|
+
? "Which holdings report earnings this week?"
|
|
161
|
+
: watched && config.watchlists.length > 0
|
|
162
|
+
? "Which watchlist names report this week?"
|
|
163
|
+
: "What is on this week's economic calendar?",
|
|
164
|
+
];
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
/** Rate the answer the keyboard is on: the selected one, else the latest. */
|
|
168
|
+
const GOOD_ANSWER_KEY = "g";
|
|
169
|
+
const BAD_ANSWER_KEY = "b";
|
|
170
|
+
/** Send the answer a thumbs down is open on. */
|
|
171
|
+
const SHARE_ANSWER_KEY = "s";
|
|
172
|
+
|
|
173
|
+
/** `hint` is the footer's short form; the row and the pane menu say it in full. */
|
|
174
|
+
const FEEDBACK_REASONS: ReadonlyArray<{ reason: ASKGFeedbackReason; label: string; hint: string; title: string; key: string }> = [
|
|
175
|
+
{ reason: "wrong", label: "Wrong", hint: "wrong", title: "Reason: Wrong", key: "1" },
|
|
176
|
+
{ reason: "slow", label: "Too slow", hint: "slow", title: "Reason: Too Slow", key: "2" },
|
|
177
|
+
{ reason: "missing_data", label: "Missing data", hint: "missing", title: "Reason: Missing Data", key: "3" },
|
|
178
|
+
{ reason: "other", label: "Other", hint: "other", title: "Reason: Other", key: "4" },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Exactly what the send carries. The answer goes as written, so whatever it
|
|
183
|
+
* says about the person's holdings goes with it; the rows the tools returned
|
|
184
|
+
* never do. A consent line must not promise less than what is sent.
|
|
185
|
+
*/
|
|
186
|
+
const SHARE_DISCLOSURE = "Sends your question, this answer as written (it can mention your holdings) and which tools ran, not the data they returned.";
|
|
187
|
+
|
|
188
|
+
/** True when the only thing that changed between two transcripts is a rating. */
|
|
189
|
+
function onlyFeedbackChanged(previous: readonly ASKGTurn[], next: readonly ASKGTurn[]): boolean {
|
|
190
|
+
if (previous === next || previous.length !== next.length) return false;
|
|
191
|
+
return previous.every((turn, index) => {
|
|
192
|
+
const other = next[index];
|
|
193
|
+
if (!other) return false;
|
|
194
|
+
if (turn === other) return true;
|
|
195
|
+
const { feedback: _before, ...rest } = turn;
|
|
196
|
+
const { feedback: _after, ...otherRest } = other;
|
|
197
|
+
return (Object.keys(rest) as Array<keyof typeof rest>).every((key) => rest[key] === otherRest[key]);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The timeline stop an answer's rating sits on, beside the tool rows. */
|
|
202
|
+
function answerStopId(turnId: string): string {
|
|
203
|
+
return `answer:${turnId}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The rating under a finished answer: two quiet thumbs, and after a thumbs
|
|
208
|
+
* down the reasons and the one action that sends the answer, with a line that
|
|
209
|
+
* says what goes and what never does. Keys show beside the thumbs on the
|
|
210
|
+
* answer the keyboard would rate.
|
|
211
|
+
*/
|
|
212
|
+
function AnswerFeedback({
|
|
213
|
+
feedback,
|
|
148
214
|
width,
|
|
215
|
+
keysShown,
|
|
216
|
+
followUpOpen,
|
|
149
217
|
selected,
|
|
150
|
-
expanded,
|
|
151
218
|
selectedRowRef,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
onRate,
|
|
220
|
+
onReason,
|
|
221
|
+
onShare,
|
|
155
222
|
}: {
|
|
156
|
-
|
|
223
|
+
feedback: ASKGTurnFeedback | undefined;
|
|
157
224
|
width: number;
|
|
225
|
+
/** This is the answer `g` and `b` rate. */
|
|
226
|
+
keysShown: boolean;
|
|
227
|
+
followUpOpen: boolean;
|
|
158
228
|
selected: boolean;
|
|
159
|
-
expanded: boolean;
|
|
160
|
-
/** Takes the row while it is selected, so the transcript can scroll to it. */
|
|
161
229
|
selectedRowRef: (node: BoxRenderable | null) => void;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
230
|
+
onRate: (rating: ASKGFeedbackRating) => void;
|
|
231
|
+
onReason: (reason: ASKGFeedbackReason) => void;
|
|
232
|
+
onShare: () => void;
|
|
165
233
|
}) {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const status = describeToolStatus(row);
|
|
170
|
-
const { label, summary } = toolRowHeadline(row);
|
|
171
|
-
const undoLabel = row.undo?.status === "running"
|
|
172
|
-
? "undoing…"
|
|
173
|
-
: row.undo?.status === "done"
|
|
174
|
-
? "undone"
|
|
175
|
-
: row.undo?.status === "failed"
|
|
176
|
-
? "undo failed"
|
|
177
|
-
: row.undoToken
|
|
178
|
-
? "undo"
|
|
179
|
-
: null;
|
|
180
|
-
// The row lays its parts out with a one-cell gap between each: marker, name,
|
|
181
|
-
// " " + summary, spacer, tier, status, server mark. A summary that leaves no
|
|
182
|
-
// room for them pushes the row onto two lines, over the note below it.
|
|
183
|
-
const parts = [marker, label, " ", "", ...(tier ? [`${tier} `] : []), status, ...(row.origin === "server" ? [" · Gloom"] : [])];
|
|
184
|
-
const fixedWidth = parts.reduce((total, part) => total + part.length, 0) + parts.length;
|
|
185
|
-
const summaryWidth = Math.max(6, width - fixedWidth);
|
|
186
|
-
|
|
234
|
+
const colors = useThemeColors();
|
|
235
|
+
const rating = feedback?.rating ?? null;
|
|
236
|
+
const sharing = feedback?.pending === "share";
|
|
187
237
|
return (
|
|
188
|
-
<Box ref={selected ? selectedRowRef : undefined} flexDirection="column">
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
</
|
|
208
|
-
{
|
|
209
|
-
<Box flexDirection="
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
<Box ref={selected ? selectedRowRef : undefined} flexDirection="column" paddingTop={1}>
|
|
239
|
+
<Box flexDirection="row" height={1} alignItems="center">
|
|
240
|
+
<IconButton
|
|
241
|
+
icon="thumbs-up"
|
|
242
|
+
label="Good answer"
|
|
243
|
+
shortcut={keysShown ? GOOD_ANSWER_KEY : undefined}
|
|
244
|
+
pressed={rating === "up"}
|
|
245
|
+
onPress={() => onRate("up")}
|
|
246
|
+
/>
|
|
247
|
+
{keysShown ? <Text fg={colors.textMuted}>{`${GOOD_ANSWER_KEY} `}</Text> : null}
|
|
248
|
+
<IconButton
|
|
249
|
+
icon="thumbs-down"
|
|
250
|
+
label="Bad answer"
|
|
251
|
+
shortcut={keysShown ? BAD_ANSWER_KEY : undefined}
|
|
252
|
+
pressed={rating === "down"}
|
|
253
|
+
onPress={() => onRate("down")}
|
|
254
|
+
/>
|
|
255
|
+
{keysShown ? <Text fg={colors.textMuted}>{BAD_ANSWER_KEY}</Text> : null}
|
|
256
|
+
{feedback?.shared ? <Text fg={colors.textMuted}>{" Sent to Gloom"}</Text> : null}
|
|
257
|
+
</Box>
|
|
258
|
+
{followUpOpen ? (
|
|
259
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
260
|
+
<Box flexDirection="row" flexWrap="wrap">
|
|
261
|
+
{FEEDBACK_REASONS.map((entry) => (
|
|
262
|
+
<Box key={entry.reason} flexDirection="row" height={1} marginRight={2}>
|
|
263
|
+
<Button
|
|
264
|
+
label={entry.label}
|
|
265
|
+
variant="ghost"
|
|
266
|
+
compact
|
|
267
|
+
active={feedback?.reason === entry.reason}
|
|
268
|
+
shortcut={entry.key}
|
|
269
|
+
onPress={() => onReason(entry.reason)}
|
|
270
|
+
/>
|
|
271
|
+
</Box>
|
|
272
|
+
))}
|
|
273
|
+
</Box>
|
|
274
|
+
<Box flexDirection="row" height={1}>
|
|
275
|
+
<Button
|
|
276
|
+
label={sharing ? "Sending\u2026" : "Send this answer to Gloom"}
|
|
277
|
+
variant="secondary"
|
|
278
|
+
compact
|
|
279
|
+
disabled={sharing}
|
|
280
|
+
shortcut={SHARE_ANSWER_KEY}
|
|
281
|
+
onPress={onShare}
|
|
282
|
+
/>
|
|
283
|
+
</Box>
|
|
223
284
|
<Prose
|
|
224
|
-
text={
|
|
225
|
-
width={Math.max(10, width -
|
|
226
|
-
color={
|
|
285
|
+
text={feedback?.shareFailed ? "Could not send: Gloom no longer has this answer." : SHARE_DISCLOSURE}
|
|
286
|
+
width={Math.max(10, width - 1)}
|
|
287
|
+
color={colors.textMuted}
|
|
227
288
|
figures={false}
|
|
228
289
|
/>
|
|
229
290
|
</Box>
|
|
@@ -250,6 +311,7 @@ function ConfirmationPrompt({
|
|
|
250
311
|
onApprove: () => void;
|
|
251
312
|
onDecline: () => void;
|
|
252
313
|
}) {
|
|
314
|
+
const colors = useThemeColors();
|
|
253
315
|
const lines = previewLines(row.preview).slice(0, MAX_PREVIEW_LINES);
|
|
254
316
|
return (
|
|
255
317
|
<Box
|
|
@@ -260,9 +322,9 @@ function ConfirmationPrompt({
|
|
|
260
322
|
backgroundColor={colors.panel}
|
|
261
323
|
>
|
|
262
324
|
<Box flexDirection="row" height={1}>
|
|
263
|
-
<Text fg={colors.warning} attributes={TextAttributes.BOLD}>{`Approve ${row.name}?`}</Text>
|
|
325
|
+
<Text fg={colors.warning} attributes={TextAttributes.BOLD}>{`Approve ${toolTitle(row.name)}?`}</Text>
|
|
264
326
|
<Text fg={colors.textDim}>
|
|
265
|
-
{`
|
|
327
|
+
{` changes ${tierLabel(row) ?? "data"}${row.argumentSummary ? ` · ${row.argumentSummary}` : ""}`}
|
|
266
328
|
</Text>
|
|
267
329
|
</Box>
|
|
268
330
|
{lines.map((line, index) => (
|
|
@@ -279,124 +341,38 @@ function ConfirmationPrompt({
|
|
|
279
341
|
);
|
|
280
342
|
}
|
|
281
343
|
|
|
282
|
-
|
|
283
|
-
|
|
344
|
+
/** The first look: what to ask, and three questions that ask themselves. */
|
|
345
|
+
function ASKGWelcome({
|
|
346
|
+
examples,
|
|
284
347
|
width,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
openPaneShortcut,
|
|
288
|
-
onOpenSymbol,
|
|
289
|
-
onOpenPane,
|
|
348
|
+
selectedIndex,
|
|
349
|
+
onAsk,
|
|
290
350
|
}: {
|
|
291
|
-
|
|
351
|
+
examples: string[];
|
|
292
352
|
width: number;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
focused: boolean;
|
|
296
|
-
openPaneShortcut?: string;
|
|
297
|
-
onOpenSymbol: (symbol: string) => void;
|
|
298
|
-
onOpenPane: () => void;
|
|
353
|
+
selectedIndex: number | null;
|
|
354
|
+
onAsk: (question: string) => void;
|
|
299
355
|
}) {
|
|
300
|
-
const
|
|
301
|
-
const [tableIndex, setTableIndex] = useState(0);
|
|
302
|
-
const [rowIndex, setRowIndex] = useState(0);
|
|
303
|
-
useEffect(() => {
|
|
304
|
-
setTableIndex(0);
|
|
305
|
-
}, [row.toolCallId]);
|
|
306
|
-
useEffect(() => {
|
|
307
|
-
setRowIndex(0);
|
|
308
|
-
}, [row.toolCallId, tableIndex]);
|
|
309
|
-
const table = tables[Math.min(tableIndex, tables.length - 1)] ?? null;
|
|
310
|
-
const paneTarget = useMemo(() => resolveToolPaneTarget(row.name), [row.name]);
|
|
311
|
-
|
|
312
|
-
const columns = useMemo<DataTableColumn[]>(() => (
|
|
313
|
-
(table?.columns ?? []).map((column) => ({
|
|
314
|
-
id: column.key,
|
|
315
|
-
label: column.header,
|
|
316
|
-
width: column.width ?? Math.max(column.header.length + 2, 10),
|
|
317
|
-
align: column.align === "right" ? "right" : "left",
|
|
318
|
-
flexGrow: 1,
|
|
319
|
-
}))
|
|
320
|
-
), [table]);
|
|
321
|
-
|
|
322
|
-
const renderCell = useCallback((
|
|
323
|
-
item: Record<string, JsonValue>,
|
|
324
|
-
column: DataTableColumn,
|
|
325
|
-
): DataTableCell => {
|
|
326
|
-
const value = formatCellValue(item[column.id]);
|
|
327
|
-
const symbol = rowSymbol(item);
|
|
328
|
-
return {
|
|
329
|
-
text: value,
|
|
330
|
-
color: symbol && (column.id === "symbol" || column.id === "ticker")
|
|
331
|
-
? colors.textBright
|
|
332
|
-
: colors.text,
|
|
333
|
-
};
|
|
334
|
-
}, []);
|
|
335
|
-
|
|
336
|
-
const headerHeight = 1 + (tables.length > 1 ? 1 : 0);
|
|
337
|
-
// The timeline row above already names the tool and its arguments, so the
|
|
338
|
-
// detail header carries what the row cannot: shape, size, and cost.
|
|
339
|
-
const metadata = [
|
|
340
|
-
table?.title,
|
|
341
|
-
row.rowCount !== undefined ? `${row.rowCount} ${row.rowCount === 1 ? "row" : "rows"}` : null,
|
|
342
|
-
row.elapsedMs !== undefined ? `${row.elapsedMs}ms` : null,
|
|
343
|
-
row.truncated ? "truncated" : null,
|
|
344
|
-
].filter(Boolean).join(" · ");
|
|
356
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
345
357
|
return (
|
|
346
|
-
<Box flexDirection="column"
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
358
|
+
<Box flexDirection="column" paddingTop={1}>
|
|
359
|
+
<EmptyState
|
|
360
|
+
title="Ask about your portfolio, a ticker or the market."
|
|
361
|
+
hint="Gloom reads your panes and data to answer, and lists each source it used."
|
|
362
|
+
/>
|
|
363
|
+
{/* The empty state's actions: each sends its question. A terminal
|
|
364
|
+
button has no border, so a blank row keeps the three apart. */}
|
|
365
|
+
<Box flexDirection="column" alignItems="flex-start" paddingTop={1} gap={nativePaneChrome ? 0.5 : 1}>
|
|
366
|
+
{examples.map((question, index) => (
|
|
367
|
+
<Button
|
|
368
|
+
key={question}
|
|
369
|
+
label={truncateWithEllipsis(question, Math.max(10, width - 4))}
|
|
370
|
+
variant="ghost"
|
|
371
|
+
active={selectedIndex === index}
|
|
372
|
+
onPress={() => onAsk(question)}
|
|
373
|
+
/>
|
|
374
|
+
))}
|
|
355
375
|
</Box>
|
|
356
|
-
{tables.length > 1 ? (
|
|
357
|
-
<QueryBar
|
|
358
|
-
width={width}
|
|
359
|
-
filters={[{
|
|
360
|
-
id: "section",
|
|
361
|
-
label: "Section",
|
|
362
|
-
inline: true,
|
|
363
|
-
value: String(tableIndex),
|
|
364
|
-
options: tables.map((entry, index) => ({
|
|
365
|
-
value: String(index),
|
|
366
|
-
label: entry.title ?? `Section ${index + 1}`,
|
|
367
|
-
})),
|
|
368
|
-
onChange: (value: string) => setTableIndex(Number(value)),
|
|
369
|
-
}]}
|
|
370
|
-
/>
|
|
371
|
-
) : null}
|
|
372
|
-
{table && table.rows.length > 0 ? (
|
|
373
|
-
<DataTableView<Record<string, JsonValue>>
|
|
374
|
-
focused={focused}
|
|
375
|
-
// A row cursor only while the rows have the keyboard; the pointer
|
|
376
|
-
// opens a row as it always has.
|
|
377
|
-
selection={focused
|
|
378
|
-
? { kind: "index", selectedIndex: rowIndex, onChange: (index) => setRowIndex(index) }
|
|
379
|
-
: { kind: "none" }}
|
|
380
|
-
rootWidth={width}
|
|
381
|
-
rootHeight={Math.max(3, height - headerHeight)}
|
|
382
|
-
columns={columns}
|
|
383
|
-
items={table.rows}
|
|
384
|
-
sortColumnId={null}
|
|
385
|
-
sortDirection="asc"
|
|
386
|
-
onHeaderClick={() => {}}
|
|
387
|
-
getItemKey={(_item, index) => String(index)}
|
|
388
|
-
renderCell={renderCell}
|
|
389
|
-
onActivate={(item) => {
|
|
390
|
-
const symbol = rowSymbol(item);
|
|
391
|
-
if (symbol) onOpenSymbol(symbol);
|
|
392
|
-
}}
|
|
393
|
-
emptyStateTitle="No rows returned"
|
|
394
|
-
/>
|
|
395
|
-
) : (
|
|
396
|
-
<Box paddingX={1}>
|
|
397
|
-
<Text fg={colors.textMuted}>{row.note ?? "This tool returned no rows."}</Text>
|
|
398
|
-
</Box>
|
|
399
|
-
)}
|
|
400
376
|
</Box>
|
|
401
377
|
);
|
|
402
378
|
}
|
|
@@ -404,33 +380,43 @@ function ToolResultDetail({
|
|
|
404
380
|
function TurnView({
|
|
405
381
|
turn,
|
|
406
382
|
width,
|
|
407
|
-
selectedToolCallId,
|
|
408
|
-
expandedToolCallId,
|
|
409
|
-
selectedRowRef,
|
|
410
383
|
latest,
|
|
384
|
+
rateable,
|
|
385
|
+
feedbackSelected,
|
|
386
|
+
feedbackKeysShown,
|
|
387
|
+
feedbackFollowUpOpen,
|
|
388
|
+
selectedRowRef,
|
|
411
389
|
catalog,
|
|
412
390
|
openTicker,
|
|
413
|
-
|
|
414
|
-
onToggleTool,
|
|
415
|
-
onUndo,
|
|
391
|
+
tools,
|
|
416
392
|
onRetry,
|
|
417
393
|
onUpgrade,
|
|
394
|
+
onRate,
|
|
395
|
+
onReason,
|
|
396
|
+
onShare,
|
|
418
397
|
}: {
|
|
419
398
|
turn: ASKGTurn;
|
|
420
399
|
width: number;
|
|
421
|
-
selectedToolCallId: string | null;
|
|
422
|
-
expandedToolCallId: string | null;
|
|
423
|
-
selectedRowRef: (node: BoxRenderable | null) => void;
|
|
424
400
|
/** The last turn: its Retry and Upgrade answer the pane's keys. */
|
|
425
401
|
latest: boolean;
|
|
402
|
+
/** The answer shows the rating control. */
|
|
403
|
+
rateable: boolean;
|
|
404
|
+
/** The keyboard is on this answer's rating. */
|
|
405
|
+
feedbackSelected: boolean;
|
|
406
|
+
feedbackKeysShown: boolean;
|
|
407
|
+
feedbackFollowUpOpen: boolean;
|
|
408
|
+
selectedRowRef: (node: BoxRenderable | null) => void;
|
|
426
409
|
catalog: ReturnType<typeof useInlineTickers>["catalog"];
|
|
427
410
|
openTicker: (symbol: string) => void;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
onUndo: (toolCallId: string) => void;
|
|
411
|
+
/** The turn's tool timeline. */
|
|
412
|
+
tools: ReactNode;
|
|
431
413
|
onRetry: () => void;
|
|
432
414
|
onUpgrade: () => void;
|
|
415
|
+
onRate: (rating: ASKGFeedbackRating) => void;
|
|
416
|
+
onReason: (reason: ASKGFeedbackReason) => void;
|
|
417
|
+
onShare: () => void;
|
|
433
418
|
}) {
|
|
419
|
+
const colors = useThemeColors();
|
|
434
420
|
return (
|
|
435
421
|
<Box flexDirection="column" paddingTop={1}>
|
|
436
422
|
<Prose
|
|
@@ -440,23 +426,7 @@ function TurnView({
|
|
|
440
426
|
figures={false}
|
|
441
427
|
attributes={TextAttributes.BOLD}
|
|
442
428
|
/>
|
|
443
|
-
{
|
|
444
|
-
<Box flexDirection="column" paddingTop={1}>
|
|
445
|
-
{turn.tools.map((row) => (
|
|
446
|
-
<ToolTimelineRow
|
|
447
|
-
key={row.toolCallId}
|
|
448
|
-
row={row}
|
|
449
|
-
width={width}
|
|
450
|
-
selected={row.toolCallId === selectedToolCallId}
|
|
451
|
-
expanded={row.toolCallId === expandedToolCallId}
|
|
452
|
-
selectedRowRef={selectedRowRef}
|
|
453
|
-
onSelect={() => onSelectTool(row.toolCallId)}
|
|
454
|
-
onToggle={() => onToggleTool(row.toolCallId)}
|
|
455
|
-
onUndo={() => onUndo(row.toolCallId)}
|
|
456
|
-
/>
|
|
457
|
-
))}
|
|
458
|
-
</Box>
|
|
459
|
-
) : null}
|
|
429
|
+
{tools}
|
|
460
430
|
{turn.answer ? (
|
|
461
431
|
<Box paddingTop={1}>
|
|
462
432
|
<MarkdownText
|
|
@@ -470,6 +440,25 @@ function TurnView({
|
|
|
470
440
|
) : turn.status === "streaming" && turn.tools.length === 0 ? (
|
|
471
441
|
<Box paddingTop={1}><Spinner label="Gloom is thinking…" /></Box>
|
|
472
442
|
) : null}
|
|
443
|
+
{rateable ? (
|
|
444
|
+
<AnswerFeedback
|
|
445
|
+
feedback={turn.feedback}
|
|
446
|
+
width={width}
|
|
447
|
+
keysShown={feedbackKeysShown}
|
|
448
|
+
followUpOpen={feedbackFollowUpOpen}
|
|
449
|
+
selected={feedbackSelected}
|
|
450
|
+
selectedRowRef={selectedRowRef}
|
|
451
|
+
onRate={onRate}
|
|
452
|
+
onReason={onReason}
|
|
453
|
+
onShare={onShare}
|
|
454
|
+
/>
|
|
455
|
+
) : null}
|
|
456
|
+
{turn.status === "cancelled" ? (
|
|
457
|
+
<Box flexDirection="row" paddingTop={1} gap={2}>
|
|
458
|
+
<Text fg={colors.textMuted}>Stopped.</Text>
|
|
459
|
+
{latest ? <Button label="Ask again" variant="ghost" compact shortcut="r" onPress={onRetry} /> : null}
|
|
460
|
+
</Box>
|
|
461
|
+
) : null}
|
|
473
462
|
{turn.error ? (
|
|
474
463
|
<Box flexDirection="column" paddingTop={1}>
|
|
475
464
|
{/* A failure that loses its tail tells the user nothing, so it wraps
|
|
@@ -477,15 +466,17 @@ function TurnView({
|
|
|
477
466
|
<Prose
|
|
478
467
|
text={describeASKGError(turn.error)}
|
|
479
468
|
width={width}
|
|
480
|
-
|
|
469
|
+
// Running out of questions is a state, said again in the footer, not a failure.
|
|
470
|
+
color={turn.error.code === "daily_turn_cap" ? colors.textDim : colors.negative}
|
|
481
471
|
figures={false}
|
|
482
472
|
/>
|
|
473
|
+
{/* A row of its own, so the desktop draws a button rather than a bar. */}
|
|
483
474
|
{turn.error.code === "tier_required" ? (
|
|
484
|
-
<Box paddingTop={1}>
|
|
475
|
+
<Box flexDirection="row" paddingTop={1}>
|
|
485
476
|
<Button label="Upgrade to Pro" variant="primary" shortcut={latest ? UPGRADE_KEY : undefined} onPress={onUpgrade} />
|
|
486
477
|
</Box>
|
|
487
478
|
) : canRetryASKGError(turn.error) ? (
|
|
488
|
-
<Box paddingTop={1}>
|
|
479
|
+
<Box flexDirection="row" paddingTop={1}>
|
|
489
480
|
<Button label="Retry" variant="primary" shortcut={latest ? "r" : undefined} onPress={onRetry} />
|
|
490
481
|
</Box>
|
|
491
482
|
) : null}
|
|
@@ -568,11 +559,20 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
568
559
|
|
|
569
560
|
const [inputValue, setInputValue] = useState("");
|
|
570
561
|
const [inputFocused, setInputFocused] = useState(false);
|
|
571
|
-
|
|
562
|
+
// A tool row or a turn's folded calls; j/k walk them.
|
|
563
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
572
564
|
const [expandedToolCallId, setExpandedToolCallId] = useState<string | null>(null);
|
|
565
|
+
const [openGroups, setOpenGroups] = useState<ReadonlySet<string>>(() => new Set());
|
|
566
|
+
// The answer whose thumbs down shows its reasons and the send action.
|
|
567
|
+
const [feedbackFollowUpTurnId, setFeedbackFollowUpTurnId] = useState<string | null>(null);
|
|
568
|
+
// The example question the keyboard is on while the pane is empty.
|
|
569
|
+
const [exampleIndex, setExampleIndex] = useState<number | null>(null);
|
|
573
570
|
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
574
571
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
575
572
|
const [queuedQuestion, setQueuedQuestion] = useState<string | null>(null);
|
|
573
|
+
const queuedQuestionRef = useRef<string | null>(null);
|
|
574
|
+
// The conversation list is a drawer: open only while it has the keyboard or
|
|
575
|
+
// the pointer, so it never takes the left of the pane unasked.
|
|
576
576
|
const [sidebarFocused, setSidebarFocused] = useState(false);
|
|
577
577
|
// The row the keyboard is on while the sidebar has focus. Arrows move it and
|
|
578
578
|
// Enter opens it, so walking the list does not load a transcript per keypress.
|
|
@@ -593,32 +593,60 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
593
593
|
askgConversationListStore.ensureLoaded();
|
|
594
594
|
}, [planAccess.emailVerified]);
|
|
595
595
|
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const sidebarWidth = showSidebar
|
|
604
|
-
?
|
|
605
|
-
:
|
|
596
|
+
// Any stored conversation is worth going back to, including the only one
|
|
597
|
+
// when it is not the one on screen. A pane too narrow for the list beside
|
|
598
|
+
// the answer shows it in the answer's place until one is picked.
|
|
599
|
+
const listRoom = conversations.conversations.length > 0 && height >= 8;
|
|
600
|
+
const listBeside = shouldShowPaneSidebar(conversations.conversations.length, width, height, 1);
|
|
601
|
+
const showSidebar = listRoom && sidebarFocused;
|
|
602
|
+
const listCoversBody = showSidebar && !listBeside;
|
|
603
|
+
const sidebarWidth = !showSidebar
|
|
604
|
+
? 0
|
|
605
|
+
: listCoversBody
|
|
606
|
+
? width
|
|
607
|
+
: getPaneSidebarWidth(width, !!nativePaneChrome, conversations.width);
|
|
606
608
|
|
|
607
609
|
const running = isTurnRunning(state);
|
|
608
610
|
const confirmation = pendingConfirmation(state);
|
|
609
|
-
|
|
611
|
+
|
|
612
|
+
// Names the profile gives the ids tools take, so a row reads "Interactive
|
|
613
|
+
// Brokers U1234567" rather than the id Gloom passed.
|
|
614
|
+
const displayContext = useMemo<ToolDisplayContext>(() => ({
|
|
615
|
+
collectionName: (id: string) => {
|
|
616
|
+
const portfolio = config.portfolios.find((entry) => entry.id === id);
|
|
617
|
+
if (portfolio) return describePortfolioTab(portfolio, config.brokerInstances);
|
|
618
|
+
return config.watchlists.find((entry) => entry.id === id)?.name ?? null;
|
|
619
|
+
},
|
|
620
|
+
toolTitle: (name: string) => resolveToolPaneTarget(name)?.label ?? null,
|
|
621
|
+
}), [config.brokerInstances, config.portfolios, config.watchlists]);
|
|
622
|
+
|
|
623
|
+
// What j/k walk: each turn's tool rows (or their fold), then its answer's rating.
|
|
624
|
+
const timelineIds = useMemo(
|
|
625
|
+
() => state.turns.flatMap((turn) => [
|
|
626
|
+
...turnTimelineIds(turn, openGroups),
|
|
627
|
+
...(canRateTurn(state, turn) ? [answerStopId(turn.id)] : []),
|
|
628
|
+
]),
|
|
629
|
+
[openGroups, state],
|
|
630
|
+
);
|
|
631
|
+
const toolRows = useMemo(
|
|
610
632
|
() => state.turns.flatMap((turn) => turn.tools),
|
|
611
633
|
[state.turns],
|
|
612
634
|
);
|
|
613
635
|
const expandedRow = useMemo(
|
|
614
|
-
() =>
|
|
615
|
-
[expandedToolCallId,
|
|
636
|
+
() => toolRows.find((row) => row.toolCallId === expandedToolCallId) ?? null,
|
|
637
|
+
[expandedToolCallId, toolRows],
|
|
616
638
|
);
|
|
639
|
+
const selectedRow = toolRows.find((row) => row.toolCallId === selectedId) ?? null;
|
|
640
|
+
const selectedGroupTurn = selectedId?.startsWith("group:")
|
|
641
|
+
? state.turns.find((turn) => toolGroupId(turn.id) === selectedId) ?? null
|
|
642
|
+
: null;
|
|
617
643
|
// The last question is the one a retry would repeat; an older failure is
|
|
618
644
|
// already history the user moved past.
|
|
619
645
|
const retryableTurn = useMemo(() => {
|
|
620
646
|
const turn = activeTurn(state);
|
|
621
|
-
if (!turn
|
|
647
|
+
if (!turn) return null;
|
|
648
|
+
if (turn.status === "cancelled") return turn;
|
|
649
|
+
if (turn.status !== "error" || !turn.error) return null;
|
|
622
650
|
return canRetryASKGError(turn.error) ? turn : null;
|
|
623
651
|
}, [state]);
|
|
624
652
|
const needsUpgrade = useMemo(() => {
|
|
@@ -626,13 +654,64 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
626
654
|
return turn?.status === "error" && turn.error?.code === "tier_required";
|
|
627
655
|
}, [state]);
|
|
628
656
|
|
|
657
|
+
const examples = useMemo(() => {
|
|
658
|
+
const appState = getAppStateRef.current();
|
|
659
|
+
return askgExampleQuestions({
|
|
660
|
+
config: appState.config,
|
|
661
|
+
tickers: appState.tickers.values(),
|
|
662
|
+
activeSymbol,
|
|
663
|
+
});
|
|
664
|
+
}, [activeSymbol, config.portfolios, config.watchlists]);
|
|
665
|
+
|
|
629
666
|
const ask = useCallback((question: string) => {
|
|
630
667
|
const trimmed = question.trim();
|
|
631
668
|
if (!trimmed) return;
|
|
632
669
|
void controller.ask(trimmed);
|
|
633
670
|
}, [controller]);
|
|
634
671
|
|
|
672
|
+
const focusInput = useCallback(() => {
|
|
673
|
+
setInputFocused(true);
|
|
674
|
+
setExampleIndex(null);
|
|
675
|
+
setResultFocused(false);
|
|
676
|
+
setSidebarFocused(false);
|
|
677
|
+
setSidebarCursorId(null);
|
|
678
|
+
dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
|
|
679
|
+
inputRef.current?.focus?.();
|
|
680
|
+
}, [dispatch]);
|
|
681
|
+
|
|
682
|
+
const blurInput = useCallback(() => {
|
|
683
|
+
setInputFocused(false);
|
|
684
|
+
dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
|
|
685
|
+
}, [dispatch]);
|
|
686
|
+
|
|
687
|
+
const focusedRef = useRef(focused);
|
|
688
|
+
focusedRef.current = focused;
|
|
689
|
+
|
|
690
|
+
// Asking from anywhere but the composer (an example, Retry) hands the
|
|
691
|
+
// keyboard back to it, so what is typed next is the follow-up and never a
|
|
692
|
+
// string of shortcuts. A click on the desktop would otherwise leave the
|
|
693
|
+
// keyboard on the button it pressed.
|
|
694
|
+
const askAndCompose = useCallback((question: string) => {
|
|
695
|
+
ask(question);
|
|
696
|
+
if (focusedRef.current) focusInput();
|
|
697
|
+
}, [ask, focusInput]);
|
|
698
|
+
const retryAndCompose = useCallback((turnId: string) => {
|
|
699
|
+
void controller.retryTurn(turnId);
|
|
700
|
+
if (focusedRef.current) focusInput();
|
|
701
|
+
}, [controller, focusInput]);
|
|
702
|
+
|
|
703
|
+
const resetTimeline = useCallback(() => {
|
|
704
|
+
setSelectedId(null);
|
|
705
|
+
setExpandedToolCallId(null);
|
|
706
|
+
setOpenGroups(new Set());
|
|
707
|
+
setExampleIndex(null);
|
|
708
|
+
setFeedbackFollowUpTurnId(null);
|
|
709
|
+
}, []);
|
|
710
|
+
|
|
635
711
|
const openConversation = useCallback(async (conversationId: string) => {
|
|
712
|
+
setSidebarFocused(false);
|
|
713
|
+
setSidebarCursorId(null);
|
|
714
|
+
if (focusedRef.current) focusInput();
|
|
636
715
|
if (conversationId === state.conversationId) return;
|
|
637
716
|
const conversation = await apiClient.askg.loadConversation(conversationId);
|
|
638
717
|
// A row the sidebar still shows may be gone; the refresh drops it.
|
|
@@ -640,16 +719,16 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
640
719
|
void askgConversationListStore.refresh();
|
|
641
720
|
return;
|
|
642
721
|
}
|
|
643
|
-
|
|
644
|
-
setExpandedToolCallId(null);
|
|
722
|
+
resetTimeline();
|
|
645
723
|
controller.openConversation(conversation);
|
|
646
|
-
}, [controller, state.conversationId]);
|
|
724
|
+
}, [controller, focusInput, resetTimeline, state.conversationId]);
|
|
647
725
|
|
|
648
726
|
const newConversation = useCallback(() => {
|
|
649
|
-
|
|
650
|
-
|
|
727
|
+
resetTimeline();
|
|
728
|
+
askgConversationListStore.rememberOpen(null);
|
|
651
729
|
controller.startConversation();
|
|
652
|
-
|
|
730
|
+
if (focusedRef.current) focusInput();
|
|
731
|
+
}, [controller, focusInput, resetTimeline]);
|
|
653
732
|
|
|
654
733
|
// A transcript has no undo, so it is never one click from gone.
|
|
655
734
|
const deleteConversation = useCallback(async (conversationId: string): Promise<boolean> => {
|
|
@@ -664,21 +743,24 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
664
743
|
width: 48,
|
|
665
744
|
});
|
|
666
745
|
if (!confirmed) return false;
|
|
667
|
-
if (conversationId === state.conversationId)
|
|
746
|
+
if (conversationId === state.conversationId) {
|
|
747
|
+
resetTimeline();
|
|
748
|
+
controller.startConversation();
|
|
749
|
+
}
|
|
668
750
|
void askgConversationListStore.delete(conversationId);
|
|
669
751
|
return true;
|
|
670
|
-
}, [controller, conversations.conversations, dialog, state.conversationId]);
|
|
752
|
+
}, [controller, conversations.conversations, dialog, resetTimeline, state.conversationId]);
|
|
671
753
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}, [
|
|
754
|
+
// Typing lands in the composer as soon as the pane has the keyboard: when it
|
|
755
|
+
// opens, when it is focused again, once the account is ready. Only this
|
|
756
|
+
// pane's own focus moves it, so no other pane loses the keyboard.
|
|
757
|
+
const keyboardReady = focused && planAccess.emailVerified;
|
|
758
|
+
const wasReadyRef = useRef(false);
|
|
759
|
+
useEffect(() => {
|
|
760
|
+
const gained = keyboardReady && !wasReadyRef.current;
|
|
761
|
+
wasReadyRef.current = keyboardReady;
|
|
762
|
+
if (gained && !confirmation) focusInput();
|
|
763
|
+
}, [confirmation, focusInput, keyboardReady]);
|
|
682
764
|
|
|
683
765
|
useEffect(() => {
|
|
684
766
|
if (!focused && inputFocused) blurInput();
|
|
@@ -690,17 +772,47 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
690
772
|
|
|
691
773
|
// A question typed at the command bar, either opening this pane or landing
|
|
692
774
|
// in the conversation already open here.
|
|
693
|
-
useEffect(() => subscribeASKGQuestions(
|
|
775
|
+
useEffect(() => subscribeASKGQuestions((question) => {
|
|
776
|
+
queuedQuestionRef.current = question;
|
|
777
|
+
setQueuedQuestion(question);
|
|
778
|
+
}), []);
|
|
694
779
|
|
|
695
780
|
useEffect(() => {
|
|
696
781
|
// The restored cloud session arrives after the first render, so a question
|
|
697
782
|
// that beat it waits instead of being dropped.
|
|
698
783
|
if (!queuedQuestion || !planAccess.emailVerified) return;
|
|
699
784
|
setQueuedQuestion(null);
|
|
785
|
+
queuedQuestionRef.current = null;
|
|
700
786
|
ask(queuedQuestion);
|
|
701
787
|
}, [ask, planAccess.emailVerified, queuedQuestion]);
|
|
702
788
|
|
|
789
|
+
// Closing the pane is two Escs away, so the conversation it showed comes
|
|
790
|
+
// back with the next one this session opens. A question typed with the
|
|
791
|
+
// command that opened it starts a new one instead.
|
|
792
|
+
const restoredRef = useRef(false);
|
|
703
793
|
useEffect(() => {
|
|
794
|
+
if (restoredRef.current || !planAccess.emailVerified) return;
|
|
795
|
+
restoredRef.current = true;
|
|
796
|
+
const remembered = askgConversationListStore.rememberedOpen();
|
|
797
|
+
if (!remembered || queuedQuestionRef.current || controller.getState().turns.length > 0) return;
|
|
798
|
+
void apiClient.askg.loadConversation(remembered).then((conversation) => {
|
|
799
|
+
const current = controller.getState();
|
|
800
|
+
if (!conversation || current.turns.length > 0 || current.conversationId) return;
|
|
801
|
+
controller.openConversation(conversation);
|
|
802
|
+
}).catch(() => {});
|
|
803
|
+
}, [controller, planAccess.emailVerified]);
|
|
804
|
+
|
|
805
|
+
useEffect(() => {
|
|
806
|
+
if (state.conversationId) askgConversationListStore.rememberOpen(state.conversationId);
|
|
807
|
+
}, [state.conversationId]);
|
|
808
|
+
|
|
809
|
+
// A rating changes a turn but not what was said, so it leaves the view on
|
|
810
|
+
// the answer being rated instead of pulling it to the newest one.
|
|
811
|
+
const scrolledTurnsRef = useRef(state.turns);
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
const previous = scrolledTurnsRef.current;
|
|
814
|
+
scrolledTurnsRef.current = state.turns;
|
|
815
|
+
if (onlyFeedbackChanged(previous, state.turns)) return;
|
|
704
816
|
const scroll = scrollRef.current;
|
|
705
817
|
if (!scroll?.viewport) return;
|
|
706
818
|
scroll.scrollTo({ x: 0, y: Math.max(0, scroll.scrollHeight - scroll.viewport.height) });
|
|
@@ -728,23 +840,46 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
728
840
|
|
|
729
841
|
// A list that shrank below the point of switching leaves nothing to focus.
|
|
730
842
|
useEffect(() => {
|
|
731
|
-
if (!
|
|
843
|
+
if (!listRoom && sidebarFocused) {
|
|
732
844
|
setSidebarFocused(false);
|
|
733
845
|
setSidebarCursorId(null);
|
|
734
846
|
}
|
|
735
|
-
}, [
|
|
847
|
+
}, [listRoom, sidebarFocused]);
|
|
736
848
|
|
|
737
|
-
|
|
738
|
-
|
|
849
|
+
const readDraft = useCallback(
|
|
850
|
+
() => inputRef.current?.editBuffer.getText() ?? inputValue,
|
|
851
|
+
[inputValue],
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
// The question stays in the composer while Gloom answers, and goes once it
|
|
855
|
+
// is asked; the composer keeps the keyboard for the next one.
|
|
739
856
|
const submitInput = useCallback(() => {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
if (!trimmed) return;
|
|
857
|
+
const trimmed = readDraft().trim();
|
|
858
|
+
if (!trimmed || isTurnRunning(controller.getState())) return;
|
|
743
859
|
setInputValue("");
|
|
744
860
|
inputRef.current?.editBuffer.setText?.("");
|
|
745
|
-
|
|
861
|
+
resetTimeline();
|
|
746
862
|
ask(trimmed);
|
|
747
|
-
}, [ask,
|
|
863
|
+
}, [ask, controller, readDraft, resetTimeline]);
|
|
864
|
+
|
|
865
|
+
// The terminal textarea reports edits only through `onContentChange`, and
|
|
866
|
+
// the composer grows with what it holds, so the pane binds whichever
|
|
867
|
+
// textarea is mounted after each render.
|
|
868
|
+
const boundTextareaRef = useRef<TextareaRenderable | null>(null);
|
|
869
|
+
useLayoutEffect(() => {
|
|
870
|
+
const textarea = inputRef.current;
|
|
871
|
+
const bound = boundTextareaRef.current;
|
|
872
|
+
if (textarea === bound) return;
|
|
873
|
+
if (bound) bound.onContentChange = undefined;
|
|
874
|
+
boundTextareaRef.current = textarea;
|
|
875
|
+
if (!textarea) return;
|
|
876
|
+
textarea.onContentChange = () => setInputValue(textarea.editBuffer.getText());
|
|
877
|
+
});
|
|
878
|
+
useLayoutEffect(() => () => {
|
|
879
|
+
const bound = boundTextareaRef.current;
|
|
880
|
+
boundTextareaRef.current = null;
|
|
881
|
+
if (bound) bound.onContentChange = undefined;
|
|
882
|
+
}, []);
|
|
748
883
|
|
|
749
884
|
const toggleExpanded = useCallback((toolCallId: string) => {
|
|
750
885
|
setExpandedToolCallId((current) => (current === toolCallId ? null : toolCallId));
|
|
@@ -753,15 +888,24 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
753
888
|
if (!expandedToolCallId) setResultFocused(false);
|
|
754
889
|
}, [expandedToolCallId]);
|
|
755
890
|
|
|
891
|
+
const toggleGroup = useCallback((groupId: string) => {
|
|
892
|
+
setOpenGroups((current) => {
|
|
893
|
+
const next = new Set(current);
|
|
894
|
+
if (next.has(groupId)) next.delete(groupId);
|
|
895
|
+
else next.add(groupId);
|
|
896
|
+
return next;
|
|
897
|
+
});
|
|
898
|
+
}, []);
|
|
899
|
+
|
|
756
900
|
const moveSelection = useCallback((direction: -1 | 1) => {
|
|
757
|
-
if (
|
|
758
|
-
const index =
|
|
901
|
+
if (timelineIds.length === 0) return;
|
|
902
|
+
const index = timelineIds.indexOf(selectedId ?? "");
|
|
759
903
|
// Selection starts at the newest row: that is the answer being read.
|
|
760
904
|
const nextIndex = index < 0
|
|
761
|
-
?
|
|
762
|
-
: (index + direction +
|
|
763
|
-
|
|
764
|
-
}, [
|
|
905
|
+
? timelineIds.length - 1
|
|
906
|
+
: (index + direction + timelineIds.length) % timelineIds.length;
|
|
907
|
+
setSelectedId(timelineIds[nextIndex] ?? null);
|
|
908
|
+
}, [selectedId, timelineIds]);
|
|
765
909
|
|
|
766
910
|
// The selected row follows the keyboard into view in a long conversation.
|
|
767
911
|
const selectedRowNodeRef = useRef<BoxRenderable | null>(null);
|
|
@@ -769,9 +913,9 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
769
913
|
selectedRowNodeRef.current = node;
|
|
770
914
|
}, []);
|
|
771
915
|
useEffect(() => {
|
|
772
|
-
if (!
|
|
916
|
+
if (!selectedId) return;
|
|
773
917
|
return afterLayout(() => revealInScrollBox(scrollRef.current, selectedRowNodeRef.current));
|
|
774
|
-
}, [
|
|
918
|
+
}, [expandedToolCallId, feedbackFollowUpTurnId, openGroups, selectedId]);
|
|
775
919
|
|
|
776
920
|
const openPaneForTool = useCallback((row: ASKGToolRow) => {
|
|
777
921
|
const target = resolveToolPaneTarget(row.name);
|
|
@@ -790,10 +934,14 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
790
934
|
}, [pinTicker]);
|
|
791
935
|
|
|
792
936
|
// A write waiting on the user owns the keyboard, so the answer never gets
|
|
793
|
-
// typed into the composer instead.
|
|
937
|
+
// typed into the composer instead; once it is answered the composer has it back.
|
|
938
|
+
const hadConfirmationRef = useRef(false);
|
|
794
939
|
useEffect(() => {
|
|
940
|
+
const had = hadConfirmationRef.current;
|
|
941
|
+
hadConfirmationRef.current = !!confirmation;
|
|
795
942
|
if (confirmation && inputFocused) blurInput();
|
|
796
|
-
|
|
943
|
+
if (had && !confirmation && focusedRef.current) focusInput();
|
|
944
|
+
}, [blurInput, confirmation, focusInput, inputFocused]);
|
|
797
945
|
|
|
798
946
|
const leaveSidebar = useCallback(() => {
|
|
799
947
|
setSidebarFocused(false);
|
|
@@ -811,10 +959,13 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
811
959
|
}, [conversations.conversations, sidebarCursorId]);
|
|
812
960
|
|
|
813
961
|
const focusSidebar = useCallback(() => {
|
|
962
|
+
if (!listRoom) return;
|
|
963
|
+
if (inputFocused) blurInput();
|
|
814
964
|
setResultFocused(false);
|
|
815
965
|
setSidebarFocused(true);
|
|
816
|
-
|
|
817
|
-
|
|
966
|
+
// The list opens on the conversation on screen, else on the newest one.
|
|
967
|
+
setSidebarCursorId(state.conversationId ?? conversations.conversations[0]?.id ?? null);
|
|
968
|
+
}, [blurInput, conversations.conversations, inputFocused, listRoom, state.conversationId]);
|
|
818
969
|
|
|
819
970
|
// The row the sidebar's keyboard is on, or the open conversation.
|
|
820
971
|
const sidebarTargetId = sidebarCursorId ?? state.conversationId;
|
|
@@ -829,7 +980,6 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
829
980
|
});
|
|
830
981
|
}, [conversations.conversations, deleteConversation, sidebarTargetId]);
|
|
831
982
|
|
|
832
|
-
|
|
833
983
|
const answerTexts = useMemo(
|
|
834
984
|
() => state.turns.map((turn) => turn.answer).filter(Boolean),
|
|
835
985
|
[state.turns],
|
|
@@ -863,15 +1013,72 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
863
1013
|
|
|
864
1014
|
const upgrade = useCallback(() => openCommandBar("Upgrade to Pro"), [openCommandBar]);
|
|
865
1015
|
|
|
866
|
-
|
|
1016
|
+
// The answer `g` and `b` rate: the one the keyboard is on (its rating, its
|
|
1017
|
+
// tool rows or their fold), else the newest answer, which is the one being read.
|
|
1018
|
+
const feedbackTarget = useMemo<ASKGTurn | null>(() => {
|
|
1019
|
+
if (selectedId) {
|
|
1020
|
+
const turn = state.turns.find((entry) => (
|
|
1021
|
+
selectedId === answerStopId(entry.id)
|
|
1022
|
+
|| selectedId === toolGroupId(entry.id)
|
|
1023
|
+
|| entry.tools.some((row) => row.toolCallId === selectedId)
|
|
1024
|
+
));
|
|
1025
|
+
if (turn) return canRateTurn(state, turn) ? turn : null;
|
|
1026
|
+
}
|
|
1027
|
+
return state.turns.findLast((turn) => canRateTurn(state, turn)) ?? null;
|
|
1028
|
+
}, [selectedId, state]);
|
|
1029
|
+
// Open until the thumb changes, the answer is sent, or Esc.
|
|
1030
|
+
const feedbackFollowUp = useMemo<ASKGTurn | null>(() => {
|
|
1031
|
+
const turn = state.turns.find((entry) => entry.id === feedbackFollowUpTurnId);
|
|
1032
|
+
return turn && canRateTurn(state, turn) && turn.feedback?.rating === "down" && !turn.feedback.shared
|
|
1033
|
+
? turn
|
|
1034
|
+
: null;
|
|
1035
|
+
}, [feedbackFollowUpTurnId, state]);
|
|
1036
|
+
|
|
1037
|
+
// A second thumbs down on the same answer opens or closes its reasons, so
|
|
1038
|
+
// they are always one press away and never in the way. A thumbs down hands
|
|
1039
|
+
// the keyboard to the answer, so its reasons answer 1 to 4 straight away.
|
|
1040
|
+
const rateAnswer = useCallback((turn: ASKGTurn, rating: ASKGFeedbackRating) => {
|
|
1041
|
+
setSelectedId(answerStopId(turn.id));
|
|
1042
|
+
const current = turn.feedback?.rating ?? null;
|
|
1043
|
+
if (rating === "down") {
|
|
1044
|
+
if (inputFocused) blurInput();
|
|
1045
|
+
setFeedbackFollowUpTurnId((open) => (current === "down" && open === turn.id ? null : turn.id));
|
|
1046
|
+
} else {
|
|
1047
|
+
setFeedbackFollowUpTurnId((open) => (open === turn.id ? null : open));
|
|
1048
|
+
if (inputFocused) inputRef.current?.focus?.();
|
|
1049
|
+
}
|
|
1050
|
+
if (current === rating) return;
|
|
1051
|
+
void controller.rateAnswer(turn.id, rating);
|
|
1052
|
+
}, [blurInput, controller, inputFocused]);
|
|
1053
|
+
const chooseFeedbackReason = useCallback((turn: ASKGTurn, reason: ASKGFeedbackReason) => {
|
|
1054
|
+
if (inputFocused) inputRef.current?.focus?.();
|
|
1055
|
+
if (turn.feedback?.reason === reason) return;
|
|
1056
|
+
void controller.chooseFeedbackReason(turn.id, reason);
|
|
1057
|
+
}, [controller, inputFocused]);
|
|
1058
|
+
const shareAnswer = useCallback((turn: ASKGTurn) => {
|
|
1059
|
+
if (inputFocused) inputRef.current?.focus?.();
|
|
1060
|
+
void controller.shareAnswer(turn.id);
|
|
1061
|
+
}, [controller, inputFocused]);
|
|
1062
|
+
|
|
1063
|
+
const expandedTables = useMemo(() => toolResultTables(expandedRow?.result), [expandedRow?.result]);
|
|
1064
|
+
const expandedHasRows = expandedTables.some((table) => table.rows.length > 0);
|
|
1065
|
+
// The panel is as tall as its longest table needs (divider, heading, the
|
|
1066
|
+
// section bar, column header and rows), up to half the pane.
|
|
1067
|
+
const expandedNeed = 3 + (expandedTables.length > 1 ? 1 : 0)
|
|
1068
|
+
+ Math.max(0, ...expandedTables.map((table) => table.rows.length));
|
|
867
1069
|
const confirmationHeight = confirmation ? confirmationBlockHeight(confirmation) : 0;
|
|
868
|
-
const
|
|
1070
|
+
const draftLines = useMemo(() => {
|
|
1071
|
+
const textWidth = Math.max(8, width - sidebarWidth - (nativePaneChrome ? 6 : 5));
|
|
1072
|
+
return Math.min(MAX_COMPOSER_LINES, Math.max(1, wrapTextLines(inputValue, textWidth).length));
|
|
1073
|
+
}, [inputValue, nativePaneChrome, sidebarWidth, width]);
|
|
1074
|
+
const composerHeight = nativePaneChrome ? draftLines + 2 : draftLines;
|
|
1075
|
+
const composerBlockHeight = nativePaneChrome ? composerHeight : composerHeight + 2;
|
|
869
1076
|
// The conversation keeps a readable slice no matter what else is open.
|
|
870
|
-
const detailBudget = height -
|
|
871
|
-
const detailHeight = expandedRow && detailBudget >= MIN_DETAIL_HEIGHT
|
|
872
|
-
? Math.min(Math.floor(height / 2), detailBudget)
|
|
1077
|
+
const detailBudget = height - composerBlockHeight - confirmationHeight - 6;
|
|
1078
|
+
const detailHeight = expandedRow && expandedHasRows && detailBudget >= MIN_DETAIL_HEIGHT
|
|
1079
|
+
? Math.min(Math.floor(height / 2), detailBudget, Math.max(MIN_DETAIL_HEIGHT, expandedNeed))
|
|
873
1080
|
: 0;
|
|
874
|
-
const resultActive = resultFocused && !!expandedRow && detailHeight > 0 && !inputFocused && !
|
|
1081
|
+
const resultActive = resultFocused && !!expandedRow && detailHeight > 0 && !inputFocused && !showSidebar;
|
|
875
1082
|
// `o` opens the pane of the result the rows belong to, else the selected row's.
|
|
876
1083
|
const paneRow = resultActive ? expandedRow : selectedRow;
|
|
877
1084
|
const canOpenPane = !!paneRow && !!resolveToolPaneTarget(paneRow.name);
|
|
@@ -883,6 +1090,29 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
883
1090
|
setResultFocused(expanding && toolResultTables(row.result).some((table) => table.rows.length > 0));
|
|
884
1091
|
}, [expandedToolCallId, toggleExpanded]);
|
|
885
1092
|
|
|
1093
|
+
// A click opens or closes a row without taking the keyboard from the
|
|
1094
|
+
// composer: the next thing typed is still the follow-up.
|
|
1095
|
+
const pressRow = useCallback((toolCallId: string) => {
|
|
1096
|
+
setSelectedId(toolCallId);
|
|
1097
|
+
toggleExpanded(toolCallId);
|
|
1098
|
+
if (inputFocused) inputRef.current?.focus?.();
|
|
1099
|
+
}, [inputFocused, toggleExpanded]);
|
|
1100
|
+
const pressGroup = useCallback((groupId: string) => {
|
|
1101
|
+
setSelectedId(groupId);
|
|
1102
|
+
toggleGroup(groupId);
|
|
1103
|
+
if (inputFocused) inputRef.current?.focus?.();
|
|
1104
|
+
}, [inputFocused, toggleGroup]);
|
|
1105
|
+
|
|
1106
|
+
// Leaves the composer for the timeline, on the newest row (or the last example).
|
|
1107
|
+
const enterTimeline = useCallback(() => {
|
|
1108
|
+
blurInput();
|
|
1109
|
+
if (state.turns.length === 0) {
|
|
1110
|
+
setExampleIndex(examples.length - 1);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (!selectedId && timelineIds.length > 0) setSelectedId(timelineIds[timelineIds.length - 1] ?? null);
|
|
1114
|
+
}, [blurInput, examples.length, selectedId, state.turns.length, timelineIds]);
|
|
1115
|
+
|
|
886
1116
|
useShortcut((event) => {
|
|
887
1117
|
// Behind the sign-in wall the keys are the wall's (Enter logs in).
|
|
888
1118
|
if (!focused || !planAccess.emailVerified) return;
|
|
@@ -904,15 +1134,17 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
904
1134
|
}
|
|
905
1135
|
// The sidebar owns the keyboard while it has focus, so the same arrows
|
|
906
1136
|
// that walk the tool timeline walk the conversation list instead.
|
|
907
|
-
if (
|
|
1137
|
+
if (showSidebar) {
|
|
908
1138
|
if (isPlainKey(event, "escape", "right")) {
|
|
909
1139
|
consume();
|
|
910
1140
|
leaveSidebar();
|
|
1141
|
+
focusInput();
|
|
911
1142
|
} else if (isPlainKey(event, "enter", "return")) {
|
|
912
1143
|
consume();
|
|
913
1144
|
const target = sidebarTargetId;
|
|
914
1145
|
leaveSidebar();
|
|
915
1146
|
if (target) void openConversation(target);
|
|
1147
|
+
else focusInput();
|
|
916
1148
|
} else if (isPlainKey(event, "j", "down")) {
|
|
917
1149
|
consume();
|
|
918
1150
|
moveSidebarCursor(1);
|
|
@@ -931,12 +1163,22 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
931
1163
|
return;
|
|
932
1164
|
}
|
|
933
1165
|
if (inputFocused) {
|
|
1166
|
+
const empty = !readDraft().trim();
|
|
934
1167
|
if (isPlainKey(event, "escape")) {
|
|
935
1168
|
consume();
|
|
936
1169
|
// Leaving an empty composer counts toward a double-Esc close; a draft does not.
|
|
937
|
-
|
|
938
|
-
if (!value.trim()) countEscapeTowardClose(event);
|
|
1170
|
+
if (empty) countEscapeTowardClose(event);
|
|
939
1171
|
blurInput();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (empty && isPlainKey(event, "up")) {
|
|
1175
|
+
consume();
|
|
1176
|
+
enterTimeline();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (empty && isPlainKey(event, "left") && listRoom) {
|
|
1180
|
+
consume();
|
|
1181
|
+
focusSidebar();
|
|
940
1182
|
}
|
|
941
1183
|
return;
|
|
942
1184
|
}
|
|
@@ -949,7 +1191,56 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
949
1191
|
// The result table walks and opens its own rows.
|
|
950
1192
|
if (isPlainKey(event, "j", "k", "up", "down", "enter", "return", "home", "end", "pageup", "pagedown")) return;
|
|
951
1193
|
}
|
|
952
|
-
if (
|
|
1194
|
+
if (feedbackFollowUp) {
|
|
1195
|
+
if (isPlainKey(event, "escape")) {
|
|
1196
|
+
consume();
|
|
1197
|
+
setFeedbackFollowUpTurnId(null);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const reason = FEEDBACK_REASONS.find((entry) => isPlainKey(event, entry.key));
|
|
1201
|
+
if (reason) {
|
|
1202
|
+
consume();
|
|
1203
|
+
chooseFeedbackReason(feedbackFollowUp, reason.reason);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (isPlainKey(event, SHARE_ANSWER_KEY)) {
|
|
1207
|
+
consume();
|
|
1208
|
+
shareAnswer(feedbackFollowUp);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (isPlainKey(event, GOOD_ANSWER_KEY, BAD_ANSWER_KEY) && feedbackTarget) {
|
|
1213
|
+
consume();
|
|
1214
|
+
rateAnswer(feedbackTarget, event.name === GOOD_ANSWER_KEY ? "up" : "down");
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (state.turns.length === 0) {
|
|
1218
|
+
if (isPlainKey(event, "j", "down")) {
|
|
1219
|
+
consume();
|
|
1220
|
+
setExampleIndex((index) => (index === null || index >= examples.length - 1 ? null : index + 1));
|
|
1221
|
+
if (exampleIndex === examples.length - 1) focusInput();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (isPlainKey(event, "k", "up")) {
|
|
1225
|
+
consume();
|
|
1226
|
+
setExampleIndex((index) => (index === null ? examples.length - 1 : Math.max(0, index - 1)));
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (isPlainKey(event, "enter", "return") && exampleIndex !== null) {
|
|
1230
|
+
consume();
|
|
1231
|
+
const question = examples[exampleIndex];
|
|
1232
|
+
setExampleIndex(null);
|
|
1233
|
+
if (question) askAndCompose(question);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (isPlainKey(event, "escape") && (selectedId || exampleIndex !== null)) {
|
|
1238
|
+
consume();
|
|
1239
|
+
setSelectedId(null);
|
|
1240
|
+
setExampleIndex(null);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (isPlainKey(event, "left") && listRoom) {
|
|
953
1244
|
consume();
|
|
954
1245
|
focusSidebar();
|
|
955
1246
|
return;
|
|
@@ -960,16 +1251,21 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
960
1251
|
return;
|
|
961
1252
|
}
|
|
962
1253
|
// With tool rows the arrows walk them; without, they scroll the answer.
|
|
963
|
-
if (isPlainKey(event, "j", "down") &&
|
|
1254
|
+
if (isPlainKey(event, "j", "down") && timelineIds.length > 0) {
|
|
964
1255
|
consume();
|
|
965
1256
|
moveSelection(1);
|
|
966
1257
|
return;
|
|
967
1258
|
}
|
|
968
|
-
if (isPlainKey(event, "k", "up") &&
|
|
1259
|
+
if (isPlainKey(event, "k", "up") && timelineIds.length > 0) {
|
|
969
1260
|
consume();
|
|
970
1261
|
moveSelection(-1);
|
|
971
1262
|
return;
|
|
972
1263
|
}
|
|
1264
|
+
if (isPlainKey(event, "x") && selectedGroupTurn && !resultActive) {
|
|
1265
|
+
consume();
|
|
1266
|
+
toggleGroup(toolGroupId(selectedGroupTurn.id));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
973
1269
|
if (isPlainKey(event, "x") && expandTarget) {
|
|
974
1270
|
consume();
|
|
975
1271
|
toggleResult(expandTarget);
|
|
@@ -990,6 +1286,11 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
990
1286
|
void chooseAnswerTicker();
|
|
991
1287
|
return;
|
|
992
1288
|
}
|
|
1289
|
+
if (isPlainKey(event, "n") && state.turns.length > 0 && !confirmation) {
|
|
1290
|
+
consume();
|
|
1291
|
+
newConversation();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
993
1294
|
if (event.name === UPGRADE_KEY && !event.ctrl && !event.meta && !event.alt && needsUpgrade) {
|
|
994
1295
|
consume();
|
|
995
1296
|
upgrade();
|
|
@@ -997,7 +1298,7 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
997
1298
|
}
|
|
998
1299
|
if (isPlainKey(event, "r") && retryableTurn) {
|
|
999
1300
|
consume();
|
|
1000
|
-
|
|
1301
|
+
retryAndCompose(retryableTurn.id);
|
|
1001
1302
|
return;
|
|
1002
1303
|
}
|
|
1003
1304
|
if (isPlainKey(event, "c") && running) {
|
|
@@ -1006,9 +1307,9 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1006
1307
|
}
|
|
1007
1308
|
}, { allowEditable: true });
|
|
1008
1309
|
|
|
1009
|
-
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1310
|
+
// Hints follow the keyboard: the composer keeps only the conversation's own
|
|
1311
|
+
// actions (each one also a click), the sidebar its list keys, and the
|
|
1312
|
+
// timeline everything else.
|
|
1012
1313
|
const hints = useMemo<PaneHint[]>(() => {
|
|
1013
1314
|
const list: PaneHint[] = [];
|
|
1014
1315
|
if (!planAccess.emailVerified) return list;
|
|
@@ -1018,96 +1319,152 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1018
1319
|
{ id: "decline", key: "n", label: "o decline", title: "Decline", onPress: () => controller.resolveConfirmation(confirmation.toolCallId, false) },
|
|
1019
1320
|
);
|
|
1020
1321
|
}
|
|
1021
|
-
if (
|
|
1022
|
-
if (showSidebar && sidebarFocused) {
|
|
1322
|
+
if (showSidebar) {
|
|
1023
1323
|
// While a write waits, n answers it.
|
|
1024
1324
|
if (!confirmation) list.push({ id: "new", key: "n", label: "ew conversation", onPress: () => { leaveSidebar(); newConversation(); } });
|
|
1025
1325
|
if (sidebarTargetId) list.push({ id: "delete", key: "d", label: "elete", onPress: deleteSidebarConversation });
|
|
1026
1326
|
return list;
|
|
1027
1327
|
}
|
|
1028
|
-
if (running) list.push({ id: "cancel", key: "c", label: "ancel", onPress: () => controller.cancel() });
|
|
1029
|
-
if (retryableTurn) list.push({ id: "retry", key: "r", label: "etry", onPress: () =>
|
|
1328
|
+
if (running) list.push({ id: "cancel", key: "c", label: "ancel", title: "Stop Answer", onPress: () => controller.cancel() });
|
|
1329
|
+
if (retryableTurn) list.push({ id: "retry", key: "r", label: retryableTurn.status === "cancelled" ? " ask again" : "etry", title: "Ask Again", onPress: () => retryAndCompose(retryableTurn.id) });
|
|
1030
1330
|
if (needsUpgrade) list.push({ id: "upgrade", key: UPGRADE_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: upgrade });
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1331
|
+
if (!inputFocused) {
|
|
1332
|
+
if (resultActive) list.push({ id: "result-back", key: "Esc", label: "timeline", title: "Back to Timeline", onPress: () => setResultFocused(false) });
|
|
1333
|
+
if (selectedGroupTurn && !resultActive) {
|
|
1334
|
+
const open = openGroups.has(toolGroupId(selectedGroupTurn.id));
|
|
1335
|
+
list.push({
|
|
1336
|
+
id: "expand",
|
|
1337
|
+
key: "x",
|
|
1338
|
+
label: open ? " fold calls" : " show calls",
|
|
1339
|
+
title: open ? "Fold Calls" : "Show Calls",
|
|
1340
|
+
onPress: () => toggleGroup(toolGroupId(selectedGroupTurn.id)),
|
|
1341
|
+
});
|
|
1342
|
+
} else if (expandTarget) {
|
|
1343
|
+
const expanded = expandTarget.toolCallId === expandedToolCallId;
|
|
1344
|
+
list.push({
|
|
1345
|
+
id: "expand",
|
|
1346
|
+
key: "x",
|
|
1347
|
+
label: expanded ? " collapse" : "pand",
|
|
1348
|
+
title: expanded ? "Collapse Row" : "Expand Row",
|
|
1349
|
+
onPress: () => toggleResult(expandTarget),
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (paneRow && canOpenPane) list.push({ id: "open-pane", key: "o", label: "pen pane", onPress: () => openPaneForTool(paneRow) });
|
|
1353
|
+
if (selectedRow && canUndo(selectedRow)) {
|
|
1354
|
+
list.push({ id: "undo", key: UNDO_KEY, label: " undo", title: "Undo", onPress: () => void controller.undo(selectedRow.toolCallId) });
|
|
1355
|
+
}
|
|
1356
|
+
// An open thumbs down takes the keys for its reasons and the send; the
|
|
1357
|
+
// thumbs keep working, they just leave the footer room for these.
|
|
1358
|
+
if (feedbackFollowUp) {
|
|
1359
|
+
for (const entry of FEEDBACK_REASONS) {
|
|
1360
|
+
list.push({
|
|
1361
|
+
id: `feedback-${entry.reason}`,
|
|
1362
|
+
key: entry.key,
|
|
1363
|
+
label: entry.hint,
|
|
1364
|
+
title: entry.title,
|
|
1365
|
+
onPress: () => chooseFeedbackReason(feedbackFollowUp, entry.reason),
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
list.push({ id: "feedback-share", key: SHARE_ANSWER_KEY, label: "end", title: "Send This Answer to Gloom", onPress: () => shareAnswer(feedbackFollowUp) });
|
|
1369
|
+
} else if (feedbackTarget) {
|
|
1370
|
+
list.push(
|
|
1371
|
+
{ id: "feedback-good", key: GOOD_ANSWER_KEY, label: "ood", title: "Good Answer", onPress: () => rateAnswer(feedbackTarget, "up") },
|
|
1372
|
+
{ id: "feedback-bad", key: BAD_ANSWER_KEY, label: "ad", title: "Bad Answer", onPress: () => rateAnswer(feedbackTarget, "down") },
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (answerTickers.length > 0) list.push({ id: "tickers", key: "t", label: "ickers", onPress: () => void chooseAnswerTicker() });
|
|
1045
1376
|
}
|
|
1046
|
-
if (
|
|
1047
|
-
if (
|
|
1377
|
+
if (state.turns.length > 0 && !confirmation) list.push({ id: "new", key: "n", label: "ew", title: "New Conversation", onPress: newConversation });
|
|
1378
|
+
if (listRoom) list.push({ id: "conversations", key: "←", label: " conversations", title: "Conversations", onPress: focusSidebar });
|
|
1048
1379
|
return list;
|
|
1049
1380
|
}, [
|
|
1050
1381
|
planAccess.emailVerified,
|
|
1051
1382
|
answerTickers.length,
|
|
1052
1383
|
canOpenPane,
|
|
1053
1384
|
chooseAnswerTicker,
|
|
1385
|
+
chooseFeedbackReason,
|
|
1054
1386
|
confirmation,
|
|
1055
1387
|
controller,
|
|
1056
1388
|
deleteSidebarConversation,
|
|
1057
1389
|
expandTarget,
|
|
1058
1390
|
expandedToolCallId,
|
|
1391
|
+
feedbackFollowUp,
|
|
1392
|
+
feedbackTarget,
|
|
1059
1393
|
focusSidebar,
|
|
1060
1394
|
inputFocused,
|
|
1061
1395
|
leaveSidebar,
|
|
1396
|
+
listRoom,
|
|
1062
1397
|
needsUpgrade,
|
|
1063
1398
|
newConversation,
|
|
1399
|
+
openGroups,
|
|
1064
1400
|
openPaneForTool,
|
|
1065
1401
|
paneRow,
|
|
1402
|
+
rateAnswer,
|
|
1066
1403
|
resultActive,
|
|
1404
|
+
retryAndCompose,
|
|
1067
1405
|
retryableTurn,
|
|
1068
1406
|
running,
|
|
1407
|
+
selectedGroupTurn,
|
|
1069
1408
|
selectedRow,
|
|
1409
|
+
shareAnswer,
|
|
1070
1410
|
showSidebar,
|
|
1071
|
-
sidebarFocused,
|
|
1072
1411
|
sidebarTargetId,
|
|
1412
|
+
state.turns.length,
|
|
1413
|
+
toggleGroup,
|
|
1073
1414
|
toggleResult,
|
|
1074
1415
|
upgrade,
|
|
1075
1416
|
]);
|
|
1076
1417
|
|
|
1418
|
+
const quota = state.limits && state.limits.turnsRemainingToday >= 0
|
|
1419
|
+
? state.limits.turnsRemainingToday
|
|
1420
|
+
: null;
|
|
1077
1421
|
usePaneFooter(`askg:${paneId}`, () => ({
|
|
1078
1422
|
info: [
|
|
1079
1423
|
...(running
|
|
1080
|
-
? [{ id: "streaming", parts: [{ text: "
|
|
1424
|
+
? [{ id: "streaming", parts: [{ text: "Answering", tone: "muted" as const }] }]
|
|
1081
1425
|
: []),
|
|
1082
1426
|
...(confirmation
|
|
1083
1427
|
? [{ id: "confirm", parts: [{ text: "Waiting for approval", tone: "warning" as const }] }]
|
|
1084
1428
|
: []),
|
|
1085
|
-
|
|
1429
|
+
// The answer that hit the cap already says so.
|
|
1430
|
+
...(quota !== null && quota <= LOW_QUOTA && activeTurn(state)?.error?.code !== "daily_turn_cap"
|
|
1086
1431
|
? [{
|
|
1087
1432
|
id: "quota",
|
|
1088
1433
|
parts: [{
|
|
1089
|
-
text: `${
|
|
1090
|
-
tone: "muted" as const,
|
|
1434
|
+
text: quota === 0 ? "No questions left today" : `${quota} ${quota === 1 ? "question" : "questions"} left today`,
|
|
1435
|
+
tone: quota === 0 ? "warning" as const : "muted" as const,
|
|
1091
1436
|
}],
|
|
1092
1437
|
}]
|
|
1093
1438
|
: []),
|
|
1094
1439
|
],
|
|
1095
1440
|
hints,
|
|
1096
|
-
|
|
1441
|
+
menu: state.turns.length === 0 && planAccess.emailVerified
|
|
1442
|
+
? examples.map((question, index): ContextMenuItem => ({
|
|
1443
|
+
id: `example-${index}`,
|
|
1444
|
+
label: `Ask: ${question}`,
|
|
1445
|
+
onSelect: () => askAndCompose(question),
|
|
1446
|
+
}))
|
|
1447
|
+
: [],
|
|
1448
|
+
}), [askAndCompose, confirmation, examples, hints, planAccess.emailVerified, quota, running, state]);
|
|
1097
1449
|
|
|
1098
1450
|
if (!planAccess.emailVerified) {
|
|
1099
1451
|
return (
|
|
1100
1452
|
<SignInWall
|
|
1101
1453
|
placement="gp-signin"
|
|
1102
|
-
action="ask questions about
|
|
1454
|
+
action="ask questions about anything on screen"
|
|
1103
1455
|
needsVerification={planAccess.signedIn}
|
|
1104
1456
|
/>
|
|
1105
1457
|
);
|
|
1106
1458
|
}
|
|
1107
1459
|
|
|
1108
|
-
const bodyWidth = Math.max(24, width - sidebarWidth);
|
|
1460
|
+
const bodyWidth = listCoversBody ? width : Math.max(24, width - sidebarWidth);
|
|
1109
1461
|
const contentWidth = Math.max(24, bodyWidth - (nativePaneChrome ? 2 : 4));
|
|
1110
1462
|
const lastTurnId = activeTurn(state)?.id ?? null;
|
|
1463
|
+
const placeholder = running
|
|
1464
|
+
? "Gloom is answering…"
|
|
1465
|
+
: state.turns.length > 0
|
|
1466
|
+
? "Ask a follow-up…"
|
|
1467
|
+
: "Ask Gloom a question…";
|
|
1111
1468
|
|
|
1112
1469
|
return (
|
|
1113
1470
|
<Box
|
|
@@ -1118,26 +1475,28 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1118
1475
|
>
|
|
1119
1476
|
{showSidebar ? (
|
|
1120
1477
|
<ASKGConversationSidebar
|
|
1121
|
-
activeConversationId={
|
|
1122
|
-
sidebarFocused ? sidebarCursorId ?? state.conversationId : state.conversationId
|
|
1123
|
-
}
|
|
1478
|
+
activeConversationId={sidebarCursorId ?? state.conversationId}
|
|
1124
1479
|
width={sidebarWidth}
|
|
1125
1480
|
paneWidth={width}
|
|
1126
1481
|
height={height}
|
|
1127
1482
|
focused={focused}
|
|
1128
|
-
keyboardFocused={focused
|
|
1483
|
+
keyboardFocused={focused}
|
|
1129
1484
|
onSelect={(conversationId) => void openConversation(conversationId)}
|
|
1130
1485
|
onFocusRequest={() => {
|
|
1131
1486
|
if (inputFocused) blurInput();
|
|
1132
1487
|
setResultFocused(false);
|
|
1133
1488
|
setSidebarFocused(true);
|
|
1134
1489
|
}}
|
|
1135
|
-
onNewConversation={
|
|
1490
|
+
onNewConversation={() => {
|
|
1491
|
+
leaveSidebar();
|
|
1492
|
+
newConversation();
|
|
1493
|
+
}}
|
|
1136
1494
|
onDelete={(conversationId) => void deleteConversation(conversationId)}
|
|
1137
1495
|
/>
|
|
1138
1496
|
) : null}
|
|
1139
1497
|
|
|
1140
1498
|
<Box
|
|
1499
|
+
visible={!listCoversBody}
|
|
1141
1500
|
flexDirection="column"
|
|
1142
1501
|
width={nativePaneChrome ? undefined : bodyWidth}
|
|
1143
1502
|
height={nativePaneChrome ? "100%" : height}
|
|
@@ -1151,26 +1510,55 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1151
1510
|
>
|
|
1152
1511
|
<ScrollBox ref={scrollRef} flexGrow={1} minHeight={0} scrollY focusable={false} paddingX={1}>
|
|
1153
1512
|
{state.turns.length === 0 ? (
|
|
1154
|
-
<
|
|
1155
|
-
|
|
1156
|
-
|
|
1513
|
+
<ASKGWelcome
|
|
1514
|
+
examples={examples}
|
|
1515
|
+
width={contentWidth}
|
|
1516
|
+
selectedIndex={inputFocused ? null : exampleIndex}
|
|
1517
|
+
onAsk={askAndCompose}
|
|
1157
1518
|
/>
|
|
1158
1519
|
) : state.turns.map((turn) => (
|
|
1159
1520
|
<TurnView
|
|
1160
1521
|
key={turn.id}
|
|
1161
1522
|
turn={turn}
|
|
1162
1523
|
width={contentWidth}
|
|
1163
|
-
selectedToolCallId={selectedToolCallId}
|
|
1164
|
-
expandedToolCallId={expandedToolCallId}
|
|
1165
|
-
selectedRowRef={selectedRowRef}
|
|
1166
1524
|
latest={turn.id === lastTurnId}
|
|
1525
|
+
rateable={canRateTurn(state, turn)}
|
|
1526
|
+
feedbackSelected={selectedId === answerStopId(turn.id)}
|
|
1527
|
+
feedbackKeysShown={focused && !inputFocused && !showSidebar && feedbackTarget?.id === turn.id}
|
|
1528
|
+
feedbackFollowUpOpen={feedbackFollowUp?.id === turn.id}
|
|
1529
|
+
selectedRowRef={selectedRowRef}
|
|
1167
1530
|
catalog={catalog}
|
|
1168
1531
|
openTicker={openTicker}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1532
|
+
tools={(
|
|
1533
|
+
<TurnTools
|
|
1534
|
+
turn={turn}
|
|
1535
|
+
width={contentWidth}
|
|
1536
|
+
context={displayContext}
|
|
1537
|
+
selectedId={selectedId}
|
|
1538
|
+
expandedToolCallId={expandedToolCallId}
|
|
1539
|
+
openGroups={openGroups}
|
|
1540
|
+
selectedRowRef={selectedRowRef}
|
|
1541
|
+
onPressRow={pressRow}
|
|
1542
|
+
onPressGroup={pressGroup}
|
|
1543
|
+
onUndo={(toolCallId) => void controller.undo(toolCallId)}
|
|
1544
|
+
/>
|
|
1545
|
+
)}
|
|
1546
|
+
onRetry={() => retryAndCompose(turn.id)}
|
|
1173
1547
|
onUpgrade={upgrade}
|
|
1548
|
+
// The controls keep their press from the column behind them, so
|
|
1549
|
+
// they close the list themselves.
|
|
1550
|
+
onRate={(rating) => {
|
|
1551
|
+
leaveSidebar();
|
|
1552
|
+
rateAnswer(turn, rating);
|
|
1553
|
+
}}
|
|
1554
|
+
onReason={(reason) => {
|
|
1555
|
+
leaveSidebar();
|
|
1556
|
+
chooseFeedbackReason(turn, reason);
|
|
1557
|
+
}}
|
|
1558
|
+
onShare={() => {
|
|
1559
|
+
leaveSidebar();
|
|
1560
|
+
shareAnswer(turn);
|
|
1561
|
+
}}
|
|
1174
1562
|
/>
|
|
1175
1563
|
))}
|
|
1176
1564
|
</ScrollBox>
|
|
@@ -1185,22 +1573,27 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1185
1573
|
) : null}
|
|
1186
1574
|
|
|
1187
1575
|
{expandedRow && detailHeight > 0 ? (
|
|
1188
|
-
<
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1576
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
1577
|
+
<Divider />
|
|
1578
|
+
<ToolResultDetail
|
|
1579
|
+
row={expandedRow}
|
|
1580
|
+
view={describeToolRow(expandedRow, displayContext)}
|
|
1581
|
+
width={nativePaneChrome ? bodyWidth : bodyWidth - 2}
|
|
1582
|
+
height={detailHeight - 1}
|
|
1583
|
+
focused={focused && resultActive}
|
|
1584
|
+
canOpenPane={!!resolveToolPaneTarget(expandedRow.name)}
|
|
1585
|
+
openPaneShortcut={paneRow?.toolCallId === expandedRow.toolCallId && canOpenPane ? "o" : undefined}
|
|
1586
|
+
onOpenSymbol={openSymbol}
|
|
1587
|
+
onOpenPane={() => openPaneForTool(expandedRow)}
|
|
1588
|
+
/>
|
|
1589
|
+
</Box>
|
|
1197
1590
|
) : null}
|
|
1198
1591
|
|
|
1199
1592
|
<MessageComposer
|
|
1200
1593
|
inputRef={inputRef}
|
|
1201
1594
|
initialValue={inputValue}
|
|
1202
1595
|
focused={inputFocused && focused}
|
|
1203
|
-
placeholder={
|
|
1596
|
+
placeholder={placeholder}
|
|
1204
1597
|
width="100%"
|
|
1205
1598
|
height={composerHeight}
|
|
1206
1599
|
terminalPrefix=" > "
|
|
@@ -1210,6 +1603,8 @@ export function ASKGPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
1210
1603
|
keyBindings={[
|
|
1211
1604
|
{ name: "return", action: "submit" },
|
|
1212
1605
|
{ name: "linefeed", action: "submit" },
|
|
1606
|
+
{ name: "return", shift: true, action: "newline" },
|
|
1607
|
+
{ name: "linefeed", shift: true, action: "newline" },
|
|
1213
1608
|
]}
|
|
1214
1609
|
onSubmit={submitInput}
|
|
1215
1610
|
wrapText
|