gloomberb 0.12.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
package/src/remote/schema.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
RemoteControlSchema,
|
|
3
|
+
RemoteJsonSchema,
|
|
4
|
+
RemoteOperationDescriptor,
|
|
5
|
+
RemoteOperationSchema,
|
|
6
|
+
RemoteResourceSchema,
|
|
7
|
+
RemoteSideEffectLevel,
|
|
8
|
+
RemoteWriteTier,
|
|
9
|
+
} from "./types";
|
|
2
10
|
|
|
3
11
|
export const REMOTE_RESOURCES: RemoteResourceSchema[] = [
|
|
4
12
|
{ uri: "app://snapshot", description: "Current app snapshot including layout, focus, panes, command bar, and plugin/capability catalogs." },
|
|
@@ -19,43 +27,262 @@ export const REMOTE_RESOURCES: RemoteResourceSchema[] = [
|
|
|
19
27
|
{ uri: "ui://tree", description: "Live semantic UI node tree populated by shared controls and interactive primitives." },
|
|
20
28
|
];
|
|
21
29
|
|
|
30
|
+
const stringSchema = { type: "string" } satisfies RemoteJsonSchema;
|
|
31
|
+
const requiredStringSchema = { type: "string", minLength: 1 } satisfies RemoteJsonSchema;
|
|
32
|
+
const booleanSchema = { type: "boolean" } satisfies RemoteJsonSchema;
|
|
33
|
+
const indexSchema = { type: "integer", minimum: 0 } satisfies RemoteJsonSchema;
|
|
34
|
+
const openObjectSchema = { type: "object", additionalProperties: true } satisfies RemoteJsonSchema;
|
|
35
|
+
const anySchema = {} satisfies RemoteJsonSchema;
|
|
36
|
+
|
|
37
|
+
function objectSchema(
|
|
38
|
+
properties: Record<string, RemoteJsonSchema> = {},
|
|
39
|
+
required: string[] = [],
|
|
40
|
+
): RemoteJsonSchema {
|
|
41
|
+
return {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties,
|
|
44
|
+
...(required.length > 0 ? { required } : {}),
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const paneIdInput = objectSchema({ paneId: requiredStringSchema }, ["paneId"]);
|
|
50
|
+
|
|
22
51
|
export const REMOTE_OPERATIONS: RemoteOperationSchema[] = [
|
|
23
|
-
op(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
op("
|
|
34
|
-
op(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
op(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
op(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
op(
|
|
52
|
+
op(
|
|
53
|
+
"app.openCommandBar",
|
|
54
|
+
"Open the command bar with an optional query and optional mode.",
|
|
55
|
+
"{ query?: string, mode?: 'command' | 'ticker' | 'default' }",
|
|
56
|
+
"local-write",
|
|
57
|
+
objectSchema({
|
|
58
|
+
query: stringSchema,
|
|
59
|
+
mode: { type: "string", enum: ["command", "ticker", "default"] },
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
op("app.closeCommandBar", "Close the command bar.", "{}", "local-write", objectSchema()),
|
|
63
|
+
op(
|
|
64
|
+
"app.setCommandBarQuery",
|
|
65
|
+
"Set the command bar query.",
|
|
66
|
+
"{ query: string }",
|
|
67
|
+
"local-write",
|
|
68
|
+
objectSchema({ query: requiredStringSchema }, ["query"]),
|
|
69
|
+
),
|
|
70
|
+
op(
|
|
71
|
+
"app.search",
|
|
72
|
+
"Open command-bar search without requiring UI prefix syntax.",
|
|
73
|
+
"{ mode?: 'command' | 'ticker' | 'default', query?: string }",
|
|
74
|
+
"local-write",
|
|
75
|
+
objectSchema({
|
|
76
|
+
mode: { type: "string", enum: ["command", "ticker", "default"] },
|
|
77
|
+
query: stringSchema,
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
80
|
+
op(
|
|
81
|
+
"app.switchPanel",
|
|
82
|
+
"Switch the active panel.",
|
|
83
|
+
"{ panel: 'left' | 'right' }",
|
|
84
|
+
"local-write",
|
|
85
|
+
objectSchema({ panel: { type: "string", enum: ["left", "right"] } }, ["panel"]),
|
|
86
|
+
),
|
|
87
|
+
op(
|
|
88
|
+
"app.notify",
|
|
89
|
+
"Show an in-app notification.",
|
|
90
|
+
"{ body: string, type?: 'info' | 'success' | 'error' }",
|
|
91
|
+
"local-write",
|
|
92
|
+
objectSchema({
|
|
93
|
+
body: requiredStringSchema,
|
|
94
|
+
type: { type: "string", enum: ["info", "success", "error"] },
|
|
95
|
+
}, ["body"]),
|
|
96
|
+
),
|
|
97
|
+
op(
|
|
98
|
+
"commandBar.activateResult",
|
|
99
|
+
"Activate a visible command-bar result by zero-based index, node id, item id, or label.",
|
|
100
|
+
"{ index?: number, nodeId?: string, itemId?: string, label?: string }",
|
|
101
|
+
"local-write",
|
|
102
|
+
objectSchema({ index: indexSchema, nodeId: stringSchema, itemId: stringSchema, label: stringSchema }),
|
|
103
|
+
),
|
|
104
|
+
op("pane.show", "Show or focus a pane type.", "{ paneId: string }", "local-write", paneIdInput),
|
|
105
|
+
op("pane.focus", "Focus a pane instance or pane type.", "{ paneId: string }", "local-write", paneIdInput),
|
|
106
|
+
op("pane.close", "Close a pane instance or pane type.", "{ paneId: string }", "local-write", paneIdInput),
|
|
107
|
+
op(
|
|
108
|
+
"pane.createFromTemplate",
|
|
109
|
+
"Create a pane from a registered template.",
|
|
110
|
+
"{ templateId: string, options?: object }",
|
|
111
|
+
"local-write",
|
|
112
|
+
objectSchema({ templateId: requiredStringSchema, options: openObjectSchema }, ["templateId"]),
|
|
113
|
+
),
|
|
114
|
+
op(
|
|
115
|
+
"pane.setState",
|
|
116
|
+
"Patch pane runtime state.",
|
|
117
|
+
"{ paneId: string, patch: object }",
|
|
118
|
+
"local-write",
|
|
119
|
+
objectSchema({ paneId: requiredStringSchema, patch: openObjectSchema }, ["paneId", "patch"]),
|
|
120
|
+
),
|
|
121
|
+
op(
|
|
122
|
+
"pane.setSetting",
|
|
123
|
+
"Set one pane setting using the pane's registered setting field when available.",
|
|
124
|
+
"{ paneId: string, key: string, value: any }",
|
|
125
|
+
"local-write",
|
|
126
|
+
objectSchema({ paneId: requiredStringSchema, key: requiredStringSchema, value: anySchema }, ["paneId", "key", "value"]),
|
|
127
|
+
),
|
|
128
|
+
op(
|
|
129
|
+
"ticker.navigate",
|
|
130
|
+
"Navigate a ticker into the best available ticker research target.",
|
|
131
|
+
"{ symbol: string, sourcePaneId?: string }",
|
|
132
|
+
"local-write",
|
|
133
|
+
objectSchema({ symbol: requiredStringSchema, sourcePaneId: stringSchema }, ["symbol"]),
|
|
134
|
+
),
|
|
135
|
+
op(
|
|
136
|
+
"ticker.pin",
|
|
137
|
+
"Open or focus a fixed ticker research pane.",
|
|
138
|
+
"{ symbol: string, floating?: boolean, forceNewPane?: boolean, paneType?: string }",
|
|
139
|
+
"local-write",
|
|
140
|
+
objectSchema({
|
|
141
|
+
symbol: requiredStringSchema,
|
|
142
|
+
floating: booleanSchema,
|
|
143
|
+
forceNewPane: booleanSchema,
|
|
144
|
+
paneType: stringSchema,
|
|
145
|
+
}, ["symbol"]),
|
|
146
|
+
),
|
|
147
|
+
op(
|
|
148
|
+
"ticker.select",
|
|
149
|
+
"Select a ticker in a target pane.",
|
|
150
|
+
"{ symbol: string, paneId?: string }",
|
|
151
|
+
"local-write",
|
|
152
|
+
objectSchema({ symbol: requiredStringSchema, paneId: stringSchema }, ["symbol"]),
|
|
153
|
+
),
|
|
154
|
+
op(
|
|
155
|
+
"ticker.switchTab",
|
|
156
|
+
"Switch ticker research tab.",
|
|
157
|
+
"{ tabId: string, paneId?: string }",
|
|
158
|
+
"local-write",
|
|
159
|
+
objectSchema({ tabId: requiredStringSchema, paneId: stringSchema }, ["tabId"]),
|
|
160
|
+
),
|
|
161
|
+
op(
|
|
162
|
+
"layout.switch",
|
|
163
|
+
"Switch active layout by index.",
|
|
164
|
+
"{ index: number }",
|
|
165
|
+
"local-write",
|
|
166
|
+
objectSchema({ index: indexSchema }, ["index"]),
|
|
167
|
+
),
|
|
168
|
+
op(
|
|
169
|
+
"layout.new",
|
|
170
|
+
"Create a new blank layout.",
|
|
171
|
+
"{ name: string }",
|
|
172
|
+
"local-write",
|
|
173
|
+
objectSchema({ name: requiredStringSchema }, ["name"]),
|
|
174
|
+
),
|
|
175
|
+
op(
|
|
176
|
+
"layout.rename",
|
|
177
|
+
"Rename a layout.",
|
|
178
|
+
"{ index: number, name: string }",
|
|
179
|
+
"local-write",
|
|
180
|
+
objectSchema({ index: indexSchema, name: requiredStringSchema }, ["index", "name"]),
|
|
181
|
+
),
|
|
182
|
+
op(
|
|
183
|
+
"layout.duplicate",
|
|
184
|
+
"Duplicate a layout.",
|
|
185
|
+
"{ index: number }",
|
|
186
|
+
"local-write",
|
|
187
|
+
objectSchema({ index: indexSchema }, ["index"]),
|
|
188
|
+
),
|
|
189
|
+
op(
|
|
190
|
+
"layout.delete",
|
|
191
|
+
"Delete a layout.",
|
|
192
|
+
"{ index: number }",
|
|
193
|
+
"local-write",
|
|
194
|
+
objectSchema({ index: indexSchema }, ["index"]),
|
|
195
|
+
"user-data",
|
|
196
|
+
),
|
|
197
|
+
op("layout.undo", "Undo last layout change.", "{}", "local-write", objectSchema()),
|
|
198
|
+
op("layout.redo", "Redo last layout change.", "{}", "local-write", objectSchema()),
|
|
199
|
+
op("layout.gridlock", "Tidy all panes into a dense layout.", "{}", "local-write", objectSchema()),
|
|
200
|
+
op(
|
|
201
|
+
"layout.closeFloating",
|
|
202
|
+
"Close all floating panes in the active layout.",
|
|
203
|
+
"{}",
|
|
204
|
+
"local-write",
|
|
205
|
+
objectSchema(),
|
|
206
|
+
),
|
|
207
|
+
op(
|
|
208
|
+
"layout.placePane",
|
|
209
|
+
"Move a pane to a layout region.",
|
|
210
|
+
"{ paneId: string, region: 'left' | 'right' | 'top' | 'bottom' | 'floating', relativeTo?: string }",
|
|
211
|
+
"local-write",
|
|
212
|
+
objectSchema({
|
|
213
|
+
paneId: requiredStringSchema,
|
|
214
|
+
region: { type: "string", enum: ["left", "right", "top", "bottom", "floating"] },
|
|
215
|
+
relativeTo: stringSchema,
|
|
216
|
+
}, ["paneId", "region"]),
|
|
217
|
+
),
|
|
218
|
+
op(
|
|
219
|
+
"layout.focusRegion",
|
|
220
|
+
"Focus a pane by visual layout region.",
|
|
221
|
+
"{ region: 'left' | 'right' | 'top' | 'bottom' | 'center' }",
|
|
222
|
+
"local-write",
|
|
223
|
+
objectSchema({
|
|
224
|
+
region: { type: "string", enum: ["left", "right", "top", "bottom", "center"] },
|
|
225
|
+
}, ["region"]),
|
|
226
|
+
),
|
|
227
|
+
op(
|
|
228
|
+
"layout.setGrid",
|
|
229
|
+
"Dock visible or specified panes into a simple grid.",
|
|
230
|
+
"{ paneIds?: string[], columns?: number }",
|
|
231
|
+
"local-write",
|
|
232
|
+
objectSchema({
|
|
233
|
+
paneIds: { type: "array", items: requiredStringSchema },
|
|
234
|
+
columns: { type: "integer", minimum: 1 },
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
op("desktop.popOutPane", "Pop a pane into a detached desktop window.", "{ paneId: string }", "local-write", paneIdInput),
|
|
238
|
+
op("desktop.dockPane", "Dock a detached desktop pane.", "{ paneId: string }", "local-write", paneIdInput),
|
|
239
|
+
op(
|
|
240
|
+
"desktop.closeDetachedPane",
|
|
241
|
+
"Close a detached desktop pane.",
|
|
242
|
+
"{ paneId: string }",
|
|
243
|
+
"local-write",
|
|
244
|
+
paneIdInput,
|
|
245
|
+
),
|
|
246
|
+
op(
|
|
247
|
+
"desktop.focusDetachedPane",
|
|
248
|
+
"Focus a detached desktop pane.",
|
|
249
|
+
"{ paneId: string }",
|
|
250
|
+
"local-write",
|
|
251
|
+
paneIdInput,
|
|
252
|
+
),
|
|
253
|
+
op(
|
|
254
|
+
"capability.invoke",
|
|
255
|
+
"Invoke a registered plugin capability operation.",
|
|
256
|
+
"{ capabilityId: string, operationId: string, payload?: object }",
|
|
257
|
+
"external-side-effect",
|
|
258
|
+
objectSchema({
|
|
259
|
+
capabilityId: requiredStringSchema,
|
|
260
|
+
operationId: requiredStringSchema,
|
|
261
|
+
payload: openObjectSchema,
|
|
262
|
+
}, ["capabilityId", "operationId"]),
|
|
263
|
+
),
|
|
264
|
+
op(
|
|
265
|
+
"ui.invoke",
|
|
266
|
+
"Invoke an action on a live semantic UI node.",
|
|
267
|
+
"{ nodeId: string, action?: string, input?: any }",
|
|
268
|
+
"local-write",
|
|
269
|
+
objectSchema({ nodeId: requiredStringSchema, action: stringSchema, input: anySchema }, ["nodeId"]),
|
|
270
|
+
),
|
|
271
|
+
op(
|
|
272
|
+
"ui.invokeMatching",
|
|
273
|
+
"Invoke an action on the first semantic UI node matching role, label, index, or metadata.",
|
|
274
|
+
"{ role?: string, label?: string, contains?: string, index?: number, action?: string, input?: any, metadata?: object }",
|
|
275
|
+
"local-write",
|
|
276
|
+
objectSchema({
|
|
277
|
+
role: stringSchema,
|
|
278
|
+
label: stringSchema,
|
|
279
|
+
contains: stringSchema,
|
|
280
|
+
index: indexSchema,
|
|
281
|
+
action: stringSchema,
|
|
282
|
+
input: anySchema,
|
|
283
|
+
metadata: openObjectSchema,
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
59
286
|
];
|
|
60
287
|
|
|
61
288
|
export const REMOTE_AGENT_HELP = {
|
|
@@ -134,11 +361,67 @@ export function remoteControlSchema(): RemoteControlSchema {
|
|
|
134
361
|
};
|
|
135
362
|
}
|
|
136
363
|
|
|
364
|
+
export function writeTierForSideEffectLevel(
|
|
365
|
+
sideEffectLevel: RemoteSideEffectLevel,
|
|
366
|
+
): RemoteWriteTier {
|
|
367
|
+
switch (sideEffectLevel) {
|
|
368
|
+
case "none":
|
|
369
|
+
return "read";
|
|
370
|
+
case "local-write":
|
|
371
|
+
return "ui-write";
|
|
372
|
+
case "network-write":
|
|
373
|
+
return "user-data";
|
|
374
|
+
case "external-side-effect":
|
|
375
|
+
case "external-trade":
|
|
376
|
+
return "broker";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function remoteOperationDescriptors(): RemoteOperationDescriptor[] {
|
|
381
|
+
return REMOTE_OPERATIONS.map(({ id, title, description, writeTier, inputSchema }) => ({
|
|
382
|
+
name: remoteOperationToolName(id),
|
|
383
|
+
title,
|
|
384
|
+
description,
|
|
385
|
+
writeTier,
|
|
386
|
+
inputSchema,
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function remoteOperationToolName(id: string): string {
|
|
391
|
+
return id
|
|
392
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
393
|
+
.toLowerCase();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function operationTitle(id: string): string {
|
|
397
|
+
const [scope = "", action = ""] = id.split(".");
|
|
398
|
+
return `${humanizeIdentifier(scope)}: ${humanizeIdentifier(action)}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function humanizeIdentifier(value: string): string {
|
|
402
|
+
if (value === "ui") return "UI";
|
|
403
|
+
return value
|
|
404
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
405
|
+
.replace(/^./, (character) => character.toUpperCase());
|
|
406
|
+
}
|
|
407
|
+
|
|
137
408
|
function op(
|
|
138
409
|
id: string,
|
|
139
410
|
description: string,
|
|
140
411
|
inputShape: string,
|
|
141
|
-
sideEffectLevel:
|
|
412
|
+
sideEffectLevel: RemoteSideEffectLevel,
|
|
413
|
+
inputSchema: RemoteJsonSchema,
|
|
414
|
+
writeTier = writeTierForSideEffectLevel(sideEffectLevel),
|
|
142
415
|
): RemoteOperationSchema {
|
|
143
|
-
return {
|
|
416
|
+
return {
|
|
417
|
+
id,
|
|
418
|
+
title: operationTitle(id),
|
|
419
|
+
description,
|
|
420
|
+
inputShape,
|
|
421
|
+
inputSchema,
|
|
422
|
+
sideEffectLevel,
|
|
423
|
+
writeTier,
|
|
424
|
+
requiresConfirmation: writeTier === "user-data" || writeTier === "broker",
|
|
425
|
+
dryRun: sideEffectLevel !== "none",
|
|
426
|
+
};
|
|
144
427
|
}
|
package/src/remote/types.ts
CHANGED
|
@@ -100,19 +100,66 @@ export interface RemoteResourceSchema {
|
|
|
100
100
|
patchable?: boolean;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
export type RemoteSideEffectLevel =
|
|
104
|
+
| "none"
|
|
105
|
+
| "local-write"
|
|
106
|
+
| "network-write"
|
|
107
|
+
| "external-side-effect"
|
|
108
|
+
| "external-trade";
|
|
109
|
+
|
|
110
|
+
export type RemoteWriteTier = "read" | "ui-write" | "user-data" | "broker";
|
|
111
|
+
|
|
112
|
+
export type RemoteJsonSchemaType =
|
|
113
|
+
| "object"
|
|
114
|
+
| "array"
|
|
115
|
+
| "string"
|
|
116
|
+
| "number"
|
|
117
|
+
| "integer"
|
|
118
|
+
| "boolean"
|
|
119
|
+
| "null";
|
|
120
|
+
|
|
121
|
+
/** Serializable subset of JSON Schema used for remote operation inputs. */
|
|
122
|
+
export interface RemoteJsonSchema {
|
|
123
|
+
type?: RemoteJsonSchemaType | RemoteJsonSchemaType[];
|
|
124
|
+
description?: string;
|
|
125
|
+
properties?: Record<string, RemoteJsonSchema>;
|
|
126
|
+
required?: string[];
|
|
127
|
+
additionalProperties?: boolean;
|
|
128
|
+
items?: RemoteJsonSchema;
|
|
129
|
+
enum?: Array<string | number | boolean | null>;
|
|
130
|
+
minimum?: number;
|
|
131
|
+
maximum?: number;
|
|
132
|
+
minItems?: number;
|
|
133
|
+
maxItems?: number;
|
|
134
|
+
minLength?: number;
|
|
135
|
+
maxLength?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
103
138
|
export interface RemoteOperationSchema {
|
|
104
139
|
id: string;
|
|
140
|
+
title: string;
|
|
105
141
|
description: string;
|
|
106
|
-
inputShape
|
|
142
|
+
inputShape: string;
|
|
143
|
+
inputSchema: RemoteJsonSchema;
|
|
107
144
|
outputShape?: string;
|
|
108
145
|
returns?: string;
|
|
109
146
|
recommendedNextReads?: string[];
|
|
110
147
|
examples?: unknown[];
|
|
111
|
-
sideEffectLevel:
|
|
148
|
+
sideEffectLevel: RemoteSideEffectLevel;
|
|
149
|
+
writeTier: RemoteWriteTier;
|
|
112
150
|
requiresConfirmation?: boolean;
|
|
113
151
|
dryRun?: boolean;
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
/** Stable operation metadata suitable for advertising outside the process. */
|
|
155
|
+
export interface RemoteOperationDescriptor {
|
|
156
|
+
name: string;
|
|
157
|
+
title: string;
|
|
158
|
+
description: string;
|
|
159
|
+
writeTier: RemoteWriteTier;
|
|
160
|
+
inputSchema: RemoteJsonSchema;
|
|
161
|
+
}
|
|
162
|
+
|
|
116
163
|
export interface RemoteControlSchema {
|
|
117
164
|
protocolVersion: number;
|
|
118
165
|
resources: RemoteResourceSchema[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordResearchActivity } from "../../api-client/research-activity";
|
|
1
2
|
import { newsProvider, type NewsCapability } from "../../capabilities";
|
|
2
3
|
import type { AppRuntimeServices, AppServicesFactoryOptions } from "../../core/app-service-ports";
|
|
3
4
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
@@ -23,6 +24,7 @@ export function createBrowserAppServices({
|
|
|
23
24
|
);
|
|
24
25
|
const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository, persistence);
|
|
25
26
|
dataProvider.attachRegistry(pluginRegistry);
|
|
27
|
+
const stopSavedActivity = pluginRegistry.events.on("command-bar:portfolio-membership-persisted", () => recordResearchActivity("ticker_saved"));
|
|
26
28
|
const newsService = new NewsService({
|
|
27
29
|
pollIntervalMs: () => Math.max(1, config.refreshIntervalMinutes) * 60_000,
|
|
28
30
|
});
|
|
@@ -51,6 +53,7 @@ export function createBrowserAppServices({
|
|
|
51
53
|
pluginRegistry,
|
|
52
54
|
ready,
|
|
53
55
|
destroy() {
|
|
56
|
+
stopSavedActivity();
|
|
54
57
|
setSharedMarketDataCoordinator(null);
|
|
55
58
|
setSharedNewsService(null);
|
|
56
59
|
newsService.stop();
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
normalizeConfigForSave,
|
|
7
7
|
normalizeLoadedConfig,
|
|
8
8
|
} from "../../data/config/store/normalize";
|
|
9
|
-
import { createDefaultConfig, type AppConfig } from "../../types/config";
|
|
9
|
+
import { createDefaultConfig, TICKER_RESEARCH_PANE_ID, type AppConfig } from "../../types/config";
|
|
10
|
+
import { researchEntryFromSearch } from "./research-entry";
|
|
10
11
|
import { BROWSER_STORAGE_KEYS, SafeJsonStorage, type StorageLike } from "./storage";
|
|
11
12
|
|
|
12
13
|
export const BROWSER_DATA_DIR = "browser://local";
|
|
@@ -15,18 +16,33 @@ function browserReady(config: AppConfig): AppConfig {
|
|
|
15
16
|
return { ...config, onboardingComplete: true, onboardingProgress: undefined };
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function createBrowserDefaultConfig(dataDir: string): AppConfig {
|
|
19
|
-
|
|
19
|
+
function createBrowserDefaultConfig(dataDir: string, search = ""): AppConfig {
|
|
20
|
+
const config = createDefaultConfig(dataDir);
|
|
21
|
+
const entry = researchEntryFromSearch(search) ?? { symbol: "NVDA", tab: "overview" };
|
|
22
|
+
// A first visit starts with one useful research pane. Saved layouts retain
|
|
23
|
+
// their existing panes and bindings when a visitor returns.
|
|
24
|
+
config.layouts[0] = {
|
|
25
|
+
name: "Research",
|
|
26
|
+
layout: {
|
|
27
|
+
dockRoot: { kind: "pane", instanceId: "ticker-detail:main" },
|
|
28
|
+
instances: [{ instanceId: "ticker-detail:main", paneId: TICKER_RESEARCH_PANE_ID,
|
|
29
|
+
binding: { kind: "fixed", symbol: entry.symbol }, settings: { hideTabs: false } }],
|
|
30
|
+
floating: [], detached: [],
|
|
31
|
+
},
|
|
32
|
+
paneState: { "ticker-detail:main": { activeTabId: entry.tab } },
|
|
33
|
+
};
|
|
34
|
+
config.layout = config.layouts[0].layout;
|
|
35
|
+
return browserReady(config);
|
|
20
36
|
}
|
|
21
37
|
|
|
22
|
-
export function createBrowserConfigStore(storage: StorageLike): ConfigStoreHost {
|
|
38
|
+
export function createBrowserConfigStore(storage: StorageLike, search = ""): ConfigStoreHost {
|
|
23
39
|
const data = new SafeJsonStorage<unknown>(storage, BROWSER_STORAGE_KEYS.config, null);
|
|
24
40
|
return {
|
|
25
41
|
async getDataDir() { return BROWSER_DATA_DIR; },
|
|
26
42
|
async loadConfig(dataDir) {
|
|
27
43
|
const saved = data.get();
|
|
28
44
|
if (!saved || typeof saved !== "object" || Array.isArray(saved)) {
|
|
29
|
-
return createBrowserDefaultConfig(dataDir);
|
|
45
|
+
return createBrowserDefaultConfig(dataDir, search);
|
|
30
46
|
}
|
|
31
47
|
return browserReady(normalizeLoadedConfig(saved as Record<string, unknown>, dataDir).config);
|
|
32
48
|
},
|
|
@@ -34,7 +50,7 @@ export function createBrowserConfigStore(storage: StorageLike): ConfigStoreHost
|
|
|
34
50
|
data.set(normalizeConfigForSave(browserReady({ ...config, dataDir: BROWSER_DATA_DIR })));
|
|
35
51
|
},
|
|
36
52
|
async initDataDir(dataDir) {
|
|
37
|
-
const config = createBrowserDefaultConfig(dataDir);
|
|
53
|
+
const config = createBrowserDefaultConfig(dataDir, search);
|
|
38
54
|
data.set(config);
|
|
39
55
|
return config;
|
|
40
56
|
},
|
|
@@ -42,7 +58,7 @@ export function createBrowserConfigStore(storage: StorageLike): ConfigStoreHost
|
|
|
42
58
|
for (const key of Object.values(BROWSER_STORAGE_KEYS)) {
|
|
43
59
|
try { storage.removeItem(key); } catch {}
|
|
44
60
|
}
|
|
45
|
-
data.set(createBrowserDefaultConfig(dataDir));
|
|
61
|
+
data.set(createBrowserDefaultConfig(dataDir, search));
|
|
46
62
|
},
|
|
47
63
|
async exportConfig() {
|
|
48
64
|
throw new Error("Config file export is unavailable in the browser.");
|
|
@@ -54,5 +70,5 @@ export function createBrowserConfigStore(storage: StorageLike): ConfigStoreHost
|
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export function installBrowserConfigStore(storage: StorageLike = localStorage): void {
|
|
57
|
-
setConfigStoreHost(createBrowserConfigStore(storage));
|
|
73
|
+
setConfigStoreHost(createBrowserConfigStore(storage, typeof location !== "undefined" ? location.search : ""));
|
|
58
74
|
}
|
|
@@ -9,6 +9,17 @@ afterEach(() => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
describe("browser social share handoff", () => {
|
|
12
|
+
test("preserves the incoming ticker while the previous workspace restores", () => {
|
|
13
|
+
const location = { search: "?ticker=NVDA&tab=earnings-calls" };
|
|
14
|
+
Object.defineProperty(globalThis, "window", { configurable: true, value: {
|
|
15
|
+
location, addEventListener() {}, removeEventListener() {},
|
|
16
|
+
} });
|
|
17
|
+
const bridge = createBrowserDeepLinkBridge();
|
|
18
|
+
location.search = "?ticker=AAPL&tab=overview";
|
|
19
|
+
const seen: string[] = [];
|
|
20
|
+
bridge.subscribe(({ url }) => seen.push(url));
|
|
21
|
+
expect(seen).toEqual(["gloomberb://ticker/NVDA?tab=earnings-calls"]);
|
|
22
|
+
});
|
|
12
23
|
test("maps a valid pane share query to the common deep-link runtime", () => {
|
|
13
24
|
const id = "0123456789abcdef0123456789abcdef";
|
|
14
25
|
Object.defineProperty(globalThis, "window", {
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { marketplaceLayoutIdFromSearch } from "../../layout-marketplace/api";
|
|
2
2
|
import { paneShareIdFromSearch } from "../../shares/location";
|
|
3
3
|
import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
|
|
4
|
+
import { researchEntryFromSearch } from "./research-entry";
|
|
4
5
|
|
|
5
6
|
export function createBrowserDeepLinkBridge(): DesktopDeepLinkBridge {
|
|
7
|
+
// Restoring a saved pane can update the address before App subscribes.
|
|
8
|
+
// Keep the incoming link intact until it has been delivered once.
|
|
9
|
+
const initialSearch = window.location.search;
|
|
6
10
|
return {
|
|
7
11
|
subscribe(listener) {
|
|
12
|
+
let initial = true;
|
|
8
13
|
const emit = () => {
|
|
9
|
-
const
|
|
14
|
+
const search = initial ? initialSearch : window.location.search;
|
|
15
|
+
initial = false;
|
|
16
|
+
const layoutId = marketplaceLayoutIdFromSearch(search);
|
|
10
17
|
if (layoutId) {
|
|
11
18
|
listener({ url: `gloomberb://layout/${layoutId}` });
|
|
12
19
|
return;
|
|
13
20
|
}
|
|
14
|
-
const shareId = paneShareIdFromSearch(
|
|
15
|
-
if (shareId)
|
|
21
|
+
const shareId = paneShareIdFromSearch(search);
|
|
22
|
+
if (shareId) {
|
|
23
|
+
listener({ url: `gloomberb://share/${shareId}` });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const entry = researchEntryFromSearch(search);
|
|
27
|
+
if (entry) listener({ url: `gloomberb://ticker/${encodeURIComponent(entry.symbol)}?tab=${entry.tab}` });
|
|
16
28
|
};
|
|
17
29
|
emit();
|
|
18
30
|
window.addEventListener("popstate", emit);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { BROWSER_DATA_DIR, installBrowserConfigStore } from "./config-host";
|
|
22
22
|
import { browserRendererHost, browserUiHost } from "./ui-host";
|
|
23
23
|
import { createBrowserDeepLinkBridge } from "./deeplink-bridge";
|
|
24
|
+
import { initializeBrowserResearchActivity, recordResearchActivity } from "../../api-client/research-activity";
|
|
24
25
|
|
|
25
26
|
// Declared here rather than sniffed: the desktop view and the hosted browser
|
|
26
27
|
// app are both browser contexts but differ in what plugins may do.
|
|
@@ -36,8 +37,10 @@ root.render(<div className="gloom-loading">Starting Gloomberb...</div>);
|
|
|
36
37
|
async function boot(): Promise<void> {
|
|
37
38
|
installBrowserConfigStore();
|
|
38
39
|
installBrowserFetchTransports();
|
|
40
|
+
initializeBrowserResearchActivity();
|
|
39
41
|
installFocusScopeRelease();
|
|
40
42
|
await restoreBrowserCloudSession();
|
|
43
|
+
recordResearchActivity("workspace_opened");
|
|
41
44
|
const config = await loadConfig(BROWSER_DATA_DIR);
|
|
42
45
|
applyLanguageFromConfig(config);
|
|
43
46
|
const deepLinkBridge = createBrowserDeepLinkBridge();
|
|
@@ -53,7 +56,6 @@ async function boot(): Promise<void> {
|
|
|
53
56
|
plugins={getBrowserBuiltinPlugins()}
|
|
54
57
|
desktopDeepLinkBridge={deepLinkBridge}
|
|
55
58
|
updatesEnabled={false}
|
|
56
|
-
requireSignIn
|
|
57
59
|
/>
|
|
58
60
|
</WebDialogHostProvider>
|
|
59
61
|
</WebToastHostProvider>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { researchEntryFromSearch } from "./research-entry";
|
|
3
|
+
|
|
4
|
+
test("normalizes ticker links while rejecting commands and invalid tab paths", () => {
|
|
5
|
+
expect(researchEntryFromSearch("?ticker=brk.b&tab=earnings-calls")).toEqual({ symbol: "BRK.B", tab: "earnings-calls" });
|
|
6
|
+
expect(researchEntryFromSearch("?ticker=NVDA&tab=../../bad")).toEqual({ symbol: "NVDA", tab: "overview" });
|
|
7
|
+
for (const search of ["", "?ticker=", "?ticker=NVDA%20%3B%20rm", "?ticker=%3Cscript%3E"]) expect(researchEntryFromSearch(search)).toBeNull();
|
|
8
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function researchEntryFromSearch(search: string): { symbol: string; tab: string } | null {
|
|
2
|
+
const params = new URLSearchParams(search);
|
|
3
|
+
const symbol = params.get("ticker")?.trim().toUpperCase();
|
|
4
|
+
if (!symbol || !/^[A-Z0-9.^=:_-]{1,24}$/.test(symbol)) return null;
|
|
5
|
+
const tab = params.get("tab") ?? "overview";
|
|
6
|
+
return { symbol, tab: /^[a-z-]{1,40}$/.test(tab) ? tab : "overview" };
|
|
7
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAppServices, type AppServices } from "../../../../core/app-services";
|
|
2
|
-
import {
|
|
2
|
+
import { loadDesktopBackendPlugins } from "../../../../plugins/catalog-backend";
|
|
3
3
|
import type { AppSessionSnapshot } from "../../../../core/state/session-persistence";
|
|
4
4
|
import {
|
|
5
5
|
exportConfig,
|
|
@@ -63,7 +63,7 @@ async function importDesktopConfig({
|
|
|
63
63
|
setCurrentConfig(await importConfig(payload.dataDir, payload.srcPath));
|
|
64
64
|
setServices(createAppServices({
|
|
65
65
|
config: getConfig(),
|
|
66
|
-
plugins:
|
|
66
|
+
plugins: await loadDesktopBackendPlugins(),
|
|
67
67
|
}));
|
|
68
68
|
syncConfigAccessors();
|
|
69
69
|
registerCoreCapabilities();
|