gloomberb 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act, useState } from "react";
|
|
2
|
+
import { act, useCallback, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
|
|
4
4
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
5
|
-
import { AppContext, createInitialState } from "../../state/app-context";
|
|
6
|
-
import { createTestPluginRuntime } from "../../test-support/plugin-runtime";
|
|
5
|
+
import { AppContext, appReducer, createInitialState, PaneInstanceProvider } from "../../state/app-context";
|
|
6
|
+
import { createConfigBackedTestPluginRuntime, createTestPluginRuntime } from "../../test-support/plugin-runtime";
|
|
7
7
|
import { colors } from "../../theme/colors";
|
|
8
|
-
import { createDefaultConfig } from "../../types/config";
|
|
8
|
+
import { createDefaultConfig, findPaneInstance, type PaneInstanceConfig } from "../../types/config";
|
|
9
9
|
import type { PersistedResourceValue } from "../../types/persistence";
|
|
10
10
|
import type { PluginPersistence } from "../../types/plugin";
|
|
11
11
|
import { Box } from "../../ui";
|
|
12
|
-
import { apiClient, type ChatMessage } from "../../utils/api-client";
|
|
12
|
+
import { apiClient, type ChatChannel, type ChatMessage } from "../../utils/api-client";
|
|
13
13
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
|
|
14
14
|
import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
|
|
15
|
-
import { ChatContent, ChatStatusWidget, getSelectedMessageScrollTop, gloomberbCloudPlugin } from "./chat";
|
|
16
|
-
import { ChatController } from "./chat-controller";
|
|
15
|
+
import { ChatContent, ChatStatusWidget, getScrollTopForElementIntoView, getSelectedMessageScrollTop, gloomberbCloudPlugin } from "./chat";
|
|
16
|
+
import { chatController, ChatController } from "./chat-controller";
|
|
17
17
|
|
|
18
18
|
const TRANSCRIPT_KIND = "channel-transcript";
|
|
19
19
|
const TRANSCRIPT_KEY = "everyone";
|
|
20
20
|
const TRANSCRIPT_SOURCE = "server";
|
|
21
21
|
const TRANSCRIPT_SCHEMA_VERSION = 2;
|
|
22
22
|
const originalConnectChannel = apiClient.connectChannel.bind(apiClient);
|
|
23
|
+
const originalGetChannels = apiClient.getChannels.bind(apiClient);
|
|
23
24
|
|
|
24
25
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
25
26
|
|
|
27
|
+
const TEST_CHAT_CHANNELS: ChatChannel[] = [
|
|
28
|
+
{ id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
|
|
29
|
+
{ id: "equities", name: "equities", created_at: "2026-05-09T00:00:00.000Z" },
|
|
30
|
+
{ id: "options", name: "options", created_at: "2026-05-09T00:00:00.000Z" },
|
|
31
|
+
{ id: "macro", name: "macro", created_at: "2026-05-09T00:00:00.000Z" },
|
|
32
|
+
{ id: "crypto", name: "crypto", created_at: "2026-05-09T00:00:00.000Z" },
|
|
33
|
+
{ id: "energy", name: "energy", created_at: "2026-05-09T00:00:00.000Z" },
|
|
34
|
+
{ id: "help", name: "help", created_at: "2026-05-09T00:00:00.000Z" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function installServerChannels(controller: ChatController, channels = TEST_CHAT_CHANNELS): void {
|
|
38
|
+
(controller as any).channels = channels;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
class MemoryPersistence implements PluginPersistence {
|
|
27
42
|
private readonly state = new Map<string, { schemaVersion: number; value: unknown }>();
|
|
28
43
|
private readonly resources = new Map<string, PersistedResourceValue<unknown>>();
|
|
@@ -237,6 +252,7 @@ afterEach(async () => {
|
|
|
237
252
|
setSharedRegistryForTests(undefined);
|
|
238
253
|
setSharedMarketDataForTests(undefined);
|
|
239
254
|
apiClient.connectChannel = originalConnectChannel;
|
|
255
|
+
apiClient.getChannels = originalGetChannels;
|
|
240
256
|
apiClient.setSessionToken(null);
|
|
241
257
|
|
|
242
258
|
if (testSetup) {
|
|
@@ -356,6 +372,341 @@ describe("ChatContent", () => {
|
|
|
356
372
|
expect(frame).not.toContain("click a message");
|
|
357
373
|
});
|
|
358
374
|
|
|
375
|
+
test("shows the channel sidebar on wide panes and hides it on narrow panes", async () => {
|
|
376
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
377
|
+
installServerChannels(controller);
|
|
378
|
+
controller.refreshChannels = async () => {};
|
|
379
|
+
controller.refreshChannelMessages = async () => {};
|
|
380
|
+
|
|
381
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
382
|
+
const renderChannelPane = (width: number) => (
|
|
383
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
384
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
385
|
+
<ChatContent
|
|
386
|
+
controller={controller}
|
|
387
|
+
width={width}
|
|
388
|
+
height={12}
|
|
389
|
+
focused
|
|
390
|
+
channelId="options"
|
|
391
|
+
onChannelChange={() => {}}
|
|
392
|
+
/>
|
|
393
|
+
</PluginRenderProvider>
|
|
394
|
+
</AppContext>
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
await act(async () => {
|
|
398
|
+
testSetup = await testRender(renderChannelPane(90), {
|
|
399
|
+
width: 90,
|
|
400
|
+
height: 12,
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await flushFrame();
|
|
405
|
+
expect(testSetup.captureCharFrame()).toContain("options");
|
|
406
|
+
|
|
407
|
+
await act(async () => {
|
|
408
|
+
testSetup?.renderer.destroy();
|
|
409
|
+
testSetup = await testRender(renderChannelPane(60), {
|
|
410
|
+
width: 60,
|
|
411
|
+
height: 12,
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await flushFrame();
|
|
416
|
+
expect(testSetup.captureCharFrame()).not.toContain("options");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("selects a sidebar channel from a single text click", async () => {
|
|
420
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
421
|
+
installServerChannels(controller);
|
|
422
|
+
controller.refreshChannels = async () => {};
|
|
423
|
+
controller.refreshChannelMessages = async () => {};
|
|
424
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
425
|
+
|
|
426
|
+
function ChannelPane() {
|
|
427
|
+
const [channelId, setChannelId] = useState("equities");
|
|
428
|
+
return (
|
|
429
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
430
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
431
|
+
<ChatContent
|
|
432
|
+
controller={controller}
|
|
433
|
+
width={90}
|
|
434
|
+
height={12}
|
|
435
|
+
focused
|
|
436
|
+
channelId={channelId}
|
|
437
|
+
onChannelChange={setChannelId}
|
|
438
|
+
/>
|
|
439
|
+
</PluginRenderProvider>
|
|
440
|
+
</AppContext>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await act(async () => {
|
|
445
|
+
testSetup = await testRender(<ChannelPane />, {
|
|
446
|
+
width: 90,
|
|
447
|
+
height: 12,
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await flushFrame();
|
|
452
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
453
|
+
|
|
454
|
+
const lines = testSetup.captureCharFrame().split("\n");
|
|
455
|
+
const row = lines.findIndex((line) => line.includes("options"));
|
|
456
|
+
const col = lines[row]?.indexOf("options") ?? -1;
|
|
457
|
+
|
|
458
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
459
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
460
|
+
|
|
461
|
+
await act(async () => {
|
|
462
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
463
|
+
await testSetup!.renderOnce();
|
|
464
|
+
await testSetup!.renderOnce();
|
|
465
|
+
});
|
|
466
|
+
await flushFrame();
|
|
467
|
+
|
|
468
|
+
expect(testSetup.captureCharFrame()).toContain("#options");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("uses arrows to move between channel sidebar and chat content", async () => {
|
|
472
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
473
|
+
installServerChannels(controller);
|
|
474
|
+
controller.refreshChannels = async () => {};
|
|
475
|
+
controller.refreshChannelMessages = async () => {};
|
|
476
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
477
|
+
|
|
478
|
+
function ChannelPane() {
|
|
479
|
+
const [channelId, setChannelId] = useState("equities");
|
|
480
|
+
return (
|
|
481
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
482
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
483
|
+
<ChatContent
|
|
484
|
+
controller={controller}
|
|
485
|
+
width={90}
|
|
486
|
+
height={12}
|
|
487
|
+
focused
|
|
488
|
+
channelId={channelId}
|
|
489
|
+
onChannelChange={setChannelId}
|
|
490
|
+
/>
|
|
491
|
+
</PluginRenderProvider>
|
|
492
|
+
</AppContext>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await act(async () => {
|
|
497
|
+
testSetup = await testRender(<ChannelPane />, {
|
|
498
|
+
width: 90,
|
|
499
|
+
height: 12,
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await flushFrame();
|
|
504
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
505
|
+
const getActiveChannelBackgrounds = () => {
|
|
506
|
+
const activeLine = testSetup!.captureSpans().lines.find((line) => lineText(line).includes("#equities"));
|
|
507
|
+
expect(activeLine).toBeDefined();
|
|
508
|
+
return activeLine!.spans.map((span) => span.bg.toInts().join(","));
|
|
509
|
+
};
|
|
510
|
+
const getContentBackgrounds = () => {
|
|
511
|
+
const contentLine = testSetup!.captureSpans().lines.find((line) => lineText(line).includes("No messages yet"));
|
|
512
|
+
expect(contentLine).toBeDefined();
|
|
513
|
+
return contentLine!.spans.map((span) => span.bg.toInts().join(","));
|
|
514
|
+
};
|
|
515
|
+
const contentFocusedBackgrounds = getActiveChannelBackgrounds();
|
|
516
|
+
const rightFocusedBackgrounds = getContentBackgrounds();
|
|
517
|
+
|
|
518
|
+
await emitKeypress({ name: "down", sequence: "\u001b[B" });
|
|
519
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
520
|
+
|
|
521
|
+
await emitKeypress({ name: "left", sequence: "\u001b[D" });
|
|
522
|
+
await flushFrame();
|
|
523
|
+
expect(getActiveChannelBackgrounds()).not.toEqual(contentFocusedBackgrounds);
|
|
524
|
+
expect(getContentBackgrounds()).not.toEqual(rightFocusedBackgrounds);
|
|
525
|
+
await emitKeypress({ name: "down", sequence: "\u001b[B" });
|
|
526
|
+
await flushFrame();
|
|
527
|
+
expect(testSetup.captureCharFrame()).toContain("#options");
|
|
528
|
+
|
|
529
|
+
await emitKeypress({ name: "up", sequence: "\u001b[A" });
|
|
530
|
+
await flushFrame();
|
|
531
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
532
|
+
|
|
533
|
+
await emitKeypress({ name: "right", sequence: "\u001b[C" });
|
|
534
|
+
await flushFrame();
|
|
535
|
+
expect(getActiveChannelBackgrounds()).toEqual(contentFocusedBackgrounds);
|
|
536
|
+
expect(getContentBackgrounds()).toEqual(rightFocusedBackgrounds);
|
|
537
|
+
await emitKeypress({ name: "down", sequence: "\u001b[B" });
|
|
538
|
+
await flushFrame();
|
|
539
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("keeps the channel sidebar visible while a channel loads", async () => {
|
|
543
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
544
|
+
installServerChannels(controller);
|
|
545
|
+
const getSnapshot = controller.getSnapshot.bind(controller);
|
|
546
|
+
controller.getSnapshot = ((channelId?: string) => ({
|
|
547
|
+
...getSnapshot(channelId),
|
|
548
|
+
loading: true,
|
|
549
|
+
messages: [],
|
|
550
|
+
})) as typeof controller.getSnapshot;
|
|
551
|
+
controller.refreshChannels = async () => {};
|
|
552
|
+
controller.refreshChannelMessages = async () => {};
|
|
553
|
+
|
|
554
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
555
|
+
|
|
556
|
+
await act(async () => {
|
|
557
|
+
testSetup = await testRender(
|
|
558
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
559
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
560
|
+
<ChatContent
|
|
561
|
+
controller={controller}
|
|
562
|
+
width={90}
|
|
563
|
+
height={12}
|
|
564
|
+
focused
|
|
565
|
+
channelId="options"
|
|
566
|
+
onChannelChange={() => {}}
|
|
567
|
+
/>
|
|
568
|
+
</PluginRenderProvider>
|
|
569
|
+
</AppContext>,
|
|
570
|
+
{
|
|
571
|
+
width: 90,
|
|
572
|
+
height: 12,
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
await flushFrame();
|
|
578
|
+
|
|
579
|
+
const frame = testSetup.captureCharFrame();
|
|
580
|
+
expect(frame).toContain("#options");
|
|
581
|
+
expect(frame).toContain("Loading...");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("persists a pane channel selection without the last-visited write reverting it", async () => {
|
|
585
|
+
const registeredPanes: Array<{ id: string; component: (props: any) => JSX.Element }> = [];
|
|
586
|
+
gloomberbCloudPlugin.setup({
|
|
587
|
+
persistence: new MemoryPersistence(),
|
|
588
|
+
resume: {
|
|
589
|
+
getState: () => null,
|
|
590
|
+
setState: () => {},
|
|
591
|
+
deleteState: () => {},
|
|
592
|
+
},
|
|
593
|
+
registerPane: (pane: { id: string; component: (props: any) => JSX.Element }) => {
|
|
594
|
+
registeredPanes.push(pane);
|
|
595
|
+
},
|
|
596
|
+
registerPaneTemplate: () => {},
|
|
597
|
+
registerDetailTab: () => {},
|
|
598
|
+
registerShortcut: () => {},
|
|
599
|
+
registerCommand: () => {},
|
|
600
|
+
showPane: () => {},
|
|
601
|
+
hidePane: () => {},
|
|
602
|
+
notify: () => {},
|
|
603
|
+
} as any);
|
|
604
|
+
const ChatPaneComponent = registeredPanes.find((pane) => pane.id === "chat")?.component;
|
|
605
|
+
expect(ChatPaneComponent).toBeDefined();
|
|
606
|
+
const ResolvedChatPaneComponent = ChatPaneComponent!;
|
|
607
|
+
|
|
608
|
+
const originalRefreshChannels = chatController.refreshChannels;
|
|
609
|
+
const originalRefreshSession = chatController.refreshSession;
|
|
610
|
+
const originalRefreshChannelMessages = chatController.refreshChannelMessages;
|
|
611
|
+
installServerChannels(chatController);
|
|
612
|
+
chatController.refreshChannels = async () => {};
|
|
613
|
+
chatController.refreshSession = async () => {};
|
|
614
|
+
chatController.refreshChannelMessages = async () => {};
|
|
615
|
+
|
|
616
|
+
const paneInstanceId = "chat:test";
|
|
617
|
+
const chatInstance: PaneInstanceConfig = {
|
|
618
|
+
instanceId: paneInstanceId,
|
|
619
|
+
paneId: "chat",
|
|
620
|
+
settings: { channelId: "equities" },
|
|
621
|
+
};
|
|
622
|
+
const layout = {
|
|
623
|
+
dockRoot: { kind: "pane" as const, instanceId: paneInstanceId },
|
|
624
|
+
floating: [],
|
|
625
|
+
detached: [],
|
|
626
|
+
instances: [chatInstance],
|
|
627
|
+
};
|
|
628
|
+
const initialState = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
629
|
+
initialState.focusedPaneId = paneInstanceId;
|
|
630
|
+
initialState.config = {
|
|
631
|
+
...initialState.config,
|
|
632
|
+
layout,
|
|
633
|
+
layouts: [{ name: "Test", layout }],
|
|
634
|
+
pluginConfig: {
|
|
635
|
+
"gloomberb-cloud": {
|
|
636
|
+
lastChatChannelId: "equities",
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
let latestState = initialState;
|
|
641
|
+
|
|
642
|
+
function ChatPaneHarness() {
|
|
643
|
+
const [state, setState] = useState(initialState);
|
|
644
|
+
const stateRef = useRef(state);
|
|
645
|
+
stateRef.current = state;
|
|
646
|
+
latestState = state;
|
|
647
|
+
const dispatch = useCallback((action: any) => {
|
|
648
|
+
setState((current) => {
|
|
649
|
+
const next = appReducer(current, action);
|
|
650
|
+
latestState = next;
|
|
651
|
+
return next;
|
|
652
|
+
});
|
|
653
|
+
}, []);
|
|
654
|
+
const runtime = useMemo(() => createConfigBackedTestPluginRuntime({
|
|
655
|
+
getConfig: () => stateRef.current.config,
|
|
656
|
+
setConfig: (config) => dispatch({ type: "SET_CONFIG", config }),
|
|
657
|
+
}), [dispatch]);
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<AppContext value={{ state, dispatch }}>
|
|
661
|
+
<PaneInstanceProvider paneId={paneInstanceId}>
|
|
662
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={runtime}>
|
|
663
|
+
<ResolvedChatPaneComponent
|
|
664
|
+
width={90}
|
|
665
|
+
height={12}
|
|
666
|
+
focused
|
|
667
|
+
close={() => {}}
|
|
668
|
+
/>
|
|
669
|
+
</PluginRenderProvider>
|
|
670
|
+
</PaneInstanceProvider>
|
|
671
|
+
</AppContext>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
await act(async () => {
|
|
677
|
+
testSetup = await testRender(<ChatPaneHarness />, {
|
|
678
|
+
width: 90,
|
|
679
|
+
height: 12,
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await flushFrame();
|
|
684
|
+
const lines = testSetup.captureCharFrame().split("\n");
|
|
685
|
+
const row = lines.findIndex((line) => line.includes("options"));
|
|
686
|
+
const col = lines[row]?.indexOf("options") ?? -1;
|
|
687
|
+
|
|
688
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
689
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
690
|
+
|
|
691
|
+
await act(async () => {
|
|
692
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
693
|
+
await testSetup!.renderOnce();
|
|
694
|
+
await testSetup!.renderOnce();
|
|
695
|
+
});
|
|
696
|
+
await flushFrame();
|
|
697
|
+
|
|
698
|
+
expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.settings?.channelId).toBe("options");
|
|
699
|
+
expect(latestState.config.pluginConfig["gloomberb-cloud"]?.lastChatChannelId).toBe("options");
|
|
700
|
+
expect(testSetup.captureCharFrame()).toContain("#options");
|
|
701
|
+
} finally {
|
|
702
|
+
chatController.refreshChannels = originalRefreshChannels;
|
|
703
|
+
chatController.refreshSession = originalRefreshSession;
|
|
704
|
+
chatController.refreshChannelMessages = originalRefreshChannelMessages;
|
|
705
|
+
installServerChannels(chatController, []);
|
|
706
|
+
chatController.dispose();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
359
710
|
test("uses the full message row width when the reply action is hidden", async () => {
|
|
360
711
|
const controller = createController({
|
|
361
712
|
messages: [{
|
|
@@ -637,6 +988,29 @@ describe("ChatContent", () => {
|
|
|
637
988
|
expect(nextScrollTop).toBe(13);
|
|
638
989
|
});
|
|
639
990
|
|
|
991
|
+
test("keeps a selected desktop message inside the scroll viewport", () => {
|
|
992
|
+
expect(getScrollTopForElementIntoView({
|
|
993
|
+
scrollTop: 120,
|
|
994
|
+
viewportHeight: 60,
|
|
995
|
+
elementTop: 170,
|
|
996
|
+
elementHeight: 24,
|
|
997
|
+
})).toBe(134);
|
|
998
|
+
|
|
999
|
+
expect(getScrollTopForElementIntoView({
|
|
1000
|
+
scrollTop: 120,
|
|
1001
|
+
viewportHeight: 60,
|
|
1002
|
+
elementTop: 92,
|
|
1003
|
+
elementHeight: 18,
|
|
1004
|
+
})).toBe(92);
|
|
1005
|
+
|
|
1006
|
+
expect(getScrollTopForElementIntoView({
|
|
1007
|
+
scrollTop: 120,
|
|
1008
|
+
viewportHeight: 60,
|
|
1009
|
+
elementTop: 132,
|
|
1010
|
+
elementHeight: 18,
|
|
1011
|
+
})).toBe(120);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
640
1014
|
test("up arrow still moves within a multi-line draft before handing off to message selection", async () => {
|
|
641
1015
|
const controller = createController({ sessionToken: "token-123", messages: [makeMessage(1)] });
|
|
642
1016
|
const sentMessages: string[] = [];
|
|
@@ -1431,13 +1805,13 @@ describe("ChatContent", () => {
|
|
|
1431
1805
|
sessionToken: "token-123",
|
|
1432
1806
|
user: { id: "u1", username: "vince", emailVerified: true },
|
|
1433
1807
|
});
|
|
1434
|
-
const
|
|
1808
|
+
const openedPanes: string[] = [];
|
|
1435
1809
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
1436
1810
|
state.config.disabledPlugins = [];
|
|
1437
1811
|
|
|
1438
1812
|
const runtime = createTestPluginRuntime({
|
|
1439
|
-
|
|
1440
|
-
|
|
1813
|
+
showPane(paneId: string) {
|
|
1814
|
+
openedPanes.push(paneId);
|
|
1441
1815
|
},
|
|
1442
1816
|
});
|
|
1443
1817
|
|
|
@@ -1480,7 +1854,7 @@ describe("ChatContent", () => {
|
|
|
1480
1854
|
await testSetup!.renderOnce();
|
|
1481
1855
|
});
|
|
1482
1856
|
|
|
1483
|
-
expect(
|
|
1857
|
+
expect(openedPanes).toEqual(["chat"]);
|
|
1484
1858
|
});
|
|
1485
1859
|
|
|
1486
1860
|
test("auth commands use a single-form layout and signup no longer prompts for display name", () => {
|
|
@@ -1500,8 +1874,8 @@ describe("ChatContent", () => {
|
|
|
1500
1874
|
registerCommand: (command: { id: string; wizardLayout?: string; wizard?: Array<{ key: string }> }) => {
|
|
1501
1875
|
registeredCommands.push(command);
|
|
1502
1876
|
},
|
|
1503
|
-
|
|
1504
|
-
|
|
1877
|
+
showPane: () => {},
|
|
1878
|
+
hidePane: () => {},
|
|
1505
1879
|
notify: () => {},
|
|
1506
1880
|
} as any);
|
|
1507
1881
|
|
|
@@ -1518,4 +1892,48 @@ describe("ChatContent", () => {
|
|
|
1518
1892
|
"_validate",
|
|
1519
1893
|
]);
|
|
1520
1894
|
});
|
|
1895
|
+
|
|
1896
|
+
test("new chat pane opens directly on the last visited channel", async () => {
|
|
1897
|
+
const originalRefreshChannels = chatController.refreshChannels;
|
|
1898
|
+
installServerChannels(chatController);
|
|
1899
|
+
chatController.refreshChannels = async () => {};
|
|
1900
|
+
const chatTemplate = gloomberbCloudPlugin.paneTemplates?.find((template) => template.id === "new-chat-pane");
|
|
1901
|
+
try {
|
|
1902
|
+
expect(chatTemplate?.wizard).toBeUndefined();
|
|
1903
|
+
expect(chatTemplate?.shortcut).toMatchObject({
|
|
1904
|
+
prefix: "CHAT",
|
|
1905
|
+
argPlaceholder: "channel",
|
|
1906
|
+
argKind: "text",
|
|
1907
|
+
});
|
|
1908
|
+
const created = await Promise.resolve(chatTemplate?.createInstance?.({
|
|
1909
|
+
config: {
|
|
1910
|
+
pluginConfig: {
|
|
1911
|
+
"gloomberb-cloud": {
|
|
1912
|
+
lastChatChannelId: "options",
|
|
1913
|
+
},
|
|
1914
|
+
},
|
|
1915
|
+
},
|
|
1916
|
+
}));
|
|
1917
|
+
expect(created).toMatchObject({
|
|
1918
|
+
settings: { channelId: "options" },
|
|
1919
|
+
placement: "floating",
|
|
1920
|
+
});
|
|
1921
|
+
const createdFromShortcut = await Promise.resolve(chatTemplate?.createInstance?.({
|
|
1922
|
+
config: {
|
|
1923
|
+
pluginConfig: {
|
|
1924
|
+
"gloomberb-cloud": {
|
|
1925
|
+
lastChatChannelId: "options",
|
|
1926
|
+
},
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
}, { arg: "#HELP" }));
|
|
1930
|
+
expect(createdFromShortcut).toMatchObject({
|
|
1931
|
+
settings: { channelId: "help" },
|
|
1932
|
+
placement: "floating",
|
|
1933
|
+
});
|
|
1934
|
+
} finally {
|
|
1935
|
+
chatController.refreshChannels = originalRefreshChannels;
|
|
1936
|
+
installServerChannels(chatController, []);
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1521
1939
|
});
|