gloomberb 0.8.2 → 0.9.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/README.md +9 -7
- package/package.json +18 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/patches/electrobun@1.18.1.patch +85 -0
- package/src/app/global-shortcuts.test.tsx +66 -4
- package/src/app/global-shortcuts.ts +12 -6
- package/src/assets/gloomberb-logo-windows.ico +0 -0
- package/src/assets/gloomberb-logo-windows.png +0 -0
- package/src/assets/gloomberb-logo.ico +0 -0
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/layout/detached-pane-shell.tsx +8 -4
- package/src/components/layout/header.tsx +13 -6
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/titlebar-overlay.ts +16 -0
- package/src/components/layout/window-controls.tsx +90 -0
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/ticker/list-table-view.tsx +6 -0
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/tabs.tsx +95 -80
- package/src/components/ui/ui.test.tsx +54 -0
- package/src/core/state/app/types.ts +12 -4
- package/src/plugins/builtin/chat/content/channel-navigation.ts +44 -11
- package/src/plugins/builtin/chat/content/index.tsx +5 -4
- package/src/plugins/builtin/chat/content/shortcuts.ts +7 -0
- package/src/plugins/builtin/chat/sidebar.test.tsx +71 -0
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +324 -168
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +12 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +4 -0
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +43 -2
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +64 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +123 -0
- package/src/renderers/electrobun/bun/index.ts +28 -6
- package/src/renderers/electrobun/bun/window/frame.test.ts +7 -0
- package/src/renderers/electrobun/bun/window/frame.ts +5 -0
- package/src/renderers/electrobun/view/build-assets.ts +2 -1
- package/src/renderers/electrobun/view/host/text.tsx +29 -9
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/styles.css +20 -0
- package/src/renderers/electrobun/view/ui-host.tsx +14 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/ui/host.tsx +2 -0
- package/src/updater.test.ts +47 -2
- package/src/updater.ts +67 -10
- package/src/version.ts +1 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
3
3
|
import type { ChatChannel, ChatUserSummary } from "../../../../api-client";
|
|
4
|
+
import { useThrottledCommitValue } from "../../../../react/use-throttled-commit-value";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
6
7
|
normalizeChannelId,
|
|
7
8
|
} from "../channels";
|
|
8
9
|
import type { ChatContentController } from "./types";
|
|
9
10
|
|
|
11
|
+
const CHANNEL_NAVIGATION_COMMIT_DELAY_MS = 150;
|
|
12
|
+
|
|
10
13
|
function isConversationChannelId(channelId: string): boolean {
|
|
11
14
|
return channelId.startsWith("dm:") || channelId.startsWith("grp:") || channelId.startsWith("group:");
|
|
12
15
|
}
|
|
@@ -38,15 +41,16 @@ export function useChatChannelNavigation({
|
|
|
38
41
|
resetTranscriptSelection: () => void;
|
|
39
42
|
showChannelSidebar: boolean;
|
|
40
43
|
}): {
|
|
41
|
-
changeChannel: (nextChannelId: string) => void;
|
|
42
44
|
cycleChannel: (direction: 1 | -1) => boolean;
|
|
43
45
|
directExpanded: boolean;
|
|
44
46
|
focusChannelSidebar: () => boolean;
|
|
45
47
|
focusChatContent: () => boolean;
|
|
46
48
|
moveSidebarChannelSelection: (direction: "up" | "down") => boolean;
|
|
47
49
|
openDirectMessage: (target: ChatUserSummary) => Promise<void>;
|
|
50
|
+
selectSidebarChannel: (channelId: string) => void;
|
|
48
51
|
setDirectExpanded: Dispatch<SetStateAction<boolean>>;
|
|
49
52
|
setSidebarFocused: (nextFocused: boolean) => void;
|
|
53
|
+
sidebarCursorChannelId: string;
|
|
50
54
|
sidebarFocused: boolean;
|
|
51
55
|
sidebarFocusedRef: MutableRefObject<boolean>;
|
|
52
56
|
} {
|
|
@@ -70,6 +74,13 @@ export function useChatChannelNavigation({
|
|
|
70
74
|
resetTranscriptSelection();
|
|
71
75
|
onChannelChange?.(normalized);
|
|
72
76
|
}, [channelIdRef, onChannelChange, resetTranscriptSelection]);
|
|
77
|
+
const {
|
|
78
|
+
value: sidebarCursorChannelId,
|
|
79
|
+
valueRef: sidebarCursorChannelIdRef,
|
|
80
|
+
setValue: setSidebarCursorChannelId,
|
|
81
|
+
flushValue: flushSidebarCursorChannelId,
|
|
82
|
+
replaceValue: replaceSidebarCursorChannelId,
|
|
83
|
+
} = useThrottledCommitValue(channelId, changeChannel, CHANNEL_NAVIGATION_COMMIT_DELAY_MS);
|
|
73
84
|
|
|
74
85
|
const openDirectMessage = useCallback(async (target: ChatUserSummary) => {
|
|
75
86
|
if (!target.id && !target.username) return;
|
|
@@ -79,10 +90,10 @@ export function useChatChannelNavigation({
|
|
|
79
90
|
username: target.username ?? undefined,
|
|
80
91
|
});
|
|
81
92
|
setDirectExpanded(true);
|
|
82
|
-
|
|
93
|
+
setSidebarCursorChannelId(channel.id, { immediate: true });
|
|
83
94
|
closeProfilePopover();
|
|
84
95
|
} catch {}
|
|
85
|
-
}, [
|
|
96
|
+
}, [closeProfilePopover, controller, setSidebarCursorChannelId]);
|
|
86
97
|
|
|
87
98
|
useEffect(() => {
|
|
88
99
|
if (!onChannelChange || channelsLoading || channels.length === 0) return;
|
|
@@ -109,9 +120,9 @@ export function useChatChannelNavigation({
|
|
|
109
120
|
const nextIndex = (currentIndex + direction + channels.length) % channels.length;
|
|
110
121
|
const nextChannel = channels[nextIndex];
|
|
111
122
|
if (!nextChannel) return false;
|
|
112
|
-
|
|
123
|
+
setSidebarCursorChannelId(nextChannel.id, { immediate: true });
|
|
113
124
|
return true;
|
|
114
|
-
}, [
|
|
125
|
+
}, [channelIdRef, channels, onChannelChange, setSidebarCursorChannelId]);
|
|
115
126
|
|
|
116
127
|
const focusChannelSidebar = useCallback(() => {
|
|
117
128
|
if (!showChannelSidebar || !onChannelChange) return false;
|
|
@@ -119,28 +130,49 @@ export function useChatChannelNavigation({
|
|
|
119
130
|
blurInput();
|
|
120
131
|
}
|
|
121
132
|
resetTranscriptSelection();
|
|
133
|
+
replaceSidebarCursorChannelId(channelId);
|
|
122
134
|
setSidebarFocused(true);
|
|
123
135
|
return true;
|
|
124
|
-
}, [
|
|
136
|
+
}, [
|
|
137
|
+
blurInput,
|
|
138
|
+
channelId,
|
|
139
|
+
inputFocused,
|
|
140
|
+
onChannelChange,
|
|
141
|
+
replaceSidebarCursorChannelId,
|
|
142
|
+
resetTranscriptSelection,
|
|
143
|
+
setSidebarFocused,
|
|
144
|
+
showChannelSidebar,
|
|
145
|
+
]);
|
|
125
146
|
|
|
126
147
|
const focusChatContent = useCallback(() => {
|
|
127
148
|
if (!showChannelSidebar) return false;
|
|
149
|
+
flushSidebarCursorChannelId(sidebarCursorChannelIdRef.current);
|
|
128
150
|
setSidebarFocused(false);
|
|
129
151
|
return true;
|
|
130
|
-
}, [setSidebarFocused, showChannelSidebar]);
|
|
152
|
+
}, [flushSidebarCursorChannelId, setSidebarFocused, showChannelSidebar, sidebarCursorChannelIdRef]);
|
|
131
153
|
|
|
132
154
|
const moveSidebarChannelSelection = useCallback((direction: "up" | "down") => {
|
|
133
155
|
if (!showChannelSidebar || sidebarNavigationChannels.length <= 1 || !onChannelChange) return false;
|
|
134
|
-
const currentIndex = sidebarNavigationChannels.findIndex((channel) => channel.id ===
|
|
156
|
+
const currentIndex = sidebarNavigationChannels.findIndex((channel) => channel.id === sidebarCursorChannelIdRef.current);
|
|
135
157
|
const baseIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
136
158
|
const nextIndex = direction === "down"
|
|
137
159
|
? Math.min(baseIndex + 1, sidebarNavigationChannels.length - 1)
|
|
138
160
|
: Math.max(baseIndex - 1, 0);
|
|
139
161
|
const nextChannel = sidebarNavigationChannels[nextIndex];
|
|
140
162
|
if (!nextChannel || nextIndex === baseIndex) return true;
|
|
141
|
-
|
|
163
|
+
setSidebarCursorChannelId(nextChannel.id);
|
|
142
164
|
return true;
|
|
143
|
-
}, [
|
|
165
|
+
}, [
|
|
166
|
+
onChannelChange,
|
|
167
|
+
setSidebarCursorChannelId,
|
|
168
|
+
showChannelSidebar,
|
|
169
|
+
sidebarCursorChannelIdRef,
|
|
170
|
+
sidebarNavigationChannels,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const selectSidebarChannel = useCallback((nextChannelId: string) => {
|
|
174
|
+
setSidebarCursorChannelId(nextChannelId, { immediate: true });
|
|
175
|
+
}, [setSidebarCursorChannelId]);
|
|
144
176
|
|
|
145
177
|
useEffect(() => {
|
|
146
178
|
if (focused && showChannelSidebar) return;
|
|
@@ -148,15 +180,16 @@ export function useChatChannelNavigation({
|
|
|
148
180
|
}, [focused, setSidebarFocused, showChannelSidebar]);
|
|
149
181
|
|
|
150
182
|
return {
|
|
151
|
-
changeChannel,
|
|
152
183
|
cycleChannel,
|
|
153
184
|
directExpanded,
|
|
154
185
|
focusChannelSidebar,
|
|
155
186
|
focusChatContent,
|
|
156
187
|
moveSidebarChannelSelection,
|
|
157
188
|
openDirectMessage,
|
|
189
|
+
selectSidebarChannel,
|
|
158
190
|
setDirectExpanded,
|
|
159
191
|
setSidebarFocused,
|
|
192
|
+
sidebarCursorChannelId,
|
|
160
193
|
sidebarFocused,
|
|
161
194
|
sidebarFocusedRef,
|
|
162
195
|
};
|
|
@@ -212,15 +212,16 @@ export function ChatContent({
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
const {
|
|
215
|
-
changeChannel,
|
|
216
215
|
cycleChannel,
|
|
217
216
|
directExpanded,
|
|
218
217
|
focusChannelSidebar,
|
|
219
218
|
focusChatContent,
|
|
220
219
|
moveSidebarChannelSelection,
|
|
221
220
|
openDirectMessage,
|
|
221
|
+
selectSidebarChannel,
|
|
222
222
|
setDirectExpanded,
|
|
223
223
|
setSidebarFocused,
|
|
224
|
+
sidebarCursorChannelId,
|
|
224
225
|
sidebarFocused,
|
|
225
226
|
sidebarFocusedRef,
|
|
226
227
|
} = useChatChannelNavigation({
|
|
@@ -367,7 +368,7 @@ export function ChatContent({
|
|
|
367
368
|
<ChannelSidebar
|
|
368
369
|
channels={channels}
|
|
369
370
|
channelStates={channelStates}
|
|
370
|
-
activeChannelId={channelId}
|
|
371
|
+
activeChannelId={sidebarFocused ? sidebarCursorChannelId : channelId}
|
|
371
372
|
onlineCount={onlineCount}
|
|
372
373
|
width={channelSidebarWidth}
|
|
373
374
|
height={height}
|
|
@@ -376,7 +377,7 @@ export function ChatContent({
|
|
|
376
377
|
loading={channelsLoading}
|
|
377
378
|
canManageNotifications={!!user?.emailVerified}
|
|
378
379
|
directExpanded={directExpanded}
|
|
379
|
-
onSelect={
|
|
380
|
+
onSelect={selectSidebarChannel}
|
|
380
381
|
onFocusRequest={() => setSidebarFocused(true)}
|
|
381
382
|
onToggleNotifications={(nextChannelId, enabled) => {
|
|
382
383
|
controller.setChannelNotificationsEnabled(nextChannelId, enabled);
|
|
@@ -392,7 +393,7 @@ export function ChatContent({
|
|
|
392
393
|
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
393
394
|
backgroundColor={chatContentBg}
|
|
394
395
|
position="relative"
|
|
395
|
-
onMouseDown={() =>
|
|
396
|
+
onMouseDown={() => focusChatContent()}
|
|
396
397
|
style={nativeFillStyle}
|
|
397
398
|
>
|
|
398
399
|
{!nativePaneChrome && (
|
|
@@ -75,6 +75,13 @@ export function useChatContentShortcuts({
|
|
|
75
75
|
const isEnterKey = event.name === "return" || event.name === "enter";
|
|
76
76
|
|
|
77
77
|
if (sidebarFocusedRef.current && showChannelSidebar) {
|
|
78
|
+
if (isEnterKey) {
|
|
79
|
+
event.preventDefault?.();
|
|
80
|
+
event.stopPropagation?.();
|
|
81
|
+
focusChatContent();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
if (isPlainKey(event, "left")) {
|
|
79
86
|
event.preventDefault?.();
|
|
80
87
|
event.stopPropagation?.();
|
|
@@ -9,6 +9,7 @@ import { PluginRenderProvider } from "../../runtime";
|
|
|
9
9
|
import { gloomberbCloudPlugin } from "../cloud";
|
|
10
10
|
import { ChatContent } from "../chat";
|
|
11
11
|
import { chatController } from "./controller";
|
|
12
|
+
import { useChatChannelNavigation } from "./content/channel-navigation";
|
|
12
13
|
import {
|
|
13
14
|
cleanupChatTest,
|
|
14
15
|
createChatTestControls,
|
|
@@ -285,6 +286,76 @@ describe("ChatContent channel sidebar", () => {
|
|
|
285
286
|
expect(setup().captureCharFrame()).toContain("#equities");
|
|
286
287
|
});
|
|
287
288
|
|
|
289
|
+
test("coalesces rapid sidebar navigation to the final channel", async () => {
|
|
290
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
291
|
+
const channels = [
|
|
292
|
+
{ id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
|
|
293
|
+
{ id: "equities", name: "equities", created_at: "2026-05-09T00:00:00.000Z" },
|
|
294
|
+
{ id: "options", name: "options", created_at: "2026-05-09T00:00:00.000Z" },
|
|
295
|
+
{ id: "macro", name: "macro", created_at: "2026-05-09T00:00:00.000Z" },
|
|
296
|
+
{ id: "crypto", name: "crypto", created_at: "2026-05-09T00:00:00.000Z" },
|
|
297
|
+
];
|
|
298
|
+
const channelChanges: string[] = [];
|
|
299
|
+
let navigation: ReturnType<typeof useChatChannelNavigation> | null = null;
|
|
300
|
+
let latestCursorChannelId = "";
|
|
301
|
+
let latestCommittedChannelId = "";
|
|
302
|
+
|
|
303
|
+
function NavigationHarness() {
|
|
304
|
+
const [channelId, setChannelId] = useState("equities");
|
|
305
|
+
const channelIdRef = useRef(channelId);
|
|
306
|
+
channelIdRef.current = channelId;
|
|
307
|
+
const handleChannelChange = useCallback((nextChannelId: string) => {
|
|
308
|
+
channelChanges.push(nextChannelId);
|
|
309
|
+
setChannelId(nextChannelId);
|
|
310
|
+
}, []);
|
|
311
|
+
navigation = useChatChannelNavigation({
|
|
312
|
+
blurInput: () => {},
|
|
313
|
+
channelId,
|
|
314
|
+
channelIdRef,
|
|
315
|
+
channels,
|
|
316
|
+
channelsLoading: false,
|
|
317
|
+
closeProfilePopover: () => {},
|
|
318
|
+
controller,
|
|
319
|
+
focused: true,
|
|
320
|
+
inputFocused: false,
|
|
321
|
+
onChannelChange: handleChannelChange,
|
|
322
|
+
resetTranscriptSelection: () => {},
|
|
323
|
+
showChannelSidebar: true,
|
|
324
|
+
});
|
|
325
|
+
latestCursorChannelId = navigation.sidebarCursorChannelId;
|
|
326
|
+
latestCommittedChannelId = channelId;
|
|
327
|
+
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await act(async () => {
|
|
332
|
+
testSetup = await testRender(<NavigationHarness />, {
|
|
333
|
+
width: 90,
|
|
334
|
+
height: 12,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await act(async () => {
|
|
339
|
+
navigation?.focusChannelSidebar();
|
|
340
|
+
navigation?.moveSidebarChannelSelection("down");
|
|
341
|
+
navigation?.moveSidebarChannelSelection("down");
|
|
342
|
+
navigation?.moveSidebarChannelSelection("down");
|
|
343
|
+
await setup().renderOnce();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(latestCursorChannelId).toBe("crypto");
|
|
347
|
+
expect(latestCommittedChannelId).toBe("equities");
|
|
348
|
+
expect(channelChanges).toEqual([]);
|
|
349
|
+
|
|
350
|
+
await act(async () => {
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 180));
|
|
352
|
+
await setup().renderOnce();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(channelChanges).toEqual(["crypto"]);
|
|
356
|
+
expect(latestCommittedChannelId).toBe("crypto");
|
|
357
|
+
});
|
|
358
|
+
|
|
288
359
|
test("keeps the channel sidebar visible while a channel loads", async () => {
|
|
289
360
|
const controller = createController({ sessionToken: "token-123" });
|
|
290
361
|
installServerChannels(controller);
|
|
@@ -29,7 +29,7 @@ export function ShortcutRow({
|
|
|
29
29
|
return (
|
|
30
30
|
<Box flexDirection="row" gap={1}>
|
|
31
31
|
<Box flexDirection="row" gap={1} flexShrink={0}>
|
|
32
|
-
{badges.map((badge) => <ShortcutBadge key={badge} label={badge} />)}
|
|
32
|
+
{badges.map((badge, index) => <ShortcutBadge key={`${badge}:${index}`} label={badge} />)}
|
|
33
33
|
</Box>
|
|
34
34
|
<Box flexGrow={1}>
|
|
35
35
|
<Text fg={colors.text} wrapText>{description}</Text>
|
|
@@ -34,32 +34,69 @@ async function renderHelpPane(
|
|
|
34
34
|
size,
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
+
await testSetup.renderOnce();
|
|
37
38
|
await testSetup.renderOnce();
|
|
38
39
|
return testSetup;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
async function moveHelpTabRight(count = 1) {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
for (let index = 0; index < count; index += 1) {
|
|
45
|
+
testSetup!.mockInput.pressArrow("right");
|
|
46
|
+
}
|
|
47
|
+
await testSetup!.renderOnce();
|
|
48
|
+
await testSetup!.renderOnce();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function resetRenderedHelpPane(
|
|
53
|
+
runtime = createTestPluginRuntime(),
|
|
54
|
+
size = { width: 88, height: 36 },
|
|
55
|
+
) {
|
|
56
|
+
if (testSetup) {
|
|
57
|
+
act(() => {
|
|
58
|
+
testSetup!.renderer.destroy();
|
|
59
|
+
});
|
|
60
|
+
testSetup = undefined;
|
|
61
|
+
}
|
|
62
|
+
return renderHelpPane(runtime, size);
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
function withoutScrollbar(line: string) {
|
|
42
66
|
const scrollThumb = String.fromCharCode(0x2588);
|
|
43
67
|
return (line.endsWith(scrollThumb) ? line.slice(0, -1) : line).trimEnd();
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
describe("HelpPane", () => {
|
|
47
|
-
test("puts section spacing before headings instead of after them", async () => {
|
|
71
|
+
test("starts on basics and puts section spacing before headings instead of after them", async () => {
|
|
48
72
|
await renderHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
|
|
49
73
|
|
|
50
74
|
const lines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
|
|
51
75
|
const commandBarRow = lines.findIndex((line) => line.includes("Command Bar"));
|
|
52
|
-
const
|
|
53
|
-
|
|
76
|
+
const layoutRow = lines.findIndex((line) => line.includes("Layout Basics"));
|
|
77
|
+
|
|
78
|
+
expect(lines[0]).toContain("Basics");
|
|
79
|
+
expect(testSetup!.captureCharFrame()).toContain("Basics");
|
|
80
|
+
expect(testSetup!.captureCharFrame()).toContain("Functions");
|
|
81
|
+
expect(testSetup!.captureCharFrame()).toContain("Issues");
|
|
82
|
+
expect(testSetup!.captureCharFrame()).not.toContain("Issues/Debug");
|
|
83
|
+
expect(testSetup!.captureCharFrame()).not.toContain("Window Templates");
|
|
84
|
+
expect(testSetup!.captureCharFrame()).not.toContain("Search for a ticker or open the best matching security.");
|
|
54
85
|
expect(commandBarRow).toBeGreaterThan(0);
|
|
55
|
-
expect(
|
|
86
|
+
expect(layoutRow).toBeGreaterThan(commandBarRow);
|
|
56
87
|
expect(lines[commandBarRow - 1]?.trim()).toBe("");
|
|
57
|
-
expect(lines[commandBarRow + 1]).toContain("Open
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
88
|
+
expect(lines[commandBarRow + 1]).toContain("Open command mode");
|
|
89
|
+
expect(testSetup!.captureCharFrame()).toContain("Open ticker search directly.");
|
|
90
|
+
expect(testSetup!.captureCharFrame()).toContain("DES");
|
|
91
|
+
expect(testSetup!.captureCharFrame()).toContain("Clear command text.");
|
|
92
|
+
expect(testSetup!.captureCharFrame()).toContain("Delete the previous word");
|
|
93
|
+
expect(testSetup!.captureCharFrame()).toContain("Submit multiline command forms.");
|
|
94
|
+
expect(testSetup!.captureCharFrame()).toContain("close all floating panes");
|
|
95
|
+
expect(lines[layoutRow - 1]?.trim()).toBe("");
|
|
96
|
+
expect(lines[layoutRow + 1]?.trim()).not.toBe("");
|
|
60
97
|
});
|
|
61
98
|
|
|
62
|
-
test("
|
|
99
|
+
test("separates functions from keyboard shortcuts", async () => {
|
|
63
100
|
setSharedRegistryForTests({
|
|
64
101
|
commands: new Map([[
|
|
65
102
|
"set-alert",
|
|
@@ -106,24 +143,52 @@ describe("HelpPane", () => {
|
|
|
106
143
|
} as any);
|
|
107
144
|
|
|
108
145
|
await renderHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(
|
|
113
|
-
expect(
|
|
114
|
-
expect(
|
|
115
|
-
expect(
|
|
116
|
-
expect(
|
|
117
|
-
expect(
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
120
|
-
expect(
|
|
121
|
-
expect(
|
|
122
|
-
expect(
|
|
123
|
-
expect(
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
126
|
-
expect(
|
|
146
|
+
await moveHelpTabRight();
|
|
147
|
+
|
|
148
|
+
const functionsFrame = testSetup!.captureCharFrame();
|
|
149
|
+
expect(functionsFrame).toContain("Manage Plugins");
|
|
150
|
+
expect(functionsFrame).toMatch(/AW\s+<ticker>/);
|
|
151
|
+
expect(functionsFrame).toMatch(/SA\s+<symbol condition price>/);
|
|
152
|
+
expect(functionsFrame).toMatch(/QQ\s+<tickers>/);
|
|
153
|
+
expect(functionsFrame).toContain("Alerts");
|
|
154
|
+
expect(functionsFrame).toContain("Add Alert");
|
|
155
|
+
expect(functionsFrame).toContain("Shift+X");
|
|
156
|
+
expect(functionsFrame).toContain("Gloom Cloud");
|
|
157
|
+
expect(functionsFrame).toContain("Sync data");
|
|
158
|
+
expect(functionsFrame).toContain("Ticker Research");
|
|
159
|
+
expect(functionsFrame).toContain("Quote Monitor");
|
|
160
|
+
expect(functionsFrame).not.toContain("Add Alert (Alerts)");
|
|
161
|
+
expect(functionsFrame).not.toContain("Sync data (Gloom Cloud)");
|
|
162
|
+
expect(functionsFrame).not.toContain("Quote Monitor (Ticker Research)");
|
|
163
|
+
expect(functionsFrame).not.toContain("Ctrl+W");
|
|
164
|
+
|
|
165
|
+
await resetRenderedHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
|
|
166
|
+
await moveHelpTabRight(2);
|
|
167
|
+
|
|
168
|
+
const shortcutsFrame = testSetup!.captureCharFrame();
|
|
169
|
+
expect(shortcutsFrame).toContain("Ctrl+W");
|
|
170
|
+
expect(shortcutsFrame).toContain("Ctrl+Alt+W");
|
|
171
|
+
expect(shortcutsFrame).toContain("Ctrl+Shift+M");
|
|
172
|
+
expect(shortcutsFrame).toContain("Ctrl+Shift+R");
|
|
173
|
+
expect(shortcutsFrame).toContain("Ctrl+,");
|
|
174
|
+
expect(shortcutsFrame).toContain("Navigation");
|
|
175
|
+
expect(shortcutsFrame).toContain("Up/Down");
|
|
176
|
+
expect(shortcutsFrame).toContain("j/k");
|
|
177
|
+
expect(shortcutsFrame).toContain("Left/Right");
|
|
178
|
+
expect(shortcutsFrame).toContain("h/l");
|
|
179
|
+
expect(shortcutsFrame).toContain("PageUp/PageDown");
|
|
180
|
+
expect(shortcutsFrame).toContain("Window Mode");
|
|
181
|
+
expect(shortcutsFrame).toContain("h/j/k/l");
|
|
182
|
+
expect(shortcutsFrame).toContain("Close all floating panes.");
|
|
183
|
+
expect(shortcutsFrame).not.toContain("AW");
|
|
184
|
+
expect(shortcutsFrame).not.toContain("Ctrl+Shift+O");
|
|
185
|
+
expect(shortcutsFrame).not.toContain("Pop the focused pane");
|
|
186
|
+
expect(shortcutsFrame).not.toContain("Cmd+W");
|
|
187
|
+
expect(shortcutsFrame).not.toContain("Cmd+,");
|
|
188
|
+
expect(shortcutsFrame).not.toContain("Cmd+K");
|
|
189
|
+
expect(shortcutsFrame).not.toContain("Cmd/Ctrl");
|
|
190
|
+
expect(shortcutsFrame).not.toContain("CmdOrCtrl");
|
|
191
|
+
expect(shortcutsFrame).not.toContain("Open ticker actions for the focused ticker.");
|
|
127
192
|
});
|
|
128
193
|
|
|
129
194
|
test("keeps shortcuts and descriptions on the same row when narrow", async () => {
|
|
@@ -131,14 +196,18 @@ describe("HelpPane", () => {
|
|
|
131
196
|
|
|
132
197
|
const lines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
|
|
133
198
|
const closeCommandRow = lines.findIndex((line) => line.includes("Close the command bar."));
|
|
134
|
-
const securityDetailsRow = lines.findIndex((line) => line.includes("Open security details"));
|
|
135
199
|
|
|
136
200
|
expect(closeCommandRow).toBeGreaterThanOrEqual(0);
|
|
137
201
|
expect(lines[closeCommandRow]).toContain("Esc");
|
|
138
202
|
expect(lines[closeCommandRow]).toContain("`");
|
|
203
|
+
|
|
204
|
+
await moveHelpTabRight();
|
|
205
|
+
|
|
206
|
+
const functionLines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
|
|
207
|
+
const securityDetailsRow = functionLines.findIndex((line) => line.includes("Open security details"));
|
|
139
208
|
expect(securityDetailsRow).toBeGreaterThanOrEqual(0);
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
209
|
+
expect(functionLines[securityDetailsRow]).toContain("DES");
|
|
210
|
+
expect(functionLines[securityDetailsRow]).toContain("<ticker>");
|
|
142
211
|
});
|
|
143
212
|
|
|
144
213
|
test("opens the debug log from the mouse action", async () => {
|
|
@@ -149,8 +218,14 @@ describe("HelpPane", () => {
|
|
|
149
218
|
});
|
|
150
219
|
|
|
151
220
|
await renderHelpPane(runtime);
|
|
221
|
+
await moveHelpTabRight(3);
|
|
222
|
+
|
|
223
|
+
const frame = testSetup!.captureCharFrame();
|
|
224
|
+
expect(frame).toContain("GitHub Issues");
|
|
225
|
+
expect(frame).not.toContain("Issues/Debug");
|
|
226
|
+
expect(frame).not.toContain("plugin,\n");
|
|
152
227
|
|
|
153
|
-
const lines =
|
|
228
|
+
const lines = frame.split("\n");
|
|
154
229
|
const row = lines.findIndex((line) => line.includes("Open Debug Log"));
|
|
155
230
|
const col = lines[row]?.indexOf("Open Debug Log") ?? -1;
|
|
156
231
|
|