gloomberb 0.4.0 → 0.5.0

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 (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -218,7 +218,7 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
218
218
  showWidget: () => {},
219
219
  updateLayoutFn: () => {},
220
220
  getTermSizeFn: () => ({ width: 80, height: 24 }),
221
- showToastFn: () => {},
221
+ notify: () => {},
222
222
  createPaneFromTemplateFn: () => {},
223
223
  createPaneFromTemplateAsyncFn: async () => {},
224
224
  openPaneSettingsFn: () => {},
@@ -352,6 +352,308 @@ describe("CommandBar", () => {
352
352
  expect(testSetup.captureCharFrame()).not.toContain("Commands");
353
353
  });
354
354
 
355
+ test("runs plugin command shortcuts from the root query", async () => {
356
+ const calls: string[] = [];
357
+
358
+ testSetup = await testRender(<CommandBarHarness
359
+ query="GL"
360
+ configurePluginRegistry={(pluginRegistry) => {
361
+ (pluginRegistry.commands as Map<string, any>).set("gridlock-all", {
362
+ id: "gridlock-all",
363
+ label: "Gridlock All Windows",
364
+ description: "Arrange all visible panes into a tiled grid",
365
+ keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
366
+ shortcut: "GL",
367
+ category: "config",
368
+ execute: async () => {
369
+ calls.push("gridlock-all");
370
+ },
371
+ });
372
+ (pluginRegistry.allPlugins as Map<string, any>).set("layout-manager", {
373
+ id: "layout-manager",
374
+ name: "Layout Manager",
375
+ version: "1.0.0",
376
+ description: "Pane layout management commands",
377
+ });
378
+ const getCommandPluginId = pluginRegistry.getCommandPluginId;
379
+ pluginRegistry.getCommandPluginId = (commandId: string) => (
380
+ commandId === "gridlock-all" ? "layout-manager" : getCommandPluginId(commandId)
381
+ );
382
+ }}
383
+ />, {
384
+ width: 80,
385
+ height: 24,
386
+ });
387
+
388
+ await testSetup.renderOnce();
389
+
390
+ const frame = testSetup.captureCharFrame();
391
+ expect(frame).toContain("Gridlock All Windows");
392
+ expect(frame).toContain("GL");
393
+
394
+ await act(async () => {
395
+ testSetup!.mockInput.pressEnter();
396
+ await Bun.sleep(0);
397
+ await testSetup!.renderOnce();
398
+ });
399
+
400
+ expect(calls).toEqual(["gridlock-all"]);
401
+ });
402
+
403
+ test("surfaces plugin commands by add-style search terms", async () => {
404
+ testSetup = await testRender(<CommandBarHarness
405
+ query="add alert"
406
+ configurePluginRegistry={(pluginRegistry) => {
407
+ (pluginRegistry.commands as Map<string, any>).set("set-alert", {
408
+ id: "set-alert",
409
+ label: "Add Alert",
410
+ description: "Create a price alert from a symbol, condition, and target price",
411
+ keywords: ["add", "set", "alert", "price", "trigger"],
412
+ shortcut: "SA",
413
+ category: "data",
414
+ execute: async () => {},
415
+ });
416
+ }}
417
+ />, {
418
+ width: 80,
419
+ height: 24,
420
+ });
421
+
422
+ await testSetup.renderOnce();
423
+
424
+ const frame = testSetup.captureCharFrame();
425
+ expect(frame).toContain("Add Alert");
426
+ expect(frame).toContain("SA");
427
+ });
428
+
429
+ test("opens plugin command shortcut arguments in the wizard for confirmation", async () => {
430
+ const calls: Array<Record<string, string> | undefined> = [];
431
+
432
+ testSetup = await testRender(<CommandBarHarness
433
+ query="SA AAPL above 200"
434
+ configurePluginRegistry={(pluginRegistry) => {
435
+ (pluginRegistry.commands as Map<string, any>).set("set-alert", {
436
+ id: "set-alert",
437
+ label: "Add Alert",
438
+ description: "Create a price alert from a symbol, condition, and target price",
439
+ keywords: ["add", "set", "alert", "price", "trigger"],
440
+ shortcut: "SA",
441
+ shortcutArg: {
442
+ placeholder: "symbol condition price",
443
+ kind: "text",
444
+ parse: (arg: string) => {
445
+ const [symbol, condition, price] = arg.split(/\s+/);
446
+ return { symbol, condition, price };
447
+ },
448
+ },
449
+ category: "data",
450
+ wizardLayout: "form",
451
+ wizard: [
452
+ { key: "symbol", label: "Symbol", type: "text" },
453
+ {
454
+ key: "condition",
455
+ label: "Condition",
456
+ type: "select",
457
+ options: [
458
+ { label: "Above", value: "above" },
459
+ { label: "Below", value: "below" },
460
+ ],
461
+ },
462
+ { key: "price", label: "Target Price", type: "number" },
463
+ ],
464
+ execute: async (values?: Record<string, string>) => {
465
+ calls.push(values);
466
+ },
467
+ });
468
+ }}
469
+ />, {
470
+ width: 80,
471
+ height: 24,
472
+ });
473
+
474
+ await testSetup.renderOnce();
475
+
476
+ const frame = testSetup.captureCharFrame();
477
+ expect(frame).toContain("Add Alert");
478
+ expect(frame).toContain("AAPL above 200");
479
+
480
+ await act(async () => {
481
+ testSetup!.mockInput.pressEnter();
482
+ await Bun.sleep(0);
483
+ await testSetup!.renderOnce();
484
+ });
485
+
486
+ const workflowFrame = await waitForFrameToContain("Target Price");
487
+ expect(workflowFrame).toContain("AAPL");
488
+ expect(workflowFrame).toContain("Above");
489
+ expect(workflowFrame).toContain("200");
490
+ expect(calls).toEqual([]);
491
+ });
492
+
493
+ test("opens partial plugin command shortcut arguments in the wizard", async () => {
494
+ testSetup = await testRender(<CommandBarHarness
495
+ query="SA AMD"
496
+ configurePluginRegistry={(pluginRegistry) => {
497
+ (pluginRegistry.commands as Map<string, any>).set("set-alert", {
498
+ id: "set-alert",
499
+ label: "Add Alert",
500
+ description: "Create a price alert from a symbol, condition, and target price",
501
+ keywords: ["add", "set", "alert", "price", "trigger"],
502
+ shortcut: "SA",
503
+ shortcutArg: {
504
+ placeholder: "symbol condition price",
505
+ kind: "text",
506
+ parse: (arg: string) => ({ symbol: arg.trim().toUpperCase() }),
507
+ },
508
+ category: "data",
509
+ wizardLayout: "form",
510
+ wizard: [
511
+ { key: "symbol", label: "Symbol", type: "text" },
512
+ {
513
+ key: "condition",
514
+ label: "Condition",
515
+ type: "select",
516
+ options: [{ label: "Above", value: "above" }],
517
+ },
518
+ { key: "price", label: "Target Price", type: "number" },
519
+ ],
520
+ execute: async () => {},
521
+ });
522
+ }}
523
+ />, {
524
+ width: 80,
525
+ height: 24,
526
+ });
527
+
528
+ await testSetup.renderOnce();
529
+
530
+ await act(async () => {
531
+ testSetup!.mockInput.pressEnter();
532
+ await Bun.sleep(0);
533
+ await testSetup!.renderOnce();
534
+ });
535
+
536
+ const workflowFrame = await waitForFrameToContain("Target Price");
537
+ expect(workflowFrame).toContain("AMD");
538
+ });
539
+
540
+ test("updates workflow select fields from the option picker", async () => {
541
+ testSetup = await testRender(<CommandBarHarness
542
+ query="SA AMD"
543
+ configurePluginRegistry={(pluginRegistry) => {
544
+ (pluginRegistry.commands as Map<string, any>).set("set-alert", {
545
+ id: "set-alert",
546
+ label: "Add Alert",
547
+ description: "Create a price alert from a symbol, condition, and target price",
548
+ keywords: ["add", "set", "alert", "price", "trigger"],
549
+ shortcut: "SA",
550
+ shortcutArg: {
551
+ placeholder: "symbol condition price",
552
+ kind: "text",
553
+ parse: (arg: string) => ({ symbol: arg.trim().toUpperCase() }),
554
+ },
555
+ category: "data",
556
+ wizardLayout: "form",
557
+ wizard: [
558
+ { key: "symbol", label: "Symbol", type: "text" },
559
+ {
560
+ key: "condition",
561
+ label: "Condition",
562
+ type: "select",
563
+ options: [
564
+ { label: "Above", value: "above" },
565
+ { label: "Below", value: "below" },
566
+ { label: "Crosses", value: "crosses" },
567
+ ],
568
+ },
569
+ { key: "price", label: "Target Price", type: "number" },
570
+ ],
571
+ execute: async () => {},
572
+ });
573
+ }}
574
+ />, {
575
+ width: 80,
576
+ height: 24,
577
+ });
578
+
579
+ await testSetup.renderOnce();
580
+ await act(async () => {
581
+ testSetup!.mockInput.pressEnter();
582
+ await Bun.sleep(0);
583
+ await testSetup!.renderOnce();
584
+ });
585
+ await waitForFrameToContain("Target Price");
586
+
587
+ await act(async () => {
588
+ testSetup!.mockInput.pressTab();
589
+ await testSetup!.renderOnce();
590
+ });
591
+ await act(async () => {
592
+ testSetup!.mockInput.pressEnter();
593
+ await testSetup!.renderOnce();
594
+ });
595
+ let frame = await waitForFrameToContain("Below");
596
+ expect(frame).toContain("Crosses");
597
+
598
+ await act(async () => {
599
+ testSetup!.mockInput.pressArrow("down");
600
+ await testSetup!.renderOnce();
601
+ });
602
+ await act(async () => {
603
+ testSetup!.mockInput.pressEnter();
604
+ await testSetup!.renderOnce();
605
+ });
606
+
607
+ frame = await waitForFrameToContain("Target Price");
608
+ expect(frame).toContain("Below");
609
+ });
610
+
611
+ test("opens plugin command workflows from a launch request", async () => {
612
+ testSetup = await testRender(<CommandBarHarness
613
+ query=""
614
+ configureState={(state) => ({
615
+ ...state,
616
+ commandBarLaunchRequest: {
617
+ kind: "plugin-command",
618
+ commandId: "set-alert",
619
+ sequence: 1,
620
+ },
621
+ })}
622
+ configurePluginRegistry={(pluginRegistry) => {
623
+ (pluginRegistry.commands as Map<string, any>).set("set-alert", {
624
+ id: "set-alert",
625
+ label: "Set Alert",
626
+ keywords: ["alert", "price", "trigger"],
627
+ shortcut: "SA",
628
+ category: "data",
629
+ wizardLayout: "form",
630
+ wizard: [
631
+ { key: "symbol", label: "Symbol", type: "text" },
632
+ {
633
+ key: "condition",
634
+ label: "Condition",
635
+ type: "select",
636
+ options: [
637
+ { label: "Above", value: "above" },
638
+ { label: "Below", value: "below" },
639
+ ],
640
+ },
641
+ { key: "price", label: "Target Price", type: "number" },
642
+ ],
643
+ execute: async () => {},
644
+ });
645
+ }}
646
+ />, {
647
+ width: 80,
648
+ height: 24,
649
+ });
650
+
651
+ const frame = await waitForFrameToContain("Target Price");
652
+ expect(frame).toContain("Set Alert");
653
+ expect(frame).toContain("Symbol");
654
+ expect(frame).toContain("Condition");
655
+ });
656
+
355
657
  test("renders theme prefix results in the root query", async () => {
356
658
  testSetup = await testRender(<CommandBarHarness query="TH " />, {
357
659
  width: 80,
@@ -659,11 +961,42 @@ describe("CommandBar", () => {
659
961
  expect(saved.at(-1)?.metadata.watchlists).toEqual(["watchlist"]);
660
962
  });
661
963
 
662
- test("bare AW without a compatible active target opens inline target selection", async () => {
964
+ test("bare AW without a compatible active target adds to the sole watchlist directly", async () => {
965
+ const saved: TickerRecord[] = [];
966
+
663
967
  testSetup = await testRender(
664
968
  <CommandBarHarness
665
969
  query="AW"
666
970
  selectedTicker="AAPL"
971
+ onSaveTicker={(ticker) => {
972
+ saved.push(ticker);
973
+ }}
974
+ />,
975
+ { width: 100, height: 20 },
976
+ );
977
+
978
+ await testSetup.renderOnce();
979
+ await act(async () => {
980
+ testSetup!.mockInput.pressEnter();
981
+ await Bun.sleep(0);
982
+ await testSetup!.renderOnce();
983
+ });
984
+
985
+ expect(saved.at(-1)?.metadata.watchlists).toEqual(["watchlist"]);
986
+ });
987
+
988
+ test("bare AW without a compatible active target opens inline target selection when multiple watchlists exist", async () => {
989
+ testSetup = await testRender(
990
+ <CommandBarHarness
991
+ query="AW"
992
+ selectedTicker="AAPL"
993
+ configureConfig={(config) => ({
994
+ ...config,
995
+ watchlists: [
996
+ { id: "watchlist", name: "Watchlist" },
997
+ { id: "growth", name: "Growth Radar" },
998
+ ],
999
+ })}
667
1000
  />,
668
1001
  { width: 100, height: 20 },
669
1002
  );
@@ -710,7 +1043,6 @@ describe("CommandBar", () => {
710
1043
 
711
1044
  const frame = testSetup.captureCharFrame();
712
1045
  expect(frame).toContain("Add AAPL to Portfolio");
713
- expect(frame).toContain('in "Research"');
714
1046
  });
715
1047
 
716
1048
  test("AP opens the add-to-portfolio workflow and prefills avg cost from the current price", async () => {
@@ -720,7 +1052,7 @@ describe("CommandBar", () => {
720
1052
  selectedTicker="AAPL"
721
1053
  configureConfig={(config) => ({
722
1054
  ...config,
723
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1055
+ portfolios: [{ id: "research", name: "Only Manual Portfolio", currency: "USD" }],
724
1056
  })}
725
1057
  configureState={(state) => ({
726
1058
  ...state,
@@ -762,6 +1094,7 @@ describe("CommandBar", () => {
762
1094
  expect(frame).toContain("Shares");
763
1095
  expect(frame).toContain("Avg Cost");
764
1096
  expect(frame).toContain("205.5");
1097
+ expect(frame).not.toContain("Only Manual Portfolio");
765
1098
  expectSingleBackControl(frame);
766
1099
  });
767
1100
 
@@ -873,7 +1206,6 @@ describe("CommandBar", () => {
873
1206
 
874
1207
  const frame = testSetup.captureCharFrame();
875
1208
  expect(frame).toContain("Set Position for AAPL");
876
- expect(frame).toContain('in "Research"');
877
1209
  });
878
1210
 
879
1211
  test("prefills the portfolio position workflow from the active manual portfolio and ticker", async () => {