gloomberb 0.17.3 → 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.
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/data.ts +4 -1
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/index.ts +4 -1
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/research-activity.ts +10 -3
- package/src/api-client/socket.ts +27 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/app/runtime/update.ts +50 -3
- package/src/app.tsx +2 -0
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/glossary.ts +34 -4
- package/src/cli/index.ts +6 -0
- package/src/cli/pane-functions/function-help.ts +2 -2
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/components/chart/composite/format.ts +7 -0
- package/src/components/chart/composite/legend.tsx +8 -11
- package/src/components/command-bar/commands/direct/index.ts +47 -0
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +16 -1
- package/src/components/command-bar/surface/index.tsx +2 -1
- package/src/components/command-bar/workflow/ops.ts +4 -3
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/config/store/normalize.ts +8 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-cn.ts +2 -0
- package/src/i18n/zh-tw.ts +2 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +4 -0
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/hooks.tsx +168 -29
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +69 -21
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/controller/state.ts +15 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +2 -0
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/docs/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +2 -3
- package/src/plugins/builtin/futures/model.ts +4 -2
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures-curve/headless.ts +13 -6
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +64 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/options-positioning/model.ts +14 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -3
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +4 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
- package/src/renderers/electrobun/shared/protocol.ts +3 -2
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +12 -4
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/time-series/resolve.ts +26 -9
- package/src/time-series/spec.ts +8 -0
- package/src/types/config.ts +14 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +125 -16
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/semver.ts +47 -2
- package/src/utils/utc-time.ts +14 -3
package/src/cli/glossary.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { cliStyles, cliTerminalWidth, renderDefinitions, renderSection, wrapText
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Plain-words meanings of the rates, auction, energy, perpetual-futures, portfolio,
|
|
5
|
-
* fund, short interest, quote-board, futures and economic-calendar terms the functions
|
|
5
|
+
* fund, short interest, quote-board, futures (basis, contango, roll yield) and economic-calendar terms the functions
|
|
6
6
|
* show in their rows, headers and footers.
|
|
7
7
|
* `catalog glossary` lists them, `catalog explain <term>` prints one, and
|
|
8
8
|
* `fn <function> --explain` and `portfolio show --explain` follow a report with
|
|
@@ -301,10 +301,11 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
301
301
|
+ "The funding rate keeps its price close to the asset it tracks.",
|
|
302
302
|
},
|
|
303
303
|
{
|
|
304
|
-
term: "Funding rate", aliases: ["funding", "funding payment", "funding fee", "funding 8h"], functions: ["PERP"],
|
|
305
|
-
shown: ["Funding %/int", "Funding 8h", "Paid %/int"],
|
|
304
|
+
term: "Funding rate", aliases: ["funding", "funding payment", "funding fee", "funding 8h", "funding apr", "apr"], functions: ["PERP"],
|
|
305
|
+
shown: ["Funding %/int", "Funding 8h", "Paid %/int", "APR %"],
|
|
306
306
|
definition: "A regular payment between the two sides of a perpetual contract, set to pull its price toward the asset's. "
|
|
307
|
-
+ "Positive means longs pay shorts, negative means shorts pay longs."
|
|
307
|
+
+ "Positive means longs pay shorts, negative means shorts pay longs, so a basis trade (long spot, short the perpetual) collects it when positive. "
|
|
308
|
+
+ "APR % is the current 8h rate × 3 × 365: a simple annualisation of the rate now, not funding actually paid.",
|
|
308
309
|
},
|
|
309
310
|
{
|
|
310
311
|
term: "Open interest", short: "OI", aliases: ["open interest usd"], functions: ["PERP"],
|
|
@@ -473,6 +474,35 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
473
474
|
definition: "A futures contract's official price for the day, set by its exchange from trading near the close. "
|
|
474
475
|
+ "The day's change and margin are measured from it, and it can differ from the last trade. FUT's PREV is the previous one.",
|
|
475
476
|
},
|
|
477
|
+
// Futures basis and curve shape (CTM, FUT, PERP).
|
|
478
|
+
{
|
|
479
|
+
term: "Futures basis", aliases: ["basis", "basis trade", "cash and carry", "cash-and-carry", "vs spot", "spot spread"],
|
|
480
|
+
functions: ["CTM", "FUT", "PERP"], shown: ["Vs Spot", "VS SPOT"],
|
|
481
|
+
definition: "The gap between a futures price and the spot price of what it tracks, in percent: positive when the future costs more, negative when it costs less. "
|
|
482
|
+
+ "It closes by expiry. A basis trade buys spot and sells the future to earn it. A perpetual has no expiry, so its funding rate plays that part.",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
term: "Annualised basis", aliases: ["annualized basis", "ann basis", "ann. basis", "basis annualised"], functions: ["CTM", "PERP"], shown: ["Ann Basis", "ANN BASIS"],
|
|
486
|
+
definition: "The basis restated as a yearly rate, so contracts of different length compare with each other and with a perpetual's funding APR: "
|
|
487
|
+
+ "(Last / spot - 1) × 365 / days to expiry, simple rather than compounded, on the latest trade against the spot quote. "
|
|
488
|
+
+ "Inside 30 days a few basis points of spot noise move it by whole points: 10 bp is about ±2 points at 18 days to expiry.",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
term: "Contango", aliases: ["contango curve", "upward curve"], functions: ["CTM", "FUT"], shown: ["contango", "Contango"],
|
|
492
|
+
definition: "A futures curve that slopes up: each contract costs more than the one before it, and the future trades above spot. "
|
|
493
|
+
+ "A long rolling into the next contract pays that difference, a short earns it, and the basis closes as expiry nears.",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
term: "Backwardation", aliases: ["backwardation curve", "downward curve", "inverted futures curve"], functions: ["CTM", "FUT"], shown: ["backwardation", "Backwardation"],
|
|
497
|
+
definition: "A futures curve that slopes down: later contracts cost less than nearer ones, and the future trades below spot, often when the asset is scarce now. "
|
|
498
|
+
+ "A long rolling into the next contract earns that difference, a short pays it.",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
term: "Roll yield", aliases: ["roll", "roll return", "annualised roll yield", "annualized roll yield", "ann roll yield", "ann. roll yield"], functions: ["CTM", "FUT"],
|
|
502
|
+
shown: ["Ann. roll yield", "Annualized Roll Yield"],
|
|
503
|
+
definition: "What a long future earns or loses as it is rolled into the next contract. CTM's annualised roll yield is the front price over the next contract's "
|
|
504
|
+
+ "less 1, scaled to a year: (front / next - 1) × 365 / days between the two. It is positive in backwardation and negative in contango.",
|
|
505
|
+
},
|
|
476
506
|
// The economic calendar (ECO).
|
|
477
507
|
{
|
|
478
508
|
term: "Actual", aliases: ["actual release", "actual value"], functions: ["ECO"], shown: ["ACTUAL", "Actual"],
|
package/src/cli/index.ts
CHANGED
|
@@ -251,6 +251,12 @@ function createCoreCliCommands(
|
|
|
251
251
|
help: {
|
|
252
252
|
group: CLI_COMMAND_GROUPS.plugins,
|
|
253
253
|
usage: ["update [name]"],
|
|
254
|
+
sections: [{
|
|
255
|
+
title: "Updating Gloomberb itself",
|
|
256
|
+
lines: [
|
|
257
|
+
"The app updates itself from stable releases. config set updates.channel nightly adds the nightly build of main; see gloomberb help config.",
|
|
258
|
+
],
|
|
259
|
+
}],
|
|
254
260
|
examples: ["update", "update gloom-tv"],
|
|
255
261
|
},
|
|
256
262
|
execute: async (args) => {
|
|
@@ -749,14 +749,14 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
749
749
|
bloomberg: [],
|
|
750
750
|
},
|
|
751
751
|
FUT: {
|
|
752
|
-
summary: "Each root's most active futures contract across equity index, rates, energy, metals, agriculture, livestock and FX with last price, unit and session change, grouped by sector.",
|
|
752
|
+
summary: "Each root's most active futures contract across equity index, equity sectors, rates, energy, metals, agriculture, livestock and FX with last price, unit and session change, grouped by sector.",
|
|
753
753
|
usage: ["FUT"],
|
|
754
754
|
keys: [SEARCH, OPEN],
|
|
755
755
|
data: same(DELAYED),
|
|
756
756
|
bloomberg: ["GLCO"],
|
|
757
757
|
},
|
|
758
758
|
CTM: {
|
|
759
|
-
summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote.",
|
|
759
|
+
summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote: Ann Basis = (Last / spot - 1) × 365 / days to expiry, simple actual/365.",
|
|
760
760
|
usage: ["CTM GC", "CTM BTC"],
|
|
761
761
|
keys: [key("d", "ate"), key("c", "urrent"), STEP],
|
|
762
762
|
data: same("Delayed, usually 10 minutes; VIX at settlement"),
|
|
@@ -48,7 +48,8 @@ const US_EXCHANGE_TIME_ZONE = "America/New_York";
|
|
|
48
48
|
/**
|
|
49
49
|
* A venue's session now, from its calendar (weekends and published holidays),
|
|
50
50
|
* or from the state its source reports when the app has no calendar for it,
|
|
51
|
-
* such as an index publisher or a futures exchange
|
|
51
|
+
* such as an index publisher or a futures exchange; an index quote that states
|
|
52
|
+
* none is placed on the sessions it carries. Null when none of them says.
|
|
52
53
|
*/
|
|
53
54
|
function venueSession(exchange: string, marketState: string | undefined, now: number, calendar?: QuoteIndexSession): VenueSession | null {
|
|
54
55
|
if (isAlwaysOpenExchange(exchange)) return { state: "always" };
|
|
@@ -68,8 +69,22 @@ function venueSession(exchange: string, marketState: string | undefined, now: nu
|
|
|
68
69
|
}
|
|
69
70
|
if (marketState === "REGULAR") return { state: "open" };
|
|
70
71
|
const stated = marketState === "PRE" ? "pre-market" : marketState === "POST" ? "after-hours" : marketState && CLOSED_STATES.has(marketState) ? "closed" : null;
|
|
71
|
-
if (!
|
|
72
|
-
|
|
72
|
+
if (!calendar) return stated ? { state: stated } : null;
|
|
73
|
+
const state = stated ?? quotedState(calendar, now);
|
|
74
|
+
return state ? { state, ...quotedSessions(calendar, now) } : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where an index quote's own sessions put the market now, for a quote that
|
|
79
|
+
* states no session. Before the open it is pre-market on the day of the open,
|
|
80
|
+
* as the venue calendars have it, and closed on any earlier day (a weekend, a holiday).
|
|
81
|
+
*/
|
|
82
|
+
function quotedState({ timeZone, current, regular }: QuoteIndexSession, now: number): ReportMarketState | null {
|
|
83
|
+
if (now >= current.end) return "closed";
|
|
84
|
+
if (now >= current.start) return "open";
|
|
85
|
+
// A regular session still running is not today's: the sessions disagree, so say nothing.
|
|
86
|
+
if (regular.some((window) => window.end > now)) return null;
|
|
87
|
+
return zonedDateKey(now, timeZone) === zonedDateKey(current.start, timeZone) ? "pre-market" : "closed";
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
@@ -500,6 +500,13 @@ export function formatCompositeCursorAxisDate(
|
|
|
500
500
|
|
|
501
501
|
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
502
502
|
|
|
503
|
+
/** A dataset observation's day as the axis words it, `Oct 4`, with the year when the chart ends in another. */
|
|
504
|
+
export function formatCompositeDatasetDate(date: Date, endTime: number | undefined): string {
|
|
505
|
+
const text = `${MONTH_NAMES[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
506
|
+
const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
|
|
507
|
+
return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
503
510
|
/**
|
|
504
511
|
* When a value carried under the cursor was observed: its time when it is
|
|
505
512
|
* from the cursor's own day, else its date (`Sep 2`, with the year when that
|
|
@@ -5,7 +5,13 @@ import { CANONICAL_EXCHANGE_ALIASES } from "../../../utils/exchanges";
|
|
|
5
5
|
import { displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
6
6
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
7
7
|
import { consumeChartMouseEvent, type ChartMouseEvent } from "../core/pointer";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatCompositeCarriedStamp,
|
|
10
|
+
formatCompositeDatasetDate,
|
|
11
|
+
formatCompositePointDetails,
|
|
12
|
+
formatCompositeSeriesValue,
|
|
13
|
+
seriesPriceReference,
|
|
14
|
+
} from "./format";
|
|
9
15
|
import type { CompositeChartProps, CompositeChartScene } from "./types";
|
|
10
16
|
import { seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
|
|
11
17
|
|
|
@@ -45,15 +51,6 @@ function axisPriceReference(scene: CompositeChartScene | null, series: ResolvedS
|
|
|
45
51
|
return own;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
49
|
-
|
|
50
|
-
/** `Oct 4`, the axis's own wording, with the year when the chart ends in another. */
|
|
51
|
-
function legendDateText(date: Date, endTime: number | undefined): string {
|
|
52
|
-
const text = `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
53
|
-
const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
|
|
54
|
-
return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
/**
|
|
58
55
|
* A dataset's values arrive days late, so the legend dates them. Idle, one
|
|
59
56
|
* date leads the legend when it is every value's; otherwise, and under the
|
|
@@ -72,7 +69,7 @@ function legendValueDates(
|
|
|
72
69
|
valued += 1;
|
|
73
70
|
if (entry.observationKind === "dataset" && cursorValue.point) dates.set(entry.id, cursorValue.point.date);
|
|
74
71
|
}
|
|
75
|
-
const text = new Map([...dates].map(([id, date]) => [id,
|
|
72
|
+
const text = new Map([...dates].map(([id, date]) => [id, formatCompositeDatasetDate(date, scene?.endTime)] as const));
|
|
76
73
|
const cursorDate = scene?.cursorDate;
|
|
77
74
|
if (cursorDate) {
|
|
78
75
|
const cursorDay = cursorDate.toISOString().slice(0, 10);
|
|
@@ -29,6 +29,9 @@ import { parseWindowModeCommandArg } from "../../layout-items";
|
|
|
29
29
|
import type { CommandBarRoute } from "../../workflow/types";
|
|
30
30
|
import { recordFunctionOpen } from "../../../../telemetry/usage-counts";
|
|
31
31
|
import { reportTelemetryConfig } from "../../../../telemetry/live-config";
|
|
32
|
+
import { NIGHTLY_CHANNEL_SUMMARY, withUpdateChannel } from "../../../../updater/channel";
|
|
33
|
+
import { isPrereleaseVersion } from "../../../../utils/semver";
|
|
34
|
+
import { VERSION } from "../../../../version";
|
|
32
35
|
|
|
33
36
|
type NotifyFn = (body: string, options?: { type?: "info" | "success" | "error" }) => void;
|
|
34
37
|
|
|
@@ -373,6 +376,50 @@ export function runDirectCommandAction(options: {
|
|
|
373
376
|
void onCheckForUpdates?.();
|
|
374
377
|
closeAll({ revertThemePreview: false });
|
|
375
378
|
return;
|
|
379
|
+
case "release-channel": {
|
|
380
|
+
// The runtime checks again as soon as the channel changes.
|
|
381
|
+
const setChannel = (channel: "stable" | "nightly") => {
|
|
382
|
+
const nextConfig = withUpdateChannel(getState().config, channel);
|
|
383
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
384
|
+
persistConfig(nextConfig);
|
|
385
|
+
notify(`${t(command.label)}: ${channel}`, { type: "success" });
|
|
386
|
+
};
|
|
387
|
+
// The saved setting flips; GLOOMBERB_UPDATE_CHANNEL still wins for the run it is set on.
|
|
388
|
+
// Back to stable asks too, to say a nightly build already running stays.
|
|
389
|
+
if (state.config.updates?.channel === "nightly") {
|
|
390
|
+
openInlineConfirm({
|
|
391
|
+
confirmId: "switch-to-stable",
|
|
392
|
+
title: "Switch to Stable?",
|
|
393
|
+
body: [
|
|
394
|
+
isPrereleaseVersion(VERSION)
|
|
395
|
+
? "Updates come from stable releases only. This nightly build stays until a newer stable release is out."
|
|
396
|
+
: "Updates come from stable releases only.",
|
|
397
|
+
],
|
|
398
|
+
confirmLabel: "Switch to Stable",
|
|
399
|
+
cancelLabel: "Stay on Nightly",
|
|
400
|
+
tone: "default",
|
|
401
|
+
onConfirm: async () => { setChannel("stable"); },
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
openInlineConfirm({
|
|
406
|
+
confirmId: "switch-to-nightly",
|
|
407
|
+
title: "Switch to Nightly?",
|
|
408
|
+
body: [
|
|
409
|
+
`Nightly is ${NIGHTLY_CHANNEL_SUMMARY}. Stable releases still reach you when they are newer.`,
|
|
410
|
+
isPrereleaseVersion(VERSION)
|
|
411
|
+
? "Switch back to Stable at any time."
|
|
412
|
+
: "Switch back to Stable at any time: the nightly build stays until a newer stable release is out.",
|
|
413
|
+
],
|
|
414
|
+
confirmLabel: "Switch to Nightly",
|
|
415
|
+
cancelLabel: "Stay on Stable",
|
|
416
|
+
tone: "default",
|
|
417
|
+
// An opt-in to builds that can break: a stray Enter stays on stable.
|
|
418
|
+
defaultAction: "cancel",
|
|
419
|
+
onConfirm: async () => { setChannel("nightly"); },
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
376
423
|
case "theme":
|
|
377
424
|
setRootThemeBaseId(getState().config.theme);
|
|
378
425
|
setRootQuery(arg ? `TH ${arg}` : "TH ");
|
|
@@ -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
|
|
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(
|
|
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. */
|
package/src/components/index.ts
CHANGED
|
@@ -147,7 +147,7 @@ export type {
|
|
|
147
147
|
NoticeProps, NumberFieldProps, PaneStatusBodyProps, PopoverProps, ProseProps, QueryBarFilter,
|
|
148
148
|
QueryBarMultiFilter, QueryBarProps, QueryBarSearch, QueryBarSearchFocus, QueryBarSelectFilter, QueryBarTextFilter,
|
|
149
149
|
QueryBarToggleFilter, QueryBarView, RangeTrackProps, RatioBarProps, SectionHeadingProps, SectionProps, SegmentedControlProps,
|
|
150
|
-
SelectButtonOption, SelectButtonProps, SelectControl, StatGridProps, StatItem, TabsProps,
|
|
150
|
+
SelectButtonOption, SelectButtonProps, SelectControl, StatGridProps, StatItem, StatusRetry, TabsProps,
|
|
151
151
|
TextFieldProps, TextPromptDialogProps,
|
|
152
152
|
} from "./ui";
|
|
153
153
|
export { usePaneTicker } from "../state/app/context";
|
|
@@ -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
|
-
//
|
|
183
|
-
//
|
|
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, () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{ id: "sidebar-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 {
|
|
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
|
|
49
|
-
*
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
115
|
+
personalData: capturedPersonalData({
|
|
110
116
|
registry: current.pluginRegistry,
|
|
111
117
|
state: stateRef.current,
|
|
112
118
|
layout: current.layout,
|
|
113
|
-
|
|
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 ||
|
|
126
|
-
|
|
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(() =>
|
|
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
|
|
152
|
+
}), [capture, captureOnce, dialog, pluginRegistry]);
|
|
136
153
|
}
|