gloomberb 0.9.9 → 0.10.0
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/README.md +41 -3
- package/README.zh-CN.md +294 -0
- package/package.json +10 -4
- package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
- package/src/api-client/auth.ts +2 -0
- package/src/api-client/index.test.ts +4 -0
- package/src/api-client/types.ts +3 -0
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
- package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
- package/src/app/pane-template-dialog-wizard.tsx +10 -3
- package/src/app/runtime/desktop-deeplink.test.ts +15 -0
- package/src/app/runtime/desktop-deeplink.ts +21 -6
- package/src/app.tsx +40 -33
- package/src/capabilities/service-capabilities.ts +7 -2
- package/src/cli/commands/automation.test.ts +202 -0
- package/src/cli/commands/automation.ts +153 -36
- package/src/cli/commands/market.ts +1 -14
- package/src/cli/commands/system.ts +4 -1
- package/src/cli/context.ts +1 -1
- package/src/cli/desktop-pane-shot.ts +65 -4
- package/src/cli/entry.ts +1 -1
- package/src/cli/pane-functions/capabilities.ts +532 -0
- package/src/cli/pane-functions/catalog.ts +39 -4
- package/src/cli/pane-functions/data.test.ts +17 -0
- package/src/cli/pane-functions/data.ts +55 -13
- package/src/cli/pane-functions/discovery.ts +36 -9
- package/src/cli/pane-functions/index.test.ts +104 -1
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/options.ts +21 -6
- package/src/cli/pane-functions/report.ts +735 -72
- package/src/cli/pane-functions/resolver.ts +70 -6
- package/src/cli/pane-functions/screenshot.test.ts +431 -0
- package/src/cli/pane-functions/screenshot.ts +952 -8
- package/src/components/chart/composite/column-layout.ts +215 -0
- package/src/components/chart/composite/composite-chart.test.tsx +630 -0
- package/src/components/chart/composite/composite-chart.tsx +1148 -0
- package/src/components/chart/composite/format.test.ts +137 -0
- package/src/components/chart/composite/format.ts +135 -0
- package/src/components/chart/composite/index.ts +38 -0
- package/src/components/chart/composite/interactions.test.ts +109 -0
- package/src/components/chart/composite/interactions.ts +247 -0
- package/src/components/chart/composite/price-series.test.ts +39 -0
- package/src/components/chart/composite/price-series.ts +78 -0
- package/src/components/chart/composite/rasterizer.ts +359 -0
- package/src/components/chart/composite/renderers.test.ts +610 -0
- package/src/components/chart/composite/scene.test.ts +464 -0
- package/src/components/chart/composite/scene.ts +553 -0
- package/src/components/chart/composite/text-renderer.ts +285 -0
- package/src/components/chart/composite/time-scale.test.ts +88 -0
- package/src/components/chart/composite/time-scale.ts +217 -0
- package/src/components/chart/composite/types.ts +141 -0
- package/src/components/chart/core/data.ts +81 -5
- package/src/components/chart/core/index.test.ts +128 -43
- package/src/components/chart/core/resolution.ts +0 -6
- package/src/components/chart/native/chart-rasterizer.ts +1 -3
- package/src/components/chart/native/kitty/index.test.ts +99 -120
- package/src/components/chart/native/kitty/manager.ts +3 -0
- package/src/components/chart/native/kitty/protocol.ts +4 -1
- package/src/components/chart/native/raster/crosshair.ts +79 -46
- package/src/components/chart/native/raster/types.ts +2 -11
- package/src/components/chart/native/surface/manager.ts +48 -3
- package/src/components/chart/price-axis-labels.tsx +18 -4
- package/src/components/command-bar/commands/direct/index.ts +25 -0
- package/src/components/command-bar/commands/registry.ts +9 -0
- package/src/components/command-bar/helpers.test.ts +4 -1
- package/src/components/command-bar/list/view.tsx +18 -6
- package/src/components/command-bar/multi-select-picker.tsx +6 -5
- package/src/components/command-bar/pane-settings.test.ts +141 -0
- package/src/components/command-bar/pane-settings.ts +61 -1
- package/src/components/command-bar/panel/body.tsx +7 -6
- package/src/components/command-bar/routes/root/command-items.ts +4 -4
- package/src/components/command-bar/routes/root/results.ts +10 -1
- package/src/components/command-bar/routes/root/selection.test.ts +54 -0
- package/src/components/command-bar/routes/root/selection.ts +13 -0
- package/src/components/command-bar/routing/actions.ts +18 -2
- package/src/components/command-bar/routing/list-state.ts +2 -1
- package/src/components/command-bar/surface/index.test.tsx +36 -4
- package/src/components/command-bar/surface/pane.test.tsx +93 -0
- package/src/components/command-bar/theme-picker.tsx +2 -1
- package/src/components/command-bar/view-model.ts +19 -20
- package/src/components/command-bar/workflow/body.tsx +2 -1
- package/src/components/command-bar/workflow/field-row.tsx +3 -2
- package/src/components/command-bar/workflow/fields.ts +4 -2
- package/src/components/command-bar/workflow/ops.test.ts +161 -0
- package/src/components/command-bar/workflow/ops.ts +20 -50
- package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
- package/src/components/command-bar/workflow/route-actions.ts +4 -0
- package/src/components/command-bar/workflow/types.ts +1 -0
- package/src/components/feed-data-table/stack-view.tsx +6 -5
- package/src/components/index.ts +13 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -1
- package/src/components/layout/floating-pane.tsx +3 -0
- package/src/components/layout/header.tsx +15 -8
- package/src/components/layout/pane/footer/index.test.tsx +22 -3
- package/src/components/layout/pane/footer/index.tsx +3 -1
- package/src/components/layout/pane/footer/model.ts +9 -2
- package/src/components/layout/pane/header.tsx +4 -6
- package/src/components/layout/pane/index.tsx +3 -0
- package/src/components/layout/pane/sidebar.test.tsx +100 -0
- package/src/components/layout/pane/sidebar.tsx +239 -0
- package/src/components/layout/pane/title.ts +4 -3
- package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
- package/src/components/layout/shell/drag/runtime.ts +2 -0
- package/src/components/layout/shell/menu.test.ts +19 -0
- package/src/components/layout/shell/menu.ts +8 -5
- package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
- package/src/components/layout/shell/pane/layers.tsx +2 -2
- package/src/components/layout/status-bar.tsx +8 -7
- package/src/components/layout/window-controls.tsx +4 -3
- package/src/components/metric-treemap/index.tsx +3 -2
- package/src/components/onboarding/onboarding-steps.tsx +30 -29
- package/src/components/onboarding/onboarding-wizard.tsx +5 -4
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
- package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
- package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
- package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
- package/src/components/pane-settings-dialog/desktop.tsx +25 -3
- package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
- package/src/components/pane-settings-dialog/tui.tsx +11 -9
- package/src/components/pane-settings-dialog/value.ts +8 -5
- package/src/components/pane-settings-dialog.test.tsx +235 -0
- package/src/components/pane-settings-dialog.tsx +24 -0
- package/src/components/pane-template-wizard.tsx +6 -2
- package/src/components/ticker/list-table-view.tsx +3 -2
- package/src/components/ui/button.tsx +3 -1
- package/src/components/ui/choice-dialog.tsx +6 -1
- package/src/components/ui/confirm-dialog.tsx +3 -2
- package/src/components/ui/fields.tsx +10 -1
- package/src/components/ui/frame.tsx +4 -1
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/inline-quick-add.tsx +104 -0
- package/src/components/ui/list-view.tsx +5 -4
- package/src/components/ui/loading.tsx +2 -1
- package/src/components/ui/multi-select/dialog.tsx +188 -44
- package/src/components/ui/popover.tsx +10 -0
- package/src/components/ui/shortcut-hint.tsx +2 -0
- package/src/components/ui/status.tsx +4 -3
- package/src/components/ui/tabs.tsx +19 -8
- package/src/components/ui/toggle.tsx +44 -3
- package/src/components/ui/ui.test.tsx +67 -5
- package/src/core/app-services.ts +4 -6
- package/src/core/state/session-persistence.ts +3 -2
- package/src/data/config/chart-settings.ts +712 -0
- package/src/data/config/layout.ts +24 -5
- package/src/data/config/store/index.test.ts +377 -14
- package/src/data/config/store/normalize.ts +79 -65
- package/src/data/fred-series.ts +137 -0
- package/src/i18n/index.test.ts +114 -0
- package/src/i18n/index.ts +147 -0
- package/src/i18n/ja.ts +1013 -0
- package/src/i18n/ko.ts +1011 -0
- package/src/i18n/languages.test.ts +31 -0
- package/src/i18n/languages.ts +72 -0
- package/src/i18n/zh-cn.ts +1013 -0
- package/src/i18n/zh-tw.ts +1013 -0
- package/src/plugins/builtin/account-management/footer.ts +3 -2
- package/src/plugins/builtin/account-management/form-components.tsx +3 -2
- package/src/plugins/builtin/account-management/keyboard.ts +6 -0
- package/src/plugins/builtin/account-management/model.ts +25 -21
- package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
- package/src/plugins/builtin/account-management/navigation.ts +24 -0
- package/src/plugins/builtin/account-management/pane.tsx +120 -85
- package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
- package/src/plugins/builtin/ai/agent-history.ts +139 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
- package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
- package/src/plugins/builtin/ai/index.test.ts +302 -0
- package/src/plugins/builtin/ai/index.tsx +374 -40
- package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
- package/src/plugins/builtin/ai/pane-settings.ts +319 -0
- package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
- package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
- package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
- package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
- package/src/plugins/builtin/ai/pi/host.ts +564 -0
- package/src/plugins/builtin/ai/pi/index.ts +5 -0
- package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
- package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
- package/src/plugins/builtin/ai/pi/providers.ts +68 -0
- package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
- package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
- package/src/plugins/builtin/ai/providers.test.ts +68 -0
- package/src/plugins/builtin/ai/providers.ts +163 -38
- package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
- package/src/plugins/builtin/ai/runner-selection.ts +193 -0
- package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
- package/src/plugins/builtin/ai/runner.test.ts +198 -0
- package/src/plugins/builtin/ai/runner.ts +276 -73
- package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
- package/src/plugins/builtin/ai/screener/contract.ts +14 -52
- package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
- package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
- package/src/plugins/builtin/ai/screener/footer.ts +82 -67
- package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
- package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
- package/src/plugins/builtin/ai/screener/model.ts +9 -3
- package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
- package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
- package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
- package/src/plugins/builtin/ai/screener/runner.ts +41 -25
- package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
- package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
- package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
- package/src/plugins/builtin/ai/workspace/model.ts +308 -0
- package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
- package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
- package/src/plugins/builtin/analytics/index.test.tsx +3 -3
- package/src/plugins/builtin/analytics/index.tsx +3 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
- package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
- package/src/plugins/builtin/broker-manager/index.tsx +9 -11
- package/src/plugins/builtin/broker-manager/model.ts +11 -10
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
- package/src/plugins/builtin/broker-manager/table.ts +9 -8
- package/src/plugins/builtin/changelog/index.tsx +3 -8
- package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
- package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
- package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
- package/src/plugins/builtin/chart-composer/index.tsx +228 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
- package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
- package/src/plugins/builtin/chart-composer/presets.ts +759 -0
- package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
- package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
- package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
- package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
- package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
- package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
- package/src/plugins/builtin/chart-composer/settings.ts +397 -0
- package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
- package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
- package/src/plugins/builtin/chat/channel-labels.ts +5 -4
- package/src/plugins/builtin/chat/channels.ts +12 -11
- package/src/plugins/builtin/chat/chat.test.tsx +32 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
- package/src/plugins/builtin/chat/content/composer.tsx +9 -8
- package/src/plugins/builtin/chat/content/index.tsx +36 -1
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
- package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
- package/src/plugins/builtin/chat/controller/utils.ts +10 -9
- package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
- package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
- package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
- package/src/plugins/builtin/chat/pane-state.ts +30 -0
- package/src/plugins/builtin/chat/pane.tsx +27 -4
- package/src/plugins/builtin/chat/profile-popover.ts +62 -5
- package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
- package/src/plugins/builtin/chat/sidebar.tsx +107 -168
- package/src/plugins/builtin/chat/test-harness.tsx +6 -0
- package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
- package/src/plugins/builtin/cloud/plugin.tsx +150 -127
- package/src/plugins/builtin/composite-plugins.ts +71 -0
- package/src/plugins/builtin/correlation/index.test.tsx +4 -4
- package/src/plugins/builtin/correlation/index.tsx +3 -8
- package/src/plugins/builtin/earnings/index.tsx +37 -21
- package/src/plugins/builtin/earnings/model.test.ts +52 -0
- package/src/plugins/builtin/earnings/model.ts +32 -0
- package/src/plugins/builtin/econ/detail-view.tsx +17 -4
- package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
- package/src/plugins/builtin/econ/index.tsx +16 -20
- package/src/plugins/builtin/fear-greed/index.tsx +2 -8
- package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
- package/src/plugins/builtin/help/components.tsx +5 -4
- package/src/plugins/builtin/help/index.tsx +14 -17
- package/src/plugins/builtin/holders/index.tsx +2 -8
- package/src/plugins/builtin/insider/index.tsx +2 -7
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
- package/src/plugins/builtin/layout-manager/index.tsx +3 -7
- package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
- package/src/plugins/builtin/market-movers/index.tsx +3 -8
- package/src/plugins/builtin/news/index.tsx +12 -16
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
- package/src/plugins/builtin/news/wire/index.tsx +83 -73
- package/src/plugins/builtin/options/index.tsx +2 -8
- package/src/plugins/builtin/plugin-module.test.ts +89 -0
- package/src/plugins/builtin/plugin-module.ts +116 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
- package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
- package/src/plugins/builtin/research/index.tsx +2 -8
- package/src/plugins/builtin/sec/index.tsx +2 -8
- package/src/plugins/builtin/sectors/index.tsx +3 -8
- package/src/plugins/builtin/thirteenf/index.tsx +3 -8
- package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
- package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
- package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
- package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
- package/src/plugins/builtin/tv/channels.ts +27 -0
- package/src/plugins/builtin/tv/index.tsx +40 -0
- package/src/plugins/builtin/tv/pane.tsx +286 -0
- package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
- package/src/plugins/builtin/world-indices/index.tsx +3 -8
- package/src/plugins/builtin/yield-curve/index.tsx +8 -8
- package/src/plugins/catalog-backend.test.ts +21 -0
- package/src/plugins/catalog-backend.ts +42 -0
- package/src/plugins/catalog-ui.ts +6 -14
- package/src/plugins/ibkr/instance-selection.test.ts +0 -1
- package/src/plugins/ownership.ts +143 -0
- package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
- package/src/plugins/prediction-markets/chart.test.tsx +7 -5
- package/src/plugins/prediction-markets/chart.tsx +25 -7
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +8 -0
- package/src/plugins/registry/pane-settings.ts +1 -1
- package/src/plugins/runtime/index.test.tsx +2 -2
- package/src/remote/app-host.tsx +16 -3
- package/src/remote/controller.test.ts +102 -1
- package/src/remote/controller.ts +122 -0
- package/src/remote/types.ts +11 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
- package/src/renderers/electrobun/bun/index.ts +5 -0
- package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
- package/src/renderers/electrobun/view/ai-host.ts +124 -19
- package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
- package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
- package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
- package/src/renderers/electrobun/view/host/box.tsx +5 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
- package/src/renderers/electrobun/view/host/input.tsx +9 -19
- package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
- package/src/renderers/electrobun/view/main.tsx +10 -3
- package/src/renderers/electrobun/view/styles.css +17 -0
- package/src/renderers/electrobun/view/ui-host.tsx +7 -0
- package/src/renderers/opentui/chart-surface.test.tsx +52 -6
- package/src/renderers/opentui/chart-surface.tsx +95 -11
- package/src/renderers/opentui/dialog-host.tsx +36 -5
- package/src/renderers/opentui/host.tsx +39 -0
- package/src/renderers/opentui/start.tsx +30 -0
- package/src/renderers/opentui/ui-host.tsx +2 -0
- package/src/sources/provider-router/index.test.ts +43 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/provider-router/supplemental.ts +46 -0
- package/src/sources/sec-edgar.test.ts +203 -2
- package/src/sources/sec-edgar.ts +70 -15
- package/src/state/app/context/index.tsx +13 -1
- package/src/state/app/context/selector.test.tsx +62 -1
- package/src/state/session-persistence.test.ts +13 -2
- package/src/sync/controller.test.ts +190 -3
- package/src/sync/controller.ts +76 -45
- package/src/sync/core-contributors.test.ts +121 -0
- package/src/sync/core-contributors.ts +104 -27
- package/src/sync/react.ts +6 -8
- package/src/sync/types.ts +3 -0
- package/src/time-series/alignment.ts +158 -0
- package/src/time-series/economic.ts +49 -0
- package/src/time-series/field-catalog.ts +306 -0
- package/src/time-series/fundamentals.test.ts +505 -0
- package/src/time-series/fundamentals.ts +842 -0
- package/src/time-series/hooks.ts +45 -0
- package/src/time-series/index.ts +14 -0
- package/src/time-series/live-quotes.test.ts +219 -0
- package/src/time-series/live-quotes.ts +203 -0
- package/src/time-series/market-resolution.ts +53 -0
- package/src/time-series/market.ts +125 -0
- package/src/time-series/reporting.ts +98 -0
- package/src/time-series/resolve.test.ts +1281 -0
- package/src/time-series/resolve.ts +783 -0
- package/src/time-series/spec-studies.test.ts +400 -0
- package/src/time-series/spec.ts +487 -0
- package/src/time-series/studies.ts +563 -0
- package/src/time-series/transforms.test.ts +105 -0
- package/src/time-series/transforms.ts +152 -0
- package/src/time-series/types.ts +162 -0
- package/src/time-series/use-chart-resolution.ts +166 -0
- package/src/types/config.ts +9 -11
- package/src/types/desktop-window.ts +1 -0
- package/src/types/financials.ts +4 -0
- package/src/types/media.ts +17 -0
- package/src/types/plugin.ts +32 -1
- package/src/ui/context-menu.tsx +15 -3
- package/src/ui/dialog.tsx +14 -2
- package/src/ui/host.tsx +40 -1
- package/src/ui/index.tsx +9 -0
- package/src/utils/exchanges.test.ts +13 -1
- package/src/utils/exchanges.ts +10 -0
- package/src/utils/financial-statements.test.ts +138 -0
- package/src/utils/financial-statements.ts +115 -8
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +7 -0
- package/src/version.ts +1 -1
- package/src/components/chart/bar-chart-renderer.test.ts +0 -233
- package/src/components/chart/bar-chart-renderer.ts +0 -445
- package/src/components/chart/comparison/data.test.ts +0 -159
- package/src/components/chart/comparison/data.ts +0 -302
- package/src/components/chart/comparison/renderer.test.ts +0 -187
- package/src/components/chart/comparison/renderer.ts +0 -282
- package/src/components/chart/comparison/scene.ts +0 -178
- package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
- package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
- package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
- package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
- package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
- package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
- package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
- package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
- package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
- package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
- package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
- package/src/components/chart/comparison-stock-chart/types.ts +0 -22
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
- package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
- package/src/components/chart/comparison-stock-chart.tsx +0 -24
- package/src/components/chart/core/axis-measure.ts +0 -33
- package/src/components/chart/core/controller.test.ts +0 -157
- package/src/components/chart/core/controller.ts +0 -349
- package/src/components/chart/core/indicator-selector.tsx +0 -75
- package/src/components/chart/core/keyboard.ts +0 -24
- package/src/components/chart/core/pane-settings.test.ts +0 -52
- package/src/components/chart/core/pane-settings.ts +0 -141
- package/src/components/chart/core/scroll.test.ts +0 -64
- package/src/components/chart/core/scroll.ts +0 -82
- package/src/components/chart/core/window-viewport.test.ts +0 -69
- package/src/components/chart/core/window-viewport.ts +0 -162
- package/src/components/chart/display-cursor.ts +0 -153
- package/src/components/chart/indicators/bands.test.ts +0 -36
- package/src/components/chart/indicators/bands.ts +0 -29
- package/src/components/chart/indicators/moving-averages.test.ts +0 -39
- package/src/components/chart/indicators/moving-averages.ts +0 -42
- package/src/components/chart/indicators/options.test.ts +0 -32
- package/src/components/chart/indicators/options.ts +0 -107
- package/src/components/chart/indicators/oscillators.test.ts +0 -58
- package/src/components/chart/indicators/oscillators.ts +0 -92
- package/src/components/chart/native/canvas-bitmaps.ts +0 -140
- package/src/components/chart/native/raster/comparison-chart.ts +0 -162
- package/src/components/chart/native/surface/hooks.ts +0 -144
- package/src/components/chart/native/surface/rendering.ts +0 -211
- package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
- package/src/components/chart/static/bar-chart-surface.tsx +0 -292
- package/src/components/chart/stock-chart/auto.ts +0 -216
- package/src/components/chart/stock-chart/axis-width.ts +0 -146
- package/src/components/chart/stock-chart/bitmaps.ts +0 -22
- package/src/components/chart/stock-chart/canvas.ts +0 -202
- package/src/components/chart/stock-chart/controls.tsx +0 -149
- package/src/components/chart/stock-chart/cursor.ts +0 -60
- package/src/components/chart/stock-chart/data-runtime.ts +0 -128
- package/src/components/chart/stock-chart/footer.test.ts +0 -65
- package/src/components/chart/stock-chart/footer.ts +0 -303
- package/src/components/chart/stock-chart/geometry.ts +0 -160
- package/src/components/chart/stock-chart/index.test.ts +0 -532
- package/src/components/chart/stock-chart/index.tsx +0 -29
- package/src/components/chart/stock-chart/indicators.ts +0 -226
- package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
- package/src/components/chart/stock-chart/interactions.ts +0 -300
- package/src/components/chart/stock-chart/keyboard.ts +0 -373
- package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
- package/src/components/chart/stock-chart/layout.tsx +0 -177
- package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
- package/src/components/chart/stock-chart/presentation.ts +0 -67
- package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
- package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
- package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
- package/src/components/chart/stock-chart/rendering/data.ts +0 -370
- package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
- package/src/components/chart/stock-chart/rendering/output.ts +0 -304
- package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
- package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
- package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
- package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
- package/src/components/chart/stock-chart/rendering/state.ts +0 -283
- package/src/components/chart/stock-chart/requests.ts +0 -337
- package/src/components/chart/stock-chart/resolution-support.ts +0 -96
- package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
- package/src/components/chart/stock-chart/resolved.tsx +0 -359
- package/src/components/chart/stock-chart/settings.ts +0 -81
- package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
- package/src/components/chart/stock-chart/types.ts +0 -25
- package/src/components/chart/stock-chart/view.tsx +0 -155
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
- package/src/components/chart/stock-chart/viewport/index.ts +0 -144
- package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
- package/src/components/chart/time-axis-label.tsx +0 -175
- package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
- package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
- package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
- package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
- package/src/plugins/builtin/econ/fred-cache.ts +0 -106
- package/src/plugins/builtin/plugin-groups.ts +0 -130
- package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PaneSettingActionContext,
|
|
3
|
+
PaneSettingField,
|
|
4
|
+
PaneSettingOption,
|
|
5
|
+
PaneSettingsDef,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import { migrateLegacyAiProviderId } from "./providers";
|
|
8
|
+
|
|
9
|
+
export const AI_DEFAULT_PROVIDER_SETTING_KEY = "defaultProviderId";
|
|
10
|
+
export const AI_DEFAULT_MODEL_SETTING_KEY = "defaultModelId";
|
|
11
|
+
export const AI_PANE_PROVIDER_SETTING_KEY = "providerId";
|
|
12
|
+
export const AI_PANE_MODEL_SETTING_KEY = "modelId";
|
|
13
|
+
export const AI_INHERIT_SETTING_VALUE = "";
|
|
14
|
+
|
|
15
|
+
export interface AiSharedDefaults {
|
|
16
|
+
providerId: string;
|
|
17
|
+
modelId: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResolveAiPaneSelectionOptions {
|
|
21
|
+
settings: Record<string, unknown> | undefined;
|
|
22
|
+
savedProviderId: string | null | undefined;
|
|
23
|
+
savedModelId: string | null | undefined;
|
|
24
|
+
defaultProviderId: string;
|
|
25
|
+
defaultModelId: string | null | undefined;
|
|
26
|
+
providerKey?: string;
|
|
27
|
+
modelKey?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AiSettingsModel {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
providerId: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AiSettingsProvider {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AiAccountSettingRow {
|
|
44
|
+
providerId: string;
|
|
45
|
+
providerLabel: string;
|
|
46
|
+
description: string;
|
|
47
|
+
actionLabel: string;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
action: (context: PaneSettingActionContext) => void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AiManageAccountsSettingRow {
|
|
53
|
+
description?: string;
|
|
54
|
+
actionLabel?: string;
|
|
55
|
+
action: (context: PaneSettingActionContext) => void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AiPaneOverrideSettings {
|
|
59
|
+
providerId: string | null | undefined;
|
|
60
|
+
modelId: string | null | undefined;
|
|
61
|
+
providerKey?: string;
|
|
62
|
+
modelKey?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BuildAiPaneSettingsOptions {
|
|
66
|
+
title: string;
|
|
67
|
+
providers: AiSettingsProvider[];
|
|
68
|
+
models: AiSettingsModel[];
|
|
69
|
+
defaultProviderId: string | null | undefined;
|
|
70
|
+
defaultModelId: string | null | undefined;
|
|
71
|
+
paneOverride?: AiPaneOverrideSettings;
|
|
72
|
+
accountRows?: AiAccountSettingRow[];
|
|
73
|
+
manageAccounts?: AiManageAccountsSettingRow;
|
|
74
|
+
additional?: PaneSettingsDef;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeId(value: string | null | undefined): string {
|
|
78
|
+
return value?.trim() ?? "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeProviderId(value: string | null | undefined): string {
|
|
82
|
+
const providerId = normalizeId(value);
|
|
83
|
+
return providerId ? migrateLegacyAiProviderId(providerId) : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function settingValue(
|
|
87
|
+
settings: Record<string, unknown> | undefined,
|
|
88
|
+
key: string,
|
|
89
|
+
): { configured: boolean; value: string } {
|
|
90
|
+
if (!settings || !Object.prototype.hasOwnProperty.call(settings, key)) {
|
|
91
|
+
return { configured: false, value: "" };
|
|
92
|
+
}
|
|
93
|
+
const value = settings[key];
|
|
94
|
+
return {
|
|
95
|
+
configured: true,
|
|
96
|
+
value: typeof value === "string" ? value.trim() : "",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveAiSharedDefaults(
|
|
101
|
+
pluginConfig: Record<string, unknown> | undefined,
|
|
102
|
+
fallbackProviderId: string,
|
|
103
|
+
): AiSharedDefaults {
|
|
104
|
+
const providerId = typeof pluginConfig?.[AI_DEFAULT_PROVIDER_SETTING_KEY] === "string"
|
|
105
|
+
? normalizeProviderId(pluginConfig[AI_DEFAULT_PROVIDER_SETTING_KEY] as string)
|
|
106
|
+
: "";
|
|
107
|
+
const modelId = typeof pluginConfig?.[AI_DEFAULT_MODEL_SETTING_KEY] === "string"
|
|
108
|
+
? normalizeId(pluginConfig[AI_DEFAULT_MODEL_SETTING_KEY] as string)
|
|
109
|
+
: "";
|
|
110
|
+
return {
|
|
111
|
+
providerId: providerId || normalizeProviderId(fallbackProviderId),
|
|
112
|
+
modelId: modelId || null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolves a pane's effective runner without rewriting the saved thread or tab.
|
|
118
|
+
* Missing settings preserve legacy saved selections. An explicitly blank value
|
|
119
|
+
* means "use the shared default", which lets old panes opt in intentionally.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveAiPaneSelection({
|
|
122
|
+
settings,
|
|
123
|
+
savedProviderId,
|
|
124
|
+
savedModelId,
|
|
125
|
+
defaultProviderId,
|
|
126
|
+
defaultModelId,
|
|
127
|
+
providerKey = AI_PANE_PROVIDER_SETTING_KEY,
|
|
128
|
+
modelKey = AI_PANE_MODEL_SETTING_KEY,
|
|
129
|
+
}: ResolveAiPaneSelectionOptions): AiSharedDefaults {
|
|
130
|
+
const providerSetting = settingValue(settings, providerKey);
|
|
131
|
+
const modelSetting = settingValue(settings, modelKey);
|
|
132
|
+
const configuredProvider = normalizeProviderId(providerSetting.value);
|
|
133
|
+
const savedProvider = normalizeProviderId(savedProviderId);
|
|
134
|
+
const sharedProvider = normalizeProviderId(defaultProviderId);
|
|
135
|
+
const providerId = providerSetting.configured
|
|
136
|
+
? configuredProvider || sharedProvider
|
|
137
|
+
: savedProvider || sharedProvider;
|
|
138
|
+
|
|
139
|
+
let modelId: string | null;
|
|
140
|
+
if (modelSetting.configured) {
|
|
141
|
+
modelId = modelSetting.value
|
|
142
|
+
|| (providerId === sharedProvider ? normalizeId(defaultModelId) : "")
|
|
143
|
+
|| null;
|
|
144
|
+
} else if (providerSetting.configured && configuredProvider !== savedProvider) {
|
|
145
|
+
modelId = (providerId === sharedProvider ? normalizeId(defaultModelId) : "") || null;
|
|
146
|
+
} else {
|
|
147
|
+
modelId = normalizeId(savedModelId) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { providerId, modelId };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function appendCurrentOption(
|
|
154
|
+
options: PaneSettingOption[],
|
|
155
|
+
currentValue: string,
|
|
156
|
+
label: string,
|
|
157
|
+
): PaneSettingOption[] {
|
|
158
|
+
if (!currentValue || options.some((option) => option.value === currentValue)) return options;
|
|
159
|
+
return [
|
|
160
|
+
...options,
|
|
161
|
+
{
|
|
162
|
+
value: currentValue,
|
|
163
|
+
label,
|
|
164
|
+
description: "Currently selected, but not present in the latest provider catalog.",
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function uniqueOptions(options: PaneSettingOption[]): PaneSettingOption[] {
|
|
170
|
+
const seen = new Set<string>();
|
|
171
|
+
return options.filter((option) => {
|
|
172
|
+
if (seen.has(option.value)) return false;
|
|
173
|
+
seen.add(option.value);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function providerOptions(
|
|
179
|
+
providers: AiSettingsProvider[],
|
|
180
|
+
currentValue: string,
|
|
181
|
+
): PaneSettingOption[] {
|
|
182
|
+
const options = uniqueOptions(providers
|
|
183
|
+
.map((provider) => ({
|
|
184
|
+
value: normalizeId(provider.id),
|
|
185
|
+
label: provider.label,
|
|
186
|
+
description: provider.description,
|
|
187
|
+
}))
|
|
188
|
+
.filter((provider) => provider.value));
|
|
189
|
+
return appendCurrentOption(options, currentValue, `${currentValue} (current)`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function modelOptions(
|
|
193
|
+
models: AiSettingsModel[],
|
|
194
|
+
providerId: string,
|
|
195
|
+
currentValue: string,
|
|
196
|
+
automaticLabel: string,
|
|
197
|
+
): PaneSettingOption[] {
|
|
198
|
+
const options = uniqueOptions([
|
|
199
|
+
{
|
|
200
|
+
value: AI_INHERIT_SETTING_VALUE,
|
|
201
|
+
label: automaticLabel,
|
|
202
|
+
},
|
|
203
|
+
...models
|
|
204
|
+
.filter((model) => normalizeId(model.providerId) === providerId)
|
|
205
|
+
.map((model) => ({
|
|
206
|
+
value: normalizeId(model.id),
|
|
207
|
+
label: model.label,
|
|
208
|
+
description: model.description,
|
|
209
|
+
}))
|
|
210
|
+
.filter((model) => model.value),
|
|
211
|
+
]);
|
|
212
|
+
return appendCurrentOption(options, currentValue, `${currentValue} (current)`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getProviderLabel(providers: AiSettingsProvider[], providerId: string): string {
|
|
216
|
+
return providers.find((provider) => normalizeId(provider.id) === providerId)?.label ?? providerId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function buildAiPaneSettingsDef(options: BuildAiPaneSettingsOptions): PaneSettingsDef {
|
|
220
|
+
const defaultProviderId = normalizeProviderId(options.defaultProviderId)
|
|
221
|
+
|| normalizeProviderId(options.providers[0]?.id);
|
|
222
|
+
const defaultModelId = normalizeId(options.defaultModelId);
|
|
223
|
+
const paneProviderId = normalizeProviderId(options.paneOverride?.providerId);
|
|
224
|
+
const paneModelId = normalizeId(options.paneOverride?.modelId);
|
|
225
|
+
const effectiveProviderId = paneProviderId || defaultProviderId;
|
|
226
|
+
const providerLabel = getProviderLabel(options.providers, defaultProviderId) || "configured provider";
|
|
227
|
+
const paneProviderKey = options.paneOverride?.providerKey ?? AI_PANE_PROVIDER_SETTING_KEY;
|
|
228
|
+
const paneModelKey = options.paneOverride?.modelKey ?? AI_PANE_MODEL_SETTING_KEY;
|
|
229
|
+
|
|
230
|
+
const fields: PaneSettingField[] = [
|
|
231
|
+
{
|
|
232
|
+
key: AI_DEFAULT_PROVIDER_SETTING_KEY,
|
|
233
|
+
label: "Default provider",
|
|
234
|
+
description: "Used by new AI panes and conversations.",
|
|
235
|
+
type: "select",
|
|
236
|
+
storage: "plugin",
|
|
237
|
+
clearOnChange: [AI_DEFAULT_MODEL_SETTING_KEY],
|
|
238
|
+
options: providerOptions(options.providers, defaultProviderId),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: AI_DEFAULT_MODEL_SETTING_KEY,
|
|
242
|
+
label: "Default model",
|
|
243
|
+
description: "Used when a pane does not have its own model override.",
|
|
244
|
+
type: "select",
|
|
245
|
+
storage: "plugin",
|
|
246
|
+
options: modelOptions(options.models, defaultProviderId, defaultModelId, "Auto (recommended)"),
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
if (options.paneOverride) {
|
|
251
|
+
fields.push(
|
|
252
|
+
{
|
|
253
|
+
key: paneProviderKey,
|
|
254
|
+
label: "Provider for this pane",
|
|
255
|
+
description: "Overrides the shared default for this pane only.",
|
|
256
|
+
type: "select",
|
|
257
|
+
clearOnChange: [paneModelKey],
|
|
258
|
+
options: [
|
|
259
|
+
{
|
|
260
|
+
value: AI_INHERIT_SETTING_VALUE,
|
|
261
|
+
label: `Use default · ${providerLabel}`,
|
|
262
|
+
},
|
|
263
|
+
...providerOptions(options.providers, paneProviderId),
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: paneModelKey,
|
|
268
|
+
label: "Model for this pane",
|
|
269
|
+
description: "Overrides the shared default for this pane only.",
|
|
270
|
+
type: "select",
|
|
271
|
+
options: modelOptions(options.models, effectiveProviderId, paneModelId, "Use default"),
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const account of options.accountRows ?? []) {
|
|
277
|
+
fields.push({
|
|
278
|
+
key: `account:${account.providerId}`,
|
|
279
|
+
label: `${account.providerLabel} account`,
|
|
280
|
+
description: account.description,
|
|
281
|
+
type: "action",
|
|
282
|
+
actionId: `ai:account:${account.providerId}`,
|
|
283
|
+
actionLabel: account.actionLabel,
|
|
284
|
+
disabled: account.disabled,
|
|
285
|
+
action: account.action,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (options.manageAccounts) {
|
|
290
|
+
fields.push({
|
|
291
|
+
key: "manageAiAccounts",
|
|
292
|
+
label: "AI accounts",
|
|
293
|
+
description: options.manageAccounts.description ?? "Connect, reconnect, or disconnect AI providers.",
|
|
294
|
+
type: "action",
|
|
295
|
+
actionId: "ai:manage-accounts",
|
|
296
|
+
actionLabel: options.manageAccounts.actionLabel ?? "Manage",
|
|
297
|
+
action: options.manageAccounts.action,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
title: options.title,
|
|
303
|
+
values: {
|
|
304
|
+
[AI_DEFAULT_PROVIDER_SETTING_KEY]: defaultProviderId,
|
|
305
|
+
[AI_DEFAULT_MODEL_SETTING_KEY]: defaultModelId,
|
|
306
|
+
...(options.paneOverride
|
|
307
|
+
? {
|
|
308
|
+
[paneProviderKey]: paneProviderId,
|
|
309
|
+
[paneModelKey]: paneModelId,
|
|
310
|
+
}
|
|
311
|
+
: {}),
|
|
312
|
+
...(options.additional?.values ?? {}),
|
|
313
|
+
},
|
|
314
|
+
fields: [
|
|
315
|
+
...fields,
|
|
316
|
+
...(options.additional?.fields ?? []),
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { PiFileCredentialStore, resolvePiCredentialPath } from "./credential-store";
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
async function tempDataDir(): Promise<string> {
|
|
10
|
+
const path = await mkdtemp(join(tmpdir(), "gloomberb-pi-credentials-"));
|
|
11
|
+
tempDirs.push(path);
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await Promise.all(tempDirs.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("PiFileCredentialStore", () => {
|
|
20
|
+
test("persists credentials outside AppConfig and never returns mutable storage references", async () => {
|
|
21
|
+
const dataDir = await tempDataDir();
|
|
22
|
+
const store = new PiFileCredentialStore(dataDir);
|
|
23
|
+
await store.modify("anthropic", async () => ({
|
|
24
|
+
type: "oauth",
|
|
25
|
+
access: "access-one",
|
|
26
|
+
refresh: "refresh-one",
|
|
27
|
+
expires: 123,
|
|
28
|
+
accountId: "account-one",
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const first = await store.read("anthropic");
|
|
32
|
+
expect(first?.type).toBe("oauth");
|
|
33
|
+
if (first?.type === "oauth") first.access = "mutated-only-in-memory";
|
|
34
|
+
|
|
35
|
+
const reloaded = await new PiFileCredentialStore(dataDir).read("anthropic");
|
|
36
|
+
expect(reloaded).toMatchObject({
|
|
37
|
+
type: "oauth",
|
|
38
|
+
access: "access-one",
|
|
39
|
+
refresh: "refresh-one",
|
|
40
|
+
expires: 123,
|
|
41
|
+
accountId: "account-one",
|
|
42
|
+
});
|
|
43
|
+
expect(resolvePiCredentialPath(dataDir)).toBe(join(dataDir, "ai", "credentials.json"));
|
|
44
|
+
|
|
45
|
+
if (process.platform !== "win32") {
|
|
46
|
+
expect((await stat(resolvePiCredentialPath(dataDir))).mode & 0o777).toBe(0o600);
|
|
47
|
+
expect((await stat(dirname(resolvePiCredentialPath(dataDir)))).mode & 0o777).toBe(0o700);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("serializes updates made through separate store instances", async () => {
|
|
52
|
+
const dataDir = await tempDataDir();
|
|
53
|
+
const first = new PiFileCredentialStore(dataDir);
|
|
54
|
+
const second = new PiFileCredentialStore(dataDir);
|
|
55
|
+
|
|
56
|
+
await Promise.all([
|
|
57
|
+
first.modify("anthropic", async () => ({ type: "api_key", key: "anthropic-key" })),
|
|
58
|
+
second.modify("openai", async () => ({ type: "api_key", key: "openai-key" })),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
expect(await first.list()).toEqual([
|
|
62
|
+
{ providerId: "anthropic", type: "api_key" },
|
|
63
|
+
{ providerId: "openai", type: "api_key" },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("fails closed instead of overwriting a malformed credential file", async () => {
|
|
68
|
+
const dataDir = await tempDataDir();
|
|
69
|
+
const path = resolvePiCredentialPath(dataDir);
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
await writeFile(path, JSON.stringify({ version: 1, credentials: { anthropic: { type: "oauth" } } }), "utf8");
|
|
72
|
+
|
|
73
|
+
const store = new PiFileCredentialStore(dataDir);
|
|
74
|
+
await expect(store.read("anthropic")).rejects.toThrow("stored AI credential");
|
|
75
|
+
await expect(store.modify("openai", async () => ({ type: "api_key", key: "key" }))).rejects.toThrow(
|
|
76
|
+
"stored AI credential",
|
|
77
|
+
);
|
|
78
|
+
expect(await readFile(path, "utf8")).toContain('"type":"oauth"');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiKeyCredential,
|
|
3
|
+
Credential,
|
|
4
|
+
CredentialInfo,
|
|
5
|
+
CredentialStore,
|
|
6
|
+
OAuthCredential,
|
|
7
|
+
} from "@earendil-works/pi-ai";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { LockedJsonFile } from "./file-store";
|
|
10
|
+
|
|
11
|
+
const CREDENTIAL_FILE_VERSION = 1;
|
|
12
|
+
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
13
|
+
|
|
14
|
+
interface CredentialFile {
|
|
15
|
+
version: typeof CREDENTIAL_FILE_VERSION;
|
|
16
|
+
credentials: Record<string, Credential>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertProviderId(providerId: string): void {
|
|
20
|
+
if (!PROVIDER_ID_PATTERN.test(providerId)) {
|
|
21
|
+
throw new Error(`Invalid AI provider id: ${providerId}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
26
|
+
return !!value
|
|
27
|
+
&& typeof value === "object"
|
|
28
|
+
&& !Array.isArray(value)
|
|
29
|
+
&& Object.values(value).every((entry) => typeof entry === "string");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isApiKeyCredential(value: unknown): value is ApiKeyCredential {
|
|
33
|
+
if (!value || typeof value !== "object" || (value as { type?: unknown }).type !== "api_key") return false;
|
|
34
|
+
const credential = value as { key?: unknown; env?: unknown };
|
|
35
|
+
return (credential.key === undefined || typeof credential.key === "string")
|
|
36
|
+
&& (credential.env === undefined || isStringRecord(credential.env));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isOAuthCredential(value: unknown): value is OAuthCredential {
|
|
40
|
+
if (!value || typeof value !== "object" || (value as { type?: unknown }).type !== "oauth") return false;
|
|
41
|
+
const credential = value as { access?: unknown; refresh?: unknown; expires?: unknown };
|
|
42
|
+
return typeof credential.access === "string"
|
|
43
|
+
&& typeof credential.refresh === "string"
|
|
44
|
+
&& typeof credential.expires === "number"
|
|
45
|
+
&& Number.isFinite(credential.expires);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCredential(value: unknown): value is Credential {
|
|
49
|
+
return isApiKeyCredential(value) || isOAuthCredential(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cloneCredential<T extends Credential | undefined>(credential: T): T {
|
|
53
|
+
return credential === undefined ? credential : structuredClone(credential);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateCredentialFile(value: unknown): CredentialFile {
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
throw new Error("The AI credential file is not an object.");
|
|
59
|
+
}
|
|
60
|
+
const candidate = value as { version?: unknown; credentials?: unknown };
|
|
61
|
+
if (candidate.version !== CREDENTIAL_FILE_VERSION) {
|
|
62
|
+
throw new Error(`Unsupported AI credential file version: ${String(candidate.version)}`);
|
|
63
|
+
}
|
|
64
|
+
if (!candidate.credentials || typeof candidate.credentials !== "object" || Array.isArray(candidate.credentials)) {
|
|
65
|
+
throw new Error("The AI credential file does not contain a credentials object.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const credentials: Record<string, Credential> = Object.create(null) as Record<string, Credential>;
|
|
69
|
+
for (const [providerId, credential] of Object.entries(candidate.credentials)) {
|
|
70
|
+
assertProviderId(providerId);
|
|
71
|
+
if (!isCredential(credential)) {
|
|
72
|
+
throw new Error(`The stored AI credential for ${providerId} is invalid.`);
|
|
73
|
+
}
|
|
74
|
+
credentials[providerId] = cloneCredential(credential);
|
|
75
|
+
}
|
|
76
|
+
return { version: CREDENTIAL_FILE_VERSION, credentials };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createCredentialFile(): CredentialFile {
|
|
80
|
+
return {
|
|
81
|
+
version: CREDENTIAL_FILE_VERSION,
|
|
82
|
+
credentials: Object.create(null) as Record<string, Credential>,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolvePiCredentialPath(dataDir: string): string {
|
|
87
|
+
return join(dataDir, "ai", "credentials.json");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Persistent backend credential store for Pi. Secrets never enter AppConfig. */
|
|
91
|
+
export class PiFileCredentialStore implements CredentialStore {
|
|
92
|
+
readonly path: string;
|
|
93
|
+
private readonly file: LockedJsonFile<CredentialFile>;
|
|
94
|
+
|
|
95
|
+
constructor(dataDir: string) {
|
|
96
|
+
this.path = resolvePiCredentialPath(dataDir);
|
|
97
|
+
this.file = new LockedJsonFile(this.path, {
|
|
98
|
+
create: createCredentialFile,
|
|
99
|
+
validate: validateCredentialFile,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async read(providerId: string): Promise<Credential | undefined> {
|
|
104
|
+
assertProviderId(providerId);
|
|
105
|
+
const file = await this.file.read();
|
|
106
|
+
return cloneCredential(file.credentials[providerId]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async list(): Promise<readonly CredentialInfo[]> {
|
|
110
|
+
const file = await this.file.read();
|
|
111
|
+
return Object.entries(file.credentials)
|
|
112
|
+
.map(([providerId, credential]) => ({ providerId, type: credential.type }))
|
|
113
|
+
.sort((left, right) => left.providerId.localeCompare(right.providerId));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async modify(
|
|
117
|
+
providerId: string,
|
|
118
|
+
update: (current: Credential | undefined) => Promise<Credential | undefined>,
|
|
119
|
+
): Promise<Credential | undefined> {
|
|
120
|
+
assertProviderId(providerId);
|
|
121
|
+
return this.file.update(async (file) => {
|
|
122
|
+
const current = cloneCredential(file.credentials[providerId]);
|
|
123
|
+
const next = await update(current);
|
|
124
|
+
if (next !== undefined) {
|
|
125
|
+
if (!isCredential(next)) throw new Error(`Refusing to store an invalid AI credential for ${providerId}.`);
|
|
126
|
+
file.credentials[providerId] = cloneCredential(next);
|
|
127
|
+
}
|
|
128
|
+
return cloneCredential(file.credentials[providerId]);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async delete(providerId: string): Promise<void> {
|
|
133
|
+
assertProviderId(providerId);
|
|
134
|
+
await this.file.update((file) => {
|
|
135
|
+
delete file.credentials[providerId];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { chmod, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const DIRECTORY_MODE = 0o700;
|
|
6
|
+
const FILE_MODE = 0o600;
|
|
7
|
+
|
|
8
|
+
export interface LockedJsonFileOptions<T> {
|
|
9
|
+
create: () => T;
|
|
10
|
+
validate: (value: unknown) => T;
|
|
11
|
+
lockTimeoutMs?: number;
|
|
12
|
+
staleLockMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function errorCode(error: unknown): string | null {
|
|
16
|
+
return error && typeof error === "object" && "code" in error
|
|
17
|
+
? String((error as { code?: unknown }).code)
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wait(delayMs: number): Promise<void> {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Small backend-only JSON store with atomic replacement and a cross-process
|
|
27
|
+
* lock. The lock is important because the TUI and desktop backend may share
|
|
28
|
+
* one Gloomberb data directory and Pi refreshes rotating OAuth tokens inside
|
|
29
|
+
* CredentialStore.modify().
|
|
30
|
+
*/
|
|
31
|
+
export class LockedJsonFile<T> {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
|
|
34
|
+
private readonly options: Required<Pick<LockedJsonFileOptions<T>, "lockTimeoutMs" | "staleLockMs">>
|
|
35
|
+
& Omit<LockedJsonFileOptions<T>, "lockTimeoutMs" | "staleLockMs">;
|
|
36
|
+
private writeQueue: Promise<void> = Promise.resolve();
|
|
37
|
+
|
|
38
|
+
constructor(path: string, options: LockedJsonFileOptions<T>) {
|
|
39
|
+
this.path = path;
|
|
40
|
+
this.options = {
|
|
41
|
+
...options,
|
|
42
|
+
lockTimeoutMs: options.lockTimeoutMs ?? 45_000,
|
|
43
|
+
staleLockMs: options.staleLockMs ?? 10 * 60_000,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async read(): Promise<T> {
|
|
48
|
+
let raw: string;
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(this.path, "utf8");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (errorCode(error) === "ENOENT") return this.options.create();
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let parsed: unknown;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Could not parse ${this.path}`, { cause: error });
|
|
61
|
+
}
|
|
62
|
+
return this.options.validate(parsed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
update<R>(mutate: (current: T) => R | Promise<R>): Promise<R> {
|
|
66
|
+
const queued = this.writeQueue.catch(() => {}).then(async () => {
|
|
67
|
+
const release = await this.acquireLock();
|
|
68
|
+
try {
|
|
69
|
+
const current = await this.read();
|
|
70
|
+
const result = await mutate(current);
|
|
71
|
+
await this.write(current);
|
|
72
|
+
return result;
|
|
73
|
+
} finally {
|
|
74
|
+
await release();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.writeQueue = queued.then(() => {}, () => {});
|
|
78
|
+
return queued;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async write(value: T): Promise<void> {
|
|
82
|
+
await mkdir(dirname(this.path), { recursive: true, mode: DIRECTORY_MODE });
|
|
83
|
+
await chmod(dirname(this.path), DIRECTORY_MODE).catch(() => {});
|
|
84
|
+
|
|
85
|
+
const temporaryPath = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
86
|
+
const handle = await open(temporaryPath, "wx", FILE_MODE);
|
|
87
|
+
try {
|
|
88
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
89
|
+
await handle.sync();
|
|
90
|
+
} finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await rename(temporaryPath, this.path);
|
|
96
|
+
await chmod(this.path, FILE_MODE).catch(() => {});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async acquireLock(): Promise<() => Promise<void>> {
|
|
104
|
+
const lockPath = `${this.path}.lock`;
|
|
105
|
+
const token = `${process.pid}:${randomUUID()}`;
|
|
106
|
+
const deadline = Date.now() + this.options.lockTimeoutMs;
|
|
107
|
+
await mkdir(dirname(this.path), { recursive: true, mode: DIRECTORY_MODE });
|
|
108
|
+
await chmod(dirname(this.path), DIRECTORY_MODE).catch(() => {});
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
try {
|
|
112
|
+
const handle = await open(lockPath, "wx", FILE_MODE);
|
|
113
|
+
try {
|
|
114
|
+
await handle.writeFile(token, "utf8");
|
|
115
|
+
await handle.sync();
|
|
116
|
+
} finally {
|
|
117
|
+
await handle.close();
|
|
118
|
+
}
|
|
119
|
+
return async () => {
|
|
120
|
+
const owner = await readFile(lockPath, "utf8").catch(() => null);
|
|
121
|
+
if (owner === token) await rm(lockPath, { force: true }).catch(() => {});
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lockStat = await stat(lockPath).catch(() => null);
|
|
128
|
+
if (lockStat && Date.now() - lockStat.mtimeMs >= this.options.staleLockMs) {
|
|
129
|
+
await rm(lockPath, { force: true }).catch(() => {});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (Date.now() >= deadline) {
|
|
133
|
+
throw new Error(`Timed out waiting for the AI store lock at ${lockPath}`);
|
|
134
|
+
}
|
|
135
|
+
await wait(25);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|