gloomberb 0.11.1 → 0.11.3

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 (282) hide show
  1. package/README.md +11 -9
  2. package/README.zh-CN.md +6 -6
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +12 -0
  5. package/src/api-client/index.test.ts +47 -0
  6. package/src/api-client/index.ts +66 -0
  7. package/src/api-client/paths.ts +16 -0
  8. package/src/api-client/types.ts +59 -0
  9. package/src/app/pane-runtime/index.tsx +4 -1
  10. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  11. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  12. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  13. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  14. package/src/app/runtime/desktop-deeplink.ts +66 -2
  15. package/src/app/runtime/desktop-menu.ts +3 -0
  16. package/src/app/runtime/plugin-bindings.ts +30 -28
  17. package/src/app.tsx +9 -2
  18. package/src/capabilities/chart-series.ts +3 -2
  19. package/src/cli/commands/automation.test.ts +9 -0
  20. package/src/cli/commands/automation.ts +22 -73
  21. package/src/cli/commands/market.ts +2 -2
  22. package/src/cli/index.ts +0 -2
  23. package/src/cli/pane-functions/capabilities.ts +3 -3
  24. package/src/cli/registry.ts +0 -22
  25. package/src/cli/result.ts +7 -10
  26. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  27. package/src/components/chart/composite/composite-chart.tsx +12 -9
  28. package/src/components/chart/composite/scene.ts +0 -21
  29. package/src/components/chart/composite/tools.ts +49 -0
  30. package/src/components/command-bar/commands/direct/index.ts +4 -0
  31. package/src/components/command-bar/commands/registry.ts +8 -1
  32. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  33. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  34. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  35. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  36. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  37. package/src/components/command-bar/surface/index.test.tsx +6 -6
  38. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  39. package/src/components/command-bar/view-model.test.ts +2 -1
  40. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  41. package/src/components/command-bar/workflow/ops.ts +1 -1
  42. package/src/components/data-table/export.test.ts +21 -0
  43. package/src/components/data-table/export.ts +21 -0
  44. package/src/components/layout/detached-pane-shell.tsx +74 -10
  45. package/src/components/layout/pane/title.ts +31 -3
  46. package/src/components/layout/shell/active-drag.ts +4 -11
  47. package/src/components/layout/shell/drag/index.ts +4 -20
  48. package/src/components/layout/shell/drag/runtime.ts +3 -4
  49. package/src/components/layout/shell/index.test.tsx +17 -3
  50. package/src/components/layout/shell/index.tsx +86 -30
  51. package/src/components/layout/shell/menu.test.ts +37 -1
  52. package/src/components/layout/shell/menu.ts +14 -0
  53. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  54. package/src/components/layout/shell/pane/actions.ts +0 -5
  55. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  56. package/src/components/layout/shell/shortcuts.ts +6 -3
  57. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  58. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  59. package/src/components/layout/status-bar.test.tsx +62 -61
  60. package/src/components/layout/status-bar.tsx +53 -75
  61. package/src/components/ui/data-table/index.tsx +19 -2
  62. package/src/core/state/app/layout-reducer.ts +36 -0
  63. package/src/core/state/app/layout.ts +10 -5
  64. package/src/core/state/app/state.ts +0 -13
  65. package/src/core/state/app/types.ts +6 -4
  66. package/src/data/config/chart-settings.ts +5 -5
  67. package/src/data/config/layout.ts +2 -0
  68. package/src/data/config/store/index.test.ts +27 -6
  69. package/src/data/config/store/normalize.ts +3 -5
  70. package/src/data/plugin-state-store.ts +11 -0
  71. package/src/data/sqlite/cache.test.ts +10 -0
  72. package/src/i18n/es.ts +3 -4
  73. package/src/i18n/ja.ts +3 -4
  74. package/src/i18n/ko.ts +3 -4
  75. package/src/i18n/zh-cn.ts +3 -4
  76. package/src/i18n/zh-tw.ts +3 -4
  77. package/src/layout-marketplace/api.test.ts +54 -0
  78. package/src/layout-marketplace/api.ts +65 -0
  79. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  80. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  81. package/src/layout-marketplace/gallery-terminal.tsx +406 -0
  82. package/src/layout-marketplace/gallery.test.tsx +148 -0
  83. package/src/layout-marketplace/gallery.tsx +323 -0
  84. package/src/layout-marketplace/geometry.test.ts +47 -0
  85. package/src/layout-marketplace/geometry.ts +80 -0
  86. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  87. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  88. package/src/layout-marketplace/model.test.ts +118 -0
  89. package/src/layout-marketplace/model.ts +171 -0
  90. package/src/layout-marketplace/name-dialog.tsx +60 -0
  91. package/src/layout-marketplace/pane-imagery.ts +85 -0
  92. package/src/layout-marketplace/pane.tsx +20 -0
  93. package/src/layout-marketplace/payload.test.ts +246 -0
  94. package/src/layout-marketplace/payload.ts +624 -0
  95. package/src/layout-marketplace/use-marketplace.ts +63 -0
  96. package/src/market-data/hooks.tsx +0 -41
  97. package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
  98. package/src/plugins/broker-sync/normalize.ts +9 -2
  99. package/src/plugins/builtin/ai/index.tsx +18 -2
  100. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  101. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  102. package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
  103. package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
  104. package/src/plugins/builtin/alerts/index.tsx +4 -0
  105. package/src/plugins/builtin/alerts/pane.tsx +2 -8
  106. package/src/plugins/builtin/analytics/index.tsx +3 -0
  107. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  108. package/src/plugins/builtin/cds/client.test.ts +83 -0
  109. package/src/plugins/builtin/cds/client.ts +67 -0
  110. package/src/plugins/builtin/cds/index.tsx +41 -0
  111. package/src/plugins/builtin/cds/model.test.ts +232 -0
  112. package/src/plugins/builtin/cds/model.ts +401 -0
  113. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  114. package/src/plugins/builtin/cds/pane.tsx +358 -0
  115. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  116. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  117. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  118. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  119. package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
  120. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  121. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  122. package/src/plugins/builtin/chat/controller/index.ts +8 -1
  123. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  124. package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
  125. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  126. package/src/plugins/builtin/chat/controller.test.ts +64 -14
  127. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  128. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  130. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  132. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  133. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  134. package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
  135. package/src/plugins/builtin/cloud/auth-model.ts +4 -1
  136. package/src/plugins/builtin/cloud/plugin.tsx +11 -1
  137. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  138. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  139. package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
  140. package/src/plugins/builtin/composite-plugins.ts +5 -1
  141. package/src/plugins/builtin/connections/index.ts +3 -0
  142. package/src/plugins/builtin/earnings/index.tsx +1 -0
  143. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  144. package/src/plugins/builtin/econ/index.tsx +1 -0
  145. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  146. package/src/plugins/builtin/futures/index.tsx +1 -0
  147. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  148. package/src/plugins/builtin/help/index.tsx +2 -2
  149. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  150. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  151. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  152. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  153. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  154. package/src/plugins/builtin/news/index.tsx +1 -0
  155. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  156. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  157. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  158. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  159. package/src/plugins/builtin/options/analytics.ts +110 -0
  160. package/src/plugins/builtin/options/index.tsx +18 -2
  161. package/src/plugins/builtin/options/table.ts +121 -51
  162. package/src/plugins/builtin/options/types.ts +22 -15
  163. package/src/plugins/builtin/options/view.test.tsx +49 -3
  164. package/src/plugins/builtin/options/view.tsx +92 -19
  165. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  166. package/src/plugins/builtin/plugin-module.ts +0 -7
  167. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
  168. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  169. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  170. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  171. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  172. package/src/plugins/builtin/portfolio-list/index.tsx +9 -0
  173. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  174. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  175. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  176. package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
  177. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  178. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
  179. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
  180. package/src/plugins/builtin/research/index.tsx +4 -0
  181. package/src/plugins/builtin/scanner/index.ts +2 -0
  182. package/src/plugins/builtin/sectors/index.tsx +1 -0
  183. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  184. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  185. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  186. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  187. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  188. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  189. package/src/plugins/builtin/ticker-detail/index.tsx +12 -0
  190. package/src/plugins/builtin/treasury-auctions/index.tsx +3 -0
  191. package/src/plugins/builtin/world-indices/index.tsx +3 -0
  192. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  193. package/src/plugins/builtin/world-venue-map/map.tsx +337 -0
  194. package/src/plugins/builtin/world-venue-map/model.test.ts +53 -0
  195. package/src/plugins/builtin/world-venue-map/model.ts +147 -0
  196. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  197. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  198. package/src/plugins/catalog-backend.ts +6 -44
  199. package/src/plugins/catalog-browser.test.ts +10 -0
  200. package/src/plugins/catalog-browser.ts +5 -1
  201. package/src/plugins/gridlock-notification.ts +2 -1
  202. package/src/plugins/pane-manager/gridlock.ts +87 -3
  203. package/src/plugins/pane-manager.test.ts +30 -0
  204. package/src/plugins/pane-manager.ts +6 -1
  205. package/src/plugins/prediction-markets/chart.tsx +2 -2
  206. package/src/plugins/prediction-markets/controller/index.ts +5 -1
  207. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  208. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  209. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  210. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  211. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  212. package/src/plugins/prediction-markets/index.tsx +8 -4
  213. package/src/plugins/prediction-markets/navigation.ts +0 -1
  214. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  215. package/src/plugins/prediction-markets/pane.tsx +4 -0
  216. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  217. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  218. package/src/plugins/registry/index.ts +2 -0
  219. package/src/plugins/registry/pane-settings.test.ts +53 -0
  220. package/src/plugins/registry/pane-settings.ts +36 -3
  221. package/src/plugins/ticker-navigation.test.ts +24 -0
  222. package/src/plugins/ticker-navigation.ts +40 -0
  223. package/src/remote/schema.ts +1 -1
  224. package/src/renderers/browser/cloud-transport.test.ts +36 -0
  225. package/src/renderers/browser/cloud-transport.ts +19 -0
  226. package/src/renderers/browser/config-host.ts +0 -4
  227. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  228. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  229. package/src/renderers/browser/main.tsx +3 -0
  230. package/src/renderers/browser/ui-host.tsx +11 -0
  231. package/src/renderers/cloudflare/worker.test.ts +8 -5
  232. package/src/renderers/cloudflare/worker.ts +6 -5
  233. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  234. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  235. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  236. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  237. package/src/renderers/electrobun/bun/index.ts +1 -0
  238. package/src/renderers/electrobun/shared/protocol.ts +4 -0
  239. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  240. package/src/renderers/electrobun/view/host/native.ts +0 -5
  241. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  242. package/src/renderers/electrobun/view/styles.css +107 -7
  243. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  244. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  245. package/src/renderers/electrobun/view/ui-host.tsx +7 -1
  246. package/src/renderers/opentui/host.tsx +4 -0
  247. package/src/renderers/share/layout-view.test.tsx +58 -0
  248. package/src/renderers/share/layout-view.tsx +133 -0
  249. package/src/renderers/share/main.tsx +54 -6
  250. package/src/renderers/share/styles.css +35 -0
  251. package/src/renderers/share/view.test.tsx +78 -0
  252. package/src/renderers/share/view.tsx +71 -0
  253. package/src/shares/api.test.ts +52 -3
  254. package/src/shares/api.ts +39 -15
  255. package/src/shares/location.test.ts +19 -0
  256. package/src/shares/location.ts +11 -0
  257. package/src/shares/pane.test.ts +110 -0
  258. package/src/shares/pane.ts +54 -0
  259. package/src/shares/payload.ts +74 -1
  260. package/src/state/app/bootstrap.ts +6 -7
  261. package/src/state/app/context/index.test.ts +41 -1
  262. package/src/state/app/context/index.test.tsx +34 -16
  263. package/src/state/pane-table-export-registry.ts +31 -0
  264. package/src/state/selectors-ui.ts +0 -8
  265. package/src/sync/controller.test.ts +57 -1
  266. package/src/sync/controller.ts +22 -11
  267. package/src/sync/react.ts +13 -0
  268. package/src/theme/font-scale.ts +0 -11
  269. package/src/time-series/range.ts +12 -0
  270. package/src/time-series/resolution.ts +3 -15
  271. package/src/time-series/spec.ts +5 -5
  272. package/src/types/config.ts +35 -5
  273. package/src/types/desktop-menu.ts +1 -0
  274. package/src/types/plugin.ts +38 -20
  275. package/src/ui/context-menu.tsx +1 -1
  276. package/src/ui/host.tsx +7 -0
  277. package/src/ui/toast.tsx +2 -0
  278. package/src/utils/back-navigation.ts +0 -4
  279. package/src/utils/csv.test.ts +13 -0
  280. package/src/utils/csv.ts +37 -0
  281. package/src/utils/save-text-file.test.ts +24 -0
  282. package/src/utils/save-text-file.ts +36 -0
