gloomberb 0.7.2 → 0.7.4

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 (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -1,8 +1,8 @@
1
1
  import { Box, Span, Text, TextAttributes, contextMenuDivider, useContextMenu, useUiCapabilities } from "../../ui";
2
2
  import { useCallback, useEffect, useState } from "react";
3
- import { colors, hoverBg } from "../../theme/colors";
3
+ import { blendHex, colors, hoverBg } from "../../theme/colors";
4
4
  import { useThemeColors } from "../../theme/theme-context";
5
- import { useAppDispatch, useAppSelector, useFocusedTicker } from "../../state/app-context";
5
+ import { useAppDispatch, useAppSelector } from "../../state/app-context";
6
6
  import {
7
7
  selectActiveLayoutIndex,
8
8
  selectGridlockTipSequence,
@@ -10,9 +10,6 @@ import {
10
10
  selectSavedLayouts,
11
11
  selectStatusBarVisible,
12
12
  } from "../../state/selectors-ui";
13
- import {
14
- getExtendedHoursInfo,
15
- } from "../../utils/market-status";
16
13
  import { getSharedRegistry } from "../../plugins/registry";
17
14
  import { gridlockAllPanes } from "../../plugins/pane-manager";
18
15
  import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
@@ -35,8 +32,6 @@ type LayoutTabItem = {
35
32
  type StatusBarViewProps = {
36
33
  activeLayoutIdx: number;
37
34
  dismissGridlockTip: (event?: StatusBarEvent) => void;
38
- extColor: string;
39
- extText: string;
40
35
  handleGridlockTip: (event?: StatusBarEvent) => void;
41
36
  handleLayoutSelect: (value: string) => void;
42
37
  hasMultipleLayouts: boolean;
@@ -47,7 +42,6 @@ type StatusBarViewProps = {
47
42
  openLayoutContextMenu: (index: number, event: any) => void | Promise<unknown>;
48
43
  setHoveredControl: SetHoveredControl;
49
44
  showGridlockTip: boolean;
50
- symbol: string | null | undefined;
51
45
  };
52
46
 
53
47
  function truncate(text: string, width: number): string {
@@ -68,14 +62,8 @@ export function StatusBar() {
68
62
  const statusBarVisible = useAppSelector(selectStatusBarVisible);
69
63
  const gridlockTipVisible = useAppSelector(selectGridlockTipVisible);
70
64
  const gridlockTipSequence = useAppSelector(selectGridlockTipSequence);
71
- const { symbol, financials: focusedFinancials } = useFocusedTicker();
72
65
  const [hoveredControl, setHoveredControl] = useState<string | null>(null);
73
66
 
74
- // Extended hours info for selected ticker
75
- const extInfo = getExtendedHoursInfo(focusedFinancials?.quote);
76
- const extText = extInfo?.text ?? "";
77
- const extColor = extInfo?.color ?? colors.textDim;
78
-
79
67
  const hasMultipleLayouts = layouts.length > 1;
80
68
  const showGridlockTip = gridlockTipVisible && !!registry;
81
69
  const layoutTabs = layouts.map((layout, index) => ({
@@ -211,8 +199,6 @@ export function StatusBar() {
211
199
  const viewProps: StatusBarViewProps = {
212
200
  activeLayoutIdx,
213
201
  dismissGridlockTip,
214
- extColor,
215
- extText,
216
202
  handleGridlockTip,
217
203
  handleLayoutSelect,
218
204
  hasMultipleLayouts,
@@ -223,7 +209,6 @@ export function StatusBar() {
223
209
  openLayoutContextMenu,
224
210
  setHoveredControl,
225
211
  showGridlockTip,
226
- symbol,
227
212
  };
228
213
 
229
214
  if (nativePaneChrome) {
@@ -251,7 +236,7 @@ function NativeStatusBar({
251
236
  }}
252
237
  style={{
253
238
  borderTop: `1px solid ${colors.border}`,
254
- boxShadow: "inset 0 1px 0 rgba(255,255,255,0.03)",
239
+ boxShadow: `inset 0 1px 0 ${blendHex(colors.panel, colors.textBright, 0.03)}`,
255
240
  paddingInline: 8,
256
241
  }}
257
242
  >
@@ -416,15 +401,10 @@ function TerminalGridlockTip({
416
401
  );
417
402
  }
418
403
 
419
- function StatusBarWidgets({ extColor, extText, symbol }: Pick<StatusBarViewProps, "extColor" | "extText" | "symbol">) {
404
+ function StatusBarWidgets() {
420
405
  return (
421
406
  <>
422
407
  <Box flexGrow={1} />
423
- {extText && (
424
- <Box paddingRight={1}>
425
- <Text fg={extColor}>{symbol} {extText}</Text>
426
- </Box>
427
- )}
428
408
  <PluginSlot name="status:widget" />
429
409
  </>
430
410
  );
@@ -87,9 +87,32 @@ function parseLine(line: string): ParsedLine {
87
87
  return { segments: parseInlineMarkdown(line) };
88
88
  }
89
89
 
90
- function SegmentSpan({ segment }: { segment: StyledSegment }) {
90
+ function wrappedTextStyle() {
91
+ return {
92
+ minWidth: 0,
93
+ whiteSpace: "pre-wrap",
94
+ overflowWrap: "anywhere",
95
+ } as const;
96
+ }
97
+
98
+ function wrappedInlineTextStyle() {
99
+ return {
100
+ ...wrappedTextStyle(),
101
+ display: "inline",
102
+ } as const;
103
+ }
104
+
105
+ function SegmentSpan({ segment, wrap = false }: { segment: StyledSegment; wrap?: boolean }) {
106
+ const wrapProps = wrap
107
+ ? {
108
+ wrapText: true,
109
+ wrapMode: "word",
110
+ style: wrappedInlineTextStyle(),
111
+ }
112
+ : {};
113
+
91
114
  if (segment.code) {
92
- return <Span fg={colors.textDim}>{segment.text}</Span>;
115
+ return <Span fg={colors.textDim} {...wrapProps}>{segment.text}</Span>;
93
116
  }
94
117
  const attrs =
95
118
  (segment.bold ? TextAttributes.BOLD : 0) |
@@ -99,6 +122,7 @@ function SegmentSpan({ segment }: { segment: StyledSegment }) {
99
122
  <Span
100
123
  fg={segment.color ?? undefined}
101
124
  attributes={attrs || undefined}
125
+ {...wrapProps}
102
126
  >
103
127
  {segment.text}
104
128
  </Span>
@@ -124,6 +148,15 @@ function MarkdownLine({
124
148
  }) {
125
149
  const indent = parsed.indent ?? 0;
126
150
  const indentStr = indent > 0 ? " ".repeat(indent) : "";
151
+ const shouldWrap = lineWidth != null;
152
+ const textWrapProps = shouldWrap
153
+ ? {
154
+ width: lineWidth,
155
+ wrapText: true,
156
+ wrapMode: "word",
157
+ style: wrappedTextStyle(),
158
+ }
159
+ : {};
127
160
 
128
161
  // Check if any segment contains ticker symbols
129
162
  const fullText = parsed.segments.map((s) => s.text).join("");
@@ -133,10 +166,10 @@ function MarkdownLine({
133
166
  if (!hasTickers) {
134
167
  // Simple case: no tickers, render as styled text
135
168
  return (
136
- <Text fg={textColor}>
169
+ <Text fg={textColor} {...textWrapProps}>
137
170
  {indentStr}
138
171
  {parsed.segments.map((segment, i) => (
139
- <SegmentSpan key={i} segment={segment} />
172
+ <SegmentSpan key={i} segment={segment} wrap={shouldWrap} />
140
173
  ))}
141
174
  </Text>
142
175
  );
@@ -153,16 +186,28 @@ function MarkdownLine({
153
186
  if (token.kind === "text") {
154
187
  if (!token.value) return null;
155
188
  return (
156
- <Text key={`${segIdx}:${tokIdx}`} fg={textColor}>
157
- <SegmentSpan segment={{ ...segment, text: token.value }} />
189
+ <Text
190
+ key={`${segIdx}:${tokIdx}`}
191
+ fg={textColor}
192
+ {...(shouldWrap
193
+ ? { wrapText: true, wrapMode: "word", style: wrappedTextStyle() }
194
+ : {})}
195
+ >
196
+ <SegmentSpan segment={{ ...segment, text: token.value }} wrap={shouldWrap} />
158
197
  </Text>
159
198
  );
160
199
  }
161
200
  const entry = catalog[token.symbol];
162
201
  if (!entry || entry.status === "missing") {
163
202
  return (
164
- <Text key={`${segIdx}:${tokIdx}`} fg={textColor}>
165
- <SegmentSpan segment={{ ...segment, text: token.value }} />
203
+ <Text
204
+ key={`${segIdx}:${tokIdx}`}
205
+ fg={textColor}
206
+ {...(shouldWrap
207
+ ? { wrapText: true, wrapMode: "word", style: wrappedTextStyle() }
208
+ : {})}
209
+ >
210
+ <SegmentSpan segment={{ ...segment, text: token.value }} wrap={shouldWrap} />
166
211
  </Text>
167
212
  );
168
213
  }
@@ -12,6 +12,7 @@ import { resolveBrokerConfigFields, type BrokerAdapter, type BrokerConfigField }
12
12
  import { buildBrokerProfileConfig, validateBrokerProfileValues } from "../../brokers/profile-form";
13
13
  import { createBrokerInstanceId } from "../../utils/broker-instances";
14
14
  import { isBackNavigationKey, isPlainEscape } from "../../utils/back-navigation";
15
+ import { isPlainKey } from "../../utils/keyboard";
15
16
  import { detectShortcutPlatform, formatPrimaryShortcut, getShortcutDisplayMode } from "../../utils/shortcut-labels";
16
17
  import { syncBrokerInstance } from "../../brokers/sync-broker-instance";
17
18
  import { debugLog } from "../../utils/debug-log";
@@ -528,24 +529,24 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
528
529
 
529
530
  // --- Step-specific controls ---
530
531
  if (step === "theme") {
531
- if (event.name === "up" || event.name === "k") {
532
+ if (isPlainKey(event, "up", "k")) {
532
533
  setThemeIdx((i) => Math.max(0, i - 1));
533
- } else if (event.name === "down" || event.name === "j") {
534
+ } else if (isPlainKey(event, "down", "j")) {
534
535
  setThemeIdx((i) => Math.min(themeIds.length - 1, i + 1));
535
536
  }
536
537
  } else if (step === "portfolio" && portfolioSub === "choose") {
537
- if (event.name === "up" || event.name === "k") {
538
+ if (isPlainKey(event, "up", "k")) {
538
539
  setPortfolioOptionIdx((i) => Math.max(0, i - 1));
539
- } else if (event.name === "down" || event.name === "j") {
540
+ } else if (isPlainKey(event, "down", "j")) {
540
541
  setPortfolioOptionIdx((i) => Math.min(portfolioChoices.length - 1, i + 1));
541
542
  }
542
543
  } else if (step === "portfolio" && portfolioSub === "broker-fields" && selectedBrokerId) {
543
544
  const currentField = activeBrokerFields[brokerFieldIdx];
544
545
  if (currentField?.type === "select") {
545
546
  const optionCount = currentField.options?.length ?? 0;
546
- if (event.name === "up" || event.name === "k") {
547
+ if (isPlainKey(event, "up", "k")) {
547
548
  setBrokerSelectIdx((index) => Math.max(0, index - 1));
548
- } else if (event.name === "down" || event.name === "j") {
549
+ } else if (isPlainKey(event, "down", "j")) {
549
550
  setBrokerSelectIdx((index) => Math.min(optionCount - 1, index + 1));
550
551
  }
551
552
  }
@@ -10,6 +10,7 @@ import type {
10
10
  } from "../types/plugin";
11
11
  import type { PluginRegistry } from "../plugins/registry";
12
12
  import { blendHex, colors } from "../theme/colors";
13
+ import { isPlainKey } from "../utils/keyboard";
13
14
  import { Button, DialogFrame, ListView, MultiSelectDialogContent, TextField } from "./ui";
14
15
  import { NativeSelect, openNativeSelect, type NativeSelectElement } from "./ui/native-select";
15
16
 
@@ -167,7 +168,9 @@ function DesktopSwitch({
167
168
  style={{
168
169
  border: `1px solid ${checked ? colors.borderFocused : colors.border}`,
169
170
  borderRadius: 999,
170
- boxShadow: checked ? "inset 0 1px 0 rgba(255,255,255,0.10)" : "inset 0 1px 0 rgba(255,255,255,0.05)",
171
+ boxShadow: checked
172
+ ? `inset 0 1px 0 ${blendHex(colors.selected, colors.textBright, 0.1)}`
173
+ : `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.05)}`,
171
174
  cursor: "pointer",
172
175
  paddingInline: 2,
173
176
  }}
@@ -178,7 +181,7 @@ function DesktopSwitch({
178
181
  backgroundColor={checked ? colors.selectedText : colors.textMuted}
179
182
  style={{
180
183
  borderRadius: 999,
181
- boxShadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
184
+ boxShadow: `0 1px 2px ${blendHex(colors.panel, colors.bg, 0.35)}`,
182
185
  }}
183
186
  />
184
187
  </Box>
@@ -333,8 +336,8 @@ function useSelectFieldDialogController({
333
336
 
334
337
  useDialogKeyboard((event) => {
335
338
  event.stopPropagation();
336
- if (event.name === "up" || event.name === "k") setSelectedIndex((index) => Math.max(0, index - 1));
337
- else if (event.name === "down" || event.name === "j") setSelectedIndex((index) => Math.min(field.options.length - 1, index + 1));
339
+ if (isPlainKey(event, "up", "k")) setSelectedIndex((index) => Math.max(0, index - 1));
340
+ else if (isPlainKey(event, "down", "j")) setSelectedIndex((index) => Math.min(field.options.length - 1, index + 1));
338
341
  else if (event.name === "escape") dismiss();
339
342
  else if (event.name === "enter" || event.name === "return" || isSpaceKey(event)) {
340
343
  const option = field.options[selectedIndex];
@@ -716,8 +719,8 @@ export function PaneSettingsDialogContent({
716
719
 
717
720
  useDialogKeyboard((event) => {
718
721
  event.stopPropagation();
719
- if (event.name === "up" || event.name === "k") setSelectedIndex((index) => Math.max(0, index - 1));
720
- else if (event.name === "down" || event.name === "j") setSelectedIndex((index) => Math.min(fields.length - 1, index + 1));
722
+ if (isPlainKey(event, "up", "k")) setSelectedIndex((index) => Math.max(0, index - 1));
723
+ else if (isPlainKey(event, "down", "j")) setSelectedIndex((index) => Math.min(fields.length - 1, index + 1));
721
724
  else if (event.name === "escape") dismiss();
722
725
  else if (event.name === "enter" || event.name === "return" || isSpaceKey(event)) {
723
726
  void openFieldEditor(fields[selectedIndex]).catch(() => {});
@@ -4,6 +4,7 @@ import { type InputRenderable, type TextareaRenderable } from "../ui";
4
4
  import { type AlertContext, type PromptContext, useDialogKeyboard } from "../ui/dialog";
5
5
  import type { WizardStep } from "../types/plugin";
6
6
  import { colors } from "../theme/colors";
7
+ import { isPlainKey } from "../utils/keyboard";
7
8
  import { DialogFrame, ListView, TextField } from "./ui";
8
9
 
9
10
  export function PaneTemplateInfoStep({
@@ -165,9 +166,9 @@ export function PaneTemplateSelectStep({
165
166
 
166
167
  useDialogKeyboard((event) => {
167
168
  event.stopPropagation();
168
- if (event.name === "up" || event.name === "k") {
169
+ if (isPlainKey(event, "up", "k")) {
169
170
  setSelectedIndex((index) => Math.max(0, index - 1));
170
- } else if (event.name === "down" || event.name === "j") {
171
+ } else if (isPlainKey(event, "down", "j")) {
171
172
  setSelectedIndex((index) => Math.min(options.length - 1, index + 1));
172
173
  } else if (event.name === "return" || event.name === "enter") {
173
174
  resolve(options[selectedIndex]?.value ?? "");
@@ -8,6 +8,7 @@ import { colors } from "../theme/colors";
8
8
  import { formatMarketPrice } from "../utils/market-format";
9
9
  import { DialogFrame, ListView, NumberField } from "./ui";
10
10
  import { padTo } from "../utils/format";
11
+ import { isPlainKey } from "../utils/keyboard";
11
12
 
12
13
  interface PricePreset {
13
14
  label: string;
@@ -98,9 +99,9 @@ export function PriceSelectorDialog({
98
99
  }
99
100
 
100
101
  if (mode === "list") {
101
- if (event.name === "up" || event.name === "k") {
102
+ if (isPlainKey(event, "up", "k")) {
102
103
  setIndex((current) => Math.max(0, current - 1));
103
- } else if (event.name === "down" || event.name === "j") {
104
+ } else if (isPlainKey(event, "down", "j")) {
104
105
  if (index < presets.length - 1) {
105
106
  setIndex((current) => current + 1);
106
107
  } else {