gloomberb 0.9.5 → 0.9.6
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 +49 -8
- package/package.json +2 -2
- package/src/app/global-shortcuts.test.tsx +20 -1
- package/src/app/global-shortcuts.ts +2 -0
- package/src/app/pane-runtime/index.tsx +11 -4
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
- package/src/app.tsx +41 -17
- package/src/cli/commands/remote.ts +206 -0
- package/src/cli/index.ts +2 -0
- package/src/components/chart/core/keyboard.ts +10 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
- package/src/components/chart/static/chart/surface.test.tsx +59 -4
- package/src/components/chart/static/chart/surface.tsx +4 -0
- package/src/components/chart/stock-chart/index.test.ts +7 -0
- package/src/components/command-bar/list/view.tsx +47 -0
- package/src/components/command-bar/multi-select-picker.tsx +7 -0
- package/src/components/command-bar/routing/navigation-state.ts +10 -1
- package/src/components/command-bar/surface/index.test.tsx +14 -0
- package/src/components/command-bar/theme-picker.tsx +91 -79
- package/src/components/command-bar/workflow/field-row.tsx +30 -5
- package/src/components/index.ts +7 -0
- package/src/components/layout/pane/footer/index.tsx +23 -0
- package/src/components/layout/shell/index.test.tsx +2 -0
- package/src/components/layout/shell/index.tsx +8 -3
- package/src/components/layout/shell/pane/actions.ts +21 -7
- package/src/components/metric-treemap/index.tsx +317 -0
- package/src/components/metric-treemap/layout.test.ts +77 -0
- package/src/components/metric-treemap/layout.ts +476 -0
- package/src/components/toggle-list.tsx +10 -0
- package/src/components/ui/button.tsx +12 -0
- package/src/components/ui/data-table/index.tsx +51 -0
- package/src/components/ui/fields.tsx +18 -0
- package/src/components/ui/list-view.tsx +83 -3
- package/src/components/ui/message-composer.tsx +22 -0
- package/src/components/ui/tabs.tsx +45 -0
- package/src/core/state/app/layout-reducer.ts +7 -1
- package/src/core/state/app/layout.ts +6 -0
- package/src/core/state/app/state.ts +23 -1
- package/src/core/state/app/types.ts +4 -2
- package/src/plugins/builtin/chat/channels.ts +5 -18
- package/src/plugins/builtin/chat/chat.test.tsx +1 -7
- package/src/plugins/builtin/chat/status-widget.tsx +1 -7
- package/src/plugins/builtin/cloud/plugin.tsx +4 -13
- package/src/plugins/builtin/holders/treemap.tsx +28 -297
- package/src/plugins/builtin/holders/types.ts +0 -34
- package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
- package/src/plugins/builtin/market-heatmap/data.ts +296 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
- package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
- package/src/plugins/ibkr/gateway/service/index.ts +6 -9
- package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
- package/src/remote/app-host.tsx +77 -0
- package/src/remote/client.ts +34 -0
- package/src/remote/command-bar.ts +98 -0
- package/src/remote/controller-utils.ts +111 -0
- package/src/remote/controller.test.ts +365 -0
- package/src/remote/controller.ts +434 -0
- package/src/remote/endpoint.ts +41 -0
- package/src/remote/json-patch.test.ts +31 -0
- package/src/remote/json-patch.ts +89 -0
- package/src/remote/layout-helpers.ts +62 -0
- package/src/remote/resources.ts +186 -0
- package/src/remote/revision.ts +18 -0
- package/src/remote/schema.ts +143 -0
- package/src/remote/semantic-helpers.ts +161 -0
- package/src/remote/semantic-tree.tsx +112 -0
- package/src/remote/server.test.ts +50 -0
- package/src/remote/server.ts +121 -0
- package/src/remote/types.ts +119 -0
- package/src/renderers/electrobun/bun/index.ts +55 -1
- package/src/renderers/electrobun/shared/protocol.ts +11 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
- package/src/renderers/electrobun/view/build-assets.ts +1 -0
- package/src/renderers/electrobun/view/host/box.tsx +5 -0
- package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
- package/src/renderers/electrobun/view/host/mouse.ts +2 -0
- package/src/renderers/electrobun/view/main.tsx +5 -1
- package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
- package/src/renderers/opentui/chart-surface.test.tsx +101 -0
- package/src/renderers/opentui/chart-surface.tsx +15 -20
- package/src/renderers/opentui/start.tsx +32 -0
- package/src/sources/yahoo-finance/http.ts +31 -4
- package/src/state/app/context/index.test.tsx +47 -0
- package/src/state/app/context/index.tsx +48 -18
- package/src/ui/host.tsx +1 -0
- package/src/ui/index.tsx +183 -7
- package/src/version.ts +1 -1
- package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
|
@@ -2,12 +2,19 @@ import { Box, ScrollBox, Text, useUiHost } from "../../ui";
|
|
|
2
2
|
import { useEffect, useRef, useState, type ComponentType, type ReactNode } from "react";
|
|
3
3
|
import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
|
|
4
4
|
import { colors, hoverBg } from "../../theme/colors";
|
|
5
|
+
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
6
|
+
import { resolveRemoteItemIndex } from "../../remote/semantic-helpers";
|
|
5
7
|
|
|
6
8
|
export interface ListViewItem {
|
|
7
9
|
id: string;
|
|
8
10
|
label: string;
|
|
9
11
|
description?: string;
|
|
10
12
|
detail?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
kind?: string;
|
|
15
|
+
right?: string;
|
|
16
|
+
checked?: boolean;
|
|
17
|
+
current?: boolean;
|
|
11
18
|
disabled?: boolean;
|
|
12
19
|
}
|
|
13
20
|
|
|
@@ -38,6 +45,13 @@ export interface ListViewProps {
|
|
|
38
45
|
scrollable?: boolean;
|
|
39
46
|
selectOnHover?: boolean;
|
|
40
47
|
autoScrollToIndex?: boolean;
|
|
48
|
+
onMouseScroll?: (event: any) => void;
|
|
49
|
+
remoteRole?: string;
|
|
50
|
+
remoteLabel?: string;
|
|
51
|
+
remoteScope?: string;
|
|
52
|
+
remoteItemKind?: string;
|
|
53
|
+
remoteItemCategory?: string;
|
|
54
|
+
remoteMetadata?: Record<string, unknown>;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
function DefaultRow({
|
|
@@ -88,7 +102,49 @@ export function ListView({
|
|
|
88
102
|
scrollable = false,
|
|
89
103
|
selectOnHover = false,
|
|
90
104
|
autoScrollToIndex = true,
|
|
105
|
+
onMouseScroll,
|
|
106
|
+
remoteRole = "list",
|
|
107
|
+
remoteLabel,
|
|
108
|
+
remoteScope,
|
|
109
|
+
remoteItemKind,
|
|
110
|
+
remoteItemCategory,
|
|
111
|
+
remoteMetadata,
|
|
91
112
|
}: ListViewProps) {
|
|
113
|
+
useRemoteUiNode({
|
|
114
|
+
role: remoteRole,
|
|
115
|
+
label: remoteLabel ?? emptyMessage,
|
|
116
|
+
actions: {
|
|
117
|
+
select: (input) => {
|
|
118
|
+
const index = resolveListIndex(input, items);
|
|
119
|
+
if (index >= 0 && !items[index]?.disabled) onSelect?.(index);
|
|
120
|
+
},
|
|
121
|
+
activate: (input) => {
|
|
122
|
+
const index = resolveListIndex(input, items);
|
|
123
|
+
const item = index >= 0 ? items[index] : undefined;
|
|
124
|
+
if (item && !item.disabled) {
|
|
125
|
+
onSelect?.(index);
|
|
126
|
+
onActivate?.(item, index);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
metadata: {
|
|
131
|
+
...remoteMetadata,
|
|
132
|
+
scope: remoteScope,
|
|
133
|
+
selectedIndex,
|
|
134
|
+
items: items.map((item, index) => ({
|
|
135
|
+
index,
|
|
136
|
+
id: item.id,
|
|
137
|
+
label: item.label,
|
|
138
|
+
detail: item.detail,
|
|
139
|
+
category: item.category ?? remoteItemCategory,
|
|
140
|
+
kind: item.kind ?? remoteItemKind,
|
|
141
|
+
right: item.right,
|
|
142
|
+
checked: item.checked,
|
|
143
|
+
current: item.current,
|
|
144
|
+
disabled: item.disabled === true,
|
|
145
|
+
})),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
92
148
|
const HostListView = useUiHost().ListView as ComponentType<ListViewProps> | undefined;
|
|
93
149
|
if (HostListView) {
|
|
94
150
|
return (
|
|
@@ -113,6 +169,13 @@ export function ListView({
|
|
|
113
169
|
scrollable={scrollable}
|
|
114
170
|
selectOnHover={selectOnHover}
|
|
115
171
|
autoScrollToIndex={autoScrollToIndex}
|
|
172
|
+
onMouseScroll={onMouseScroll}
|
|
173
|
+
remoteRole={remoteRole}
|
|
174
|
+
remoteLabel={remoteLabel}
|
|
175
|
+
remoteScope={remoteScope}
|
|
176
|
+
remoteItemKind={remoteItemKind}
|
|
177
|
+
remoteItemCategory={remoteItemCategory}
|
|
178
|
+
remoteMetadata={remoteMetadata}
|
|
116
179
|
/>
|
|
117
180
|
);
|
|
118
181
|
}
|
|
@@ -130,7 +193,7 @@ export function ListView({
|
|
|
130
193
|
const sb = scrollRef.current;
|
|
131
194
|
if (!sb) return;
|
|
132
195
|
const safeIndex = Math.min(activeScrollIndex, items.length - 1);
|
|
133
|
-
const viewportH = Math.max(sb.viewport
|
|
196
|
+
const viewportH = Math.max(sb.viewport?.height ?? 1, 1);
|
|
134
197
|
if (safeIndex < sb.scrollTop) {
|
|
135
198
|
sb.scrollTo(safeIndex);
|
|
136
199
|
} else if (safeIndex >= sb.scrollTop + viewportH) {
|
|
@@ -142,7 +205,9 @@ export function ListView({
|
|
|
142
205
|
if (!scrollable) return;
|
|
143
206
|
const sb = scrollRef.current;
|
|
144
207
|
if (!sb) return;
|
|
145
|
-
sb.verticalScrollBar
|
|
208
|
+
if (sb.verticalScrollBar) {
|
|
209
|
+
sb.verticalScrollBar.visible = items.length > (sb.viewport?.height ?? 0);
|
|
210
|
+
}
|
|
146
211
|
}, [items.length, height, flexGrow, scrollable]);
|
|
147
212
|
|
|
148
213
|
if (items.length === 0) {
|
|
@@ -172,6 +237,7 @@ export function ListView({
|
|
|
172
237
|
if (selectOnHover) onSelect?.(index);
|
|
173
238
|
}
|
|
174
239
|
}}
|
|
240
|
+
{...(onMouseScroll ? { onMouseScroll } : {})}
|
|
175
241
|
onMouseDown={() => {
|
|
176
242
|
if (disabled) return;
|
|
177
243
|
onSelect?.(index);
|
|
@@ -188,7 +254,14 @@ export function ListView({
|
|
|
188
254
|
return (
|
|
189
255
|
<Box flexDirection="column" height={height} flexGrow={flexGrow}>
|
|
190
256
|
{scrollable ? (
|
|
191
|
-
<ScrollBox
|
|
257
|
+
<ScrollBox
|
|
258
|
+
ref={scrollRef}
|
|
259
|
+
height={height}
|
|
260
|
+
flexGrow={flexGrow}
|
|
261
|
+
scrollY
|
|
262
|
+
focusable={false}
|
|
263
|
+
{...(onMouseScroll ? { onMouseScroll } : {})}
|
|
264
|
+
>
|
|
192
265
|
{rows}
|
|
193
266
|
</ScrollBox>
|
|
194
267
|
) : rows}
|
|
@@ -204,3 +277,10 @@ export function ListView({
|
|
|
204
277
|
</Box>
|
|
205
278
|
);
|
|
206
279
|
}
|
|
280
|
+
|
|
281
|
+
function resolveListIndex(input: unknown, items: ListViewItem[]): number {
|
|
282
|
+
return resolveRemoteItemIndex(input, items, {
|
|
283
|
+
id: (item) => item.id,
|
|
284
|
+
label: (item) => item.label,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -2,6 +2,8 @@ import { Box, Text, Textarea, useUiHost } from "../../ui";
|
|
|
2
2
|
import { type ComponentType, type RefObject } from "react";
|
|
3
3
|
import { type TextareaRenderable } from "../../ui";
|
|
4
4
|
import { colors } from "../../theme/colors";
|
|
5
|
+
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
6
|
+
import { remoteStringValue } from "../../remote/semantic-helpers";
|
|
5
7
|
|
|
6
8
|
export interface MessageComposerProps {
|
|
7
9
|
inputRef?: RefObject<TextareaRenderable | null>;
|
|
@@ -54,6 +56,26 @@ export function MessageComposer({
|
|
|
54
56
|
keyBindings,
|
|
55
57
|
wrapText = false,
|
|
56
58
|
}: MessageComposerProps) {
|
|
59
|
+
useRemoteUiNode({
|
|
60
|
+
role: "message-composer",
|
|
61
|
+
label: placeholder || "Message composer",
|
|
62
|
+
actions: {
|
|
63
|
+
setValue: (input) => {
|
|
64
|
+
const value = remoteStringValue(input);
|
|
65
|
+
onFocusRequest?.();
|
|
66
|
+
onInput?.(value);
|
|
67
|
+
},
|
|
68
|
+
submit: () => {
|
|
69
|
+
onFocusRequest?.();
|
|
70
|
+
onSubmit?.();
|
|
71
|
+
},
|
|
72
|
+
focus: () => {
|
|
73
|
+
onFocusRequest?.();
|
|
74
|
+
inputRef?.current?.focus?.();
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
metadata: { focused, placeholder, height },
|
|
78
|
+
});
|
|
57
79
|
const HostMessageComposer = useUiHost().MessageComposer as ComponentType<MessageComposerProps> | undefined;
|
|
58
80
|
if (HostMessageComposer) {
|
|
59
81
|
return (
|
|
@@ -3,6 +3,7 @@ import { useShortcut } from "../../react/input";
|
|
|
3
3
|
import { Box, ScrollBox, Text, useUiHost } from "../../ui";
|
|
4
4
|
import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
|
|
5
5
|
import { colors, hoverBg } from "../../theme/colors";
|
|
6
|
+
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
6
7
|
|
|
7
8
|
type TabPointerEvent = {
|
|
8
9
|
button?: number;
|
|
@@ -76,6 +77,36 @@ export function Tabs({
|
|
|
76
77
|
navigationValueRef.current = value;
|
|
77
78
|
onSelect(value);
|
|
78
79
|
}, [onSelect]);
|
|
80
|
+
useRemoteUiNode({
|
|
81
|
+
role: "tabs",
|
|
82
|
+
label: "Tabs",
|
|
83
|
+
actions: {
|
|
84
|
+
select: (input) => {
|
|
85
|
+
const value = resolveTabValue(input, tabs);
|
|
86
|
+
if (!value) return;
|
|
87
|
+
const tab = tabs.find((entry) => entry.value === value);
|
|
88
|
+
if (!tab || tab.disabled) return;
|
|
89
|
+
handleSelect(value);
|
|
90
|
+
},
|
|
91
|
+
add: onAdd ? () => onAdd() : undefined,
|
|
92
|
+
close: (input) => {
|
|
93
|
+
const value = resolveTabValue(input, tabs);
|
|
94
|
+
if (!value) return;
|
|
95
|
+
const tab = tabs.find((entry) => entry.value === value);
|
|
96
|
+
if (!tab || tab.disabled) return;
|
|
97
|
+
tab.onClose?.(value);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
metadata: {
|
|
101
|
+
activeValue,
|
|
102
|
+
tabs: tabs.map((tab) => ({
|
|
103
|
+
label: tab.label,
|
|
104
|
+
value: tab.value,
|
|
105
|
+
disabled: tab.disabled === true,
|
|
106
|
+
closeable: !!tab.onClose,
|
|
107
|
+
})),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
79
110
|
|
|
80
111
|
const selectAdjacentTab = useCallback((direction: -1 | 1) => {
|
|
81
112
|
const enabledTabs = tabs.filter((tab) => !tab.disabled);
|
|
@@ -144,6 +175,20 @@ export function Tabs({
|
|
|
144
175
|
);
|
|
145
176
|
}
|
|
146
177
|
|
|
178
|
+
function resolveTabValue(input: unknown, tabs: TabItem[]): string | null {
|
|
179
|
+
if (typeof input === "string") return input;
|
|
180
|
+
if (typeof input === "number") return tabs[input]?.value ?? null;
|
|
181
|
+
if (input && typeof input === "object") {
|
|
182
|
+
const value = input as { value?: unknown; label?: unknown; index?: unknown };
|
|
183
|
+
if (typeof value.value === "string") return value.value;
|
|
184
|
+
if (typeof value.label === "string") {
|
|
185
|
+
return tabs.find((tab) => tab.label === value.label)?.value ?? null;
|
|
186
|
+
}
|
|
187
|
+
if (typeof value.index === "number") return tabs[value.index]?.value ?? null;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
147
192
|
function tabWidth(
|
|
148
193
|
tab: TabItem,
|
|
149
194
|
active: boolean,
|
|
@@ -64,7 +64,13 @@ export function reduceLayoutAction(state: AppState, action: AppAction): AppState
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
case "UPDATE_LAYOUT":
|
|
67
|
-
return withFocusedPane(
|
|
67
|
+
return withFocusedPane(
|
|
68
|
+
state,
|
|
69
|
+
{ ...state.config, layout: action.layout },
|
|
70
|
+
Object.prototype.hasOwnProperty.call(action, "focusedPaneId")
|
|
71
|
+
? { focusedPaneId: action.focusedPaneId ?? null }
|
|
72
|
+
: {},
|
|
73
|
+
);
|
|
68
74
|
|
|
69
75
|
case "SWITCH_LAYOUT": {
|
|
70
76
|
if (action.index < 0 || action.index >= state.config.layouts.length) return state;
|
|
@@ -351,6 +351,9 @@ export function focusPaneState(state: AppState, paneId: string): AppState {
|
|
|
351
351
|
...state,
|
|
352
352
|
config: syncConfigActiveLayoutState(config, state.paneState, paneId, state.activePanel),
|
|
353
353
|
focusedPaneId: paneId,
|
|
354
|
+
previousFocusedPaneId: state.focusedPaneId && state.focusedPaneId !== paneId
|
|
355
|
+
? state.focusedPaneId
|
|
356
|
+
: state.previousFocusedPaneId,
|
|
354
357
|
recentTickers,
|
|
355
358
|
};
|
|
356
359
|
}
|
|
@@ -386,6 +389,9 @@ export function withFocusedPane(
|
|
|
386
389
|
paneState: nextPaneState,
|
|
387
390
|
brokerAccounts: reconcileBrokerAccounts(syncedConfig, state.brokerAccounts),
|
|
388
391
|
focusedPaneId,
|
|
392
|
+
previousFocusedPaneId: state.focusedPaneId && state.focusedPaneId !== focusedPaneId
|
|
393
|
+
? state.focusedPaneId
|
|
394
|
+
: state.previousFocusedPaneId,
|
|
389
395
|
activePanel,
|
|
390
396
|
};
|
|
391
397
|
}
|
|
@@ -108,7 +108,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
108
108
|
return { ...state, recentTickers: nextRecentTickers(state.recentTickers, action.symbol) };
|
|
109
109
|
|
|
110
110
|
case "SET_ACTIVE_PANEL": {
|
|
111
|
-
const firstPane =
|
|
111
|
+
const firstPane = action.preserveFocus
|
|
112
|
+
? state.focusedPaneId
|
|
113
|
+
: getPanelFocusTarget(state.config.layout, action.panel);
|
|
112
114
|
return withFocusedPane(state, state.config, {
|
|
113
115
|
activePanel: action.panel,
|
|
114
116
|
focusedPaneId: firstPane ?? state.focusedPaneId,
|
|
@@ -260,6 +262,25 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
260
262
|
};
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
case "REPLACE_PANE_STATE": {
|
|
266
|
+
const current = state.paneState[action.paneId] ?? {};
|
|
267
|
+
const nextState = action.state;
|
|
268
|
+
if (Object.is(current, nextState)) return state;
|
|
269
|
+
const paneState = {
|
|
270
|
+
...state.paneState,
|
|
271
|
+
[action.paneId]: nextState,
|
|
272
|
+
};
|
|
273
|
+
const recentTickers = current.cursorSymbol !== nextState.cursorSymbol
|
|
274
|
+
? nextRecentTickers(state.recentTickers, typeof nextState.cursorSymbol === "string" ? nextState.cursorSymbol : null)
|
|
275
|
+
: state.recentTickers;
|
|
276
|
+
return {
|
|
277
|
+
...state,
|
|
278
|
+
config: syncConfigActiveLayoutState(state.config, paneState, state.focusedPaneId, state.activePanel),
|
|
279
|
+
paneState,
|
|
280
|
+
recentTickers,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
263
284
|
case "UPDATE_PLUGIN_PANE_STATE": {
|
|
264
285
|
const current = state.paneState[action.paneId] ?? {};
|
|
265
286
|
const currentPluginState = current.pluginState ?? {};
|
|
@@ -319,6 +340,7 @@ export function createInitialState(config: AppConfig, sessionSnapshot: AppSessio
|
|
|
319
340
|
brokerAccounts: {},
|
|
320
341
|
activePanel,
|
|
321
342
|
focusedPaneId,
|
|
343
|
+
previousFocusedPaneId: null,
|
|
322
344
|
paneState,
|
|
323
345
|
recentTickers: config.recentTickers,
|
|
324
346
|
commandBarOpen: false,
|
|
@@ -48,6 +48,7 @@ export interface AppState {
|
|
|
48
48
|
brokerAccounts: Record<string, BrokerAccount[]>;
|
|
49
49
|
activePanel: "left" | "right";
|
|
50
50
|
focusedPaneId: string | null;
|
|
51
|
+
previousFocusedPaneId: string | null;
|
|
51
52
|
paneState: Record<string, PaneRuntimeState>;
|
|
52
53
|
recentTickers: string[];
|
|
53
54
|
commandBarOpen: boolean;
|
|
@@ -76,7 +77,7 @@ export type AppAction =
|
|
|
76
77
|
| { type: "MERGE_QUOTE"; symbol: string; quote: Quote }
|
|
77
78
|
| { type: "HYDRATE_FINANCIALS"; financials: Map<string, TickerFinancials> }
|
|
78
79
|
| { type: "TRACK_TICKER"; symbol: string | null }
|
|
79
|
-
| { type: "SET_ACTIVE_PANEL"; panel: "left" | "right" }
|
|
80
|
+
| { type: "SET_ACTIVE_PANEL"; panel: "left" | "right"; preserveFocus?: boolean }
|
|
80
81
|
| { type: "TOGGLE_COMMAND_BAR" }
|
|
81
82
|
| {
|
|
82
83
|
type: "SET_COMMAND_BAR";
|
|
@@ -104,7 +105,7 @@ export type AppAction =
|
|
|
104
105
|
| { type: "PUSH_LAYOUT_HISTORY" }
|
|
105
106
|
| { type: "UNDO_LAYOUT" }
|
|
106
107
|
| { type: "REDO_LAYOUT" }
|
|
107
|
-
| { type: "UPDATE_LAYOUT"; layout: LayoutConfig }
|
|
108
|
+
| { type: "UPDATE_LAYOUT"; layout: LayoutConfig; focusedPaneId?: string | null }
|
|
108
109
|
| { type: "SWITCH_LAYOUT"; index: number }
|
|
109
110
|
| { type: "NEW_LAYOUT"; name: string }
|
|
110
111
|
| { type: "DELETE_LAYOUT"; index: number }
|
|
@@ -121,4 +122,5 @@ export type AppAction =
|
|
|
121
122
|
key: string;
|
|
122
123
|
value: unknown;
|
|
123
124
|
}
|
|
125
|
+
| { type: "REPLACE_PANE_STATE"; paneId: string; state: PaneRuntimeState }
|
|
124
126
|
| { type: "UPDATE_PANE_STATE"; paneId: string; patch: Partial<PaneRuntimeState> };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandResultDef, GloomPluginContext } from "../../../types/plugin";
|
|
2
2
|
import type { ChatChannel, ChatChannelState } from "../../../api-client";
|
|
3
|
-
import type { AppConfig
|
|
3
|
+
import type { AppConfig } from "../../../types/config";
|
|
4
4
|
import { chatController } from "./controller";
|
|
5
5
|
import { formatChannelLabel } from "./channel-labels";
|
|
6
6
|
|
|
@@ -25,14 +25,6 @@ export function getLastVisitedChatChannelId(config: { pluginConfig: Record<strin
|
|
|
25
25
|
return normalizeChannelId(config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY] as string | undefined);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function findChatPaneInstanceForChannel(config: AppConfig, channelId: string): PaneInstanceConfig | undefined {
|
|
29
|
-
const normalizedChannelId = normalizeChannelId(channelId);
|
|
30
|
-
return config.layout.instances.find((instance) => (
|
|
31
|
-
instance.paneId === "chat"
|
|
32
|
-
&& normalizeChannelId(typeof instance.settings?.channelId === "string" ? instance.settings.channelId : null) === normalizedChannelId
|
|
33
|
-
));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
export function getPreferredChatOpenChannelId(
|
|
37
29
|
config: AppConfig,
|
|
38
30
|
snapshot?: { channels: ChatChannel[]; channelStates: ChatChannelState[] },
|
|
@@ -80,17 +72,12 @@ export function hasOnlyDmUsernameArgs(value: string): boolean {
|
|
|
80
72
|
return parts.length > 0 && parts.every((part) => CHAT_USERNAME_ARG.test(part));
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
function openChatChannelFromCommand(ctx: Pick<GloomPluginContext, "createPaneFromTemplate"
|
|
84
|
-
const existingInstance = findChatPaneInstanceForChannel(ctx.getConfig(), channelId);
|
|
85
|
-
if (existingInstance) {
|
|
86
|
-
ctx.focusPane(existingInstance.instanceId);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
75
|
+
function openChatChannelFromCommand(ctx: Pick<GloomPluginContext, "createPaneFromTemplate">, channelId: string): void {
|
|
89
76
|
ctx.createPaneFromTemplate("new-chat-pane", { arg: channelId });
|
|
90
77
|
}
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "
|
|
79
|
+
function openDefaultChatPane(
|
|
80
|
+
ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "getConfig">,
|
|
94
81
|
): void {
|
|
95
82
|
const config = ctx.getConfig();
|
|
96
83
|
openChatChannelFromCommand(ctx, getPreferredChatOpenChannelId(config, chatController.getSnapshot()));
|
|
@@ -98,7 +85,7 @@ export function openDefaultChatFromCommand(
|
|
|
98
85
|
|
|
99
86
|
export async function openDmTargetFromCommand(ctx: GloomPluginContext, usernames: string[]): Promise<void> {
|
|
100
87
|
if (usernames.length === 0) {
|
|
101
|
-
|
|
88
|
+
openDefaultChatPane(ctx);
|
|
102
89
|
return;
|
|
103
90
|
}
|
|
104
91
|
const channel = usernames.length === 1
|
|
@@ -1178,7 +1178,6 @@ describe("ChatContent", () => {
|
|
|
1178
1178
|
user: { id: "u1", username: "vince", emailVerified: true },
|
|
1179
1179
|
});
|
|
1180
1180
|
const openedTemplates: Array<{ templateId: string; options?: { arg?: string } }> = [];
|
|
1181
|
-
const focusedPanes: string[] = [];
|
|
1182
1181
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
1183
1182
|
state.config.disabledPlugins = [];
|
|
1184
1183
|
|
|
@@ -1186,9 +1185,6 @@ describe("ChatContent", () => {
|
|
|
1186
1185
|
createPaneFromTemplate(templateId: string, options?: { arg?: string }) {
|
|
1187
1186
|
openedTemplates.push({ templateId, options });
|
|
1188
1187
|
},
|
|
1189
|
-
focusPane(paneId: string) {
|
|
1190
|
-
focusedPanes.push(paneId);
|
|
1191
|
-
},
|
|
1192
1188
|
});
|
|
1193
1189
|
|
|
1194
1190
|
await act(async () => {
|
|
@@ -1230,9 +1226,7 @@ describe("ChatContent", () => {
|
|
|
1230
1226
|
await setup().renderOnce();
|
|
1231
1227
|
});
|
|
1232
1228
|
|
|
1233
|
-
expect(openedTemplates).toEqual([]);
|
|
1234
|
-
expect(focusedPanes).toHaveLength(1);
|
|
1235
|
-
expect(focusedPanes[0]?.startsWith("chat:")).toBe(true);
|
|
1229
|
+
expect(openedTemplates).toEqual([{ templateId: "new-chat-pane", options: { arg: "everyone" } }]);
|
|
1236
1230
|
});
|
|
1237
1231
|
|
|
1238
1232
|
test("opens an unread direct-message channel from the status widget", async () => {
|
|
@@ -5,7 +5,6 @@ import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../ui"
|
|
|
5
5
|
import { usePluginAppActions } from "../../runtime";
|
|
6
6
|
import { InlineAuthActions } from "../cloud/auth-actions";
|
|
7
7
|
import {
|
|
8
|
-
findChatPaneInstanceForChannel,
|
|
9
8
|
getPreferredChatOpenChannelId,
|
|
10
9
|
} from "./channels";
|
|
11
10
|
import { chatController, type ChatController } from "./controller";
|
|
@@ -53,7 +52,7 @@ function CloudStatusIcon() {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
56
|
-
const { createPaneFromTemplate
|
|
55
|
+
const { createPaneFromTemplate } = usePluginAppActions();
|
|
57
56
|
const config = useAppSelector((state) => state.config);
|
|
58
57
|
const cloudPluginDisabled = config.disabledPlugins.includes("gloomberb-cloud");
|
|
59
58
|
const initialSnapshot = controller.getSnapshot();
|
|
@@ -67,11 +66,6 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
67
66
|
event?.preventDefault?.();
|
|
68
67
|
event?.stopPropagation?.();
|
|
69
68
|
const channelId = getPreferredChatOpenChannelId(config, snapshot);
|
|
70
|
-
const existingInstance = findChatPaneInstanceForChannel(config, channelId);
|
|
71
|
-
if (existingInstance) {
|
|
72
|
-
focusPane(existingInstance.instanceId);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
69
|
createPaneFromTemplate("new-chat-pane", { arg: channelId });
|
|
76
70
|
};
|
|
77
71
|
|
|
@@ -14,10 +14,9 @@ import { registerTwitterFeedFeature } from "../cloud-tweets";
|
|
|
14
14
|
import {
|
|
15
15
|
buildDmCommandResults,
|
|
16
16
|
formatChatPaneTitle,
|
|
17
|
-
|
|
17
|
+
getPreferredChatOpenChannelId,
|
|
18
18
|
hasOnlyDmUsernameArgs,
|
|
19
19
|
normalizeShortcutChannelId,
|
|
20
|
-
openDefaultChatFromCommand,
|
|
21
20
|
openDmTargetFromCommand,
|
|
22
21
|
parseDmUsernames,
|
|
23
22
|
} from "../chat/channels";
|
|
@@ -50,7 +49,9 @@ export function createGloomberbCloudPlugin({
|
|
|
50
49
|
createInstance: async (context, options) => {
|
|
51
50
|
const channelId = options?.arg
|
|
52
51
|
? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(options.arg))
|
|
53
|
-
: await chatController.resolvePreferredChannelId(
|
|
52
|
+
: await chatController.resolvePreferredChannelId(
|
|
53
|
+
getPreferredChatOpenChannelId(context.config, chatController.getSnapshot()),
|
|
54
|
+
);
|
|
54
55
|
const channel = chatController.getChannels().find((entry) => entry.id === channelId);
|
|
55
56
|
return {
|
|
56
57
|
placement: "floating",
|
|
@@ -144,16 +145,6 @@ export function createGloomberbCloudPlugin({
|
|
|
144
145
|
|
|
145
146
|
registerTwitterFeedFeature(ctx);
|
|
146
147
|
|
|
147
|
-
ctx.registerCommand({
|
|
148
|
-
id: "open-chat",
|
|
149
|
-
label: "Chat",
|
|
150
|
-
description: "Open chat",
|
|
151
|
-
keywords: ["chat", "message", "messages"],
|
|
152
|
-
category: "navigation",
|
|
153
|
-
shortcut: "CHAT",
|
|
154
|
-
execute: () => openDefaultChatFromCommand(ctx),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
148
|
ctx.registerCommand({
|
|
158
149
|
id: "direct-message",
|
|
159
150
|
label: "DM",
|