@@ -13,38 +13,26 @@ import type { CliCommandDef } from "../../types/plugin";
13
13
  import { withCliServices, withConfigData } from "../context";
14
14
  import { requireArg, takeOption } from "./command-utils";
15
15
 
16
- function deferredResult(command: string, code: "auth_required" | "not_implemented", message: string) {
17
- return {
18
- command,
19
- status: code,
20
- code,
21
- message,
22
- };
23
- }
24
-
25
16
  export const brokerCliCommand: CliCommandDef = {
26
17
  name: "broker",
27
18
  aliases: ["brokers"],
28
- description: "Inspect broker profiles and guarded broker operations",
29
- help: { usage: ["broker list", "broker status", "broker add|edit|remove|connect|sync"] },
19
+ description: "Inspect broker profiles",
20
+ help: { usage: ["broker list", "broker status"] },
30
21
  execute: async (args, ctx) => {
31
22
  const action = args[0] ?? "list";
23
+ if (action !== "list" && action !== "status") {
24
+ ctx.fail("Usage: gloomberb broker list|status");
25
+ }
32
26
  await withCliServices(ctx, async (services) => {
33
- if (action === "list" || action === "status") {
34
- const rows = services.config.brokerInstances.map((instance) => ({
27
+ ctx.printResult({
28
+ data: services.config.brokerInstances.map((instance) => ({
35
29
  id: instance.id,
36
30
  type: instance.brokerType,
37
31
  label: instance.label,
38
32
  enabled: instance.enabled !== false,
39
33
  connectionMode: instance.connectionMode ?? "",
40
34
  lastSyncedAt: instance.lastSyncedAt ? new Date(instance.lastSyncedAt).toISOString() : "",
41
- }));
42
- ctx.printResult({ data: rows });
43
- return;
44
- }
45
-
46
- ctx.printResult({
47
- data: deferredResult("broker", "not_implemented", "Broker mutation and connection commands need shared non-UI broker service extraction before they can safely run headless."),
35
+ })),
48
36
  });
49
37
  });
50
38
  },
@@ -52,36 +40,24 @@ export const brokerCliCommand: CliCommandDef = {
52
40
 
53
41
  export const ibkrCliCommand: CliCommandDef = {
54
42
  name: "ibkr",
55
- description: "Guarded IBKR account, order preview, and order action commands",
56
- help: { usage: ["ibkr accounts|positions|orders|preview|place|cancel"] },
43
+ description: "Inspect IBKR profiles",
44
+ help: { usage: ["ibkr accounts", "ibkr status"] },
57
45
  execute: async (args, ctx) => {
58
46
  const action = args[0] ?? "status";
59
- const profile = takeOption(args, "--profile");
60
- const account = takeOption(args, "--account");
61
- if ((action === "place" || action === "cancel") && (!profile || !account || !ctx.cliOptions.yes)) {
62
- ctx.fail(`ibkr ${action} requires --profile, --account, and --yes.`);
47
+ if (action !== "accounts" && action !== "status") {
48
+ ctx.fail("Usage: gloomberb ibkr accounts|status");
63
49
  }
64
50
  await withCliServices(ctx, async (services) => {
65
- const ibkrProfiles = services.config.brokerInstances.filter((instance) => instance.brokerType === "ibkr");
66
- if (action === "accounts" || action === "status") {
67
- ctx.printResult({ data: ibkrProfiles.map((instance) => ({
68
- id: instance.id,
69
- label: instance.label,
70
- enabled: instance.enabled !== false,
71
- connectionMode: instance.connectionMode ?? "",
72
- lastSyncedAt: instance.lastSyncedAt ? new Date(instance.lastSyncedAt).toISOString() : "",
73
- })) });
74
- return;
75
- }
76
51
  ctx.printResult({
77
- data: {
78
- ...deferredResult("ibkr", "not_implemented", "IBKR trading commands are registered with safety gates, but headless order/account service extraction is still required."),
79
- action,
80
- profile: profile ?? null,
81
- account: account ?? null,
82
- dryRun: ctx.cliOptions.dryRun,
83
- confirmed: ctx.cliOptions.yes,
84
- },
52
+ data: services.config.brokerInstances
53
+ .filter((instance) => instance.brokerType === "ibkr")
54
+ .map((instance) => ({
55
+ id: instance.id,
56
+ label: instance.label,
57
+ enabled: instance.enabled !== false,
58
+ connectionMode: instance.connectionMode ?? "",
59
+ lastSyncedAt: instance.lastSyncedAt ? new Date(instance.lastSyncedAt).toISOString() : "",
60
+ })),
85
61
  });
86
62
  });
87
63
  },
@@ -133,19 +109,12 @@ export function createAiCliCommand(options: CreateAiCliCommandOptions = {}): Cli
133
109
  usage: [
134
110
  "ai providers",
135
111
  "ai ask [--provider id] [--model id] <prompt>",
136
- "ai screen list|show|delete|refresh|export",
137
112
  ],
138
113
  },
139
114
  execute: async (args, ctx) => {
140
115
  const action = args[0] ?? "providers";
141
- if (action === "screen") {
142
- ctx.printResult({
143
- data: deferredResult("ai screen", "not_implemented", "AI screener tabs are still pane-state backed; expose them through a shared non-UI screener store before headless mutation."),
144
- });
145
- return;
146
- }
147
116
  if (action !== "providers" && action !== "ask") {
148
- ctx.fail("Usage: gloomberb ai providers|ask|screen");
117
+ ctx.fail("Usage: gloomberb ai providers|ask");
149
118
  }
150
119
 
151
120
  const rawArgs = args.slice(1);
@@ -258,23 +227,3 @@ export const rssCliCommand: CliCommandDef = {
258
227
  })) });
259
228
  },
260
229
  };
