gloomberb 0.8.1 → 0.9.0
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 +106 -54
- package/package.json +14 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/app/global-shortcuts.test.tsx +54 -4
- package/src/app/global-shortcuts.ts +11 -5
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- 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/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- 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/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +17 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +115 -86
- package/src/components/ui/ui.test.tsx +83 -4
- package/src/core/state/app/types.ts +12 -4
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +66 -4
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- 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 +326 -164
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- 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/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- 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/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- 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/builtin/world-indices/index.tsx +9 -6
- 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/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/react/use-throttled-commit-value.ts +116 -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/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text } from "../../../../ui";
|
|
2
|
-
import { MESSAGE_ACTION_WIDTH, formatInlinePreview,
|
|
2
|
+
import { MESSAGE_ACTION_WIDTH, formatInlinePreview, getMessageBodyTokenLines } from "../layout";
|
|
3
3
|
import { ChatActionChip } from "./action-chip";
|
|
4
4
|
import { ResponsiveTickerBadgeText } from "./inline-tokens";
|
|
5
5
|
import { getChatMessageRenderState } from "./render-state";
|
|
@@ -26,13 +26,19 @@ export function TerminalChatMessage({
|
|
|
26
26
|
onUserHover,
|
|
27
27
|
onUserHoverEnd,
|
|
28
28
|
beginReplyTo,
|
|
29
|
+
beginEditMessage,
|
|
29
30
|
jumpToMessage,
|
|
31
|
+
latestEditableMessageId,
|
|
30
32
|
setHoveredIdx,
|
|
31
33
|
}: TerminalChatMessageProps) {
|
|
32
34
|
const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
|
|
33
|
-
const bodyLines =
|
|
35
|
+
const bodyLines = getMessageBodyTokenLines(msg.content, messageBodyWidth, catalog);
|
|
36
|
+
const canEditMessage = msg.id === latestEditableMessageId;
|
|
34
37
|
const showInlineReplyAction = !state.grouped && state.showReplyAction;
|
|
38
|
+
const showInlineEditAction = !state.grouped && state.showReplyAction && canEditMessage;
|
|
35
39
|
const showGroupedReplyAction = state.grouped && state.showReplyAction;
|
|
40
|
+
const showGroupedEditAction = state.grouped && state.showReplyAction && canEditMessage;
|
|
41
|
+
const groupedActionWidth = MESSAGE_ACTION_WIDTH * Number(showGroupedReplyAction) + MESSAGE_ACTION_WIDTH * Number(showGroupedEditAction);
|
|
36
42
|
const setHovered = () => setHoveredIdx((current) => (current === index ? current : index));
|
|
37
43
|
const clearHovered = () => setHoveredIdx((current) => (current === index ? null : current));
|
|
38
44
|
const messageRowProps = {
|
|
@@ -80,17 +86,29 @@ export function TerminalChatMessage({
|
|
|
80
86
|
{msg.user.username ?? "anon"}
|
|
81
87
|
</Text>
|
|
82
88
|
<Text fg={state.headerStatusColor}> {state.headerStatus}</Text>
|
|
83
|
-
{showInlineReplyAction && (
|
|
89
|
+
{(showInlineReplyAction || showInlineEditAction) && (
|
|
84
90
|
<>
|
|
85
91
|
<Text fg={state.headerStatusColor}> </Text>
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
{showInlineReplyAction && (
|
|
93
|
+
<Box width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
94
|
+
<ChatActionChip
|
|
95
|
+
label="Reply"
|
|
96
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
97
|
+
emphasized={state.isSelected}
|
|
98
|
+
onPress={() => beginReplyTo(index)}
|
|
99
|
+
/>
|
|
100
|
+
</Box>
|
|
101
|
+
)}
|
|
102
|
+
{showInlineEditAction && (
|
|
103
|
+
<Box width={MESSAGE_ACTION_WIDTH} height={1}>
|
|
104
|
+
<ChatActionChip
|
|
105
|
+
label="Edit"
|
|
106
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
107
|
+
emphasized={state.isSelected}
|
|
108
|
+
onPress={() => beginEditMessage(index)}
|
|
109
|
+
/>
|
|
110
|
+
</Box>
|
|
111
|
+
)}
|
|
94
112
|
</>
|
|
95
113
|
)}
|
|
96
114
|
</Box>
|
|
@@ -106,7 +124,8 @@ export function TerminalChatMessage({
|
|
|
106
124
|
>
|
|
107
125
|
<Box width={messageBodyWidth} height={1}>
|
|
108
126
|
<ResponsiveTickerBadgeText
|
|
109
|
-
|
|
127
|
+
tokens={line}
|
|
128
|
+
prewrapped
|
|
110
129
|
catalog={catalog}
|
|
111
130
|
textColor={state.bodyColor}
|
|
112
131
|
openTicker={openTicker}
|
|
@@ -115,14 +134,24 @@ export function TerminalChatMessage({
|
|
|
115
134
|
onUserHoverEnd={onUserHoverEnd}
|
|
116
135
|
/>
|
|
117
136
|
</Box>
|
|
118
|
-
{lineIndex === 0 && showGroupedReplyAction && (
|
|
119
|
-
<Box position="absolute" top={0} right={0} width={
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
{lineIndex === 0 && (showGroupedReplyAction || showGroupedEditAction) && (
|
|
138
|
+
<Box position="absolute" top={0} right={0} width={groupedActionWidth} height={1} flexDirection="row">
|
|
139
|
+
{showGroupedReplyAction && (
|
|
140
|
+
<ChatActionChip
|
|
141
|
+
label="Reply"
|
|
142
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
143
|
+
emphasized={state.isSelected}
|
|
144
|
+
onPress={() => beginReplyTo(index)}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
{showGroupedEditAction && (
|
|
148
|
+
<ChatActionChip
|
|
149
|
+
label="Edit"
|
|
150
|
+
width={MESSAGE_ACTION_WIDTH}
|
|
151
|
+
emphasized={state.isSelected}
|
|
152
|
+
onPress={() => beginEditMessage(index)}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
126
155
|
</Box>
|
|
127
156
|
)}
|
|
128
157
|
</Box>
|
|
@@ -14,5 +14,7 @@ export interface ChatMessageBaseProps {
|
|
|
14
14
|
onUserHover: (user: ChatUserSummary) => void;
|
|
15
15
|
onUserHoverEnd: () => void;
|
|
16
16
|
beginReplyTo: (index: number, options?: { deferFocus?: boolean }) => void;
|
|
17
|
+
beginEditMessage: (index: number, options?: { deferFocus?: boolean }) => boolean;
|
|
17
18
|
jumpToMessage: (messageId: string) => void;
|
|
19
|
+
latestEditableMessageId: string | null;
|
|
18
20
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { updatePaneInstance } from "../../../pane-settings";
|
|
3
3
|
import {
|
|
4
4
|
syncConfigActiveLayoutState,
|
|
5
5
|
useAppDispatch,
|
|
@@ -12,6 +12,7 @@ import { scheduleConfigSave } from "../../../state/config-save-scheduler";
|
|
|
12
12
|
import type { PaneProps } from "../../../types/plugin";
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_CHAT_CHANNEL_ID,
|
|
15
|
+
formatChatPaneTitle,
|
|
15
16
|
LAST_VISITED_CHAT_CHANNEL_KEY,
|
|
16
17
|
normalizeChannelId,
|
|
17
18
|
} from "./channels";
|
|
@@ -22,6 +23,7 @@ interface ChatPaneContentProps {
|
|
|
22
23
|
focused: boolean;
|
|
23
24
|
channelId?: string;
|
|
24
25
|
onChannelChange?: (channelId: string) => void;
|
|
26
|
+
onChannelTitleChange?: (title: string) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function createChatPane(ChatContent: (props: ChatPaneContentProps) => ReactNode) {
|
|
@@ -37,9 +39,30 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
37
39
|
));
|
|
38
40
|
const initialChannelIdRef = useRef(normalizeChannelId(rawPaneChannelId ?? lastVisitedChannelId));
|
|
39
41
|
const persistedChannelId = normalizeChannelId(rawPaneChannelId ?? initialChannelIdRef.current);
|
|
40
|
-
const
|
|
42
|
+
const persistChatPaneState = useCallback((nextChannelId: string, nextTitle?: string) => {
|
|
41
43
|
const currentState = stateRef.current;
|
|
42
|
-
const
|
|
44
|
+
const currentPane = currentState.config.layout.instances.find((instance) => instance.instanceId === paneId);
|
|
45
|
+
const nextTitleValue = nextTitle?.trim() || undefined;
|
|
46
|
+
const currentPaneChannelId = typeof currentPane?.settings?.channelId === "string"
|
|
47
|
+
? normalizeChannelId(currentPane.settings.channelId)
|
|
48
|
+
: null;
|
|
49
|
+
const currentLastVisitedChannelId = currentState.config.pluginConfig["gloomberb-cloud"]?.[LAST_VISITED_CHAT_CHANNEL_KEY];
|
|
50
|
+
if (
|
|
51
|
+
currentPaneChannelId === nextChannelId
|
|
52
|
+
&& (!nextTitleValue || currentPane?.title === nextTitleValue)
|
|
53
|
+
&& currentLastVisitedChannelId === nextChannelId
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const layout = updatePaneInstance(currentState.config.layout, paneId, (instance) => ({
|
|
59
|
+
...instance,
|
|
60
|
+
title: nextTitleValue ?? instance.title,
|
|
61
|
+
settings: {
|
|
62
|
+
...(instance.settings ?? {}),
|
|
63
|
+
channelId: nextChannelId,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
43
66
|
const pluginConfig = {
|
|
44
67
|
...currentState.config.pluginConfig,
|
|
45
68
|
"gloomberb-cloud": {
|
|
@@ -66,8 +89,8 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
66
89
|
|
|
67
90
|
useEffect(() => {
|
|
68
91
|
if (rawPaneChannelId) return;
|
|
69
|
-
|
|
70
|
-
}, [
|
|
92
|
+
persistChatPaneState(initialChannelIdRef.current, formatChatPaneTitle(undefined, initialChannelIdRef.current));
|
|
93
|
+
}, [persistChatPaneState, rawPaneChannelId]);
|
|
71
94
|
|
|
72
95
|
useEffect(() => {
|
|
73
96
|
const normalizedPersisted = normalizeChannelId(persistedChannelId);
|
|
@@ -84,8 +107,12 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
84
107
|
const normalized = normalizeChannelId(nextChannelId);
|
|
85
108
|
pendingChannelIdRef.current = normalized;
|
|
86
109
|
setLocalChannelId((current) => (current === normalized ? current : normalized));
|
|
87
|
-
|
|
88
|
-
}, [
|
|
110
|
+
persistChatPaneState(normalized, formatChatPaneTitle(undefined, normalized));
|
|
111
|
+
}, [persistChatPaneState]);
|
|
112
|
+
|
|
113
|
+
const setChannelTitle = useCallback((nextTitle: string) => {
|
|
114
|
+
persistChatPaneState(channelId, nextTitle);
|
|
115
|
+
}, [channelId, persistChatPaneState]);
|
|
89
116
|
|
|
90
117
|
return (
|
|
91
118
|
<ChatContent
|
|
@@ -94,6 +121,7 @@ export function createChatPane(ChatContent: (props: ChatPaneContentProps) => Rea
|
|
|
94
121
|
focused={focused}
|
|
95
122
|
channelId={channelId}
|
|
96
123
|
onChannelChange={setChannelId}
|
|
124
|
+
onChannelTitleChange={setChannelTitle}
|
|
97
125
|
/>
|
|
98
126
|
);
|
|
99
127
|
};
|
|
@@ -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);
|
|
@@ -442,6 +513,7 @@ describe("ChatContent channel sidebar", () => {
|
|
|
442
513
|
await flushFrame();
|
|
443
514
|
|
|
444
515
|
expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.settings?.channelId).toBe("options");
|
|
516
|
+
expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.title).toBe("#options");
|
|
445
517
|
expect(latestState.config.pluginConfig["gloomberb-cloud"]?.lastChatChannelId).toBe("options");
|
|
446
518
|
expect(setup().captureCharFrame()).toContain("#options");
|
|
447
519
|
} finally {
|
|
@@ -4,12 +4,22 @@ import { colors, hoverBg } from "../../../theme/colors";
|
|
|
4
4
|
import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../ui";
|
|
5
5
|
import { usePluginAppActions } from "../../runtime";
|
|
6
6
|
import { InlineAuthActions } from "../cloud/auth-actions";
|
|
7
|
+
import {
|
|
8
|
+
findChatPaneInstanceForChannel,
|
|
9
|
+
getPreferredChatOpenChannelId,
|
|
10
|
+
} from "./channels";
|
|
7
11
|
import { chatController, type ChatController } from "./controller";
|
|
8
12
|
|
|
9
13
|
interface ChatStatusWidgetProps {
|
|
10
14
|
controller?: Pick<ChatController, "getSnapshot" | "refreshSession" | "subscribe">;
|
|
11
15
|
}
|
|
12
16
|
|
|
17
|
+
type ChatStatusSnapshot = ReturnType<ChatController["getSnapshot"]>;
|
|
18
|
+
|
|
19
|
+
function getTotalUnreadCount(snapshot: ChatStatusSnapshot) {
|
|
20
|
+
return snapshot.channelStates.reduce((total, state) => total + Math.max(0, state.unreadCount), 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
function CloudStatusIcon() {
|
|
14
24
|
const { nativePaneChrome } = useUiCapabilities();
|
|
15
25
|
if (!nativePaneChrome) {
|
|
@@ -43,25 +53,33 @@ function CloudStatusIcon() {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
46
|
-
const {
|
|
47
|
-
const
|
|
56
|
+
const { createPaneFromTemplate, focusPane } = usePluginAppActions();
|
|
57
|
+
const config = useAppSelector((state) => state.config);
|
|
58
|
+
const cloudPluginDisabled = config.disabledPlugins.includes("gloomberb-cloud");
|
|
48
59
|
const initialSnapshot = controller.getSnapshot();
|
|
49
60
|
const [username, setUsername] = useState<string | null>(initialSnapshot.user?.username ?? null);
|
|
50
61
|
const [hasSavedSession, setHasSavedSession] = useState(initialSnapshot.hasSavedSession);
|
|
51
|
-
const [
|
|
62
|
+
const [snapshot, setSnapshot] = useState(initialSnapshot);
|
|
63
|
+
const unreadCount = getTotalUnreadCount(snapshot);
|
|
52
64
|
const [hovered, setHovered] = useState(false);
|
|
53
65
|
|
|
54
66
|
const openChat = (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
55
67
|
event?.preventDefault?.();
|
|
56
68
|
event?.stopPropagation?.();
|
|
57
|
-
|
|
69
|
+
const channelId = getPreferredChatOpenChannelId(config, snapshot);
|
|
70
|
+
const existingInstance = findChatPaneInstanceForChannel(config, channelId);
|
|
71
|
+
if (existingInstance) {
|
|
72
|
+
focusPane(existingInstance.instanceId);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
createPaneFromTemplate("new-chat-pane", { arg: channelId });
|
|
58
76
|
};
|
|
59
77
|
|
|
60
78
|
useEffect(() => {
|
|
61
|
-
const unsubscribe = controller.subscribe((
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const unsubscribe = controller.subscribe((nextSnapshot) => {
|
|
80
|
+
setSnapshot(nextSnapshot);
|
|
81
|
+
setUsername(nextSnapshot.user?.username ?? null);
|
|
82
|
+
setHasSavedSession(nextSnapshot.hasSavedSession);
|
|
65
83
|
});
|
|
66
84
|
void controller.refreshSession().catch(() => {});
|
|
67
85
|
return unsubscribe;
|
|
@@ -84,7 +102,7 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
84
102
|
onMouseOut={() => setHovered((current) => (current ? false : current))}
|
|
85
103
|
onMouseDown={openChat}
|
|
86
104
|
>
|
|
87
|
-
<Text fg={
|
|
105
|
+
<Text fg={unreadCount > 0 ? colors.text : colors.textDim}>
|
|
88
106
|
<Span fg={colors.positive}>@</Span>
|
|
89
107
|
{username ? (
|
|
90
108
|
<>
|
|
@@ -93,8 +111,8 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
93
111
|
</>
|
|
94
112
|
) : null}
|
|
95
113
|
</Text>
|
|
96
|
-
{
|
|
97
|
-
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{` [${
|
|
114
|
+
{unreadCount > 0 ? (
|
|
115
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{` [${unreadCount}]`}</Text>
|
|
98
116
|
) : null}
|
|
99
117
|
</Box>
|
|
100
118
|
)}
|
|
@@ -20,6 +20,7 @@ const originalConnectChannel = apiClient.connectChannel.bind(apiClient);
|
|
|
20
20
|
const originalGetChannels = apiClient.getChannels.bind(apiClient);
|
|
21
21
|
const originalGetChatPresence = apiClient.getChatPresence.bind(apiClient);
|
|
22
22
|
const originalUpdateChatChannelState = apiClient.updateChatChannelState.bind(apiClient);
|
|
23
|
+
const originalEditMessage = apiClient.editMessage.bind(apiClient);
|
|
23
24
|
|
|
24
25
|
export type ChatTestSetup = Awaited<ReturnType<typeof testRender>>;
|
|
25
26
|
|
|
@@ -50,6 +51,7 @@ export async function cleanupChatTest(testSetup: ChatTestSetup | undefined): Pro
|
|
|
50
51
|
apiClient.getChannels = originalGetChannels;
|
|
51
52
|
apiClient.getChatPresence = originalGetChatPresence;
|
|
52
53
|
apiClient.updateChatChannelState = originalUpdateChatChannelState;
|
|
54
|
+
apiClient.editMessage = originalEditMessage;
|
|
53
55
|
apiClient.setSessionToken(null);
|
|
54
56
|
|
|
55
57
|
if (testSetup) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { registerTwitterFeedFeature } from "../cloud-tweets";
|
|
14
14
|
import {
|
|
15
15
|
buildDmCommandResults,
|
|
16
|
+
formatChatPaneTitle,
|
|
16
17
|
getLastVisitedChatChannelId,
|
|
17
18
|
hasOnlyDmUsernameArgs,
|
|
18
19
|
normalizeShortcutChannelId,
|
|
@@ -50,8 +51,10 @@ export function createGloomberbCloudPlugin({
|
|
|
50
51
|
const channelId = options?.arg
|
|
51
52
|
? await chatController.resolveRequiredChannelId(normalizeShortcutChannelId(options.arg))
|
|
52
53
|
: await chatController.resolvePreferredChannelId(getLastVisitedChatChannelId(context.config));
|
|
54
|
+
const channel = chatController.getChannels().find((entry) => entry.id === channelId);
|
|
53
55
|
return {
|
|
54
56
|
placement: "floating",
|
|
57
|
+
title: formatChatPaneTitle(channel, channelId),
|
|
55
58
|
settings: { channelId },
|
|
56
59
|
};
|
|
57
60
|
},
|
|
@@ -297,11 +297,13 @@ export function TweetSearchTable({
|
|
|
297
297
|
onBack={() => setDetailOpen(false)}
|
|
298
298
|
detailTitle={selectedTweet ? `@${selectedTweet.author.userName || selectedTweet.author.name} - ${formatTimeAgo(selectedTweet.createdAt)}` : "Tweet"}
|
|
299
299
|
detailContent={selectedTweet ? <TweetDetail tweet={selectedTweet} width={width} onOpenUsername={openUsernameFeed} /> : null}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
300
|
+
selection={{
|
|
301
|
+
kind: "id",
|
|
302
|
+
selectedId: selectedTweetId,
|
|
303
|
+
getId: (tweet) => tweet.id,
|
|
304
|
+
onChange: (id) => setSelectedTweetId(id),
|
|
305
|
+
}}
|
|
306
|
+
onActivate={(tweet) => {
|
|
305
307
|
setSelectedTweetId(tweet.id);
|
|
306
308
|
setDetailOpen(true);
|
|
307
309
|
}}
|
|
@@ -316,12 +318,6 @@ export function TweetSearchTable({
|
|
|
316
318
|
sortDirection={sort.direction}
|
|
317
319
|
onHeaderClick={handleHeaderClick}
|
|
318
320
|
getItemKey={(tweet) => tweet.id}
|
|
319
|
-
isSelected={(tweet) => tweet.id === selectedTweetId}
|
|
320
|
-
onSelect={(tweet) => setSelectedTweetId(tweet.id)}
|
|
321
|
-
onActivate={(tweet) => {
|
|
322
|
-
setSelectedTweetId(tweet.id);
|
|
323
|
-
setDetailOpen(true);
|
|
324
|
-
}}
|
|
325
321
|
renderCell={renderCell}
|
|
326
322
|
emptyContent={emptyContent}
|
|
327
323
|
emptyStateTitle={loading ? "Loading tweets..." : error ?? emptyStateTitle ?? "No tweets"}
|
|
@@ -262,11 +262,13 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
262
262
|
onBack={() => setDetailMode(null)}
|
|
263
263
|
detailTitle={detailTitle}
|
|
264
264
|
detailContent={detailContent}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
selection={{
|
|
266
|
+
kind: "id",
|
|
267
|
+
selectedId: selectedTradeId,
|
|
268
|
+
getId: (trade) => trade.id,
|
|
269
|
+
onChange: (id) => setSelectedTradeId(id),
|
|
270
|
+
}}
|
|
271
|
+
onActivate={(trade) => {
|
|
270
272
|
setSelectedTradeId(trade.id, { immediate: true });
|
|
271
273
|
setDetailMode({ kind: "trade", tradeId: trade.id });
|
|
272
274
|
}}
|
|
@@ -280,12 +282,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
280
282
|
sortDirection={tradeSort.direction}
|
|
281
283
|
onHeaderClick={(columnId) => setTradeSort((current) => nextSort(current, columnId as TradeColumnId, columnId === "member" || columnId === "ticker" ? "asc" : "desc"))}
|
|
282
284
|
getItemKey={(trade) => trade.id}
|
|
283
|
-
isSelected={(trade) => trade.id === selectedTradeId}
|
|
284
|
-
onSelect={(trade) => setSelectedTradeId(trade.id)}
|
|
285
|
-
onActivate={(trade) => {
|
|
286
|
-
setSelectedTradeId(trade.id, { immediate: true });
|
|
287
|
-
setDetailMode({ kind: "trade", tradeId: trade.id });
|
|
288
|
-
}}
|
|
289
285
|
renderCell={renderCongressTradeCell}
|
|
290
286
|
emptyStateTitle="No House PTR trades."
|
|
291
287
|
emptyStateHint="Press r to refresh."
|
|
@@ -297,11 +293,13 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
297
293
|
onBack={() => setDetailMode(null)}
|
|
298
294
|
detailTitle={detailTitle}
|
|
299
295
|
detailContent={detailContent}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
selection={{
|
|
297
|
+
kind: "id",
|
|
298
|
+
selectedId: selectedMemberId,
|
|
299
|
+
getId: (member) => member.id,
|
|
300
|
+
onChange: (id) => setSelectedMemberId(id),
|
|
301
|
+
}}
|
|
302
|
+
onActivate={(member) => {
|
|
305
303
|
setSelectedMemberId(member.id, { immediate: true });
|
|
306
304
|
setDetailMode({ kind: "member", memberId: member.id });
|
|
307
305
|
}}
|
|
@@ -315,12 +313,6 @@ export function CongressTradesPane({ focused, width, height }: PaneProps) {
|
|
|
315
313
|
sortDirection={memberSort.direction}
|
|
316
314
|
onHeaderClick={(columnId) => setMemberSort((current) => nextSort(current, columnId as MemberColumnId, columnId === "member" || columnId === "district" ? "asc" : "desc"))}
|
|
317
315
|
getItemKey={(member) => member.id}
|
|
318
|
-
isSelected={(member) => member.id === selectedMemberId}
|
|
319
|
-
onSelect={(member) => setSelectedMemberId(member.id)}
|
|
320
|
-
onActivate={(member) => {
|
|
321
|
-
setSelectedMemberId(member.id, { immediate: true });
|
|
322
|
-
setDetailMode({ kind: "member", memberId: member.id });
|
|
323
|
-
}}
|
|
324
316
|
renderCell={renderCongressMemberCell}
|
|
325
317
|
emptyStateTitle="No House PTR members."
|
|
326
318
|
emptyStateHint="Press r to refresh."
|
|
@@ -132,12 +132,15 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
132
132
|
return (
|
|
133
133
|
<DataTableView<EarningsDisplayRow, EarningsColumn>
|
|
134
134
|
focused={focused}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
selection={{
|
|
136
|
+
kind: "index",
|
|
137
|
+
selectedIndex: selectedRowIndex,
|
|
138
|
+
onChange: (_index, row) => {
|
|
139
|
+
if (row.kind === "event") setSelectedIdx(row.eventIdx);
|
|
140
|
+
},
|
|
139
141
|
}}
|
|
140
|
-
|
|
142
|
+
isNavigable={(row) => row.kind === "event"}
|
|
143
|
+
onActivate={(row) => {
|
|
141
144
|
if (row.kind === "event") openEvent(row.event);
|
|
142
145
|
}}
|
|
143
146
|
onRootKeyDown={handleTableKeyDown}
|
|
@@ -149,8 +152,6 @@ function EarningsCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
149
152
|
sortDirection="asc"
|
|
150
153
|
onHeaderClick={() => {}}
|
|
151
154
|
getItemKey={(row) => row.key}
|
|
152
|
-
isSelected={(row) => row.kind === "event" && row.eventIdx === activeEventIdx}
|
|
153
|
-
onSelect={selectDisplayRow}
|
|
154
155
|
renderSectionHeader={renderEarningsSectionHeader}
|
|
155
156
|
renderCell={renderCell}
|
|
156
157
|
emptyStateTitle={loading ? "Loading earnings..." : "No upcoming earnings found"}
|
|
@@ -328,12 +328,12 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
328
328
|
rootWidth={width}
|
|
329
329
|
rootHeight={height}
|
|
330
330
|
onRootKeyDown={handleRootKeyDown}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
selection={{
|
|
332
|
+
kind: "index",
|
|
333
|
+
selectedIndex: eventIdxToRowIdx.get(selectedIdx) ?? selectedIdx,
|
|
334
|
+
onChange: (_index, row) => {
|
|
335
|
+
if (row.kind === "event") setSelectedIdx(row.eventIdx);
|
|
336
|
+
},
|
|
337
337
|
}}
|
|
338
338
|
columns={columns}
|
|
339
339
|
items={rows}
|
|
@@ -344,8 +344,6 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
344
344
|
headerScrollRef={headerScrollRef}
|
|
345
345
|
scrollRef={scrollRef}
|
|
346
346
|
getItemKey={(row) => row.key}
|
|
347
|
-
isSelected={(row) => row.kind === "event" && row.eventIdx === selectedIdx}
|
|
348
|
-
onSelect={selectDisplayRow}
|
|
349
347
|
onActivate={openDisplayRow}
|
|
350
348
|
renderSectionHeader={renderSectionHeader}
|
|
351
349
|
renderCell={renderCell}
|
|
@@ -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>
|