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.
Files changed (114) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/data.ts +5 -1
  3. package/src/api-client/doe.ts +19 -0
  4. package/src/api-client/index.ts +1 -0
  5. package/src/app/runtime/update.ts +16 -0
  6. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  7. package/src/cli/commands/market.ts +57 -5
  8. package/src/cli/commands/options-chain.ts +75 -1
  9. package/src/cli/commands/ticker.ts +18 -9
  10. package/src/cli/glossary.ts +1 -1
  11. package/src/cli/index.ts +14 -6
  12. package/src/cli/pane-functions/catalog.ts +39 -7
  13. package/src/cli/pane-functions/dom.ts +19 -8
  14. package/src/cli/pane-functions/function-help.ts +3 -1
  15. package/src/cli/pane-functions/headless.ts +12 -1
  16. package/src/cli/pane-functions/index.ts +39 -0
  17. package/src/cli/pane-functions/lock-note.ts +56 -0
  18. package/src/components/command-bar/list/view.tsx +2 -1
  19. package/src/components/index.ts +3 -2
  20. package/src/components/layout/header.tsx +58 -14
  21. package/src/components/layout/shell/chrome.ts +10 -4
  22. package/src/components/ui/column-grid.tsx +206 -0
  23. package/src/components/ui/display.tsx +40 -3
  24. package/src/components/ui/fill.ts +8 -0
  25. package/src/components/ui/index.ts +2 -0
  26. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  27. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  28. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  29. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  30. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  31. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  32. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  33. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  34. package/src/plugins/builtin/cloud/thesis/detail.tsx +11 -9
  35. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  36. package/src/plugins/builtin/connections/pane.tsx +16 -5
  37. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  38. package/src/plugins/builtin/doe/index.tsx +11 -4
  39. package/src/plugins/builtin/doe/pane.tsx +5 -4
  40. package/src/plugins/builtin/doe/prices.ts +84 -0
  41. package/src/plugins/builtin/doe/series.ts +4 -1
  42. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  43. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  44. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  45. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  46. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  47. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  48. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  49. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  50. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  51. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  52. package/src/plugins/builtin/executives/index.tsx +18 -0
  53. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  54. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  55. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  56. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  57. package/src/plugins/builtin/iv-history/format.ts +3 -0
  58. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  59. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  60. package/src/plugins/builtin/iv-history/model.ts +3 -1
  61. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  62. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  63. package/src/plugins/builtin/jobs/headless.ts +6 -20
  64. package/src/plugins/builtin/jobs/index.tsx +2 -0
  65. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  66. package/src/plugins/builtin/options/index.tsx +2 -1
  67. package/src/plugins/builtin/options/model-spot.ts +94 -0
  68. package/src/plugins/builtin/options/open-interest-as-of.ts +63 -0
  69. package/src/plugins/builtin/options/view.tsx +55 -9
  70. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  71. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  72. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  73. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  74. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  75. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  76. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  77. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  78. package/src/plugins/builtin/scanner/headless.ts +9 -2
  79. package/src/plugins/builtin/scanner/index.ts +3 -0
  80. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  81. package/src/plugins/builtin/series-inventory.ts +1 -1
  82. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  83. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  84. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  85. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  86. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  87. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  88. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  89. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  90. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  91. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  92. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  93. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  94. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  95. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  96. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  97. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  98. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  99. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  100. package/src/renderers/dom/data-table/index.tsx +48 -15
  101. package/src/renderers/dom/data-table/row.tsx +1 -1
  102. package/src/renderers/dom/desktop/popover.tsx +6 -15
  103. package/src/renderers/dom/host/input.tsx +27 -3
  104. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  105. package/src/renderers/dom/styles.css +22 -4
  106. package/src/renderers/electrobun/view/app-services.ts +2 -22
  107. package/src/renderers/electrobun/view/news-source.ts +42 -0
  108. package/src/series/validate.ts +5 -0
  109. package/src/shares/dialog.tsx +2 -1
  110. package/src/sources/provider-router/financials.ts +5 -0
  111. package/src/types/financials.ts +22 -0
  112. package/src/types/plugin.ts +17 -4
  113. package/src/utils/financial-statements.ts +25 -0
  114. 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 failUnknownCommand(token: string, commands: CliCommandDef[]): never {
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
- token = parseCliGlobalArgs(args).args[0] ?? token;
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
- return failUnknownCommand(token, listedCommands(registry).map((entry) => entry.command));
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 = catalogArgument(entry);
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
- catalogArgument(entry),
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, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
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 = /\bVerify your email\b/i.test(visibleText)
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 ? resolved.pane.reportLockNote?.({ gate, symbol: result.symbols[0] ?? null }) ?? null : null;
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
- // JSON and CSV say why the report is empty in the note's words, on one line.
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(...wrapText(lockNote, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH)));
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
- const loaded = await loadResolvedHeadlessPaneModel(resolved, context, rawArgument);
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
- <Text fg={palette.text}>{truncateToDisplayWidth(t(row.label), labelEdgeWidth)}</Text>
310
+ <ClippedText fg={palette.text} text={t(row.label)} width={labelEdgeWidth} />
310
311
  </Box>
311
312
  );
312
313
  }
@@ -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, ConfirmDialogOptions,
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
- function UpdateStatus({ onRestart }: { onRestart?: () => void }) {
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
- <Text fg={colors.headerText}>{tf("Update failed: {error}", { error: updateProgress.error ?? "Unknown error" })}</Text>
308
- {retry ? <Text fg={blendHex(colors.headerText, colors.header, 0.62)}>{` ${updateKey} ${t("retry")}`}</Text> : null}
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 : 1}
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 compact label="Open Help" displayLabel={t("Help")} shortcut="?" onPress={onPress} stopPropagation />
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={2} paddingRight={2} minWidth={0}>
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={2}>
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 updateColumns = headerPromptUpdateColumns(termWidth);
99
- const marketColumns = headerMarketColumns(termWidth, left, updateColumns);
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 - updateColumns - marketColumns,
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
  }