261
-
262
- function cloudCommand(name: string, description: string): CliCommandDef {
263
- return {
264
- name,
265
- description,
266
- execute: (_args, ctx) => {
267
- ctx.printResult({
268
- data: deferredResult(name, "auth_required", "This command needs an existing cloud/session token. New auth/account/chat CLI workflows are deferred in this pass."),
269
- });
270
- },
271
- };
272
- }
273
-
274
- export const cloudCliCommands: CliCommandDef[] = [
275
- cloudCommand("buildout", "Fetch Buildout company data through an existing cloud session"),
276
- cloudCommand("congress", "Fetch congressional trade data through an existing cloud session"),
277
- cloudCommand("substack", "Fetch Substack reader data through an existing session"),
278
- cloudCommand("x-feed", "Fetch X/Twitter feed data through an existing cloud session"),
279
- cloudCommand("tweets", "Alias for x-feed"),
280
- ];
@@ -1,5 +1,5 @@
1
1
  import type { CliCommandDef } from "../../types/plugin";
2
- import type { TimeRange } from "../../time-series/range";
2
+ import { TIME_RANGES, type TimeRange } from "../../time-series/range";
3
3
  import type { EarningsEvent, QuoteBatchResult, SecFilingItem } from "../../types/data-provider";
4
4
  import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
