gloomberb 0.11.1 → 0.11.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 +10 -9
- package/README.zh-CN.md +6 -6
- package/package.json +1 -1
- package/src/api-client/data.ts +7 -0
- package/src/api-client/index.test.ts +47 -0
- package/src/api-client/index.ts +61 -0
- package/src/api-client/paths.ts +16 -0
- package/src/api-client/types.ts +35 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app.tsx +9 -2
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +0 -2
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +12 -9
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +74 -10
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +86 -30
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +3 -4
- package/src/i18n/ja.ts +3 -4
- package/src/i18n/ko.ts +3 -4
- package/src/i18n/zh-cn.ts +3 -4
- package/src/i18n/zh-tw.ts +3 -4
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/hooks.tsx +0 -41
- package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
- package/src/plugins/broker-sync/normalize.ts +9 -2
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
- package/src/plugins/builtin/alerts/index.tsx +4 -0
- package/src/plugins/builtin/alerts/pane.tsx +2 -8
- package/src/plugins/builtin/analytics/index.tsx +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/index.ts +8 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller.test.ts +64 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
- package/src/plugins/builtin/cloud/auth-model.ts +4 -1
- package/src/plugins/builtin/cloud/plugin.tsx +11 -1
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +2 -2
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
- package/src/plugins/builtin/research/index.tsx +4 -0
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +10 -0
- package/src/plugins/catalog-browser.ts +3 -1
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/index.ts +5 -1
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +4 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/cloud-transport.test.ts +36 -0
- package/src/renderers/browser/cloud-transport.ts +19 -0
- package/src/renderers/browser/config-host.ts +0 -4
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/main.tsx +3 -0
- package/src/renderers/cloudflare/worker.test.ts +8 -5
- package/src/renderers/cloudflare/worker.ts +6 -5
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +4 -1
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +54 -6
- package/src/renderers/share/styles.css +35 -0
- package/src/renderers/share/view.test.tsx +78 -0
- package/src/renderers/share/view.tsx +71 -0
- package/src/shares/api.test.ts +52 -3
- package/src/shares/api.ts +39 -15
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +74 -1
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/controller.test.ts +57 -1
- package/src/sync/controller.ts +22 -11
- package/src/sync/react.ts +13 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import { DataTableView, type DataTableColumn } from "../../../components";
|
|
2
3
|
import { colors } from "../../../theme/colors";
|
|
3
4
|
import { formatNumber } from "../../../utils/format";
|
|
@@ -24,20 +25,44 @@ export function PredictionMarketTradesView({
|
|
|
24
25
|
trades: PredictionTrade[];
|
|
25
26
|
width: number;
|
|
26
27
|
}) {
|
|
28
|
+
const visibleTrades = trades.slice(0, 30);
|
|
29
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(() =>
|
|
30
|
+
visibleTrades.length > 0 ? 0 : null,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setSelectedIndex((current) => {
|
|
35
|
+
if (visibleTrades.length === 0) return null;
|
|
36
|
+
if (current == null || current >= visibleTrades.length) return 0;
|
|
37
|
+
return current;
|
|
38
|
+
});
|
|
39
|
+
}, [visibleTrades.length]);
|
|
40
|
+
|
|
27
41
|
return (
|
|
28
42
|
<DataTableView<PredictionTrade, TradeColumn>
|
|
29
43
|
focused={focused}
|
|
30
|
-
keyboardNavigation={
|
|
44
|
+
keyboardNavigation={focused}
|
|
31
45
|
rootWidth={width}
|
|
32
46
|
rootBackgroundColor={colors.panel}
|
|
33
|
-
selection={{
|
|
47
|
+
selection={{
|
|
48
|
+
kind: "index",
|
|
49
|
+
selectedIndex,
|
|
50
|
+
onChange: (index) => setSelectedIndex(index),
|
|
51
|
+
}}
|
|
34
52
|
columns={TRADE_COLUMNS}
|
|
35
|
-
items={
|
|
53
|
+
items={visibleTrades}
|
|
36
54
|
sortColumnId={null}
|
|
37
55
|
sortDirection="asc"
|
|
38
56
|
onHeaderClick={() => {}}
|
|
39
57
|
getItemKey={(trade) => trade.id}
|
|
40
|
-
|
|
58
|
+
onRowMouseDown={(_trade, index, event) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
setSelectedIndex(index);
|
|
61
|
+
return true;
|
|
62
|
+
}}
|
|
63
|
+
renderCell={(trade, column, _index, rowState) => {
|
|
64
|
+
const color = (fallback: string) =>
|
|
65
|
+
rowState.selected ? undefined : fallback;
|
|
41
66
|
const tradeColor = trade.side === "buy" ? colors.positive : colors.negative;
|
|
42
67
|
switch (column.id) {
|
|
43
68
|
case "time":
|
|
@@ -45,27 +70,27 @@ export function PredictionMarketTradesView({
|
|
|
45
70
|
text: new Date(trade.timestamp).toLocaleTimeString("en-US", {
|
|
46
71
|
hour12: false,
|
|
47
72
|
}),
|
|
48
|
-
color: colors.textDim,
|
|
73
|
+
color: color(colors.textDim),
|
|
49
74
|
};
|
|
50
75
|
case "side":
|
|
51
76
|
return {
|
|
52
77
|
text: trade.side.toUpperCase(),
|
|
53
|
-
color: tradeColor,
|
|
78
|
+
color: color(tradeColor),
|
|
54
79
|
};
|
|
55
80
|
case "outcome":
|
|
56
81
|
return {
|
|
57
82
|
text: trade.outcome.toUpperCase(),
|
|
58
|
-
color: colors.text,
|
|
83
|
+
color: color(colors.text),
|
|
59
84
|
};
|
|
60
85
|
case "price":
|
|
61
86
|
return {
|
|
62
87
|
text: formatPredictionProbability(trade.price),
|
|
63
|
-
color: tradeColor,
|
|
88
|
+
color: color(tradeColor),
|
|
64
89
|
};
|
|
65
90
|
case "size":
|
|
66
91
|
return {
|
|
67
92
|
text: formatNumber(trade.size, 0),
|
|
68
|
-
color: colors.textDim,
|
|
93
|
+
color: color(colors.textDim),
|
|
69
94
|
};
|
|
70
95
|
}
|
|
71
96
|
}}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
2
3
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
3
4
|
import {
|
|
4
5
|
ChartHarness,
|
|
@@ -32,7 +33,7 @@ describe("prediction markets detail views", () => {
|
|
|
32
33
|
expect(frame).not.toContain("TypeError");
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
test("renders grouped
|
|
36
|
+
test("renders grouped outcomes with their chart directly below", async () => {
|
|
36
37
|
testSetup = await testRender(<GroupedDetailHarness />, {
|
|
37
38
|
width: 64,
|
|
38
39
|
height: 24,
|
|
@@ -43,6 +44,9 @@ describe("prediction markets detail views", () => {
|
|
|
43
44
|
expect(frame).toContain("Outcomes");
|
|
44
45
|
expect(frame).toContain("Above 4.25%");
|
|
45
46
|
expect(frame).toContain("Above 4.50%");
|
|
47
|
+
expect(frame).toContain("1M");
|
|
48
|
+
expect(frame.indexOf("1M")).toBeGreaterThan(frame.indexOf("Above 4.50%"));
|
|
49
|
+
expect(frame).not.toContain(" Chart ");
|
|
46
50
|
expect(frame).not.toContain("Ranked by implied YES probability.");
|
|
47
51
|
expect(frame).not.toContain("TOP Above 4.25%");
|
|
48
52
|
expect(frame).not.toContain("Kalshi");
|
|
@@ -59,9 +63,32 @@ describe("prediction markets detail views", () => {
|
|
|
59
63
|
const frame = testSetup.captureCharFrame();
|
|
60
64
|
expect(frame).toContain("Outcomes");
|
|
61
65
|
expect(frame).toContain("Above 4.25%");
|
|
62
|
-
expect(frame).toContain("
|
|
66
|
+
expect(frame).toContain("Loading chart...");
|
|
63
67
|
expect(frame).not.toContain("No chart history.");
|
|
64
68
|
expect(frame).not.toContain("Loading market detail...");
|
|
65
69
|
});
|
|
66
70
|
|
|
71
|
+
test("shows the chart crosshair on pointer movement", async () => {
|
|
72
|
+
testSetup = await testRender(
|
|
73
|
+
<ChartHarness
|
|
74
|
+
history={[
|
|
75
|
+
{ date: "2026-04-01T00:00:00Z", close: 0.45 },
|
|
76
|
+
{ date: "2026-04-02T00:00:00Z", close: 0.48 },
|
|
77
|
+
{ date: "2026-04-03T00:00:00Z", close: 0.51 },
|
|
78
|
+
]}
|
|
79
|
+
/>,
|
|
80
|
+
{ width: 80, height: 12 },
|
|
81
|
+
);
|
|
82
|
+
await flushFrames(testSetup);
|
|
83
|
+
const initialFrame = testSetup.captureCharFrame();
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
await testSetup!.mockMouse.moveTo(30, 5);
|
|
87
|
+
await testSetup!.renderOnce();
|
|
88
|
+
});
|
|
89
|
+
await flushFrames(testSetup);
|
|
90
|
+
|
|
91
|
+
expect(testSetup.captureCharFrame()).not.toBe(initialFrame);
|
|
92
|
+
});
|
|
93
|
+
|
|
67
94
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GloomPlugin, GloomPluginContext } from "../../types/plugin";
|
|
2
|
+
import { resolvePaneInstance } from "../../types/config";
|
|
2
3
|
import { parsePredictionSearchShortcut } from "./navigation";
|
|
3
4
|
import { PredictionMarketsPane } from "./pane";
|
|
4
5
|
import { attachPredictionMarketsPersistence, resetPredictionMarketsPersistence } from "./services/fetch";
|
|
@@ -15,10 +16,13 @@ const MAIN_INSTANCE_ID = `${PANE_ID}:main`;
|
|
|
15
16
|
|
|
16
17
|
function openPredictionMarkets(ctx: GloomPluginContext, query = ""): void {
|
|
17
18
|
const parsed = parsePredictionSearchShortcut(query);
|
|
18
|
-
ctx.
|
|
19
|
-
|
|
20
|
-
ctx.resume.setPaneState(
|
|
21
|
-
ctx.
|
|
19
|
+
const existingInstanceId = resolvePaneInstance(ctx.getConfig().layout, PANE_ID)?.instanceId;
|
|
20
|
+
const targetInstanceId = existingInstanceId ?? MAIN_INSTANCE_ID;
|
|
21
|
+
ctx.resume.setPaneState(targetInstanceId, "venueScope", parsed.venueScope);
|
|
22
|
+
ctx.resume.setPaneState(targetInstanceId, "searchQuery", parsed.searchQuery);
|
|
23
|
+
ctx.resume.setPaneState(targetInstanceId, "selectedRowKey", null);
|
|
24
|
+
ctx.resume.setPaneState(targetInstanceId, "selectedDetailMarketKey", null);
|
|
25
|
+
ctx.focusPane(existingInstanceId ?? PANE_ID);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const predictionMarketsPlugin: GloomPlugin = {
|
|
@@ -28,7 +28,6 @@ export const DETAIL_TABS: ReadonlyArray<{
|
|
|
28
28
|
value: PredictionDetailTab;
|
|
29
29
|
}> = [
|
|
30
30
|
{ label: "Overview", value: "overview" },
|
|
31
|
-
{ label: "Chart", value: "chart" },
|
|
32
31
|
{ label: "Book", value: "book" },
|
|
33
32
|
{ label: "Trades", value: "trades" },
|
|
34
33
|
{ label: "Rules", value: "rules" },
|
|
@@ -340,6 +340,83 @@ describe("prediction markets pane interactions", () => {
|
|
|
340
340
|
expect(frame).toContain("/ search markets");
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
test("navigates the book and trades tables with arrow keys", async () => {
|
|
344
|
+
installPredictionMarketMocks();
|
|
345
|
+
|
|
346
|
+
testSetup = await testRender(<Harness />, { width: 120, height: 34 });
|
|
347
|
+
await flushFrames(testSetup);
|
|
348
|
+
await emitKeypress(testSetup, { name: "j", sequence: "j" });
|
|
349
|
+
await emitKeypress(testSetup, { name: "enter", sequence: "\r" });
|
|
350
|
+
await flushFrames(testSetup);
|
|
351
|
+
|
|
352
|
+
let rows = testSetup.captureCharFrame().split("\n");
|
|
353
|
+
const detailTabsRow = rows.findIndex((line) =>
|
|
354
|
+
line.includes("Overview") && line.includes("Book") && line.includes("Trades"),
|
|
355
|
+
);
|
|
356
|
+
const bookColumn = rows[detailTabsRow]!.indexOf("Book");
|
|
357
|
+
await act(async () => {
|
|
358
|
+
await testSetup!.mockMouse.click(bookColumn, detailTabsRow);
|
|
359
|
+
await testSetup!.renderOnce();
|
|
360
|
+
});
|
|
361
|
+
await flushFrames(testSetup);
|
|
362
|
+
|
|
363
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
364
|
+
let headerRow = rows.findIndex((line) =>
|
|
365
|
+
line.includes("OUT") && line.includes("SIDE") && line.includes("PRICE"),
|
|
366
|
+
);
|
|
367
|
+
expect(headerRow).toBeGreaterThanOrEqual(0);
|
|
368
|
+
let spans = testSetup.captureSpans().lines;
|
|
369
|
+
const bookFirstBefore = spans[headerRow + 1]!.spans.find(
|
|
370
|
+
(span) => span.text.trim(),
|
|
371
|
+
)!.bg;
|
|
372
|
+
const bookSecondBefore = spans[headerRow + 2]!.spans.find(
|
|
373
|
+
(span) => span.text.trim(),
|
|
374
|
+
)!.bg;
|
|
375
|
+
expect(bookFirstBefore).not.toEqual(bookSecondBefore);
|
|
376
|
+
|
|
377
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
378
|
+
await flushFrames(testSetup);
|
|
379
|
+
spans = testSetup.captureSpans().lines;
|
|
380
|
+
expect(
|
|
381
|
+
spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
|
|
382
|
+
).toEqual(bookSecondBefore);
|
|
383
|
+
expect(
|
|
384
|
+
spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
|
|
385
|
+
).toEqual(bookFirstBefore);
|
|
386
|
+
|
|
387
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
388
|
+
const tradesColumn = rows[detailTabsRow]!.indexOf("Trades");
|
|
389
|
+
await act(async () => {
|
|
390
|
+
await testSetup!.mockMouse.click(tradesColumn, detailTabsRow);
|
|
391
|
+
await testSetup!.renderOnce();
|
|
392
|
+
});
|
|
393
|
+
await flushFrames(testSetup);
|
|
394
|
+
|
|
395
|
+
rows = testSetup.captureCharFrame().split("\n");
|
|
396
|
+
headerRow = rows.findIndex((line) =>
|
|
397
|
+
line.includes("TIME") && line.includes("SIDE") && line.includes("PRICE"),
|
|
398
|
+
);
|
|
399
|
+
expect(headerRow).toBeGreaterThanOrEqual(0);
|
|
400
|
+
spans = testSetup.captureSpans().lines;
|
|
401
|
+
const tradeFirstBefore = spans[headerRow + 1]!.spans.find(
|
|
402
|
+
(span) => span.text.trim(),
|
|
403
|
+
)!.bg;
|
|
404
|
+
const tradeSecondBefore = spans[headerRow + 2]!.spans.find(
|
|
405
|
+
(span) => span.text.trim(),
|
|
406
|
+
)!.bg;
|
|
407
|
+
expect(tradeFirstBefore).not.toEqual(tradeSecondBefore);
|
|
408
|
+
|
|
409
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
410
|
+
await flushFrames(testSetup);
|
|
411
|
+
spans = testSetup.captureSpans().lines;
|
|
412
|
+
expect(
|
|
413
|
+
spans[headerRow + 1]!.spans.find((span) => span.text.trim())!.bg,
|
|
414
|
+
).toEqual(tradeSecondBefore);
|
|
415
|
+
expect(
|
|
416
|
+
spans[headerRow + 2]!.spans.find((span) => span.text.trim())!.bg,
|
|
417
|
+
).toEqual(tradeFirstBefore);
|
|
418
|
+
});
|
|
419
|
+
|
|
343
420
|
test("supports detail outcome navigation and escape return from the keyboard", async () => {
|
|
344
421
|
attachPredictionMarketsPersistence(new MemoryPersistence());
|
|
345
422
|
|
|
@@ -262,9 +262,13 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
262
262
|
<Box
|
|
263
263
|
flexDirection="column"
|
|
264
264
|
flexGrow={1}
|
|
265
|
+
flexShrink={1}
|
|
266
|
+
flexBasis={0}
|
|
267
|
+
minHeight={0}
|
|
265
268
|
width={width}
|
|
266
269
|
height={Math.max(height - 1, 1)}
|
|
267
270
|
paddingX={1}
|
|
271
|
+
overflow="hidden"
|
|
268
272
|
backgroundColor={colors.panel}
|
|
269
273
|
>
|
|
270
274
|
<PredictionMarketDetailPane
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createDefaultConfig } from "../../types/config";
|
|
3
|
+
import type { CommandDef } from "../../types/plugin";
|
|
2
4
|
import {
|
|
3
5
|
cleanupPredictionTest,
|
|
4
6
|
installPredictionMarketMocks,
|
|
@@ -23,12 +25,46 @@ import {
|
|
|
23
25
|
loadPolymarketDetail,
|
|
24
26
|
normalizePolymarketMarket,
|
|
25
27
|
} from "./services/polymarket/adapter";
|
|
28
|
+
import { predictionMarketsPlugin } from "./index";
|
|
26
29
|
|
|
27
30
|
afterEach(async () => {
|
|
28
31
|
await cleanupPredictionTest();
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
describe("prediction markets plugin registration and services", () => {
|
|
35
|
+
test("routes search state to a materialized pane instance", async () => {
|
|
36
|
+
const config = createDefaultConfig("/tmp/gloomberb-prediction-command-test");
|
|
37
|
+
const instanceId = "prediction-markets:shared-test";
|
|
38
|
+
config.layout.instances.push({ instanceId, paneId: "prediction-markets" });
|
|
39
|
+
const commands: CommandDef[] = [];
|
|
40
|
+
const paneStateUpdates: Array<{ paneId: string; key: string; value: unknown }> = [];
|
|
41
|
+
const focused: string[] = [];
|
|
42
|
+
|
|
43
|
+
predictionMarketsPlugin.setup?.({
|
|
44
|
+
persistence: new MemoryPersistence(),
|
|
45
|
+
getConfig: () => config,
|
|
46
|
+
resume: {
|
|
47
|
+
setPaneState(paneId: string, key: string, value: unknown) {
|
|
48
|
+
paneStateUpdates.push({ paneId, key, value });
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
registerCommand: (command: CommandDef) => commands.push(command),
|
|
52
|
+
focusPane: (paneId: string) => focused.push(paneId),
|
|
53
|
+
} as any);
|
|
54
|
+
|
|
55
|
+
await commands.find((command) => command.id === "prediction-markets-search")?.execute({
|
|
56
|
+
query: "polymarket:fed",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(paneStateUpdates).toEqual([
|
|
60
|
+
{ paneId: instanceId, key: "venueScope", value: "polymarket" },
|
|
61
|
+
{ paneId: instanceId, key: "searchQuery", value: "fed" },
|
|
62
|
+
{ paneId: instanceId, key: "selectedRowKey", value: null },
|
|
63
|
+
{ paneId: instanceId, key: "selectedDetailMarketKey", value: null },
|
|
64
|
+
]);
|
|
65
|
+
expect(focused).toEqual([instanceId]);
|
|
66
|
+
});
|
|
67
|
+
|
|
32
68
|
test("uses fresh prediction resource cache without refetching", async () => {
|
|
33
69
|
const persistence = new MemoryPersistence();
|
|
34
70
|
attachPredictionMarketsPersistence(persistence);
|
|
@@ -274,6 +274,15 @@ export function installPredictionMarketMocks() {
|
|
|
274
274
|
count_fp: "50",
|
|
275
275
|
created_time: "2026-04-01T00:00:00Z",
|
|
276
276
|
},
|
|
277
|
+
{
|
|
278
|
+
trade_id: "kal-2",
|
|
279
|
+
ticker: "KAL-1",
|
|
280
|
+
taker_side: "no",
|
|
281
|
+
yes_price_dollars: "0.46",
|
|
282
|
+
no_price_dollars: "0.54",
|
|
283
|
+
count_fp: "40",
|
|
284
|
+
created_time: "2026-04-01T00:01:00Z",
|
|
285
|
+
},
|
|
277
286
|
],
|
|
278
287
|
}),
|
|
279
288
|
{ status: 200 },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { AppPersistencePort, AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
3
|
+
import type { LayoutMarketplacePayload } from "../../layout-marketplace/payload";
|
|
3
4
|
import {
|
|
4
5
|
connectionHealth as sharedConnectionHealth,
|
|
5
6
|
type ConnectionHealthRegistry,
|
|
@@ -129,6 +130,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
129
130
|
showPaneFn: ((paneId: string) => void) = () => {};
|
|
130
131
|
createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
|
|
131
132
|
createPaneFromTemplateAsyncFn: ((templateId: string, options?: PaneTemplateCreateOptions) => Promise<void>) = async () => {};
|
|
133
|
+
openPortablePaneShareAsyncFn: ((layout: LayoutMarketplacePayload) => Promise<void>) = async () => {};
|
|
132
134
|
hidePaneFn: ((paneId: string) => void) = () => {};
|
|
133
135
|
focusPaneFn: ((paneId: string) => void) = () => {};
|
|
134
136
|
pinTickerFn: ((symbol: string, options?: PinTickerOptions) => void) = () => {};
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "../types/config";
|
|
8
8
|
import {
|
|
9
9
|
findFixedTickerPaneForSymbol,
|
|
10
|
+
findTickerResearchFollower,
|
|
11
|
+
resolveTickerActivation,
|
|
10
12
|
resolveTickerNavigationReplacementPane,
|
|
11
13
|
shouldFocusTickerNavigationTarget,
|
|
12
14
|
} from "./ticker-navigation";
|
|
@@ -26,6 +28,28 @@ function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
describe("ticker source activation", () => {
|
|
32
|
+
test("focuses a bound follower unless the caller asks for a new pane", () => {
|
|
33
|
+
const layout = createLayout([
|
|
34
|
+
createPaneInstance("portfolio-list", {
|
|
35
|
+
instanceId: "portfolio-list:main",
|
|
36
|
+
binding: { kind: "none" },
|
|
37
|
+
}),
|
|
38
|
+
createPaneInstance("ticker-detail", {
|
|
39
|
+
instanceId: "ticker-detail:main",
|
|
40
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
expect(findTickerResearchFollower(layout, "portfolio-list:main")?.instanceId).toBe("ticker-detail:main");
|
|
45
|
+
expect(resolveTickerActivation(layout, "portfolio-list:main")).toEqual({
|
|
46
|
+
kind: "focus",
|
|
47
|
+
paneId: "ticker-detail:main",
|
|
48
|
+
});
|
|
49
|
+
expect(resolveTickerActivation(layout, "portfolio-list:main", { newPane: true })).toEqual({ kind: "open" });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
29
53
|
describe("resolveTickerNavigationReplacementPane", () => {
|
|
30
54
|
test("only replaces the Ticker Research pane that opened ticker navigation", () => {
|
|
31
55
|
const layout = createLayout([
|
|
@@ -5,8 +5,48 @@ import {
|
|
|
5
5
|
type LayoutConfig,
|
|
6
6
|
type PaneInstanceConfig,
|
|
7
7
|
} from "../types/config";
|
|
8
|
+
import type { PaneDef } from "../types/plugin";
|
|
8
9
|
import { isPaneInLayout } from "./pane-manager";
|
|
9
10
|
|
|
11
|
+
/** Panes that publish a cursor symbol other panes can follow (`PaneDef.tickerSource`). */
|
|
12
|
+
export function listVisibleTickerSourcePanes(
|
|
13
|
+
layout: LayoutConfig,
|
|
14
|
+
panes: ReadonlyMap<string, PaneDef>,
|
|
15
|
+
): PaneInstanceConfig[] {
|
|
16
|
+
return layout.instances.filter((instance) => (
|
|
17
|
+
panes.get(instance.paneId)?.tickerSource === true
|
|
18
|
+
&& isPaneInLayout(layout, instance.instanceId)
|
|
19
|
+
));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The visible Ticker Research pane that follows `sourceInstanceId`, if any. */
|
|
23
|
+
export function findTickerResearchFollower(
|
|
24
|
+
layout: LayoutConfig,
|
|
25
|
+
sourceInstanceId: string | null | undefined,
|
|
26
|
+
): PaneInstanceConfig | null {
|
|
27
|
+
if (!sourceInstanceId) return null;
|
|
28
|
+
return layout.instances.find((instance) =>
|
|
29
|
+
instance.paneId === TICKER_RESEARCH_PANE_ID
|
|
30
|
+
&& instance.binding?.kind === "follow"
|
|
31
|
+
&& instance.binding.sourceInstanceId === sourceInstanceId
|
|
32
|
+
&& isPaneInLayout(layout, instance.instanceId)
|
|
33
|
+
) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enter opens the follower already bound to the source pane; Shift+Enter (`newPane`) and sources
|
|
38
|
+
* without a follower fall back to opening a ticker pane.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveTickerActivation(
|
|
41
|
+
layout: LayoutConfig,
|
|
42
|
+
sourcePaneId: string | null | undefined,
|
|
43
|
+
options?: { newPane?: boolean },
|
|
44
|
+
): { kind: "focus"; paneId: string } | { kind: "open" } {
|
|
45
|
+
if (options?.newPane) return { kind: "open" };
|
|
46
|
+
const follower = findTickerResearchFollower(layout, sourcePaneId);
|
|
47
|
+
return follower ? { kind: "focus", paneId: follower.instanceId } : { kind: "open" };
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
export function resolveTickerNavigationReplacementPane(
|
|
11
51
|
layout: LayoutConfig,
|
|
12
52
|
sourcePaneId: string | null,
|
package/src/remote/schema.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const REMOTE_OPERATIONS: RemoteOperationSchema[] = [
|
|
|
43
43
|
op("layout.delete", "Delete a layout.", "{ index: number }", "local-write"),
|
|
44
44
|
op("layout.undo", "Undo last layout change.", "{}", "local-write"),
|
|
45
45
|
op("layout.redo", "Redo last layout change.", "{}", "local-write"),
|
|
46
|
-
op("layout.gridlock", "
|
|
46
|
+
op("layout.gridlock", "Tidy all panes into a dense layout.", "{}", "local-write"),
|
|
47
47
|
op("layout.closeFloating", "Close all floating panes in the active layout.", "{}", "local-write"),
|
|
48
48
|
op("layout.placePane", "Move a pane to a layout region.", "{ paneId: string, region: 'left' | 'right' | 'top' | 'bottom' | 'floating', relativeTo?: string }", "local-write"),
|
|
49
49
|
op("layout.focusRegion", "Focus a pane by visual layout region.", "{ region: 'left' | 'right' | 'top' | 'bottom' | 'center' }", "local-write"),
|
|
@@ -24,6 +24,42 @@ test("browser cloud transport uses host cookies without forwarding forbidden hea
|
|
|
24
24
|
expect(headers.get("Accept")).toBe("application/json");
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test("browser cloud transport plants session cookies before dropping the Cookie header", async () => {
|
|
28
|
+
const planted: string[] = [];
|
|
29
|
+
const previousDocument = (globalThis as { document?: unknown }).document;
|
|
30
|
+
(globalThis as { document?: { cookie: string } }).document = {
|
|
31
|
+
get cookie() {
|
|
32
|
+
return planted.join("; ");
|
|
33
|
+
},
|
|
34
|
+
set cookie(value: string) {
|
|
35
|
+
planted.push(value);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const previousLocation = (globalThis as { location?: unknown }).location;
|
|
39
|
+
(globalThis as { location?: { protocol: string } }).location = { protocol: "https:" };
|
|
40
|
+
|
|
41
|
+
globalThis.fetch = (async () => {
|
|
42
|
+
return new Response("{}", { headers: { "content-type": "application/json" } });
|
|
43
|
+
}) as typeof fetch;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await browserCredentialedFetch("https://api.gloom.sh/auth/get-session", {
|
|
47
|
+
headers: {
|
|
48
|
+
Cookie: "__Secure-gloomberb.session_token=signed-token.value; gloomberb.session_token=signed-token.value",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
expect(planted).toEqual([
|
|
52
|
+
"__Secure-gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
|
|
53
|
+
"gloomberb.session_token=signed-token.value; Path=/; SameSite=Lax; Secure",
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousDocument === undefined) delete (globalThis as { document?: unknown }).document;
|
|
57
|
+
else (globalThis as { document?: unknown }).document = previousDocument;
|
|
58
|
+
if (previousLocation === undefined) delete (globalThis as { location?: unknown }).location;
|
|
59
|
+
else (globalThis as { location?: unknown }).location = previousLocation;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
27
63
|
test("browser boot restores an existing Gloom Cloud cookie session", async () => {
|
|
28
64
|
const getSession = spyOn(apiClient, "getSession").mockResolvedValue(null);
|
|
29
65
|
await restoreBrowserCloudSession();
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { apiClient, setCloudApiFetchTransport } from "../../api-client";
|
|
2
2
|
import { setHttpFetchTransport } from "../../utils/http-transport";
|
|
3
3
|
|
|
4
|
+
const SESSION_COOKIE_NAMES = ["__Secure-gloomberb.session_token", "gloomberb.session_token"] as const;
|
|
5
|
+
|
|
6
|
+
function plantBrowserSessionCookies(cookieHeader: string): void {
|
|
7
|
+
if (typeof document === "undefined") return;
|
|
8
|
+
const secure = typeof location !== "undefined" && location.protocol === "https:";
|
|
9
|
+
for (const part of cookieHeader.split(";")) {
|
|
10
|
+
const trimmed = part.trim();
|
|
11
|
+
const eq = trimmed.indexOf("=");
|
|
12
|
+
if (eq <= 0) continue;
|
|
13
|
+
const name = trimmed.slice(0, eq).trim();
|
|
14
|
+
const value = trimmed.slice(eq + 1);
|
|
15
|
+
if (!(SESSION_COOKIE_NAMES as readonly string[]).includes(name) || !value) continue;
|
|
16
|
+
const useSecure = secure || name.startsWith("__Secure-");
|
|
17
|
+
document.cookie = `${name}=${value}; Path=/; SameSite=Lax${useSecure ? "; Secure" : ""}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
export function browserCredentialedFetch(url: string, init: RequestInit = {}): Promise<Response> {
|
|
5
22
|
const headers = new Headers(init.headers);
|
|
23
|
+
const cookieHeader = headers.get("Cookie");
|
|
24
|
+
if (cookieHeader) plantBrowserSessionCookies(cookieHeader);
|
|
6
25
|
// These are controlled by the browser. Desktop transports may set them, but
|
|
7
26
|
// carrying them into fetch would either fail or misrepresent the web origin.
|
|
8
27
|
headers.delete("Cookie");
|
|
@@ -56,7 +56,3 @@ export function createBrowserConfigStore(storage: StorageLike): ConfigStoreHost
|
|
|
56
56
|
export function installBrowserConfigStore(storage: StorageLike = localStorage): void {
|
|
57
57
|
setConfigStoreHost(createBrowserConfigStore(storage));
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
export async function loadBrowserConfig(store = createBrowserConfigStore(localStorage)): Promise<AppConfig> {
|
|
61
|
-
return store.loadConfig(BROWSER_DATA_DIR);
|
|
62
|
-
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createBrowserDeepLinkBridge } from "./deeplink-bridge";
|
|
3
|
+
|
|
4
|
+
const originalWindow = globalThis.window;
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window;
|
|
8
|
+
else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("browser social share handoff", () => {
|
|
12
|
+
test("maps a valid pane share query to the common deep-link runtime", () => {
|
|
13
|
+
const id = "0123456789abcdef0123456789abcdef";
|
|
14
|
+
Object.defineProperty(globalThis, "window", {
|
|
15
|
+
configurable: true,
|
|
16
|
+
value: {
|
|
17
|
+
location: { search: `?share=${id}` },
|
|
18
|
+
addEventListener() {},
|
|
19
|
+
removeEventListener() {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const seen: string[] = [];
|
|
23
|
+
createBrowserDeepLinkBridge().subscribe((deeplink) => seen.push(deeplink.url));
|
|
24
|
+
expect(seen).toEqual([`gloomberb://share/${id}`]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("prefers a shared layout query", () => {
|
|
28
|
+
const id = "fedcba9876543210fedcba9876543210";
|
|
29
|
+
Object.defineProperty(globalThis, "window", {
|
|
30
|
+
configurable: true,
|
|
31
|
+
value: {
|
|
32
|
+
location: { search: `?layout=${id}&share=0123456789abcdef0123456789abcdef` },
|
|
33
|
+
addEventListener() {},
|
|
34
|
+
removeEventListener() {},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const seen: string[] = [];
|
|
38
|
+
createBrowserDeepLinkBridge().subscribe((deeplink) => seen.push(deeplink.url));
|
|
39
|
+
expect(seen).toEqual([`gloomberb://layout/${id}`]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { marketplaceLayoutIdFromSearch } from "../../layout-marketplace/api";
|
|
2
|
+
import { paneShareIdFromSearch } from "../../shares/location";
|
|
3
|
+
import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
|
|
4
|
+
|
|
5
|
+
export function createBrowserDeepLinkBridge(): DesktopDeepLinkBridge {
|
|
6
|
+
return {
|
|
7
|
+
subscribe(listener) {
|
|
8
|
+
const emit = () => {
|
|
9
|
+
const layoutId = marketplaceLayoutIdFromSearch(window.location.search);
|
|
10
|
+
if (layoutId) {
|
|
11
|
+
listener({ url: `gloomberb://layout/${layoutId}` });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const shareId = paneShareIdFromSearch(window.location.search);
|
|
15
|
+
if (shareId) listener({ url: `gloomberb://share/${shareId}` });
|
|
16
|
+
};
|
|
17
|
+
emit();
|
|
18
|
+
window.addEventListener("popstate", emit);
|
|
19
|
+
return () => window.removeEventListener("popstate", emit);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./cloud-transport";
|
|
20
20
|
import { BROWSER_DATA_DIR, installBrowserConfigStore } from "./config-host";
|
|
21
21
|
import { browserRendererHost, browserUiHost } from "./ui-host";
|
|
22
|
+
import { createBrowserDeepLinkBridge } from "./deeplink-bridge";
|
|
22
23
|
|
|
23
24
|
const rootElement = document.getElementById("root");
|
|
24
25
|
if (!rootElement) throw new Error("Missing root element");
|
|
@@ -34,6 +35,7 @@ async function boot(): Promise<void> {
|
|
|
34
35
|
await restoreBrowserCloudSession();
|
|
35
36
|
const config = await loadConfig(BROWSER_DATA_DIR);
|
|
36
37
|
applyLanguageFromConfig(config);
|
|
38
|
+
const deepLinkBridge = createBrowserDeepLinkBridge();
|
|
37
39
|
root.render(
|
|
38
40
|
<BrowserErrorBoundary>
|
|
39
41
|
<UiHostProvider ui={browserUiHost} renderer={browserRendererHost} nativeRenderer={webNativeRenderer}>
|
|
@@ -44,6 +46,7 @@ async function boot(): Promise<void> {
|
|
|
44
46
|
config={config}
|
|
45
47
|
servicesFactory={createBrowserAppServices}
|
|
46
48
|
plugins={getBrowserBuiltinPlugins()}
|
|
49
|
+
desktopDeepLinkBridge={deepLinkBridge}
|
|
47
50
|
updatesEnabled={false}
|
|
48
51
|
/>
|
|
49
52
|
</WebDialogHostProvider>
|
|
@@ -34,11 +34,14 @@ describe("static Cloudflare host", () => {
|
|
|
34
34
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test("rewrites
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
test("rewrites valid pane and layout share paths to the slim document", async () => {
|
|
38
|
+
const id = "0123456789abcdef0123456789abcdef";
|
|
39
|
+
for (const prefix of ["s", "l"]) {
|
|
40
|
+
const { env, requests } = fixture();
|
|
41
|
+
const response = await handleRequest(new Request(`https://term.example/${prefix}/${id}`), env);
|
|
42
|
+
expect(new URL(requests[0]!.url).pathname).toBe("/share.html");
|
|
43
|
+
expect(response.headers.get("x-robots-tag")).toBe("noindex, nofollow, noarchive");
|
|
44
|
+
}
|
|
42
45
|
});
|
|
43
46
|
|
|
44
47
|
test("serves the Apple app site association without touching static assets", async () => {
|