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.
Files changed (92) hide show
  1. package/README.md +49 -8
  2. package/package.json +2 -2
  3. package/src/app/global-shortcuts.test.tsx +20 -1
  4. package/src/app/global-shortcuts.ts +2 -0
  5. package/src/app/pane-runtime/index.tsx +11 -4
  6. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  7. package/src/app.tsx +41 -17
  8. package/src/cli/commands/remote.ts +206 -0
  9. package/src/cli/index.ts +2 -0
  10. package/src/components/chart/core/keyboard.ts +10 -1
  11. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  12. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  13. package/src/components/chart/static/chart/surface.tsx +4 -0
  14. package/src/components/chart/stock-chart/index.test.ts +7 -0
  15. package/src/components/command-bar/list/view.tsx +47 -0
  16. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  17. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  18. package/src/components/command-bar/surface/index.test.tsx +14 -0
  19. package/src/components/command-bar/theme-picker.tsx +91 -79
  20. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  21. package/src/components/index.ts +7 -0
  22. package/src/components/layout/pane/footer/index.tsx +23 -0
  23. package/src/components/layout/shell/index.test.tsx +2 -0
  24. package/src/components/layout/shell/index.tsx +8 -3
  25. package/src/components/layout/shell/pane/actions.ts +21 -7
  26. package/src/components/metric-treemap/index.tsx +317 -0
  27. package/src/components/metric-treemap/layout.test.ts +77 -0
  28. package/src/components/metric-treemap/layout.ts +476 -0
  29. package/src/components/toggle-list.tsx +10 -0
  30. package/src/components/ui/button.tsx +12 -0
  31. package/src/components/ui/data-table/index.tsx +51 -0
  32. package/src/components/ui/fields.tsx +18 -0
  33. package/src/components/ui/list-view.tsx +83 -3
  34. package/src/components/ui/message-composer.tsx +22 -0
  35. package/src/components/ui/tabs.tsx +45 -0
  36. package/src/core/state/app/layout-reducer.ts +7 -1
  37. package/src/core/state/app/layout.ts +6 -0
  38. package/src/core/state/app/state.ts +23 -1
  39. package/src/core/state/app/types.ts +4 -2
  40. package/src/plugins/builtin/chat/channels.ts +5 -18
  41. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  42. package/src/plugins/builtin/chat/status-widget.tsx +1 -7
  43. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  44. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  45. package/src/plugins/builtin/holders/types.ts +0 -34
  46. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  47. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  48. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  49. package/src/plugins/builtin/plugin-groups.ts +2 -0
  50. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  51. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  52. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  53. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  54. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  55. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  56. package/src/remote/app-host.tsx +77 -0
  57. package/src/remote/client.ts +34 -0
  58. package/src/remote/command-bar.ts +98 -0
  59. package/src/remote/controller-utils.ts +111 -0
  60. package/src/remote/controller.test.ts +365 -0
  61. package/src/remote/controller.ts +434 -0
  62. package/src/remote/endpoint.ts +41 -0
  63. package/src/remote/json-patch.test.ts +31 -0
  64. package/src/remote/json-patch.ts +89 -0
  65. package/src/remote/layout-helpers.ts +62 -0
  66. package/src/remote/resources.ts +186 -0
  67. package/src/remote/revision.ts +18 -0
  68. package/src/remote/schema.ts +143 -0
  69. package/src/remote/semantic-helpers.ts +161 -0
  70. package/src/remote/semantic-tree.tsx +112 -0
  71. package/src/remote/server.test.ts +50 -0
  72. package/src/remote/server.ts +121 -0
  73. package/src/remote/types.ts +119 -0
  74. package/src/renderers/electrobun/bun/index.ts +55 -1
  75. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  76. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  77. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  78. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  79. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  80. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  81. package/src/renderers/electrobun/view/main.tsx +5 -1
  82. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  83. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  84. package/src/renderers/opentui/chart-surface.tsx +15 -20
  85. package/src/renderers/opentui/start.tsx +32 -0
  86. package/src/sources/yahoo-finance/http.ts +31 -4
  87. package/src/state/app/context/index.test.tsx +47 -0
  88. package/src/state/app/context/index.tsx +48 -18
  89. package/src/ui/host.tsx +1 -0
  90. package/src/ui/index.tsx +183 -7
  91. package/src/version.ts +1 -1
  92. 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.height, 1);
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.visible = items.length > sb.viewport.height;
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 ref={scrollRef} height={height} flexGrow={flexGrow} scrollY focusable={false}>
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(state, { ...state.config, layout: action.layout });
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 = getPanelFocusTarget(state.config.layout, action.panel);
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, PaneInstanceConfig } from "../../../types/config";
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" | "focusPane" | "getConfig">, channelId: string): void {
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
- export function openDefaultChatFromCommand(
93
- ctx: Pick<GloomPluginContext, "createPaneFromTemplate" | "focusPane" | "getConfig">,
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
- openDefaultChatFromCommand(ctx);
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, focusPane } = usePluginAppActions();
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
- getLastVisitedChatChannelId,
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(getLastVisitedChatChannelId(context.config));
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",