5
5
  import type {
@@ -15,7 +15,7 @@ import { formatCompact } from "../../utils/format";
15
15
  import { withCliServices, withMarketData } from "../context";
16
16
  import { isoDate, parsePositiveInt, requireArg, takeOption } from "./command-utils";
17
17
 
18
- const VALID_RANGES = new Set<TimeRange>(["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"]);
18
+ const VALID_RANGES = new Set<TimeRange>(TIME_RANGES);
19
19
  const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
20
20
 
21
21
  type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & { error: string | null };
package/src/cli/index.ts CHANGED
@@ -23,7 +23,6 @@ import { createSystemCliCommands } from "./commands/system";
23
23
  import {
24
24
  aiCliCommand,
25
25
  brokerCliCommand,
26
- cloudCliCommands,
27
26
  ibkrCliCommand,
28
27
  rssCliCommand,
29
28
  } from "./commands/automation";
@@ -198,7 +197,6 @@ function createCoreCliCommands(
198
197
  ibkrCliCommand,
199
198
  aiCliCommand,
200
199
  rssCliCommand,
201
- ...cloudCliCommands,
202
200
  ];
203
201
  return commands;
204
202
  }
@@ -1,3 +1,4 @@
1
+ import { CHART_RESOLUTIONS } from "../../time-series/range";
1
2
  import type { PaneDef, PaneTemplateDef } from "../../types/plugin";
2
3
 
3
4
  export type PaneFunctionReadiness = "ready" | "partial" | "unsupported";
@@ -128,8 +129,7 @@ const CAPABILITIES: Record<string, PaneFunctionCapability> = {
128
129
  description: "Market-price sampling resolution.",
129
130
  type: "enum",
130
131
  aliases: ["resolution"],
131
- values: ["auto", "1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"]
132
- .map((value) => ({ value })),
132
+ values: CHART_RESOLUTIONS.map((value) => ({ value })),
133
133
  },
