gloomberb 0.3.7 → 0.4.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 (133) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +359 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +1 -1
  37. package/src/components/layout/pane-header.tsx +26 -2
  38. package/src/components/layout/shell.tsx +62 -50
  39. package/src/components/layout/status-bar.test.tsx +66 -0
  40. package/src/components/layout/status-bar.tsx +26 -5
  41. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  42. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  43. package/src/components/ticker-list-table.test.tsx +83 -0
  44. package/src/components/ticker-list-table.tsx +174 -97
  45. package/src/components/ui/data-table.tsx +27 -2
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/ui/page-stack-view.tsx +60 -0
  48. package/src/components/use-double-click-activation.ts +31 -0
  49. package/src/index.tsx +10 -10
  50. package/src/market-data/coordinator.test.ts +92 -4
  51. package/src/market-data/coordinator.ts +32 -5
  52. package/src/market-data/hooks.test.tsx +120 -0
  53. package/src/market-data/hooks.tsx +53 -16
  54. package/src/market-data/request-types.ts +4 -2
  55. package/src/market-data/selectors.ts +24 -58
  56. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  57. package/src/plugins/builtin/chat-controller.ts +310 -35
  58. package/src/plugins/builtin/chat.test.tsx +118 -6
  59. package/src/plugins/builtin/chat.tsx +175 -116
  60. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  61. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  62. package/src/plugins/builtin/news.tsx +1 -11
  63. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  64. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  65. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  66. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  67. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  68. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  69. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  70. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  71. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  72. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  73. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  74. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  75. package/src/plugins/builtin/sec.tsx +1 -11
  76. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  77. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  78. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  79. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  80. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  81. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  82. package/src/plugins/catalog.ts +65 -0
  83. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  84. package/src/plugins/ibkr/gateway-service.test.ts +45 -1
  85. package/src/plugins/ibkr/gateway-service.ts +141 -7
  86. package/src/plugins/pane-manager.test.ts +1 -1
  87. package/src/plugins/plugin-runtime.tsx +27 -7
  88. package/src/plugins/prediction-markets/cli.ts +28 -0
  89. package/src/plugins/prediction-markets/controller-data.ts +11 -3
  90. package/src/plugins/prediction-markets/controller.ts +141 -121
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/plugins/prediction-markets/launch.ts +51 -16
  93. package/src/plugins/prediction-markets/metrics.ts +9 -5
  94. package/src/plugins/prediction-markets/pane.test.tsx +119 -57
  95. package/src/plugins/prediction-markets/pane.tsx +74 -153
  96. package/src/plugins/prediction-markets/table.tsx +6 -3
  97. package/src/plugins/prediction-markets/types.ts +0 -1
  98. package/src/sources/gloomberb-cloud.test.ts +29 -0
  99. package/src/sources/gloomberb-cloud.ts +89 -1
  100. package/src/sources/provider-router.test.ts +265 -3
  101. package/src/sources/provider-router.ts +239 -11
  102. package/src/sources/yahoo-finance.test.ts +26 -0
  103. package/src/sources/yahoo-finance.ts +54 -3
  104. package/src/state/app-context-selector.test.tsx +99 -0
  105. package/src/state/app-context.test.ts +18 -0
  106. package/src/state/app-context.tsx +170 -65
  107. package/src/state/selectors.ts +25 -0
  108. package/src/theme/colors.ts +8 -8
  109. package/src/theme/themes.test.ts +2 -0
  110. package/src/theme/themes.ts +22 -10
  111. package/src/types/broker.ts +21 -0
  112. package/src/types/config.ts +17 -1
  113. package/src/types/data-provider.ts +18 -0
  114. package/src/types/financials.ts +31 -1
  115. package/src/types/plugin.ts +70 -0
  116. package/src/utils/api-client.test.ts +57 -0
  117. package/src/utils/api-client.ts +35 -12
  118. package/src/utils/back-navigation.ts +27 -0
  119. package/src/utils/format.test.ts +14 -0
  120. package/src/utils/format.ts +3 -3
  121. package/src/utils/market-status.ts +46 -1
  122. package/src/utils/quote-freshness.ts +127 -0
  123. package/src/utils/quote-resolution.test.ts +277 -0
  124. package/src/utils/quote-resolution.ts +641 -0
  125. package/src/utils/slugify.ts +3 -0
  126. package/src/utils/timestamp.ts +25 -0
  127. package/src/version.ts +1 -1
  128. package/src/cli/commands/portfolio.ts +0 -210
  129. package/src/cli/commands/watchlist.ts +0 -210
  130. package/src/plugins/builtin/data-providers.ts +0 -16
  131. package/src/sources/exchange-rates.ts +0 -18
  132. package/src/utils/ascii-chart.ts +0 -172
  133. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -46,12 +46,37 @@ async function clickFrameText(text: string): Promise<void> {
46
46
  });
