gloomberb 0.17.4 → 0.17.5

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 (119) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +80 -0
  3. package/src/api-client/data.ts +4 -1
  4. package/src/api-client/research-activity.ts +10 -3
  5. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
  6. package/src/app/runtime/update.ts +50 -3
  7. package/src/app.tsx +2 -0
  8. package/src/ask-gloom/pane-context.ts +11 -1
  9. package/src/cli/commands/overview.ts +2 -2
  10. package/src/cli/commands/system.ts +19 -1
  11. package/src/cli/glossary.ts +34 -4
  12. package/src/cli/index.ts +6 -0
  13. package/src/cli/pane-functions/function-help.ts +1 -1
  14. package/src/cli/pane-functions/market-session.ts +18 -3
  15. package/src/components/chart/composite/format.ts +7 -0
  16. package/src/components/chart/composite/legend.tsx +8 -11
  17. package/src/components/command-bar/commands/direct/index.ts +47 -0
  18. package/src/components/command-bar/commands/registry.ts +8 -0
  19. package/src/components/command-bar/panel/prompt-binding.ts +8 -0
  20. package/src/components/command-bar/routes/root/command-items.ts +16 -1
  21. package/src/components/command-bar/surface/index.tsx +2 -1
  22. package/src/components/command-bar/workflow/ops.ts +4 -3
  23. package/src/components/form-modal/confirm.tsx +1 -0
  24. package/src/components/form-modal/request.ts +2 -0
  25. package/src/components/layout/detached-pane-shell.tsx +6 -1
  26. package/src/components/layout/pane/sidebar.tsx +24 -11
  27. package/src/components/layout/shell/index.tsx +5 -1
  28. package/src/components/layout/shell/menu.ts +8 -0
  29. package/src/components/layout/shell/pane/capture.ts +29 -12
  30. package/src/components/ui/confirm-dialog.tsx +30 -4
  31. package/src/components/ui/fields.tsx +5 -0
  32. package/src/data/config/store/normalize.ts +8 -0
  33. package/src/i18n/es.ts +2 -0
  34. package/src/i18n/ja.ts +2 -0
  35. package/src/i18n/ko.ts +2 -0
  36. package/src/i18n/zh-cn.ts +2 -0
  37. package/src/i18n/zh-tw.ts +2 -0
  38. package/src/market-data/coordinator/events.ts +5 -1
  39. package/src/market-data/coordinator/index.ts +4 -0
  40. package/src/market-data/coordinator/quotes.ts +4 -0
  41. package/src/market-data/hooks.tsx +168 -29
  42. package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
  43. package/src/plugins/builtin/broker-manager/model.ts +2 -2
  44. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  45. package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
  46. package/src/plugins/builtin/chat/content/footer.tsx +5 -2
  47. package/src/plugins/builtin/chat/controller/index.ts +20 -0
  48. package/src/plugins/builtin/chat/controller/messages.ts +14 -2
  49. package/src/plugins/builtin/chat/controller/state.ts +15 -0
  50. package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
  51. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  52. package/src/plugins/builtin/chat/index.tsx +1 -1
  53. package/src/plugins/builtin/chat/share/flow.tsx +18 -9
  54. package/src/plugins/builtin/chat/unread-badge.ts +38 -7
  55. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
  56. package/src/plugins/builtin/futures-curve/headless.ts +13 -6
  57. package/src/plugins/builtin/futures-curve/index.tsx +3 -3
  58. package/src/plugins/builtin/futures-curve/model.ts +64 -4
  59. package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
  60. package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
  61. package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
  62. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  63. package/src/plugins/builtin/macro-day/release-table.ts +3 -3
  64. package/src/plugins/builtin/mna/model.ts +2 -1
  65. package/src/plugins/builtin/options-positioning/model.ts +14 -0
  66. package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
  67. package/src/plugins/builtin/perps/client.ts +77 -6
  68. package/src/plugins/builtin/perps/headless.ts +25 -14
  69. package/src/plugins/builtin/perps/index.tsx +1 -1
  70. package/src/plugins/builtin/perps/model.ts +32 -0
  71. package/src/plugins/builtin/perps/pane.tsx +5 -4
  72. package/src/plugins/builtin/rate-path/model.ts +1 -1
  73. package/src/plugins/builtin/rate-path/pane.tsx +2 -1
  74. package/src/plugins/builtin/research/event-sources.ts +1 -1
  75. package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
  76. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  77. package/src/plugins/builtin/sectors/client.ts +46 -19
  78. package/src/plugins/builtin/sectors/sector-model.ts +22 -10
  79. package/src/plugins/builtin/thirteenf/api.ts +60 -43
  80. package/src/plugins/builtin/thirteenf/data.ts +76 -35
  81. package/src/plugins/builtin/thirteenf/headless.ts +2 -0
  82. package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
  83. package/src/plugins/builtin/yield-curve/curves.ts +15 -1
  84. package/src/plugins/builtin/yield-curve/headless.ts +17 -7
  85. package/src/plugins/builtin/yield-curve/history.ts +2 -0
  86. package/src/plugins/builtin/yield-curve/index.tsx +23 -18
  87. package/src/plugins/builtin/yield-curve/series.ts +19 -9
  88. package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
  89. package/src/plugins/builtin/yield-curve/world.ts +4 -0
  90. package/src/plugins/registry/host-actions.ts +3 -0
  91. package/src/renderers/browser/app-services.ts +7 -1
  92. package/src/renderers/browser/config-host.ts +55 -2
  93. package/src/renderers/browser/first-screen.ts +193 -0
  94. package/src/renderers/browser/main.tsx +59 -7
  95. package/src/renderers/dom/desktop/controls.tsx +2 -0
  96. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
  97. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
  98. package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
  99. package/src/renderers/electrobun/shared/protocol.ts +3 -2
  100. package/src/renderers/electrobun/view/update-host.ts +2 -2
  101. package/src/shares/bug-report-dialog.tsx +162 -0
  102. package/src/shares/bug-report-flow.tsx +84 -0
  103. package/src/shares/bug-report.ts +42 -0
  104. package/src/shares/capture-flow.ts +19 -0
  105. package/src/shares/capture-preview.tsx +129 -0
  106. package/src/shares/chat.ts +10 -1
  107. package/src/shares/dialog.tsx +16 -91
  108. package/src/shares/flow.tsx +12 -16
  109. package/src/shares/personal-pane.ts +35 -1
  110. package/src/telemetry/usage-counts.ts +16 -0
  111. package/src/tickers/open-target.ts +61 -11
  112. package/src/time-series/resolve.ts +21 -6
  113. package/src/time-series/spec.ts +8 -0
  114. package/src/types/config.ts +14 -0
  115. package/src/updater/channel.ts +51 -0
  116. package/src/updater/github-releases.ts +85 -16
  117. package/src/updater/index.ts +125 -16
  118. package/src/utils/datetime-format.ts +7 -1
  119. package/src/utils/semver.ts +47 -2
