gloomberb 0.17.0 → 0.17.1
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 +1 -1
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/index.ts +1 -0
- package/src/app/runtime/update.ts +16 -0
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +57 -5
- package/src/cli/commands/options-chain.ts +75 -1
- package/src/cli/commands/ticker.ts +18 -9
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/pane-functions/catalog.ts +39 -7
- package/src/cli/pane-functions/dom.ts +19 -8
- package/src/cli/pane-functions/function-help.ts +3 -1
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +39 -0
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/thesis/detail.tsx +11 -9
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +63 -0
- package/src/plugins/builtin/options/view.tsx +55 -9
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +1 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +22 -4
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/types/financials.ts +22 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/fundamentals.ts +41 -1
package/src/cli/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { createSystemCliCommands } from "./commands/system";
|
|
|
37
37
|
import { brokerCliCommand, ibkrCliCommand } from "./commands/broker";
|
|
38
38
|
import { listPlugins, updatePlugins } from "./commands/plugins";
|
|
39
39
|
import { installListedPlugin, removePlugin } from "../plugins/installer";
|
|
40
|
-
import { runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
40
|
+
import { functionCodeCommand, runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
41
41
|
import { catalogFredSeriesIds } from "./series-search";
|
|
42
42
|
|
|
43
43
|
const { width: SHOT_WIDTH, height: SHOT_HEIGHT } = SHOT_SIZE_LIMITS;
|
|
@@ -60,11 +60,11 @@ function createCoreCliCommands(
|
|
|
60
60
|
usage: ["help [command]", "<command> --help"],
|
|
61
61
|
examples: ["help", "help quote", "quote --help"],
|
|
62
62
|
},
|
|
63
|
-
execute: (args, ctx) => {
|
|
63
|
+
execute: async (args, ctx) => {
|
|
64
64
|
const topic = args[0];
|
|
65
65
|
if (topic) {
|
|
66
66
|
const command = lookupCommand(topic);
|
|
67
|
-
if (!command) failUnknownCommand(topic, allCommands());
|
|
67
|
+
if (!command) failUnknownCommand(topic, allCommands(), await functionCodeCommand(ctx, topic, []));
|
|
68
68
|
printCommandHelp(command, ctx);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -306,7 +306,9 @@ function printCommandHelp(command: CliCommandDef, ctx: CliCommandContext): void
|
|
|
306
306
|
ctx.printResult({ data: { ...describeCliCommand(command), sections: command.help?.sections ?? [] } });
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
function
|
|
309
|
+
/** `function` is what the word is when it is a function's code, and the command that runs it. */
|
|
310
|
+
function failUnknownCommand(token: string, commands: CliCommandDef[], fn: { code: string; command: string } | null = null): never {
|
|
311
|
+
if (fn) fail(`${fn.code} is a function: ${fn.command}`);
|
|
310
312
|
const suggestion = suggestCliCommand(
|
|
311
313
|
normalizeCliCommandToken(token),
|
|
312
314
|
commands.flatMap((command) => [command.name, ...(command.aliases ?? [])]),
|
|
@@ -397,14 +399,20 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
|
|
|
397
399
|
/** Explains why dispatch found no command in `args`, suggesting the closest one. */
|
|
398
400
|
export async function failUnknownCliCommand(args: string[], options: DispatchCliOptions = {}): Promise<never> {
|
|
399
401
|
let token = args[0] ?? "";
|
|
402
|
+
let parsed: ReturnType<typeof parseCliGlobalArgs> | null = null;
|
|
400
403
|
try {
|
|
401
404
|
// Global flags may come first, so name the token dispatch actually looked up.
|
|
402
|
-
|
|
405
|
+
parsed = parseCliGlobalArgs(args);
|
|
406
|
+
token = parsed.args[0] ?? token;
|
|
403
407
|
} catch {
|
|
404
408
|
// dispatchCli already reported flags that do not parse.
|
|
405
409
|
}
|
|
406
410
|
const registry = await createRegistry(options);
|
|
407
|
-
|
|
411
|
+
// A function's code typed as a command is pointed at `fn`, with the arguments the reader gave.
|
|
412
|
+
const fn = token && parsed
|
|
413
|
+
? await functionCodeCommand(createCliCommandContext("core", registry, parsed.options, parsed.args.slice(1)), token, parsed.args.slice(1))
|
|
414
|
+
: null;
|
|
415
|
+
return failUnknownCommand(token, listedCommands(registry).map((entry) => entry.command), fn);
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
|
|
@@ -115,6 +115,43 @@ export function buildPaneFunctionLookup(registry: PaneFunctionCatalog): Map<stri
|
|
|
115
115
|
return lookup;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** What a function takes after its code, as the catalog writes it: `<ticker>`, `<fund, ticker, or CIK>`, or nothing. */
|
|
119
|
+
function argumentHint({ argKind, argPlaceholder }: Pick<PaneCatalogEntry, "argKind" | "argPlaceholder">): string {
|
|
120
|
+
if (argPlaceholder) return `<${argPlaceholder}>`;
|
|
121
|
+
return argKind && argKind !== "none" ? `<${argKind}>` : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A command-line token that is the code or an alias of a function, with what the function takes. */
|
|
125
|
+
export interface FunctionCode {
|
|
126
|
+
argument: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether `token` is the code or an alias of a function (`OVDV`, `EEO`), in any
|
|
131
|
+
* case: the function `fn` opens for it, not one it merely matches by a keyword
|
|
132
|
+
* or a label.
|
|
133
|
+
*/
|
|
134
|
+
export function findFunctionCode(registry: PaneFunctionCatalog, token: string): FunctionCode | null {
|
|
135
|
+
const wanted = normalizeLookupToken(token);
|
|
136
|
+
const found = wanted ? buildPaneFunctionLookup(registry).get(wanted) : undefined;
|
|
137
|
+
if (!found || !("paneId" in found && "description" in found)) return null;
|
|
138
|
+
const pane = registry.panes.get(found.paneId);
|
|
139
|
+
const headless = pane ? getHeadlessPaneDefinition(found, pane) : undefined;
|
|
140
|
+
const codes = [
|
|
141
|
+
found.shortcut?.prefix,
|
|
142
|
+
...(found.shortcut?.aliases ?? []),
|
|
143
|
+
headless?.discovery?.id,
|
|
144
|
+
...(headless?.discovery?.aliases ?? []),
|
|
145
|
+
];
|
|
146
|
+
if (!codes.some((code) => code && normalizeLookupToken(code) === wanted)) return null;
|
|
147
|
+
return {
|
|
148
|
+
argument: argumentHint({
|
|
149
|
+
argKind: headless?.argument.kind ?? found.shortcut?.argKind,
|
|
150
|
+
argPlaceholder: headless?.argument.placeholder ?? found.shortcut?.argPlaceholder,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
function sampleArgForTemplate(template: PaneTemplateDef): string {
|
|
119
156
|
switch (template.shortcut?.argKind) {
|
|
120
157
|
case "ticker":
|
|
@@ -438,11 +475,6 @@ function readinessCell(readiness: PaneFunctionReadiness): string {
|
|
|
438
475
|
return cliStyles.muted(label);
|
|
439
476
|
}
|
|
440
477
|
|
|
441
|
-
function catalogArgument(entry: PaneCatalogEntry): string {
|
|
442
|
-
if (entry.argPlaceholder) return `<${entry.argPlaceholder}>`;
|
|
443
|
-
return entry.argKind && entry.argKind !== "none" ? `<${entry.argKind}>` : "";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
478
|
// Choices longer than this move from the flag into its description, where they can wrap.
|
|
447
479
|
const MAX_INLINE_CHOICES = 24;
|
|
448
480
|
|
|
@@ -473,7 +505,7 @@ export function limitationSentence(text: string): string {
|
|
|
473
505
|
function renderCatalogEntry(entry: PaneCatalogEntry): string {
|
|
474
506
|
const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
|
|
475
507
|
const capability = entry.capability;
|
|
476
|
-
const argument =
|
|
508
|
+
const argument = argumentHint(entry);
|
|
477
509
|
const invocation = `${entry.token}${argument ? ` ${argument}` : ""}`;
|
|
478
510
|
const lines = [
|
|
479
511
|
`${cliStyles.accent(entry.token)} ${cliStyles.bold(entry.label)}`,
|
|
@@ -586,7 +618,7 @@ export function renderPaneCatalogReport(
|
|
|
586
618
|
shown.map((entry) => [
|
|
587
619
|
cliStyles.command(entry.token),
|
|
588
620
|
entry.label,
|
|
589
|
-
|
|
621
|
+
argumentHint(entry),
|
|
590
622
|
readinessCell(entry.capability.reportReadiness),
|
|
591
623
|
entry.description,
|
|
592
624
|
]),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
|
|
2
|
-
import { cliStyles,
|
|
2
|
+
import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
|
|
3
3
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
4
4
|
import type { MarketContext } from "../types";
|
|
5
5
|
import type { PaneFunctionReport } from "./report";
|
|
6
6
|
import type { ResolvedPaneFunction } from "./resolver";
|
|
7
7
|
import { collectShotSymbols } from "./data";
|
|
8
|
+
import { lockNoteLines } from "./lock-note";
|
|
9
|
+
import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
|
|
8
10
|
import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
|
|
9
11
|
import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
|
|
10
12
|
import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
|
|
@@ -14,21 +16,24 @@ const DOM_REPORT_WIDTH = 1280;
|
|
|
14
16
|
const DOM_REPORT_HEIGHT = 720;
|
|
15
17
|
const DOM_LIMITATION =
|
|
16
18
|
"Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
|
|
17
|
-
/** The widest a lock note wraps to, as the catalog's text. */
|
|
18
|
-
const LOCK_NOTE_WIDTH = 100;
|
|
19
19
|
const DOM_EVERY_ROW =
|
|
20
20
|
"Every row of the pane's table is returned, including rows scrolled out of view.";
|
|
21
21
|
|
|
22
22
|
/** The sign-in wall's buttons, which a rendered view reads as words: `Log in ↵ Sign up free`. */
|
|
23
23
|
const SIGN_IN_WALL_BUTTONS = /\s*\b(?:Log in|Sign up free|Resend Verification Email)(?:\s*\u21B5)?(?=\s|$)/g;
|
|
24
24
|
|
|
25
|
+
/** Whether the wall asks for the account's email to be verified, not for a sign-in. */
|
|
26
|
+
function asksToVerifyEmail(visibleText: string): boolean {
|
|
27
|
+
return /\bVerify your email\b/i.test(visibleText);
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/**
|
|
26
31
|
* A pane behind the sign-in wall, as a report reads it: the wall's headline
|
|
27
32
|
* without its buttons, and what it needs in place of them.
|
|
28
33
|
*/
|
|
29
34
|
function signInWallText(visibleText: string): string {
|
|
30
35
|
const headline = visibleText.replace(SIGN_IN_WALL_BUTTONS, "").trim();
|
|
31
|
-
const needs =
|
|
36
|
+
const needs = asksToVerifyEmail(visibleText)
|
|
32
37
|
? "Requires a verified email on your Gloom account; resend the verification email in the app."
|
|
33
38
|
: "Requires a free Gloom sign-in: log in or sign up in the app.";
|
|
34
39
|
return headline ? `${headline} ${needs}` : needs;
|
|
@@ -195,14 +200,20 @@ export function buildDomPaneReportFromRender(
|
|
|
195
200
|
// A wall in place of the whole pane: nothing was read, so the report says
|
|
196
201
|
// what is locked and how to reach it, and cites no source.
|
|
197
202
|
const gate = !hasStructuredRows ? result.render.accessGate ?? null : null;
|
|
198
|
-
const lockNote = gate
|
|
203
|
+
const lockNote = gate
|
|
204
|
+
? resolved.pane.reportLockNote?.({
|
|
205
|
+
gate,
|
|
206
|
+
symbol: result.symbols[0] ?? null,
|
|
207
|
+
token: resolved.token,
|
|
208
|
+
...gate === "sign-in" && asksToVerifyEmail(result.render.visibleText ?? "") ? { verifyEmail: true } : {},
|
|
209
|
+
}) ?? null
|
|
210
|
+
: null;
|
|
199
211
|
const locked = gate === "sign-in" || lockNote ? gate : null;
|
|
200
|
-
|
|
201
|
-
const lockReason = lockNote?.replace(/\s*\n\s*/g, " ") ?? null;
|
|
212
|
+
const lockReason = lockNote ? lockNoteReason(lockNote) : null;
|
|
202
213
|
if (hasStructuredRows) {
|
|
203
214
|
textLines.push(...renderDomTables(rows));
|
|
204
215
|
} else if (lockNote) {
|
|
205
|
-
textLines.push(...
|
|
216
|
+
textLines.push(...lockNoteLines(lockNote));
|
|
206
217
|
} else if (result.render.visibleText) {
|
|
207
218
|
textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.visibleText);
|
|
208
219
|
} else {
|
|
@@ -264,8 +264,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
264
264
|
},
|
|
265
265
|
OSA: {
|
|
266
266
|
summary: "Build a multi-leg options position and value it at any spot, date and vol shift: payoff, P&L grid, breakevens and Greeks. European pricing, so no early exercise. "
|
|
267
|
-
+ "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve."
|
|
267
|
+
+ "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve. "
|
|
268
|
+
+ "--strategy csp sells a cash-secured put: cash secured, premium, yield on cash and annualised, distance out of the money, break-even, days to expiry and an earnings flag.",
|
|
268
269
|
usage: ["OSA AAPL", "gloomberb fn OSA AAPL --strategy vertical --expiration 2027-01-15",
|
|
270
|
+
"gloomberb fn OSA KO --strategy csp --strike 85 --expiration 2026-11-20",
|
|
269
271
|
"gloomberb fn OSA SPY --strategy put --strike 700 --expiration 2027-01-15 --nav 100m --budget-bps 50 --sleeve 100m"],
|
|
270
272
|
keys: [key("a", "dd leg"), key("c", "hain"), key("d", "ate"), key("v", "ol shift")],
|
|
271
273
|
data: OPTIONS,
|
|
@@ -40,6 +40,7 @@ import { isRecord } from "../../utils/guards";
|
|
|
40
40
|
import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
|
|
41
41
|
import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, type ReportFreshness } from "./freshness";
|
|
42
42
|
import { accessGateStatus, errorsAccessGate, isProLock } from "./access-gate";
|
|
43
|
+
import { lockedHeadlessReport } from "./lock-note";
|
|
43
44
|
|
|
44
45
|
interface SerializableHeadlessColumn {
|
|
45
46
|
key: string;
|
|
@@ -721,7 +722,17 @@ export async function buildHeadlessFunctionReport(
|
|
|
721
722
|
context: MarketContext,
|
|
722
723
|
rawArgument: string,
|
|
723
724
|
): Promise<PaneFunctionReport> {
|
|
724
|
-
|
|
725
|
+
let loaded: LoadedHeadlessPaneModel;
|
|
726
|
+
try {
|
|
727
|
+
loaded = await loadResolvedHeadlessPaneModel(resolved, context, rawArgument);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
// A Pro pane the server refused reports what its wall covers, as its rendered view does.
|
|
730
|
+
const locked = lockedHeadlessReport(resolved, error, () => (
|
|
731
|
+
buildHeadlessPaneLoadArgs(resolved.headless!, resolved.token, rawArgument, resolved.options).symbols
|
|
732
|
+
));
|
|
733
|
+
if (locked) return locked;
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
725
736
|
const { complete, unavailableSymbols, rowCount, symbols } = reportCoverage(loaded);
|
|
726
737
|
const serialized = serializeHeadlessPaneResult(loaded.definition, loaded.result);
|
|
727
738
|
const freshness = deriveHeadlessFreshness(loaded.definition, loaded.result);
|
|
@@ -5,6 +5,7 @@ import { withMarketData } from "../context";
|
|
|
5
5
|
import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output";
|
|
6
6
|
import {
|
|
7
7
|
filterPaneCatalogEntries,
|
|
8
|
+
findFunctionCode,
|
|
8
9
|
renderPaneCatalogReport,
|
|
9
10
|
selectPaneCatalogMatches,
|
|
10
11
|
type PaneFunctionCatalog,
|
|
@@ -63,6 +64,44 @@ async function withPaneRuntime<T>(
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** A word that can be a function code: letters and digits, with the dots and dashes some codes carry. */
|
|
68
|
+
const FUNCTION_CODE_WORD = /^[\w.$-]{1,24}$/;
|
|
69
|
+
|
|
70
|
+
/** An argument as a shell reads it back: bare when it is one word, in quotes when it is not. */
|
|
71
|
+
function commandWord(argument: string): string {
|
|
72
|
+
return /^[\w.:,=%+@/-]+$/.test(argument) ? argument : JSON.stringify(argument);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What to run when a command name is a function's code (`gloomberb OVDV KO`,
|
|
77
|
+
* `gloomberb help OVDV`): `fn` with the reader's own arguments, or the
|
|
78
|
+
* function's argument hint when they gave none. Null when the word is no
|
|
79
|
+
* function code or the catalog cannot be read, and the command stays unknown.
|
|
80
|
+
*/
|
|
81
|
+
export async function functionCodeCommand(
|
|
82
|
+
ctx: CliCommandContext,
|
|
83
|
+
token: string,
|
|
84
|
+
args: readonly string[],
|
|
85
|
+
): Promise<{ code: string; command: string } | null> {
|
|
86
|
+
const word = token.trim();
|
|
87
|
+
if (!FUNCTION_CODE_WORD.test(word)) return null;
|
|
88
|
+
try {
|
|
89
|
+
const found = await withMarketData(ctx, async (market) => {
|
|
90
|
+
const registry = await createPaneCatalog(market, ctx.plugins);
|
|
91
|
+
try {
|
|
92
|
+
return findFunctionCode(registry, word);
|
|
93
|
+
} finally {
|
|
94
|
+
registry.destroy();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (!found) return null;
|
|
98
|
+
const code = word.replace(/^\$/, "").toUpperCase();
|
|
99
|
+
return { code, command: ["gloomberb fn", code, args.length > 0 ? args.map(commandWord).join(" ") : found.argument].filter(Boolean).join(" ") };
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
67
106
|
await runPaneCliCommand(ctx, async () => {
|
|
68
107
|
await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { refusedLock } from "../../plugins/builtin/shared/cloud-denied";
|
|
2
|
+
import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
|
|
3
|
+
import { cliTerminalWidth, wrapText } from "../../utils/cli-output";
|
|
4
|
+
import { reportFooterLines } from "../report-tables";
|
|
5
|
+
import type { PaneFunctionReport } from "./report";
|
|
6
|
+
import type { ResolvedPaneFunction } from "./resolver";
|
|
7
|
+
|
|
8
|
+
/** The widest a lock note wraps to, as the catalog's text. */
|
|
9
|
+
const LOCK_NOTE_WIDTH = 100;
|
|
10
|
+
|
|
11
|
+
/** The closing line of a headless report whose pane was walled whole, as a rendered view's says it. */
|
|
12
|
+
const LOCKED_VIEW_NOTE = "The pane is locked, so no values were read.";
|
|
13
|
+
|
|
14
|
+
/** A pane's `reportLockNote`, wrapped for the text report. */
|
|
15
|
+
export function lockNoteLines(note: string): string[] {
|
|
16
|
+
return wrapText(note, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a headless report prints when the server refused its read behind a
|
|
21
|
+
* wall, as the rendered view of the same pane prints that wall: the pane's
|
|
22
|
+
* `reportLockNote`, no source line and `locked` in JSON. The refusal may be
|
|
23
|
+
* the error itself or the cause of the one a headless read wrote for it
|
|
24
|
+
* (`explainRefusal`). Null when the error is no such refusal or the pane has
|
|
25
|
+
* no note, and the error stands.
|
|
26
|
+
*/
|
|
27
|
+
export function lockedHeadlessReport(
|
|
28
|
+
resolved: ResolvedPaneFunction,
|
|
29
|
+
error: unknown,
|
|
30
|
+
/** The symbols the report was asked for, read once the error is known to be a refusal. */
|
|
31
|
+
symbolsOf: () => string[],
|
|
32
|
+
): PaneFunctionReport | null {
|
|
33
|
+
const refused = refusedLock(error);
|
|
34
|
+
if (!refused || !resolved.pane.reportLockNote) return null;
|
|
35
|
+
const gate = refused.gate;
|
|
36
|
+
const symbols = symbolsOf();
|
|
37
|
+
const note = resolved.pane.reportLockNote({ ...refused, symbol: symbols[0] ?? null, token: resolved.token });
|
|
38
|
+
const reason = lockNoteReason(note);
|
|
39
|
+
return {
|
|
40
|
+
data: {
|
|
41
|
+
kind: resolved.headless?.shape ?? "rendered-view",
|
|
42
|
+
target: resolved.token,
|
|
43
|
+
capabilityId: resolved.capability.id,
|
|
44
|
+
symbols,
|
|
45
|
+
options: resolved.options,
|
|
46
|
+
rowCount: 0,
|
|
47
|
+
empty: true,
|
|
48
|
+
complete: false,
|
|
49
|
+
unavailableSymbols: symbols,
|
|
50
|
+
reason,
|
|
51
|
+
locked: gate,
|
|
52
|
+
},
|
|
53
|
+
text: [resolved.label, "", ...lockNoteLines(note), "", LOCKED_VIEW_NOTE].join("\n"),
|
|
54
|
+
tables: { tables: [], footer: reportFooterLines({ incomplete: reason, notes: [LOCKED_VIEW_NOTE] }) },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../../../ui";
|
|
14
14
|
import { displayWidth, truncateTextSegments, truncateToDisplayWidth } from "../../../utils/format";
|
|
15
15
|
import { Spinner } from "../../ui";
|
|
16
|
+
import { ClippedText } from "../../ui/display";
|
|
16
17
|
import { useCommandBarPalette } from "../panel/palette";
|
|
17
18
|
import { getRowPresentation } from "../view-model";
|
|
18
19
|
import {
|
|
@@ -306,7 +307,7 @@ export const CommandBarListBody = memo(function CommandBarListBody({
|
|
|
306
307
|
if (row.kind === "message") {
|
|
307
308
|
return (
|
|
308
309
|
<Box key={row.id} height={1} paddingLeft={labelEdgePadding} paddingRight={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
|
|
309
|
-
<
|
|
310
|
+
<ClippedText fg={palette.text} text={t(row.label)} width={labelEdgeWidth} />
|
|
310
311
|
</Box>
|
|
311
312
|
);
|
|
312
313
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -125,7 +125,7 @@ export { MarkdownText } from "./markdown-text";
|
|
|
125
125
|
// this file is the public `gloomberb/components` surface: a kit component
|
|
126
126
|
// reaches plugins only once it is added here.
|
|
127
127
|
export {
|
|
128
|
-
ActionRow, Badge, BulletList, Button, ButtonActionScope, Checkbox, ChoiceDialog, ConfirmDialog, confirmDialog, DataTable,
|
|
128
|
+
ActionRow, Badge, BulletList, Button, ButtonActionScope, Checkbox, ChoiceDialog, ColumnGrid, ColumnGridPair, ConfirmDialog, confirmDialog, DataTable,
|
|
129
129
|
DetailScrollBody, DialogFrame, DisclosureMarker, Divider, EmptyState, ExternalLink, ExternalLinkText, FieldGrid, FieldLabel,
|
|
130
130
|
fieldGridColumns, fieldGridRows, FigureList, getMessageComposerBlockHeight, GridFieldView, Icon, ICON_GLYPHS,
|
|
131
131
|
IconButton, InlineQuickAddRow, KeyValueRow, ListView, loadingText, Menu, MenuPopover,
|
|
@@ -136,7 +136,8 @@ export {
|
|
|
136
136
|
useFieldRing, usePaneLinkMenuEntry, useQueryBarSearch,
|
|
137
137
|
} from "./ui";
|
|
138
138
|
export type {
|
|
139
|
-
ActionRowProps, BadgeProps, BulletListProps, ButtonProps, ButtonVariant, CheckboxProps, ChoiceDialogChoice,
|
|
139
|
+
ActionRowProps, BadgeProps, BulletListProps, ButtonProps, ButtonVariant, CheckboxProps, ChoiceDialogChoice, ColumnGridCell,
|
|
140
|
+
ColumnGridColumn, ColumnGridPairProps, ColumnGridProps, ConfirmDialogOptions,
|
|
140
141
|
DataTableCell, DataTableColumn, DataTableProps, DataTableVisibleRange, DetailScrollBodyProps, DialogFrameProps,
|
|
141
142
|
DisclosureMarkerProps, DividerProps, EmptyStateProps, FieldGridProps, FieldLabelProps, FieldRing, FieldRingOptions,
|
|
142
143
|
FigureListItem, FigureListProps, GridField,
|
|
@@ -28,14 +28,29 @@ import { useAppDispatch, useAppSelector } from "../../state/app/context";
|
|
|
28
28
|
import { useViewport } from "../../react/input";
|
|
29
29
|
import { canRetryUpdate } from "../../app/global-shortcuts";
|
|
30
30
|
import { t, tf } from "../../i18n";
|
|
31
|
-
import { truncateToDisplayWidth } from "../../utils/format";
|
|
31
|
+
import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
|
|
32
|
+
import { ClippedText } from "../ui/display";
|
|
32
33
|
import { resolveMarketSummaryFit, useMarketSummary } from "./market-summary";
|
|
33
34
|
import { resolveHeaderPromptGeometry } from "./shell/chrome";
|
|
34
35
|
import { useWindowFullscreen } from "./window-fullscreen";
|
|
35
36
|
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
36
|
-
import { Button } from "../ui/button";
|
|
37
|
+
import { Button, terminalButtonColumns } from "../ui/button";
|
|
37
38
|
|
|
38
39
|
const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
40
|
+
/**
|
|
41
|
+
* The failed-update message is cut to the room it has, but not below this many
|
|
42
|
+
* columns ("Update fa..."): narrower says nothing, so the banner is left out.
|
|
43
|
+
* The retry hint gives way before the message goes under it.
|
|
44
|
+
*/
|
|
45
|
+
const UPDATE_ERROR_MIN_COLUMNS = 12;
|
|
46
|
+
/** Kept clear after the failed-update banner, so its last character never touches the Help pill. */
|
|
47
|
+
const UPDATE_ERROR_GAP_COLUMNS = 1;
|
|
48
|
+
/** Left padding of the update status's slot, in cells; the desktop's title-bar layout also pads its right. */
|
|
49
|
+
const UPDATE_SLOT_PADDING_COLUMNS = 2;
|
|
50
|
+
/** The Help pill's own props, shared by its button and the width the update status leaves it. */
|
|
51
|
+
const helpButtonProps = () => ({ label: "Open Help", displayLabel: t("Help"), shortcut: "?", compact: true });
|
|
52
|
+
/** The terminal pads the pill's right edge by a cell; the desktop pads it in px. */
|
|
53
|
+
const TERMINAL_HELP_PADDING_COLUMNS = 1;
|
|
39
54
|
|
|
40
55
|
type HeaderActionEvent = {
|
|
41
56
|
key?: string;
|
|
@@ -157,11 +172,17 @@ function HeaderPromptInput({
|
|
|
157
172
|
placeholderColor={subtleColor}
|
|
158
173
|
cursorColor={colors.textBright}
|
|
159
174
|
/>
|
|
160
|
-
{ghostSuffix && (
|
|
175
|
+
{ghostSuffix && (nativePaneChrome ? (
|
|
176
|
+
// The desktop sets the suggestion right after the typed text, one
|
|
177
|
+
// character per `ch` of the field's font, and cuts it where the field ends.
|
|
178
|
+
<Box position="absolute" top={0} height={1} flexDirection="row" style={{ left: `${displayWidth(query)}ch`, right: 0 }}>
|
|
179
|
+
<ClippedText fg={subtleColor} text={ghostSuffix} width={0} />
|
|
180
|
+
</Box>
|
|
181
|
+
) : (
|
|
161
182
|
<Box position="absolute" top={0} left={ghostLeft} width={Math.max(0, width - ghostLeft)} height={1}>
|
|
162
183
|
<Text fg={subtleColor}>{truncateToDisplayWidth(ghostSuffix, Math.max(0, width - ghostLeft))}</Text>
|
|
163
184
|
</Box>
|
|
164
|
-
)}
|
|
185
|
+
))}
|
|
165
186
|
</Box>
|
|
166
187
|
);
|
|
167
188
|
}
|
|
@@ -244,8 +265,14 @@ function HeaderCommandPrompt({
|
|
|
244
265
|
);
|
|
245
266
|
}
|
|
246
267
|
|
|
247
|
-
|
|
268
|
+
/**
|
|
269
|
+
* What the header says about an update. In the terminal it is cut to `columns`,
|
|
270
|
+
* the room its slot is guaranteed (the desktop clips it in CSS where the slot
|
|
271
|
+
* ends), so it never runs into the Help pill or the market cluster beside it.
|
|
272
|
+
*/
|
|
273
|
+
function UpdateStatus({ columns, onRestart }: { columns: number; onRestart?: () => void }) {
|
|
248
274
|
const colors = useThemeColors();
|
|
275
|
+
const { nativePaneChrome = false } = useUiCapabilities();
|
|
249
276
|
const dispatch = useAppDispatch();
|
|
250
277
|
const updateAvailable = useAppSelector((state) => state.updateAvailable);
|
|
251
278
|
const updateProgress = useAppSelector((state) => state.updateProgress);
|
|
@@ -301,11 +328,20 @@ function UpdateStatus({ onRestart }: { onRestart?: () => void }) {
|
|
|
301
328
|
return <Text fg={colors.headerText}>{t(updateProgress.message ?? "Update ready, restart to apply")}</Text>;
|
|
302
329
|
}
|
|
303
330
|
if (updateProgress.phase === "error") {
|
|
331
|
+
// One line: an error that carries a stack or a command's output must not grow the header.
|
|
332
|
+
const message = tf("Update failed: {error}", { error: (updateProgress.error ?? "Unknown error").replace(/\s+/g, " ").trim() });
|
|
304
333
|
const retry = updateKey && canRetryUpdate({ updateAvailable, updateProgress, updateCheckInProgress });
|
|
334
|
+
const hint = retry ? ` ${updateKey} ${t("retry")}` : "";
|
|
335
|
+
const hintColumns = displayWidth(hint);
|
|
336
|
+
// The message goes first, and keeps its room before the hint takes any.
|
|
337
|
+
const room = columns - UPDATE_ERROR_GAP_COLUMNS;
|
|
338
|
+
const showHint = hintColumns > 0 && (nativePaneChrome || room - hintColumns >= UPDATE_ERROR_MIN_COLUMNS);
|
|
339
|
+
const messageColumns = room - (showHint ? hintColumns : 0);
|
|
340
|
+
if (!nativePaneChrome && messageColumns < UPDATE_ERROR_MIN_COLUMNS) return null;
|
|
305
341
|
return (
|
|
306
|
-
<Box flexDirection="row">
|
|
307
|
-
<
|
|
308
|
-
{
|
|
342
|
+
<Box flexDirection="row" overflow="hidden" paddingRight={UPDATE_ERROR_GAP_COLUMNS}>
|
|
343
|
+
<ClippedText fg={colors.headerText} text={message} width={messageColumns} />
|
|
344
|
+
{showHint ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{hint}</Text> : null}
|
|
309
345
|
</Box>
|
|
310
346
|
);
|
|
311
347
|
}
|
|
@@ -404,11 +440,11 @@ function HeaderHelpButton({ nativePaneChrome, onPress }: { nativePaneChrome: boo
|
|
|
404
440
|
return (
|
|
405
441
|
<Box
|
|
406
442
|
flexShrink={0}
|
|
407
|
-
paddingRight={nativePaneChrome ? undefined :
|
|
443
|
+
paddingRight={nativePaneChrome ? undefined : TERMINAL_HELP_PADDING_COLUMNS}
|
|
408
444
|
data-gloom-role="header-help-action"
|
|
409
445
|
style={nativePaneChrome ? { marginRight: 8 } : undefined}
|
|
410
446
|
>
|
|
411
|
-
<Button
|
|
447
|
+
<Button {...helpButtonProps()} onPress={onPress} stopPropagation />
|
|
412
448
|
</Box>
|
|
413
449
|
);
|
|
414
450
|
}
|
|
@@ -437,6 +473,14 @@ export function Header({
|
|
|
437
473
|
windowFullscreen,
|
|
438
474
|
});
|
|
439
475
|
const shortcutLabel = useCommandBarShortcut();
|
|
476
|
+
// The terminal lays the header out by cell counts, so it works out what is left
|
|
477
|
+
// for the update status once the Help pill and the slot's padding are paid for.
|
|
478
|
+
const updateStatusColumns = Math.max(
|
|
479
|
+
0,
|
|
480
|
+
prompt.updateColumns
|
|
481
|
+
- (onOpenHelp ? terminalButtonColumns(helpButtonProps()) + TERMINAL_HELP_PADDING_COLUMNS : 0)
|
|
482
|
+
- UPDATE_SLOT_PADDING_COLUMNS * (titleBarOverlay ? 2 : 1),
|
|
483
|
+
);
|
|
440
484
|
// Desktop has its own window chrome to sit on; the terminal has none, so the
|
|
441
485
|
// bar itself wears the prompt's fill and reads as a single strip.
|
|
442
486
|
const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
|
|
@@ -483,8 +527,8 @@ export function Header({
|
|
|
483
527
|
>
|
|
484
528
|
<Box width={prompt.left} />
|
|
485
529
|
{commandPrompt}
|
|
486
|
-
<Box flexGrow={1} paddingLeft={
|
|
487
|
-
<UpdateStatus onRestart={onRestartForUpdate} />
|
|
530
|
+
<Box flexGrow={1} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS} paddingRight={UPDATE_SLOT_PADDING_COLUMNS} minWidth={0}>
|
|
531
|
+
<UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
|
|
488
532
|
</Box>
|
|
489
533
|
{onOpenHelp ? <HeaderHelpButton nativePaneChrome onPress={onOpenHelp} /> : null}
|
|
490
534
|
{marketSummary}
|
|
@@ -505,8 +549,8 @@ export function Header({
|
|
|
505
549
|
>
|
|
506
550
|
<Box width={prompt.left} />
|
|
507
551
|
{commandPrompt}
|
|
508
|
-
<Box flexGrow={1} minWidth={0} paddingLeft={
|
|
509
|
-
<UpdateStatus onRestart={onRestartForUpdate} />
|
|
552
|
+
<Box flexGrow={1} minWidth={0} paddingLeft={UPDATE_SLOT_PADDING_COLUMNS}>
|
|
553
|
+
<UpdateStatus columns={updateStatusColumns} onRestart={onRestartForUpdate} />
|
|
510
554
|
</Box>
|
|
511
555
|
{onOpenHelp ? <HeaderHelpButton nativePaneChrome={nativePaneChrome} onPress={onOpenHelp} /> : null}
|
|
512
556
|
{marketSummary}
|
|
@@ -73,6 +73,12 @@ export interface HeaderPromptGeometry {
|
|
|
73
73
|
width: number;
|
|
74
74
|
/** Columns held at the header's right edge for the market cluster. */
|
|
75
75
|
marketColumns: number;
|
|
76
|
+
/**
|
|
77
|
+
* Columns between the prompt and the market cluster: the update status, its
|
|
78
|
+
* padding and the Help pill share them. Never negative, so a window too narrow
|
|
79
|
+
* for the header reads as no room rather than a negative width.
|
|
80
|
+
*/
|
|
81
|
+
updateColumns: number;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/**
|
|
@@ -95,14 +101,14 @@ export function resolveHeaderPromptGeometry(options: {
|
|
|
95
101
|
? getTitlebarLeadingInset({ platform, windowFullscreen })
|
|
96
102
|
: 0;
|
|
97
103
|
const left = leadingInset + 1;
|
|
98
|
-
const
|
|
99
|
-
const marketColumns = headerMarketColumns(termWidth, left,
|
|
104
|
+
const updateReserve = headerPromptUpdateColumns(termWidth);
|
|
105
|
+
const marketColumns = headerMarketColumns(termWidth, left, updateReserve);
|
|
100
106
|
const width = Math.max(
|
|
101
107
|
HEADER_PROMPT_MIN_WIDTH,
|
|
102
108
|
Math.min(
|
|
103
109
|
resolveCommandSurfaceWidth({ nativePaneChrome, termWidth }),
|
|
104
|
-
termWidth - left -
|
|
110
|
+
termWidth - left - updateReserve - marketColumns,
|
|
105
111
|
),
|
|
106
112
|
);
|
|
107
|
-
return { left, width, marketColumns };
|
|
113
|
+
return { left, width, marketColumns, updateColumns: Math.max(0, termWidth - left - width - marketColumns) };
|
|
108
114
|
}
|