134
134
  ],
135
135
  },
@@ -251,7 +251,7 @@ const CAPABILITIES: Record<string, PaneFunctionCapability> = {
251
251
  description: "Price sampling resolution.",
252
252
  type: "enum",
253
253
  aliases: ["resolution"],
254
- values: ["1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"].map((value) => ({ value })),
254
+ values: CHART_RESOLUTIONS.filter((value) => value !== "auto").map((value) => ({ value })),
255
255
  defaultValue: "1d",
256
256
  },
257
257
  ],
@@ -4,7 +4,6 @@ import type {
4
4
  CliCommandDef,
5
5
  CliDispatchResult,
6
6
  GloomPlugin,
7
- PluginCliCommandDescriptor,
8
7
  } from "../types/plugin";
9
8
  import type { LoadedExternalPlugin } from "../plugins/loader";
10
9
  import { getPluginCatalog } from "../plugins/catalog";
@@ -35,7 +34,6 @@ export interface CliCommandRegistry {
35
34
  config: AppConfig | null;
36
35
  plugins: GloomPlugin[];
37
36
  externalPlugins: LoadedExternalPlugin[];
38
- descriptors: Array<{ pluginId: string; descriptor: PluginCliCommandDescriptor }>;
39
37
  }
40
38
 
41
39
  interface BuildCliCommandRegistryOptions {
@@ -71,19 +69,6 @@ function getCommandUsageLabel(command: CliCommandDef): string {
71
69
  return command.help?.usage?.[0] ?? command.name;
72
70
  }
73
71
 
74
- function descriptorToCommand(descriptor: PluginCliCommandDescriptor): CliCommandDef | null {
75
- if (!descriptor.execute) return null;
76
- return {
77
- name: descriptor.name,
78
- aliases: descriptor.aliases,
79
- description: descriptor.summary,
80
- help: descriptor.examples?.length
81
- ? { usage: descriptor.examples }
82
- : undefined,
83
- execute: descriptor.execute,
84
- };
85
- }
86
-
87
72
  function renderHelpSections(registry: CliCommandRegistry): string[] {
88
73
  const lines: string[] = [];
89
74
 
@@ -172,7 +157,6 @@ export function buildCliCommandRegistry({
172
157
  }
173
158
 
174
159
  const loadablePlugins: GloomPlugin[] = [];
175
- const descriptors: Array<{ pluginId: string; descriptor: PluginCliCommandDescriptor }> = [];
176
160
  for (const entry of catalog) {
177
161
  if (entry.error) {
178
162
  registryLog.warn(`Skipping external plugin "${entry.plugin.id}" for CLI registration.`, {
@@ -185,11 +169,6 @@ export function buildCliCommandRegistry({
185
169
  for (const command of entry.plugin.cliCommands ?? []) {
186
170
  registerCommand(command, entry.plugin.id, "plugin");
187
171
  }
188
- for (const descriptor of entry.plugin.cli?.commands ?? []) {
189
- descriptors.push({ pluginId: entry.plugin.id, descriptor });
190
- const command = descriptorToCommand(descriptor);
191
- if (command) registerCommand(command, entry.plugin.id, "plugin");
192
- }
193
172
  }
194
173
 
195
174
  const commands = allCommands.filter((entry) => (
@@ -210,7 +189,6 @@ export function buildCliCommandRegistry({
210
189
  config,
211
190
  plugins: loadablePlugins.filter((plugin) => !disabledPlugins.has(plugin.id)),
212
191
  externalPlugins,
213
- descriptors,
214
192
  };
215
193
  }
216
194
 
package/src/cli/result.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { CliGlobalOptions } from "./options";
2
2
  import { renderTable, type CliTableColumn } from "../utils/cli-output";
3
+ import { serializeCsv } from "../utils/csv";
3
4
 
4
5
  export interface CliResult<T = unknown> {
5
6
  data: T;
@@ -57,11 +58,6 @@ function inferColumns(rows: Record<string, unknown>[]): CliResultColumn<Record<s
57
58
  return [...keys].map((key) => ({ key, header: key }));
58
59
  }
59
60
 
60
- function csvEscape(value: string): string {
61
- if (!/[",\n\r]/.test(value)) return value;
62
- return `"${value.replace(/"/g, '""')}"`;
63
- }
64
-
65
61
  function serializeColumns<Row>(columns?: CliResultColumn<Row>[]) {
66
62
  return columns?.map((column) => ({
67
63
  key: column.key,
@@ -78,11 +74,12 @@ function renderCsv<Row extends Record<string, unknown>>(
78
74
  const resolvedColumns = columns && columns.length > 0
79
75
  ? columns
80
76
  : inferColumns(rows as Record<string, unknown>[]) as CliResultColumn<Row>[];
81
- const header = resolvedColumns.map((column) => csvEscape(column.header)).join(",");
82
- const body = rows.map((row) => resolvedColumns
83
- .map((column) => csvEscape(normalizeCell(column.value ? column.value(row) : row[column.key])))
84
- .join(","));
85
- return [header, ...body].join("\n");
77
+ return serializeCsv([
78
+ resolvedColumns.map((column) => column.header),
79
+ ...rows.map((row) => resolvedColumns.map((column) => (
80
+ column.value ? column.value(row) : row[column.key]
81
+ ))),
82
+ ]);
86
83
  }
87
84
 
88
85
  function renderTextTable<Row extends Record<string, unknown>>(
@@ -31,6 +31,9 @@ import {
31
31
  resolveCompositeMinimumSpanMs,
32
32
  zoomCompositeViewport,
33
33
  } from "./interactions";
34
+ import { getThemeColors, syncTheme } from "../../../theme/colors";
35
+ import { ThemeProvider } from "../../../theme/theme-context";
36
+ import { DEFAULT_THEME } from "../../../theme/themes";
34
37
 
35
38
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
36
39
  let chartShortcut: ((event: KeyEventLike) => void) | null = null;
@@ -131,12 +134,12 @@ function keyEvent(name: string, shift = false): KeyEventLike {
131
134
  }
132
135
 
133
136
  afterEach(async () => {
134
- if (!testSetup) return;
135
- await act(async () => testSetup!.renderer.destroy());
137
+ if (testSetup) await act(async () => testSetup!.renderer.destroy());
136
138
  testSetup = undefined;
137
139
  chartShortcut = null;
138
140
  capturedSurfaceProps = null;
139
141
  capturedSurfaceNode = null;
142
+ syncTheme(DEFAULT_THEME);
140
143
  });
141
144
 
142
145
  function point(date: string, value: number): TimeSeriesPoint {
@@ -204,6 +207,46 @@ function twoSessionCandles(): ResolvedSeries {
204
207
  }
205
208
 
206
209
  describe("CompositeChart", () => {
210
+ test("repaints cached chart rasters when the theme changes", async () => {
211
+ let setThemeId: ((themeId: string) => void) | null = null;
212
+ const chartSeries = [series("price", "main", "left", "USD", [100, 101, 102])];
213
+
214
+ function ThemedChart() {
215
+ const [themeId, setTheme] = useState(DEFAULT_THEME);
216
+ setThemeId = setTheme;
217
+ return (
218
+ <ThemeProvider themeId={themeId}>
219
+ <CaptureChartSurfaceProvider>
220
+ <CompositeChart
221
+ width={60}
222
+ height={12}
223
+ series={chartSeries}
224
+ panels={[{ id: "main" }]}
225
+ />
226
+ </CaptureChartSurfaceProvider>
227
+ </ThemeProvider>
228
+ );
229
+ }
230
+
231
+ testSetup = await testRender(<ThemedChart />, { width: 62, height: 14 });
232
+ await act(async () => {
233
+ await testSetup!.renderOnce();
234
+ await testSetup!.renderOnce();
235
+ });
236
+
237
+ const nextThemeId = "github-light";
238
+ await act(async () => {
239
+ setThemeId?.(nextThemeId);
240
+ await testSetup!.renderOnce();
241
+ await testSetup!.renderOnce();
242
+ });
243
+
244
+ const bitmap = capturedSurfaceProps!.bitmaps[0] as { pixels: Uint8Array };
245
+ const expectedBackground = getThemeColors(nextThemeId).bg;
246
+ const expectedRgb = [1, 3, 5].map((offset) => parseInt(expectedBackground.slice(offset, offset + 2), 16));
247
+ expect(Array.from(bitmap.pixels.slice(0, 3))).toEqual(expectedRgb);
248
+ });
249
+
207
250
  test("shows the regular-session move beside the latest intraday value", async () => {
208
251
  testSetup = await testRender(
209
252
  <CompositeChart
@@ -14,6 +14,7 @@ import {
14
14
  import { useShortcut } from "../../../react/input";
15
15
  import { useOptionalPaneInstanceId, usePaneSettingValue } from "../../../state/app/context";
16
16
  import { colors as themeColors, hoverBg } from "../../../theme/colors";
17
+ import { useThemeColors } from "../../../theme/theme-context";
17
18
  import { formatPercentRaw } from "../../../utils/format";
18
19
  import { isPlainKey } from "../../../utils/keyboard";
19
20
  import { truncateWithEllipsis } from "../../../utils/text-wrap";
@@ -62,6 +63,7 @@ import { renderCompositePanelBitmap } from "./rasterizer";
62
63
  import {
63
64
  buildChartToolVectors,
64
65
  CHART_DRAWING_COLORS,
66
+ CHART_DRAWINGS_SETTING_KEY,
65
67
  countMeasureBars,
66
68
  drawChartToolOverlay,
67
69
  resolveChartToolKind,
@@ -70,6 +72,7 @@ import {
70
72
  hitTestDrawings,
71
73
  isDrawingTool,
72
74
  nextDrawingColor,
75
+ parseChartDrawings,
73
76
  resolveDrawingFromDrag,
74
77
  resolveZoomBoxRange,
75
78
  shiftDrawing,
@@ -1371,7 +1374,6 @@ function CompositeLegend({
1371
1374
  );
1372
1375
  }
1373
1376
 
1374
- const CHART_DRAWINGS_SETTING_KEY = "chartDrawings";
1375
1377
  const NO_DRAWINGS: readonly ChartDrawing[] = [];
1376
1378
  /** Coalesces a drag into one write instead of one per pointer move. */
1377
1379
  const DRAWING_PERSIST_DELAY_MS = 400;
@@ -1397,7 +1399,7 @@ function ChartDrawingStore({
1397
1399
  );
1398
1400
  const restoredRef = useRef<readonly ChartDrawing[] | null>(null);
1399
1401
  if (restoredRef.current === null) {
1400
- restoredRef.current = Array.isArray(stored) ? stored : NO_DRAWINGS;
1402
+ restoredRef.current = parseChartDrawings(stored);
1401
1403
  }
1402
1404
 
1403
1405
  useEffect(() => {
@@ -1445,6 +1447,7 @@ export function CompositeChart({
1445
1447
  onToggleSeries,
1446
1448
  isSeriesToggleable,
1447
1449
  }: CompositeChartProps) {
1450
+ const activeThemeColors = useThemeColors();
1448
1451
  const { cellWidthPx = 8, pixelRatio = 1 } = useUiCapabilities();
1449
1452
  const isDesktopWeb = useUiHost().kind === "desktop-web";
1450
1453
  const showTextFallback = useShowChartTextFallback();
@@ -1692,13 +1695,13 @@ export function CompositeChart({
1692
1695
  }).join("|");
1693
1696
  const plotHeight = Math.max(panelCount, totalHeight - legendRows - timeAxisRows);
1694
1697
  const resolvedColors = useMemo<CompositeChartColors>(() => ({
1695
- background: colors?.background ?? themeColors.bg,
1696
- grid: colors?.grid ?? themeColors.border,
1697
- crosshair: colors?.crosshair ?? themeColors.borderFocused,
1698
- text: colors?.text ?? themeColors.text,
1699
- textDim: colors?.textDim ?? themeColors.textDim,
1700
- negative: colors?.negative ?? themeColors.negative,
1701
- }), [colors]);
1698
+ background: colors?.background ?? activeThemeColors.bg,
1699
+ grid: colors?.grid ?? activeThemeColors.border,
1700
+ crosshair: colors?.crosshair ?? activeThemeColors.borderFocused,
1701
+ text: colors?.text ?? activeThemeColors.text,
1702
+ textDim: colors?.textDim ?? activeThemeColors.textDim,
1703
+ negative: colors?.negative ?? activeThemeColors.negative,
1704
+ }), [activeThemeColors, colors]);
1702
1705
  const projectedScene = useMemo(() => {
1703
1706
  // lastTickKey busts this memo when a live tick mutates series identity in place.
1704
1707
  void lastTickKey;
@@ -17,7 +17,6 @@ import type {
17
17
  import {
18
18
  buildCompositeTimeScale,
19
19
  projectCompositeTimestamp,
20
- unprojectCompositeTimestamp,
21
20
  } from "./time-scale";
22
21
  import type { CompositeTimeScale } from "./types";
23
22
 
@@ -523,26 +522,6 @@ export function resolveCompositeCursorDate(scene: CompositeChartScene, localX: n
523
522
  return scene.dates[nearestIndex] ?? null;
524
523
  }
525
524
 
526
- export function resolveCompositeTimeAxisDate(
527
- scene: CompositeChartScene,
528
- ratio: number,
529
- ): Date {
530
- const safeRatio = Math.max(0, Math.min(1, ratio));
531
- if (scene.timeScale.kind === "market" && scene.dates.length > 0) {
532
- let nearestIndex = 0;
533
- let nearestDistance = Number.POSITIVE_INFINITY;
534
- scene.dateRatios.forEach((candidate, index) => {
535
- const distance = Math.abs(candidate - safeRatio);
536
- if (distance < nearestDistance) {
537
- nearestIndex = index;
538
- nearestDistance = distance;
539
- }
540
- });
541
- return scene.dates[nearestIndex] ?? new Date(scene.startTime);
542
- }
543
- return new Date(unprojectCompositeTimestamp(scene.timeScale, safeRatio));
544
- }
545
-
546
525
  export function resolveAdjacentCompositeCursorDate(
547
526
  scene: CompositeChartScene,
548
527
  current: Date | null,
@@ -27,6 +27,55 @@ export interface ChartDrawing {
27
27
  color: string;
28
28
  }
29
29
 
30
+ export const CHART_DRAWINGS_SETTING_KEY = "chartDrawings";
31
+ const MAX_SHARED_CHART_DRAWINGS = 100;
32
+ const MAX_SHARED_DRAWING_POINTS = 5_000;
33
+
34
+ export function parseChartDrawings(value: unknown): ChartDrawing[] {
35
+ if (!Array.isArray(value) || value.length > MAX_SHARED_CHART_DRAWINGS) return [];
36
+ let pointCount = 0;
37
+ const drawings: ChartDrawing[] = [];
38
+ for (const entry of value) {
39
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
40
+ const drawing = entry as Record<string, unknown>;
41
+ if (!Object.keys(drawing).every((key) => ["id", "panelId", "points", "color"].includes(key))) return [];
42
+ if (
43
+ typeof drawing.id !== "string"
44
+ || drawing.id.length === 0
45
+ || drawing.id.length > 160
46
+ || typeof drawing.panelId !== "string"
47
+ || drawing.panelId.length === 0
48
+ || drawing.panelId.length > 120
49
+ || typeof drawing.color !== "string"
50
+ || !/^#[0-9a-f]{6}$/i.test(drawing.color)
51
+ || !Array.isArray(drawing.points)
52
+ || drawing.points.length < 2
53
+ ) return [];
54
+ pointCount += drawing.points.length;
55
+ if (pointCount > MAX_SHARED_DRAWING_POINTS) return [];
56
+ const points: ChartDrawingPoint[] = [];
57
+ for (const point of drawing.points) {
58
+ if (!point || typeof point !== "object" || Array.isArray(point)) return [];
59
+ const raw = point as Record<string, unknown>;
60
+ if (
61
+ !Object.keys(raw).every((key) => key === "time" || key === "value")
62
+ || typeof raw.time !== "number"
63
+ || !Number.isFinite(raw.time)
64
+ || typeof raw.value !== "number"
65
+ || !Number.isFinite(raw.value)
66
+ ) return [];
67
+ points.push({ time: raw.time, value: raw.value });
68
+ }
69
+ drawings.push({
70
+ id: drawing.id,
71
+ panelId: drawing.panelId,
72
+ points,
73
+ color: drawing.color,
74
+ });
75
+ }
76
+ return drawings;
77
+ }
78
+
30
79
  export interface ChartDrawingHit {
31
80
  drawing: ChartDrawing;
32
81
  /** Grabbed endpoint, or null when the whole shape was grabbed. */
@@ -87,6 +87,10 @@ export function runDirectCommandAction(options: {
87
87
  closeAll({ revertThemePreview: false });
88
88
  pluginRegistry.showPane("help");
89
89
  return;
90
+ case "layout-marketplace":
91
+ closeAll({ revertThemePreview: false });
92
+ pluginRegistry.showPane("layout-marketplace");
93
+ return;
90
94
  case "pane-settings":
91
95
  if (state.focusedPaneId) openPaneSettingsRoute(state.focusedPaneId);
92
96
  return;
@@ -138,8 +138,15 @@ export const commands: Command[] = [
138
138
  category: "Config",
139
139
  },
140
140
  {
141
- id: "layout",
141
+ id: "layout-marketplace",
142
142
  prefix: "LAY",
143
+ label: "Layouts",
144
+ description: "Open the layout browser to switch, publish, or add layouts",
145
+ category: "Config",
146
+ },
147
+ {
148
+ id: "layout",
149
+ prefix: "LMA",
143
150
  label: "Layout Actions",
144
151
  description: "Organize panes and saved layouts",
145
152
  hasArg: true,
@@ -79,7 +79,7 @@ export function buildCurrentLayoutItems({
79
79
  },
80
80
  {
81
81
  id: "layout-gridlock",
82
- label: "Gridlock All Windows",
82
+ label: "Tidy Windows",
83
83
  detail: currentLayout.floating.length > 0
84
84
  ? "Infer a tiled layout from the current window positions"
85
85
  : "Retile all panes from their current arrangement",
@@ -112,6 +112,10 @@ export function useCommandBarPaneTemplateActions({
112
112
  const trimmedArg = rawArg?.trim() || "";
113
113
  const argKind = template.shortcut?.argKind ?? template.shortcut?.argPlaceholder;
114
114
  if (argKind === "ticker") {
115
+ if (!trimmedArg && template.shortcut?.argOptional) {
116
+ await openPaneTemplateDirect(template);
117
+ return;
118
+ }
115
119
  const resolvedTicker = await resolveTickerInput(
116
120
  trimmedArg || undefined,
117
121
  activeTickerSymbol,
@@ -34,6 +34,9 @@ export function buildRootShortcutFeedback({
34
34
  if (rootShortcutIntent.source === "pane-template") {
35
35
  const argKind = getPaneTemplateArgKind(rootShortcutIntent.template);
36
36
  if (argKind === "ticker") {
37
+ if (rootShortcutIntent.template.shortcut?.argOptional && !rootShortcutIntent.argText) {
38
+ return `Shortcut: ${rootShortcutIntent.label}`;
39
+ }
37
40
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
38
41
  if (symbol) {
39
42
  return rootShortcutIntent.kind === "inferred-complete"
@@ -46,6 +46,13 @@ const paneTemplates: PaneTemplateDef[] = [
46
46
  description: "Search provider instruments",
47
47
  shortcut: { prefix: "SRCH", argPlaceholder: "query", argKind: "text" },
48
48
  },
49
+ {
50
+ id: "cds-pane",
51
+ paneId: "cds",
52
+ label: "Single-Name CDS",
53
+ description: "CDS shortcut",
54
+ shortcut: { prefix: "CDS", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
55
+ },
49
56
  ];
50
57
 
51
58
  const pluginCommands: CommandDef[] = [{
@@ -135,6 +142,18 @@ describe("ticker data root shortcuts", () => {
135
142
  }
136
143
  });
137
144
 
145
+ test("optional ticker shortcuts do not infer the active ticker", () => {
146
+ const bare = parse("CDS", "MSFT");
147
+ expect(bare.kind).toBe("partial");
148
+ if (bare.kind === "none") throw new Error("Expected shortcut intent");
149
+ expect(bare.completionQuery).toBeNull();
150
+
151
+ const scoped = parse("CDS ORCL", "MSFT");
152
+ expect(scoped.kind).toBe("complete");
153
+ if (scoped.kind === "none") throw new Error("Expected shortcut intent");
154
+ expect(scoped.argText).toBe("ORCL");
155
+ });
156
+
138
157
  test("EM accepts optional text tickers without active ticker inference", () => {
139
158
  const bare = parse("EM", "AAPL");
140
159
  expect(bare.kind).toBe("partial");
@@ -145,7 +145,9 @@ export function parseRootShortcutIntent({
145
145
  if (!match) return { kind: "none" };
146
146
 
147
147
  const argText = trimmed.slice(match.prefix.length).trim();
148
- const inferredArg = inferShortcutArg(match.argKind, activeTicker);
148
+ const inferredArg = match.source === "pane-template" && match.template?.shortcut?.argOptional
149
+ ? null
150
+ : inferShortcutArg(match.argKind, activeTicker);
149
151
  const completionQuery = inferredArg ? `${match.prefix} ${inferredArg}` : null;
150
152
 
151
153
  let kind: Exclude<ShortcutIntentKind, "none">;