@@ -250,6 +250,14 @@ export const commands: Command[] = [
250
250
  description: "Check GitHub releases for a newer version",
251
251
  category: "Config",
252
252
  },
253
+ {
254
+ // Not "Update ...": "update" has to land on Check for Updates first.
255
+ id: "release-channel",
256
+ prefix: "",
257
+ label: "Release Channel",
258
+ description: "Update from stable releases, or from nightly builds as well",
259
+ category: "Config",
260
+ },
253
261
  {
254
262
  id: "toggle-crash-reports",
255
263
  prefix: "",
@@ -52,3 +52,11 @@ function getSnapshot(): CommandBarPromptBinding | null {
52
52
  export function useCommandBarPromptBinding(): CommandBarPromptBinding | null {
53
53
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
54
54
  }
55
+
56
+ /**
57
+ * Calls `listener` with the open screen's query each time the bar publishes
58
+ * its prompt, and with an empty one when it closes. For code outside React.
59
+ */
60
+ export function subscribeCommandBarQuery(listener: (query: string) => void): () => void {
61
+ return subscribe(() => listener(binding?.query ?? ""));
62
+ }
@@ -4,6 +4,9 @@ import { isManualPortfolio } from "../../../../plugins/builtin/portfolio-list/mu
4
4
  import type { Command } from "../../commands/registry";
5
5
  import type { ResultItem } from "../../list/model";
6
6
  import { canRestartToApply } from "../../../../updater";
7
+ import { NIGHTLY_CHANNEL_SUMMARY, resolveUpdateChannel } from "../../../../updater/channel";
8
+ import { isPrereleaseVersion } from "../../../../utils/semver";
9
+ import { VERSION } from "../../../../version";
7
10
 
8
11
  interface RootCommandItemBuilderOptions {
9
12
  activeCollectionId: string | null;
@@ -157,6 +160,11 @@ export function createRootCommandItemBuilder({
157
160
  return state.config.telemetry?.attention === true
158
161
  ? "Currently on: share ticker research counts"
159
162
  : "Currently off: opt in to sharing ticker research counts";
163
+ case "release-channel":
164
+ if (resolveUpdateChannel(state.config) === "nightly") return `Currently nightly: ${NIGHTLY_CHANNEL_SUMMARY}`;
165
+ return isPrereleaseVersion(VERSION)
166
+ ? "Currently stable: this nightly build stays until a newer stable release"
167
+ : "Currently stable";
160
168
  case "font-size-increase":
161
169
  case "font-size-decrease":
162
170
  return `Currently ${state.config.fontSize ?? 12}px`;
@@ -176,6 +184,8 @@ export function createRootCommandItemBuilder({
176
184
  return state.config.telemetry?.usage === false ? "off" : "on";
177
185
  case "toggle-attention-counts":
178
186
  return state.config.telemetry?.attention === true ? "on" : "off";
187
+ case "release-channel":
188
+ return resolveUpdateChannel(state.config);
179
189
  default:
180
190
  return command.prefix || undefined;
181
191
  }
@@ -185,8 +195,13 @@ export function createRootCommandItemBuilder({
185
195
  switch (command.id) {
186
196
  case "set-portfolio-position":
187
197
  return "edit position update position modify position manual position portfolio position";
198
+ // "update" is a whole word here so it lands on this command before Set
199
+ // Portfolio Position (whose keywords say "update position") and Release
200
+ // Channel, which keeps the word further back.
188
201
  case "check-for-updates":
189
- return state.updateProgress?.phase === "ready" ? "check for updates restart relaunch" : "";
202
+ return state.updateProgress?.phase === "ready" ? "update check for updates restart relaunch" : "update";
203
+ case "release-channel":
204
+ return "nightly stable beta prerelease update updates channel";
190
205
  default:
191
206
  return "";
192
207
  }
@@ -77,9 +77,10 @@ export function CommandBar({
77
77
  titleBarOverlay,
78
78
  visibleListStateRef,
79
79
  } = useCommandBarEnvironment();
80
+ // Without an updater (the web terminal) neither checking nor a channel means anything.
80
81
  const availableCommands = useMemo(() => onCheckForUpdates
81
82
  ? allAvailableCommands
82
- : allAvailableCommands.filter((command) => command.id !== "check-for-updates"), [allAvailableCommands, onCheckForUpdates]);
83
+ : allAvailableCommands.filter((command) => command.id !== "check-for-updates" && command.id !== "release-channel"), [allAvailableCommands, onCheckForUpdates]);
83
84
  const {
84
85
  applyThemePreview,
85
86
  clearThemePreview,
@@ -2,7 +2,7 @@ import { type LayoutConfig, type PaneBinding, type PaneInstanceConfig } from "..
2
2
  import type { PaneSettingField, PaneTemplateContext, PaneTemplateCreateOptions, PaneTemplateInstanceConfig, PaneTemplateDef } from "../../../types/plugin";
3
3
  import { getFocusedCollectionId, getFocusedTickerSymbol } from "../../../state/app/context";
4
4
  import { resolveInstrumentForPane, resolveListingForPane } from "../../../core/state/app/instrument";
5
- import { resolveTickerOpenTarget } from "../../../tickers/open-target";
5
+ import { resolveTickerOpenTarget, tickerOpenFailureMessage } from "../../../tickers/open-target";
6
6
  import { instrumentFromTicker } from "../../../market-data/request-types";
7
7
  import { scopedBrokerContractIdentityKey } from "../../../utils/instrument-identity";
8
8
  import { tickerSelectionFromSearchResult } from "../../../tickers/selection";
@@ -120,9 +120,10 @@ async function resolvePaneTemplateOptions(
120
120
  && (tickerNamed || !template.shortcut.argOptional);
121
121
  if (resolvesTicker) {
122
122
  if (resolvedOptions?.instrument === null && !resolvedOptions.ticker) {
123
- const publicTarget = await resolveTickerOpenTarget({ query: resolvedOptions.symbol ?? resolvedOptions.arg ?? baseContext.activeTicker ?? "",
123
+ const publicQuery = resolvedOptions.symbol ?? resolvedOptions.arg ?? baseContext.activeTicker ?? "";
124
+ const publicTarget = await resolveTickerOpenTarget({ query: publicQuery,
124
125
  tickers: state.tickers, tickerRepository: deps.tickerRepository, dataProvider: deps.dataProvider, publicOnly: true });
125
- if (!publicTarget) throw new Error("Could not resolve the selected public listing.");
126
+ if (!publicTarget) throw new Error(tickerOpenFailureMessage(publicQuery.trim().toUpperCase()));
126
127
  deps.dispatch({ type: "UPDATE_TICKER", ticker: publicTarget.ticker });
127
128
  if (publicTarget.created) deps.pluginRegistry.events.emit("ticker:added", { symbol: publicTarget.symbol, ticker: publicTarget.ticker });
128
129
  resolvedOptions = { ...resolvedOptions, symbol: publicTarget.symbol, ticker: publicTarget.ticker, listing: resolvedOptions.listing ?? publicTarget.listing };
@@ -137,6 +137,7 @@ export function ConfirmModalContent({
137
137
  confirmLabel={t(confirm.confirmLabel)}
138
138
  cancelLabel={t(confirmCancelLabel(confirm.cancelLabel))}
139
139
  confirmVariant={confirm.tone === "default" ? "primary" : "danger"}
140
+ defaultAction={confirm.defaultAction}
140
141
  width={contentWidth}
141
142
  status={status}
142
143
  busy={pending}
@@ -12,6 +12,8 @@ export interface ConfirmModalOptions {
12
12
  cancelLabel?: string;
13
13
  /** Danger unless said otherwise: most confirms guard a deletion. */
14
14
  tone?: "default" | "danger";
15
+ /** What Enter answers: `cancel` for an opt-in a stray Enter must not accept (ConfirmDialog). */
16
+ defaultAction?: "confirm" | "cancel";
15
17
  /** Reads what it acts on when it runs, not when the confirm opened. */
16
18
  onConfirm: () => void | Promise<void>;
17
19
  /** "stay" keeps the confirm open after it ran; "back" closes it, as "close" does. */
@@ -38,7 +38,8 @@ import {
38
38
  paneManagementAccelerators,
39
39
  resolvePaneManagementShortcut,
40
40
  } from "./shell/shortcuts";
41
- import { menuItemsForFallback, paneFooterMenuItems } from "./shell/menu";
41
+ import { menuItemsForFallback, paneFooterMenuItems, REPORT_BUG_MENU_LABEL } from "./shell/menu";
42
+ import { canReportPaneBug } from "../../shares/bug-report-flow";
42
43
  import { useShellPaneCapture } from "./shell/pane/capture";
43
44
  import { captureStampFooter, usePaneCaptureStamp } from "./pane/capture-stamp";
44
45
  import { pluginContextMenuSlot, withPluginContextMenuItems } from "../../ui/context-menu";
@@ -252,6 +253,10 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
252
253
  }
253
254
  items.push({ id: "pane-docs", label: "Docs", onSelect: openDocs });
254
255
  items.push({ id: "share-pane", label: "Share", accelerator: accelerators.share, onSelect: sharePane });
256
+ // A popped-out window paints its pane's DOM, so it can always picture it.
257
+ if (canReportPaneBug(true)) {
258
+ items.push({ id: "report-bug", label: REPORT_BUG_MENU_LABEL, onSelect: () => pluginRegistry.reportPaneBug(desktopWindowBridge.paneId) });
259
+ }
255
260
  items.push(pluginContextMenuSlot("pane"));
256
261
  if (dockPane) {
257
262
  items.push({ id: "dock", label: "Dock in Main Window", accelerator: accelerators.toggleFloating, onSelect: dockPane });
@@ -178,9 +178,11 @@ export function PaneSidebar({
178
178
  (resize?.onResizeEnd ?? resize?.onResize)?.(next);
179
179
  }, [resize, resolveDragWidth, stopLiveDrag]);
180
180
 
181
- // The keyboard moves the divider too: `<` and `>` while the pane is focused,
182
- // shown in the footer and listed in the pane menu. Each press is a finished
183
- // resize, so it persists.
181
+ // The keyboard moves the divider too: `<` and `>` while the pane is focused.
182
+ // They are bound and listed in the pane menu, not drawn in the footer: the
183
+ // width of a list is not one of the pane's actions, and the divider shows
184
+ // where it is (on the desktop its tooltip names the keys). Each press is a
185
+ // finished resize, so it persists.
184
186
  const resizeRegistrationId = `pane-sidebar-resize:${useId()}`;
185
187
  const canNarrow = !!resize && width > resize.min;
186
188
  const canWiden = !!resize && width < resize.max;
@@ -189,14 +191,25 @@ export function PaneSidebar({
189
191
  const next = Math.min(resize.max, Math.max(resize.min, width + delta));
190
192
  if (next !== width) (resize.onResizeEnd ?? resize.onResize)(next);
191
193
  };
192
- usePaneFooter(resizeRegistrationId, () => (resize ? {
193
- // After the pane's own actions, which register at order 0.
194
- order: 1,
195
- hints: [
196
- { id: "sidebar-narrow", key: NARROW_KEY, label: " sidebar", title: t("Narrow Sidebar"), onPress: () => stepResize(-KEYBOARD_RESIZE_STEP), disabled: !canNarrow },
197
- { id: "sidebar-widen", key: WIDEN_KEY, label: "", title: t("Widen Sidebar"), onPress: () => stepResize(KEYBOARD_RESIZE_STEP), disabled: !canWiden },
198
- ],
199
- } : null), [!!resize, canNarrow, canWiden, resize?.min, resize?.max, width]);
194
+ usePaneFooter(resizeRegistrationId, () => {
195
+ if (!resize) return null;
196
+ const steps = [
197
+ { id: "sidebar-narrow", key: NARROW_KEY, title: "Narrow Sidebar", delta: -KEYBOARD_RESIZE_STEP, disabled: !canNarrow },
198
+ { id: "sidebar-widen", key: WIDEN_KEY, title: "Widen Sidebar", delta: KEYBOARD_RESIZE_STEP, disabled: !canWiden },
199
+ ];
200
+ return {
201
+ // After the pane's own actions, which register at order 0.
202
+ order: 1,
203
+ keys: steps.map(({ id, key, delta, disabled }) => ({ id, key, label: "", onPress: () => stepResize(delta), disabled })),
204
+ // A bound that is reached drops out, as a disabled footer hint did.
205
+ menu: steps.filter((step) => !step.disabled).map(({ id, key, title, delta }) => ({
206
+ id: `pane-sidebar:${id}`,
207
+ label: title,
208
+ accelerator: key,
209
+ onSelect: () => stepResize(delta),
210
+ })),
211
+ };
212
+ }, [!!resize, canNarrow, canWiden, resize?.min, resize?.max, width]);
200
213
 
201
214
  const borderWidth = nativePaneChrome ? 0 : width > 1 ? 1 : 0;
202
215
  const listWidth = Math.max(width - borderWidth, 1);
@@ -71,6 +71,7 @@ import type { AppTickerRepositoryPort } from "../../../core/app-service-ports";
71
71
  import type { DataProvider } from "../../../types/data-provider";
72
72
  import { useShellPaneActions } from "./pane/actions";
73
73
  import { useShellPaneCapture } from "./pane/capture";
74
+ import { canReportPaneBug } from "../../../shares/bug-report-flow";
74
75
  import { useShellPaneLayoutMoves } from "./pane/new-layout";
75
76
  import { resolvePaneFocusSourceLayout } from "./fullscreen";
76
77
  import { useTransientLayout } from "../transient-layout";
@@ -668,6 +669,9 @@ export function Shell({
668
669
  paneLayoutMoveMenu(paneId),
669
670
  () => { openPaneDocs(paneId); },
670
671
  canAsk ? () => { askAboutPane(paneId, { cover: kitMenuRectRef.current }); } : undefined,
672
+ canReportPaneBug(nativePaneChrome || !!nativeRenderer.capturePaneImage)
673
+ ? () => { pluginRegistry.reportPaneBug(paneId); }
674
+ : undefined,
671
675
  );
672
676
  const showKitMenu = () => {
673
677
  const fallbackSourceItems = withPluginContextMenuItems(context, items, pluginRegistry);
@@ -710,7 +714,7 @@ export function Shell({
710
714
  void showContextMenu(context, items, event).then((shown) => {
711
715
  if (!shown) showKitMenu();
712
716
  });
713
- }, [askAboutPane, canAsk, canExportPaneCsv, closePaneMenu, contentHeight, copyPaneScreenshot, desktopWindowBridge, exportPaneCsv, focusPane, getPaneTitle, handlePaneQuickSetting, nativePaneChrome, togglePaneFullscreen, openPaneDocs, openPaneSettings, paneAccelerators, paneLayoutMoveMenu, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, sharePane, shortcutDisplayMode, showContextMenu, titleState, visibleLayout, width]);
717
+ }, [askAboutPane, canAsk, canExportPaneCsv, closePaneMenu, contentHeight, copyPaneScreenshot, desktopWindowBridge, exportPaneCsv, focusPane, getPaneTitle, handlePaneQuickSetting, nativePaneChrome, nativeRenderer, togglePaneFullscreen, openPaneDocs, openPaneSettings, paneAccelerators, paneLayoutMoveMenu, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, sharePane, shortcutDisplayMode, showContextMenu, titleState, visibleLayout, width]);
714
718
  openPaneMenuRef.current = openPaneMenu;
715
719
 
716
720
 
@@ -25,6 +25,9 @@ const MENU_MAX_WIDTH = 44;
25
25
 
26
26
  export const MENU_Z_INDEX = 10_000;
27
27
 
28
+ /** The pane menu entry that opens Report a Bug, in the main window's and a popped-out window's menu. */
29
+ export const REPORT_BUG_MENU_LABEL = "Report a Bug";
30
+
28
31
  /** One row of the kit pane menu; a divider row is skipped by the keyboard. */
29
32
  export interface PaneMenuEntry {
30
33
  id: string;
@@ -107,6 +110,8 @@ export function menuForPane(
107
110
  openDocs?: () => void,
108
111
  /** A new Ask Gloom conversation with this pane attached, the same as the button beside the `?`. */
109
112
  askGloom?: () => void,
113
+ /** Report a Bug: the pane's picture and a line to #bugs; absent where it cannot be posted or pictured. */
114
+ reportBug?: () => void,
110
115
  ): ContextMenuItem[] {
111
116
  const baseActions: ContextMenuItem[] = [...paneItems];
112
117
  if (baseActions.length > 0) baseActions.push(contextMenuDivider("pane:own-actions-divider"));
@@ -141,6 +146,9 @@ export function menuForPane(
141
146
  onSelect: sharePane,
142
147
  });
143
148
  }
149
+ if (reportBug) {
150
+ baseActions.push({ id: "report-bug", label: REPORT_BUG_MENU_LABEL, onSelect: reportBug });
151
+ }
144
152
  if (copyPaneScreenshot) {
145
153
  baseActions.push({
146
154
  id: "copy-screenshot",
@@ -4,13 +4,14 @@ import type { LayoutBounds, ResolvedPane } from "../../../../layout/pane-manager
4
4
  import type { PluginRegistry } from "../../../../plugins/registry";
5
5
  import { paneScreenshotScale, PANE_SCREENSHOT_MIN_WIDTH } from "../../../../remote/pane-screenshot";
6
6
  import { base64Bytes, formatCaptureStamp, type PaneImageCapture } from "../../../../shares/pane-image";
7
- import { panePersonalData } from "../../../../shares/personal-pane";
7
+ import { capturedPersonalData } from "../../../../shares/personal-pane";
8
8
  import { resolveTickerForPane, useAppStateRef } from "../../../../state/app/context";
9
9
  import type { LayoutConfig } from "../../../../types/config";
10
10
  import { useUiCapabilities, type NativeRendererHost } from "../../../../ui";
11
11
  import { buildPaneSharePayload } from "../../../../shares/pane";
12
12
  import { preparePaneShare } from "../../../../shares/prepare";
13
13
  import { shareFocusedPane } from "../../../../shares/flow";
14
+ import { reportPaneBug } from "../../../../shares/bug-report-flow";
14
15
  import { useDialog } from "../../../../ui/dialog";
15
16
  import { capturePaneImage as paintPaneImage } from "../../../../utils/dom-screenshot";
16
17
  import { setPaneCaptureStamp } from "../../pane/capture-stamp";
@@ -45,14 +46,16 @@ interface UseShellPaneCaptureOptions {
45
46
 
46
47
  /**
47
48
  * Binds `capturePaneImage` and `promptDialog` for plugins that post a pane's
48
- * picture or copy its link. The desktop and the web paint the pane's DOM; the
49
- * terminal draws the cells it shows, kitty charts included.
49
+ * picture or copy its link, and Share and Report a Bug, which are built on
50
+ * them. The desktop and the web paint the pane's DOM; the terminal draws the
51
+ * cells it shows, kitty charts included.
50
52
  */
51
53
  export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
52
54
  const { pluginRegistry } = options;
53
55
  const dialog = useDialog();
54
- // Shared by menu, command, key and plugin calls, including triggers without a repeat flag.
55
- const [shareOnce] = useState(createOneShot);
56
+ // Shared by Share and Report a Bug from a menu, command, key or plugin call,
57
+ // including triggers without a repeat flag: one capture flow at a time.
58
+ const [captureOnce] = useState(createOneShot);
56
59
  const { publicSharing } = useUiCapabilities();
57
60
  const stateRef = useAppStateRef();
58
61
  const latest = useRef(options);
@@ -71,6 +74,8 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
71
74
  setPaneCaptureStamp(paneId, formatCaptureStamp(capturedAt));
72
75
  try {
73
76
  let image: Pick<PaneImageCapture, "png" | "width" | "height" | "pixels" | "cells">;
77
+ // The terminal's picture is the screen's cells, so it shows whatever is drawn over the pane.
78
+ let drawn: Map<string, LayoutBounds> | null = null;
74
79
  if (current.nativePaneChrome) {
75
80
  await nextFrames(2);
76
81
  const shot = await paintPaneImage({
@@ -81,7 +86,8 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
81
86
  });
82
87
  image = { png: base64Bytes(shot.pngBase64), width: shot.width, height: shot.height };
83
88
  } else {
84
- const rect = current.screenRects().get(paneId);
89
+ drawn = current.screenRects();
90
+ const rect = drawn.get(paneId);
85
91
  if (!rect || !current.nativeRenderer.capturePaneImage) {
86
92
  throw new Error("Show the pane on screen to share it.");
87
93
  }
@@ -106,11 +112,13 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
106
112
  resolveTickerForPane(stateRef.current, paneId),
107
113
  stateRef.current.tickers,
108
114
  ) : null,
109
- personalData: panePersonalData({
115
+ personalData: capturedPersonalData({
110
116
  registry: current.pluginRegistry,
111
117
  state: stateRef.current,
112
118
  layout: current.layout,
113
- pane,
119
+ paneId,
120
+ panes: current.paneMap,
121
+ drawn,
114
122
  }),
115
123
  ...image,
116
124
  };
@@ -122,15 +130,24 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
122
130
  useEffect(() => pluginRegistry.bindHost({
123
131
  sharePane: (requestedPaneId) => {
124
132
  const paneId = requestedPaneId ?? latest.current.focusedPaneId;
125
- if (!paneId || shareOnce.refuses()) return;
126
- shareOnce.start();
133
+ if (!paneId || captureOnce.refuses()) return;
134
+ captureOnce.start();
127
135
  // Let a menu/command close before the capture starts. The flow also refuses
128
136
  // a second run for the entire dialog/post, even beyond the cooldown's timeout.
129
137
  setTimeout(() => {
130
- void shareFocusedPane(pluginRegistry, paneId).finally(() => shareOnce.end());
138
+ void shareFocusedPane(pluginRegistry, paneId).finally(() => captureOnce.end());
139
+ }, 0);
140
+ },
141
+ reportPaneBug: (requestedPaneId) => {
142
+ const paneId = requestedPaneId ?? latest.current.focusedPaneId;
143
+ if (!paneId || captureOnce.refuses()) return;
144
+ captureOnce.start();
145
+ // After the menu that chose it has closed.
146
+ setTimeout(() => {
147
+ void reportPaneBug(pluginRegistry, paneId).finally(() => captureOnce.end());
131
148
  }, 0);
132
149
  },
133
150
  capturePaneImage: capture,
134
151
  promptDialog: (prompt) => dialog.prompt(prompt),
135
- }), [capture, dialog, pluginRegistry, shareOnce]);
152
+ }), [capture, captureOnce, dialog, pluginRegistry]);
136
153
  }
@@ -36,6 +36,12 @@ export interface ConfirmDialogProps extends PromptContext<boolean> {
36
36
  * opens on its own while the person may be typing. Esc and `n` always answer.
37
37
  */
38
38
  keyGraceMs?: number;
39
+ /**
40
+ * What Enter answers. `cancel` is for an opt-in a stray Enter must not
41
+ * accept: Enter takes the cancel button, which is drawn as the primary one,
42
+ * and only `y` or the confirm button confirms.
43
+ */
44
+ defaultAction?: "confirm" | "cancel";
39
45
  }
40
46
 
41
47
  export function ConfirmDialog({
@@ -52,7 +58,9 @@ export function ConfirmDialog({
52
58
  onClose,
53
59
  formButtons = false,
54
60
  keyGraceMs = 0,
61
+ defaultAction = "confirm",
55
62
  }: ConfirmDialogProps) {
63
+ const enterCancels = defaultAction === "cancel";
56
64
  const desktop = useUiHost().kind === "desktop-web";
57
65
  const openedAt = useRef(Date.now());
58
66
  const confirm = useCallback(() => resolve(true), [resolve]);
@@ -71,7 +79,13 @@ export function ConfirmDialog({
71
79
  event.preventDefault();
72
80
  return;
73
81
  }
74
- if (event.name === "enter" || event.name === "return" || isPlainKey(event, "y")) {
82
+ const enter = event.name === "enter" || event.name === "return";
83
+ if (enter && enterCancels) {
84
+ event.preventDefault();
85
+ cancel();
86
+ return;
87
+ }
88
+ if (enter || isPlainKey(event, "y")) {
75
89
  event.preventDefault();
76
90
  if (Date.now() - openedAt.current < keyGraceMs) return;
77
91
  confirm();
@@ -88,13 +102,25 @@ export function ConfirmDialog({
88
102
 
89
103
  // The desktop shows the keys on the buttons instead of a footer line; a
90
104
  // confirm with no key line keeps its buttons plain there too.
105
+ // One primary button, the one Enter answers.
91
106
  const keys = footer !== "";
92
- const confirmButton = <Button label={confirmLabel} variant={confirmVariant} disabled={busy} desktopShortcut={keys ? "Enter" : undefined} onPress={confirm} />;
93
- const cancelButton = <Button label={cancelLabel} variant="secondary" disabled={busy} desktopShortcut={keys ? "Esc" : undefined} onPress={cancel} />;
107
+ const confirmButton = (
108
+ <Button
109
+ label={confirmLabel}
110
+ variant={enterCancels && confirmVariant === "primary" ? "secondary" : confirmVariant}
111
+ disabled={busy}
112
+ desktopShortcut={keys ? (enterCancels ? "Y" : "Enter") : undefined}
113
+ onPress={confirm}
114
+ />
115
+ );
116
+ const cancelButton = <Button label={cancelLabel} variant={enterCancels ? "primary" : "secondary"} disabled={busy} desktopShortcut={keys ? (enterCancels ? "Enter" : "Esc") : undefined} onPress={cancel} />;
117
+ const keyLine = enterCancels
118
+ ? tf("y {action} · Enter or Esc {cancel}", { action: t(confirmLabel).toLowerCase(), cancel: t(cancelLabel).toLowerCase() })
119
+ : tf("Enter {action} · Esc cancel", { action: t(confirmLabel).toLowerCase() });
94
120
  return (
95
121
  <DialogFrame
96
122
  title={title}
97
- footer={footer ?? tf("Enter {action} · Esc cancel", { action: t(confirmLabel).toLowerCase() })}
123
+ footer={footer ?? keyLine}
98
124
  desktopFooter={null}
99
125
  onClose={onClose}
100
126
  >
@@ -48,6 +48,8 @@ export interface TextFieldProps {
48
48
  labelWidth?: number;
49
49
  value?: string;
50
50
  placeholder?: string;
51
+ /** The most characters it takes; typing and pasting stop there. */
52
+ maxLength?: number;
51
53
  focused?: boolean;
52
54
  width?: number;
53
55
  inputRef?: RefObject<InputRenderable | null>;
@@ -119,6 +121,7 @@ function TextFieldControl({
119
121
  active,
120
122
  value,
121
123
  placeholder,
124
+ maxLength,
122
125
  focused,
123
126
  width,
124
127
  inputRef,
@@ -163,6 +166,7 @@ function TextFieldControl({
163
166
  label={label}
164
167
  value={value}
165
168
  placeholder={placeholder}
169
+ maxLength={maxLength}
166
170
  focused={focused}
167
171
  width={width}
168
172
  inputRef={inputRef}
@@ -226,6 +230,7 @@ function TextFieldControl({
226
230
  value={value}
227
231
  selectable={!isPassword}
228
232
  placeholder={isPassword ? "" : placeholder}
233
+ {...(maxLength === undefined ? {} : { maxLength })}
229
234
  focused={focused}
230
235
  textColor={isPassword ? backgroundColor : textColor}
231
236
  placeholderColor={placeholderColor}
@@ -27,6 +27,7 @@ import { isRecord } from "../../../utils/guards";
27
27
  import { debugLog } from "../../../utils/debug-log";
28
28
  import { DEFAULT_THEME, themes } from "../../../theme/themes";
29
29
  import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
30
+ import { sanitizeUpdatesConfig } from "../../../updater/channel";
30
31
 
31
32
  const configLog = debugLog.createLogger("config");
32
33
  const reportedUnknownThemes = new Set<string>();
@@ -106,6 +107,7 @@ export function normalizeLoadedConfig(
106
107
  ...withKeybindings(sanitizeKeybindings(candidate.keybindings)),
107
108
  ...withTelemetry(sanitizeTelemetry(candidate.telemetry)),
108
109
  ...withStarPrompt(sanitizeStarPrompt(candidate.starPrompt)),
110
+ ...withUpdates(sanitizeUpdatesConfig(candidate.updates)),
109
111
  };
110
112
 
111
113
  const needsSave =
@@ -169,6 +171,8 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
169
171
  Object.assign(persisted, withTelemetry(sanitizeTelemetry(config.telemetry)));
170
172
  delete persisted.starPrompt;
171
173
  Object.assign(persisted, withStarPrompt(sanitizeStarPrompt(config.starPrompt)));
174
+ delete persisted.updates;
175
+ Object.assign(persisted, withUpdates(sanitizeUpdatesConfig(config.updates)));
172
176
 
173
177
  return persisted;
174
178
  }
@@ -185,6 +189,10 @@ function withStarPrompt(starPrompt: AppConfig["starPrompt"]): Pick<AppConfig, "s
185
189
  return starPrompt ? { starPrompt } : {};
186
190
  }
187
191
 
192
+ function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates"> {
193
+ return updates ? { updates } : {};
194
+ }
195
+
188
196
  /** Only the switches that are set survive; an empty object is the same as none. */
189
197
  function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
190
198
  if (!isRecord(value)) return undefined;
package/src/i18n/es.ts CHANGED
@@ -70,6 +70,7 @@ export const es: Record<string, string> = {
70
70
  "Could not save this value.": "No se pudo guardar este valor.",
71
71
  "Could not apply that setting.": "No se pudo aplicar ese ajuste.",
72
72
  "Enter {action} · Esc cancel": "Enter {action} · Esc cancelar",
73
+ "y {action} · Enter or Esc {cancel}": "y {action} · Enter o Esc {cancel}",
73
74
  "Published {date}": "Publicado {date}",
74
75
  "Language": "Idioma",
75
76
  "Unsupported language": "Idioma no compatible",
@@ -151,6 +152,7 @@ export const es: Record<string, string> = {
151
152
  "Toggle Status Bar": "Alternar barra de estado",
152
153
  "Toggle Value Flashing": "Alternar parpadeo de valores",
153
154
  "Check for Updates": "Buscar actualizaciones",
155
+ "Release Channel": "Canal de versiones",
154
156
  "Crash Reports": "Informes de fallos",
155
157
  "Usage Counts": "Recuento de uso",
156
158
  "Change Theme": "Cambiar tema",
package/src/i18n/ja.ts CHANGED
@@ -70,6 +70,7 @@ export const ja: Record<string, string> = {
70
70
  "Could not save this value.": "この値を保存できませんでした。",
71
71
  "Could not apply that setting.": "その設定を適用できませんでした。",
72
72
  "Enter {action} · Esc cancel": "Enter {action} · Esc キャンセル",
73
+ "y {action} · Enter or Esc {cancel}": "y {action} · Enter または Esc {cancel}",
73
74
  "Published {date}": "{date} 公開",
74
75
  "Language": "言語",
75
76
  "Unsupported language": "対応していない言語",
@@ -152,6 +153,7 @@ export const ja: Record<string, string> = {
152
153
  "Toggle Status Bar": "ステータスバーを切り替え",
153
154
  "Toggle Value Flashing": "値の点滅を切り替え",
154
155
  "Check for Updates": "アップデートを確認",
156
+ "Release Channel": "リリースチャネル",
155
157
  "Crash Reports": "クラッシュレポート",
156
158
  "Usage Counts": "利用回数",
157
159
  "Change Theme": "テーマを変更",
package/src/i18n/ko.ts CHANGED
@@ -68,6 +68,7 @@ export const ko: Record<string, string> = {
68
68
  "Could not save this value.": "이 값을 저장할 수 없습니다.",
69
69
  "Could not apply that setting.": "해당 설정을 적용할 수 없습니다.",
70
70
  "Enter {action} · Esc cancel": "Enter {action} · Esc 취소",
71
+ "y {action} · Enter or Esc {cancel}": "y {action} · Enter 또는 Esc {cancel}",
71
72
  "Published {date}": "게시일 {date}",
72
73
  "Language": "언어",
73
74
  "Unsupported language": "지원하지 않는 언어",
@@ -150,6 +151,7 @@ export const ko: Record<string, string> = {
150
151
  "Toggle Status Bar": "상태 표시줄 전환",
151
152
  "Toggle Value Flashing": "값 깜박임 전환",
152
153
  "Check for Updates": "업데이트 확인",
154
+ "Release Channel": "릴리스 채널",
153
155
  "Crash Reports": "충돌 보고",
154
156
  "Usage Counts": "사용 횟수",
155
157
  "Change Theme": "테마 변경",
package/src/i18n/zh-cn.ts CHANGED
@@ -70,6 +70,7 @@ export const zhCN: Record<string, string> = {
70
70
  "Could not save this value.": "无法保存该值。",
71
71
  "Could not apply that setting.": "无法应用该设置。",
72
72
  "Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
73
+ "y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
73
74
  "Published {date}": "已发布 {date}",
74
75
  "Language": "语言",
75
76
  "Unsupported language": "不支持的语言",
@@ -152,6 +153,7 @@ export const zhCN: Record<string, string> = {
152
153
  "Toggle Status Bar": "切换状态栏",
153
154
  "Toggle Value Flashing": "切换数值闪烁",
154
155
  "Check for Updates": "检查更新",
156
+ "Release Channel": "发布通道",
155
157
  "Crash Reports": "崩溃报告",
156
158
  "Usage Counts": "使用统计",
157
159
  "Change Theme": "更换主题",
package/src/i18n/zh-tw.ts CHANGED
@@ -70,6 +70,7 @@ export const zhTW: Record<string, string> = {
70
70
  "Could not save this value.": "無法儲存該值。",
71
71
  "Could not apply that setting.": "無法套用該設定。",
72
72
  "Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
73
+ "y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
73
74
  "Published {date}": "已發布 {date}",
74
75
  "Language": "語言",
75
76
  "Unsupported language": "不支援的語言",
@@ -152,6 +153,7 @@ export const zhTW: Record<string, string> = {
152
153
  "Toggle Status Bar": "切換狀態列",
153
154
  "Toggle Value Flashing": "切換數值閃爍",
154
155
  "Check for Updates": "檢查更新",
156
+ "Release Channel": "發布通道",
155
157
  "Crash Reports": "當機報告",
156
158
  "Usage Counts": "使用統計",
157
159
  "Change Theme": "更換主題",
@@ -67,10 +67,14 @@ export class MarketDataCoordinatorEvents {
67
67
  return (this.keyListeners.get(key)?.size ?? 0) > 0;
68
68
  }
69
69
 
70
+ getKeyVersion(key: string): number {
71
+ return this.keyVersions.get(key) ?? 0;
72
+ }
73
+
70
74
  getKeysVersion(keys: readonly string[]): number {
71
75
  let version = 0;
72
76
  for (const key of new Set(keys)) {
73
- version += this.keyVersions.get(key) ?? 0;
77
+ version += this.getKeyVersion(key);
74
78
  }
75
79
  return version;
76
80
  }
@@ -218,6 +218,10 @@ export class MarketDataCoordinator {
218
218
  return this.events.getVersion();
219
219
  }
220
220
 
221
+ getKeyVersion(key: string): number {
222
+ return this.events.getKeyVersion(key);
223
+ }
224
+
221
225
  getKeysVersion(keys: readonly string[]): number {
222
226
  return this.events.getKeysVersion(keys);
223
227
  }
@@ -95,6 +95,10 @@ export async function loadQuoteEntry({
95
95
  resolveQuote,
96
96
  }: LoadQuoteEntryOptions): Promise<QueryEntry<Quote>> {
97
97
  const key = buildQuoteKey(instrument);
98
+ if (!options.forceRefresh) {
99
+ const current = quoteStore.get(key);
100
+ if (hasFreshQuoteEntry(current, instrument, SNAPSHOT_CACHE_TTL_MS)) return current;
101
+ }
98
102
  const flightKey = options.forceRefresh ? `${key}|refresh` : key;
99
103
  return runSingleFlight(flightKey, async () => {
100
104
  quoteStore.update(key, loadingEntry);