47
47
  }
48
48
 
49
+ async function emitKeypress(
50
+ renderer: Awaited<ReturnType<typeof testRender>>,
51
+ event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; option?: boolean },
52
+ ): Promise<void> {
53
+ await act(async () => {
54
+ renderer.renderer.keyInput.emit("keypress", {
55
+ ctrl: false,
56
+ meta: false,
57
+ option: false,
58
+ shift: false,
59
+ eventType: "press",
60
+ repeated: false,
61
+ stopPropagation: () => {},
62
+ preventDefault: () => {},
63
+ ...event,
64
+ } as any);
65
+ await renderer.renderOnce();
66
+ });
67
+ }
68
+
69
+ async function renderFrames(count = 2): Promise<void> {
70
+ for (let index = 0; index < count; index += 1) {
71
+ await testSetup!.renderOnce();
72
+ }
73
+ }
74
+
49
75
  function expectSingleBackControl(frame: string): void {
50
76
  expect(frame.match(/\bBack\b/g)?.length ?? 0).toBe(1);
51
- expect(frame).not.toContain("esc");
52
77
  }
53
78
 
54
- function makeTicker(symbol: string, name: string): TickerRecord {
79
+ function makeTicker(symbol: string, name: string, overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
55
80
  return {
56
81
  metadata: {
57
82
  ticker: symbol,
@@ -63,6 +88,7 @@ function makeTicker(symbol: string, name: string): TickerRecord {
63
88
  positions: [],
64
89
  custom: {},
65
90
  tags: [],
91
+ ...overrides,
66
92
  },
67
93
  };
68
94
  }
@@ -399,6 +425,32 @@ describe("CommandBar", () => {
399
425
  expect(frame).toContain("theme:green");
400
426
  });
401
427
 
428
+ test("keeps the selected theme after pressing enter in root theme mode", async () => {
429
+ testSetup = await testRender(<CommandBarHarness query="TH " live />, {
430
+ width: 80,
431
+ height: 24,
432
+ });
433
+
434
+ await testSetup.renderOnce();
435
+ await act(async () => {
436
+ testSetup!.mockInput.pressArrow("down");
437
+ await testSetup!.renderOnce();
438
+ });
439
+ await testSetup.renderOnce();
440
+
441
+ await act(async () => {
442
+ testSetup!.mockInput.pressEnter();
443
+ await Bun.sleep(0);
444
+ await testSetup!.renderOnce();
445
+ });
446
+ await testSetup.renderOnce();
447
+
448
+ const frame = testSetup.captureCharFrame();
449
+ expect(frame).toContain("theme:green");
450
+ expect(frame).not.toContain("theme:amber");
451
+ expect(frame).not.toContain("Commands");
452
+ });
453
+
402
454
  test("keeps typed prefixes in the root query until a result is activated", async () => {
403
455
  testSetup = await testRender(<CommandBarHarness query="T " />, {
404
456
  width: 80,
@@ -628,6 +680,432 @@ describe("CommandBar", () => {
628
680
  expect(frame).toContain("Back");
629
681
  });
630
682
 
683
+ test("typing add still surfaces Add to Portfolio for a ticker already in the active manual portfolio", async () => {
684
+ testSetup = await testRender(
685
+ <CommandBarHarness
686
+ query="add"
687
+ selectedTicker="AAPL"
688
+ configureConfig={(config) => ({
689
+ ...config,
690
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
691
+ })}
692
+ configureState={(state) => ({
693
+ ...state,
694
+ paneState: {
695
+ ...state.paneState,
696
+ "portfolio-list:main": {
697
+ collectionId: "research",
698
+ cursorSymbol: "AAPL",
699
+ },
700
+ },
701
+ })}
702
+ extraTickers={[makeTicker("AAPL", "Apple Inc.", {
703
+ portfolios: ["research"],
704
+ })]}
705
+ />,
706
+ { width: 100, height: 20 },
707
+ );
708
+
709
+ await testSetup.renderOnce();
710
+
711
+ const frame = testSetup.captureCharFrame();
712
+ expect(frame).toContain("Add AAPL to Portfolio");
713
+ expect(frame).toContain('in "Research"');
714
+ });
715
+
716
+ test("AP opens the add-to-portfolio workflow and prefills avg cost from the current price", async () => {
717
+ testSetup = await testRender(
718
+ <CommandBarHarness
719
+ query="AP AAPL"
720
+ selectedTicker="AAPL"
721
+ configureConfig={(config) => ({
722
+ ...config,
723
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
724
+ })}
725
+ configureState={(state) => ({
726
+ ...state,
727
+ paneState: {
728
+ ...state.paneState,
729
+ "portfolio-list:main": {
730
+ collectionId: "research",
731
+ cursorSymbol: "AAPL",
732
+ },
733
+ },
734
+ financials: new Map([["AAPL", {
735
+ annualStatements: [],
736
+ quarterlyStatements: [],
737
+ priceHistory: [],
738
+ quote: {
739
+ symbol: "AAPL",
740
+ price: 205.5,
741
+ currency: "USD",
742
+ change: 1.25,
743
+ changePercent: 0.61,
744
+ lastUpdated: Date.now(),
745
+ },
746
+ }]]),
747
+ })}
748
+ extraTickers={[makeTicker("AAPL", "Apple Inc.", {
749
+ portfolios: ["research"],
750
+ })]}
751
+ />,
752
+ { width: 100, height: 30 },
753
+ );
754
+
755
+ await testSetup.renderOnce();
756
+ await act(async () => {
757
+ testSetup!.mockInput.pressEnter();
758
+ await testSetup!.renderOnce();
759
+ });
760
+
761
+ const frame = await waitForFrameToContain("Avg Cost");
762
+ expect(frame).toContain("Shares");
763
+ expect(frame).toContain("Avg Cost");
764
+ expect(frame).toContain("205.5");
765
+ expectSingleBackControl(frame);
766
+ });
767
+
768
+ test("add-to-portfolio can still add membership without entering a position", async () => {
769
+ const saved: TickerRecord[] = [];
770
+
771
+ testSetup = await testRender(
772
+ <CommandBarHarness
773
+ query="AP AAPL"
774
+ selectedTicker="AAPL"
775
+ onSaveTicker={(ticker) => {
776
+ saved.push(ticker);
777
+ }}
778
+ configureConfig={(config) => ({
779
+ ...config,
780
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
781
+ })}
782
+ configureState={(state) => ({
783
+ ...state,
784
+ paneState: {
785
+ ...state.paneState,
786
+ "portfolio-list:main": {
787
+ collectionId: "research",
788
+ cursorSymbol: "AAPL",
789
+ },
790
+ },
791
+ })}
792
+ />,
793
+ { width: 100, height: 30 },
794
+ );
795
+
796
+ await testSetup.renderOnce();
797
+ await act(async () => {
798
+ testSetup!.mockInput.pressEnter();
799
+ await testSetup!.renderOnce();
800
+ });
801
+
802
+ const frame = await waitForFrameToContain("Avg Cost");
803
+ expect(frame).toContain("Shares");
804
+ expect(frame).toContain("Avg Cost");
805
+
806
+ await clickFrameText("Add to Portfolio");
807
+ await act(async () => {
808
+ await Bun.sleep(0);
809
+ await testSetup!.renderOnce();
810
+ });
811
+
812
+ expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
813
+ expect(saved.at(-1)?.metadata.positions).toEqual([]);
814
+ });
815
+
816
+ test("only surfaces Set Portfolio Position when a manual portfolio exists", async () => {
817
+ testSetup = await testRender(
818
+ <CommandBarHarness query="Set Portfolio Position" />,
819
+ { width: 100, height: 20 },
820
+ );
821
+
822
+ await testSetup.renderOnce();
823
+ expect(testSetup.captureCharFrame()).toContain("Set Portfolio Position");
824
+
825
+ testSetup.renderer.destroy();
826
+ testSetup = await testRender(
827
+ <CommandBarHarness
828
+ query="Set Portfolio Position"
829
+ configureConfig={(config) => ({
830
+ ...config,
831
+ portfolios: [{
832
+ id: "broker:ibkr",
833
+ name: "IBKR Account",
834
+ currency: "USD",
835
+ brokerId: "ibkr",
836
+ brokerInstanceId: "ibkr-live",
837
+ }],
838
+ })}
839
+ />,
840
+ { width: 100, height: 20 },
841
+ );
842
+
843
+ await testSetup.renderOnce();
844
+ const frame = testSetup.captureCharFrame();
845
+ expect(frame).toContain('No matches for "Set Portfolio Position"');
846
+ expect(frame).not.toContain("Create or update a manual position in a portfolio");
847
+ });
848
+
849
+ test("matches set portfolio position when searching edit position", async () => {
850
+ testSetup = await testRender(
851
+ <CommandBarHarness
852
+ query="edit position"
853
+ selectedTicker="AAPL"
854
+ configureConfig={(config) => ({
855
+ ...config,
856
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
857
+ })}
858
+ configureState={(state) => ({
859
+ ...state,
860
+ paneState: {
861
+ ...state.paneState,
862
+ "portfolio-list:main": {
863
+ collectionId: "research",
864
+ cursorSymbol: "AAPL",
865
+ },
866
+ },
867
+ })}
868
+ />,
869
+ { width: 100, height: 20 },
870
+ );
871
+
872
+ await testSetup.renderOnce();
873
+
874
+ const frame = testSetup.captureCharFrame();
875
+ expect(frame).toContain("Set Position for AAPL");
876
+ expect(frame).toContain('in "Research"');
877
+ });
878
+
879
+ test("prefills the portfolio position workflow from the active manual portfolio and ticker", async () => {
880
+ testSetup = await testRender(
881
+ <CommandBarHarness
882
+ query="Set Position for AAPL"
883
+ selectedTicker="AAPL"
884
+ configureConfig={(config) => ({
885
+ ...config,
886
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
887
+ })}
888
+ configureState={(state) => ({
889
+ ...state,
890
+ paneState: {
891
+ ...state.paneState,
892
+ "portfolio-list:main": {
893
+ collectionId: "research",
894
+ cursorSymbol: "AAPL",
895
+ },
896
+ },
897
+ })}
898
+ extraTickers={[makeTicker("AAPL", "Apple Inc.", {
899
+ portfolios: ["research"],
900
+ positions: [{
901
+ portfolio: "research",
902
+ shares: 10,
903
+ avgCost: 180,
904
+ currency: "USD",
905
+ broker: "manual",
906
+ }],
907
+ })]}
908
+ />,
909
+ { width: 100, height: 30 },
910
+ );
911
+
912
+ await testSetup.renderOnce();
913
+ await act(async () => {
914
+ testSetup!.mockInput.pressEnter();
915
+ await testSetup!.renderOnce();
916
+ });
917
+
918
+ const frame = await waitForFrameToContain("Avg Cost");
919
+ expect(frame).toContain("Research");
920
+ expect(frame).toContain("AAPL");
921
+ expect(frame).toContain("10");
922
+ expect(frame).toContain("180");
923
+ expectSingleBackControl(frame);
924
+ });
925
+
926
+ test("submits the portfolio position workflow and persists a manual position", async () => {
927
+ const saved: TickerRecord[] = [];
928
+
929
+ testSetup = await testRender(
930
+ <CommandBarHarness
931
+ query="Set Position for AAPL"
932
+ selectedTicker="AAPL"
933
+ onSaveTicker={(ticker) => {
934
+ saved.push(ticker);
935
+ }}
936
+ configureConfig={(config) => ({
937
+ ...config,
938
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
939
+ })}
940
+ configureState={(state) => ({
941
+ ...state,
942
+ paneState: {
943
+ ...state.paneState,
944
+ "portfolio-list:main": {
945
+ collectionId: "research",
946
+ cursorSymbol: "AAPL",
947
+ },
948
+ },
949
+ })}
950
+ />,
951
+ { width: 100, height: 30 },
952
+ );
953
+
954
+ await testSetup.renderOnce();
955
+ await act(async () => {
956
+ testSetup!.mockInput.pressEnter();
957
+ await testSetup!.renderOnce();
958
+ });
959
+
960
+ await act(async () => {
961
+ testSetup!.mockInput.pressTab();
962
+ await testSetup!.renderOnce();
963
+ });
964
+ await act(async () => {
965
+ testSetup!.mockInput.pressTab();
966
+ await testSetup!.renderOnce();
967
+ });
968
+ await act(async () => {
969
+ await testSetup!.mockInput.typeText("10");
970
+ await testSetup!.renderOnce();
971
+ });
972
+ await act(async () => {
973
+ testSetup!.mockInput.pressTab();
974
+ await testSetup!.renderOnce();
975
+ });
976
+ await act(async () => {
977
+ await testSetup!.mockInput.typeText("180");
978
+ await testSetup!.renderOnce();
979
+ });
980
+ await act(async () => {
981
+ testSetup!.mockInput.pressTab();
982
+ await testSetup!.renderOnce();
983
+ });
984
+ await act(async () => {
985
+ await testSetup!.mockInput.typeText("EUR");
986
+ await testSetup!.renderOnce();
987
+ });
988
+ await act(async () => {
989
+ testSetup!.mockInput.pressEnter();
990
+ await Bun.sleep(0);
991
+ await testSetup!.renderOnce();
992
+ });
993
+
994
+ expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
995
+ expect(saved.at(-1)?.metadata.positions).toEqual([{
996
+ portfolio: "research",
997
+ shares: 10,
998
+ avgCost: 180,
999
+ currency: "EUR",
1000
+ broker: "manual",
1001
+ }]);
1002
+ });
1003
+
1004
+ test("removing a ticker from a manual portfolio also removes its position", async () => {
1005
+ const saved: TickerRecord[] = [];
1006
+
1007
+ testSetup = await testRender(
1008
+ <CommandBarHarness
1009
+ query="RP AAPL"
1010
+ selectedTicker="AAPL"
1011
+ onSaveTicker={(ticker) => {
1012
+ saved.push(ticker);
1013
+ }}
1014
+ configureConfig={(config) => ({
1015
+ ...config,
1016
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1017
+ })}
1018
+ configureState={(state) => ({
1019
+ ...state,
1020
+ paneState: {
1021
+ ...state.paneState,
1022
+ "portfolio-list:main": {
1023
+ collectionId: "research",
1024
+ cursorSymbol: "AAPL",
1025
+ },
1026
+ },
1027
+ })}
1028
+ extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1029
+ portfolios: ["research"],
1030
+ positions: [{
1031
+ portfolio: "research",
1032
+ shares: 4,
1033
+ avgCost: 175,
1034
+ currency: "USD",
1035
+ broker: "manual",
1036
+ }],
1037
+ })]}
1038
+ />,
1039
+ { width: 100, height: 20 },
1040
+ );
1041
+
1042
+ await testSetup.renderOnce();
1043
+ await act(async () => {
1044
+ testSetup!.mockInput.pressEnter();
1045
+ await Bun.sleep(0);
1046
+ await testSetup!.renderOnce();
1047
+ });
1048
+
1049
+ expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
1050
+ expect(saved.at(-1)?.metadata.positions).toEqual([]);
1051
+ });
1052
+
1053
+ test("deleting a manual portfolio also cleans saved ticker positions", async () => {
1054
+ const saved: TickerRecord[] = [];
1055
+
1056
+ testSetup = await testRender(
1057
+ <CommandBarHarness
1058
+ query="Delete Portfolio"
1059
+ onSaveTicker={(ticker) => {
1060
+ saved.push(ticker);
1061
+ }}
1062
+ configureConfig={(config) => ({
1063
+ ...config,
1064
+ portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1065
+ })}
1066
+ configureState={(state) => ({
1067
+ ...state,
1068
+ paneState: {
1069
+ ...state.paneState,
1070
+ "portfolio-list:main": {
1071
+ collectionId: "research",
1072
+ cursorSymbol: "AAPL",
1073
+ },
1074
+ },
1075
+ })}
1076
+ extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1077
+ portfolios: ["research"],
1078
+ positions: [{
1079
+ portfolio: "research",
1080
+ shares: 2,
1081
+ avgCost: 160,
1082
+ currency: "USD",
1083
+ broker: "manual",
1084
+ }],
1085
+ })]}
1086
+ />,
1087
+ { width: 100, height: 20 },
1088
+ );
1089
+
1090
+ await testSetup.renderOnce();
1091
+ await act(async () => {
1092
+ testSetup!.mockInput.pressEnter();
1093
+ await testSetup!.renderOnce();
1094
+ });
1095
+ await act(async () => {
1096
+ testSetup!.mockInput.pressEnter();
1097
+ await testSetup!.renderOnce();
1098
+ });
1099
+ await act(async () => {
1100
+ testSetup!.mockInput.pressEnter();
1101
+ await Bun.sleep(0);
1102
+ await testSetup!.renderOnce();
1103
+ });
1104
+
1105
+ expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
1106
+ expect(saved.at(-1)?.metadata.positions).toEqual([]);
1107
+ });
1108
+
631
1109
  test("pressing enter after arrow navigation ignores stale mouse hover", async () => {
632
1110
  const pinned: string[] = [];
633
1111
 
@@ -718,6 +1196,41 @@ describe("CommandBar", () => {
718
1196
  expect(executed).toEqual(["delete-layout"]);
719
1197
  });
720
1198
 
1199
+ test("uses backspace to leave confirm routes", async () => {
1200
+ testSetup = await testRender(
1201
+ <CommandBarHarness
1202
+ query="delete"
1203
+ configurePluginRegistry={(pluginRegistry) => {
1204
+ (pluginRegistry.commands as Map<string, any>).set("delete-layout", {
1205
+ id: "delete-layout",
1206
+ label: "Delete Layout",
1207
+ description: "Delete the current layout preset",
1208
+ category: "Layout Manager",
1209
+ execute: async () => {},
1210
+ });
1211
+ }}
1212
+ />,
1213
+ { width: 100, height: 20 },
1214
+ );
1215
+
1216
+ await testSetup.renderOnce();
1217
+ await act(async () => {
1218
+ testSetup!.mockInput.pressEnter();
1219
+ await testSetup!.renderOnce();
1220
+ });
1221
+ await renderFrames();
1222
+
1223
+ await act(async () => {
1224
+ testSetup!.mockInput.pressBackspace();
1225
+ await testSetup!.renderOnce();
1226
+ });
1227
+ await renderFrames();
1228
+
1229
+ const frame = testSetup.captureCharFrame();
1230
+ expect(frame).toContain("Delete Layout");
1231
+ expect(frame).not.toContain("Back Delete Layout");
1232
+ });
1233
+
721
1234
  test("moves through long result lists with the mouse wheel", async () => {
722
1235
  testSetup = await testRender(
723
1236
  <CommandBarHarness
@@ -848,8 +1361,8 @@ describe("CommandBar", () => {
848
1361
  await testSetup.renderOnce();
849
1362
 
850
1363
  const frame = testSetup.captureCharFrame();
851
- expect(frame).toContain("New Chat Pane");
852
- expect(frame).toContain("float");
1364
+ expect(frame).toContain("Chat");
1365
+ expect(frame).not.toContain("float");
853
1366
  });
854
1367
 
855
1368
  test("shows pane shortcuts in the default browse results", async () => {
@@ -949,6 +1462,48 @@ describe("CommandBar", () => {
949
1462
  });
950
1463
  });
951
1464
 
1465
+ test("does not treat backspace as Back inside workflow text fields", async () => {
1466
+ testSetup = await testRender(<CommandBarHarness
1467
+ query="Plugin Login"
1468
+ configurePluginRegistry={(pluginRegistry) => {
1469
+ (pluginRegistry.commands as Map<string, any>).set("plugin:login", {
1470
+ id: "plugin:login",
1471
+ label: "Plugin Login",
1472
+ description: "Authenticate without leaving the command bar",
1473
+ category: "config",
1474
+ wizard: [
1475
+ { key: "username", label: "Username", type: "text", placeholder: "vince" },
1476
+ ],
1477
+ execute: async () => {},
1478
+ } as any);
1479
+ pluginRegistry.getCommandPluginId = () => "notes";
1480
+ }}
1481
+ />, {
1482
+ width: 100,
1483
+ height: 24,
1484
+ });
1485
+
1486
+ await testSetup.renderOnce();
1487
+ await act(async () => {
1488
+ testSetup!.mockInput.pressEnter();
1489
+ await testSetup!.renderOnce();
1490
+ });
1491
+
1492
+ await act(async () => {
1493
+ await testSetup!.mockInput.typeText("vince");
1494
+ await testSetup!.renderOnce();
1495
+ });
1496
+ await act(async () => {
1497
+ testSetup!.mockInput.pressBackspace();
1498
+ await testSetup!.renderOnce();
1499
+ });
1500
+
1501
+ const frame = testSetup.captureCharFrame();
1502
+ expect(frame).toContain("Plugin Login");
1503
+ expect(frame).toContain("vinc");
1504
+ expect(frame).toContain("Back");
1505
+ });
1506
+
952
1507
  test("QQ MSFT executes directly without opening a secondary workflow", async () => {
953
1508
  const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
954
1509
 
@@ -1283,6 +1838,96 @@ describe("CommandBar", () => {
1283
1838
  }]);
1284
1839
  });
1285
1840
 
1841
+ test("uses backspace as back only when a pane-settings route query is empty", async () => {
1842
+ testSetup = await testRender(<CommandBarHarness
1843
+ query="PS"
1844
+ configureState={(state) => ({
1845
+ ...state,
1846
+ focusedPaneId: "quote-monitor:main",
1847
+ })}
1848
+ hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
1849
+ configurePluginRegistry={(pluginRegistry) => {
1850
+ pluginRegistry.resolvePaneSettings = () => ({
1851
+ paneId: "quote-monitor:main",
1852
+ pane: {
1853
+ instanceId: "quote-monitor:main",
1854
+ paneId: "quote-monitor",
1855
+ title: "Quote Monitor",
1856
+ settings: {},
1857
+ },
1858
+ paneDef: pluginRegistry.panes.get("quote-monitor")!,
1859
+ settingsDef: {
1860
+ title: "Quote Monitor Settings",
1861
+ fields: [{
1862
+ key: "symbol",
1863
+ label: "Symbol",
1864
+ type: "text",
1865
+ description: "Ticker symbol to track",
1866
+ }],
1867
+ },
1868
+ context: {
1869
+ config: createDefaultConfig("/tmp/gloomberb-test"),
1870
+ layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
1871
+ paneId: "quote-monitor:main",
1872
+ paneType: "quote-monitor",
1873
+ pane: {
1874
+ instanceId: "quote-monitor:main",
1875
+ paneId: "quote-monitor",
1876
+ title: "Quote Monitor",
1877
+ settings: {},
1878
+ },
1879
+ settings: {},
1880
+ paneState: {},
1881
+ activeTicker: "AAPL",
1882
+ activeCollectionId: "main",
1883
+ },
1884
+ }) as any;
1885
+ }}
1886
+ />, {
1887
+ width: 100,
1888
+ height: 20,
1889
+ });
1890
+
1891
+ await testSetup.renderOnce();
1892
+
1893
+ await act(async () => {
1894
+ testSetup!.mockInput.pressEnter();
1895
+ await testSetup!.renderOnce();
1896
+ });
1897
+ await renderFrames();
1898
+
1899
+ let frame = testSetup.captureCharFrame();
1900
+ expect(frame).toContain("Quote Monitor Settings");
1901
+
1902
+ await act(async () => {
1903
+ await testSetup!.mockInput.typeText("s");
1904
+ await testSetup!.renderOnce();
1905
+ });
1906
+
1907
+ frame = testSetup.captureCharFrame();
1908
+ expect(frame).toContain("Quote Monitor Settings");
1909
+
1910
+ await emitKeypress(testSetup, { name: "backspace", sequence: "\b" });
1911
+
1912
+ frame = testSetup.captureCharFrame();
1913
+ expect(frame).toContain("Quote Monitor Settings");
1914
+
1915
+ await act(async () => {
1916
+ testSetup!.mockInput.pressBackspace();
1917
+ await testSetup!.renderOnce();
1918
+ });
1919
+ await renderFrames();
1920
+ await act(async () => {
1921
+ testSetup!.mockInput.pressBackspace();
1922
+ await testSetup!.renderOnce();
1923
+ });
1924
+ await renderFrames();
1925
+
1926
+ frame = testSetup.captureCharFrame();
1927
+ expect(frame).not.toContain("Quote Monitor Settings");
1928
+ expect(frame).not.toContain("Back Pane Settings");
1929
+ });
1930
+
1286
1931
  test("renders pane-setting multi-select pickers only once inside the command bar", async () => {
1287
1932
  testSetup = await testRender(<CommandBarHarness
1288
1933
  query="PS"
@@ -1377,7 +2022,7 @@ describe("CommandBar", () => {
1377
2022
  await testSetup.renderOnce();
1378
2023
 
1379
2024
  const frame = testSetup.captureCharFrame();
1380
- expect(frame).toContain("New IBKR Trading Pane");
2025
+ expect(frame).toContain("IBKR Trading");
1381
2026
  expect(frame).toContain("IBKR");
1382
2027
  });
1383
2028