gloomberb 0.7.2 → 0.7.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 (129) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +49 -15
  7. package/src/components/chart/chart-types.ts +2 -2
  8. package/src/components/chart/chart.test.ts +27 -8
  9. package/src/components/chart/comparison-stock-chart.tsx +84 -121
  10. package/src/components/chart/native/chart-rasterizer.ts +43 -17
  11. package/src/components/chart/price-axis-labels.tsx +25 -9
  12. package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
  13. package/src/components/chart/stock-chart.tsx +59 -41
  14. package/src/components/command-bar/command-bar.test.tsx +446 -120
  15. package/src/components/command-bar/command-bar.tsx +528 -383
  16. package/src/components/command-bar/command-registry.ts +16 -6
  17. package/src/components/command-bar/root-shortcuts.ts +4 -5
  18. package/src/components/command-bar/view-model.test.ts +1 -0
  19. package/src/components/command-bar/view-model.ts +2 -1
  20. package/src/components/command-bar/workflow-ops.ts +11 -32
  21. package/src/components/data-table-view.test.tsx +24 -1
  22. package/src/components/data-table-view.tsx +3 -2
  23. package/src/components/feed-data-table-stack-view.tsx +3 -2
  24. package/src/components/layout/detached-pane-shell.tsx +2 -2
  25. package/src/components/layout/floating-pane.tsx +9 -2
  26. package/src/components/layout/pane-footer.tsx +1 -1
  27. package/src/components/layout/pane-header.tsx +4 -2
  28. package/src/components/layout/pane-sizing.ts +12 -0
  29. package/src/components/layout/pane.tsx +24 -2
  30. package/src/components/layout/shell.tsx +24 -10
  31. package/src/components/layout/status-bar.test.tsx +4 -1
  32. package/src/components/layout/status-bar.tsx +4 -24
  33. package/src/components/markdown-text.tsx +53 -8
  34. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  35. package/src/components/pane-settings-dialog.tsx +9 -6
  36. package/src/components/pane-template-wizard.tsx +3 -2
  37. package/src/components/price-selector-dialog.tsx +3 -2
  38. package/src/components/price-sparkline-view.tsx +463 -0
  39. package/src/components/price-sparkline.ts +46 -0
  40. package/src/components/table-view-shared.tsx +21 -3
  41. package/src/components/ticker-list-table-view.tsx +3 -2
  42. package/src/components/ticker-list-table.tsx +11 -51
  43. package/src/components/toggle-list.tsx +3 -3
  44. package/src/components/ui/choice-dialog.tsx +3 -2
  45. package/src/components/ui/multi-select-dialog.tsx +3 -2
  46. package/src/components/ui/native-select.tsx +3 -3
  47. package/src/core/state/app-state.ts +210 -80
  48. package/src/data/config-store-node.ts +113 -25
  49. package/src/data/config-store.test.ts +65 -3
  50. package/src/data/plugin-state-store.ts +39 -13
  51. package/src/data/sqlite-cache.test.ts +13 -0
  52. package/src/market-data/coordinator.ts +8 -1
  53. package/src/news/aggregator.test.ts +41 -0
  54. package/src/news/aggregator.ts +61 -9
  55. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  56. package/src/plugins/builtin/ai/settings.ts +3 -0
  57. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  58. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  59. package/src/plugins/builtin/chat-controller.ts +258 -44
  60. package/src/plugins/builtin/chat.test.tsx +257 -6
  61. package/src/plugins/builtin/chat.tsx +225 -60
  62. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  63. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  64. package/src/plugins/builtin/correlation/colors.ts +87 -0
  65. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  66. package/src/plugins/builtin/correlation/index.tsx +92 -21
  67. package/src/plugins/builtin/debug.tsx +4 -3
  68. package/src/plugins/builtin/econ/index.tsx +3 -2
  69. package/src/plugins/builtin/help.test.tsx +2 -2
  70. package/src/plugins/builtin/holders/index.tsx +25 -13
  71. package/src/plugins/builtin/layout-manager.tsx +1 -7
  72. package/src/plugins/builtin/news-wire/index.tsx +1 -1
  73. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  74. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  75. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  76. package/src/plugins/builtin/options.tsx +9 -17
  77. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  78. package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
  79. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  80. package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
  81. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  82. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  83. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  84. package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
  85. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  86. package/src/plugins/builtin/sectors/index.tsx +252 -41
  87. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  88. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  89. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  90. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  91. package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
  92. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
  93. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  94. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  95. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  96. package/src/plugins/plugin-runtime.tsx +2 -2
  97. package/src/plugins/prediction-markets/settings.ts +4 -0
  98. package/src/plugins/registry.test.ts +55 -0
  99. package/src/plugins/registry.ts +11 -4
  100. package/src/plugins/ticker-navigation.test.ts +35 -5
  101. package/src/plugins/ticker-navigation.ts +16 -22
  102. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  103. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  104. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  105. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  106. package/src/renderers/electrobun/bun/index.ts +52 -13
  107. package/src/renderers/electrobun/view/app-services.ts +51 -9
  108. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  109. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  110. package/src/renderers/electrobun/view/main.tsx +0 -16
  111. package/src/renderers/electrobun/view/styles.css +23 -21
  112. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  113. package/src/renderers/opentui/host.tsx +2 -1
  114. package/src/renderers/opentui/start.tsx +1 -1
  115. package/src/renderers/opentui/test-utils.tsx +6 -5
  116. package/src/state/app-bootstrap.ts +3 -3
  117. package/src/state/app-context.test.ts +113 -0
  118. package/src/state/app-context.tsx +8 -4
  119. package/src/theme/colors.ts +13 -14
  120. package/src/types/config.ts +9 -4
  121. package/src/types/plugin.ts +8 -1
  122. package/src/ui/context-menu.tsx +4 -4
  123. package/src/utils/api-client.test.ts +128 -0
  124. package/src/utils/api-client.ts +112 -7
  125. package/src/utils/fuzzy-search.ts +12 -1
  126. package/src/utils/keyboard.ts +31 -0
  127. package/src/utils/ticker-search.test.ts +2 -2
  128. package/src/utils/ticker-search.ts +48 -1
  129. package/src/version.ts +1 -1
@@ -169,11 +169,33 @@ function defaultPaneStateForInstance(config: AppConfig, instance: PaneInstanceCo
169
169
  return {};
170
170
  }
171
171
 
172
- function reconcilePaneState(config: AppConfig, previous: Record<string, PaneRuntimeState>): Record<string, PaneRuntimeState> {
172
+ function cloneRuntimeValue<T>(value: T): T {
173
+ if (Array.isArray(value)) {
174
+ return value.map((entry) => cloneRuntimeValue(entry)) as T;
175
+ }
176
+ if (value && typeof value === "object") {
177
+ return Object.fromEntries(
178
+ Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, cloneRuntimeValue(entry)]),
179
+ ) as T;
180
+ }
181
+ return value;
182
+ }
183
+
184
+ export function clonePaneStateMap(previous: Record<string, Record<string, unknown>>): Record<string, PaneRuntimeState> {
185
+ return Object.fromEntries(
186
+ Object.entries(previous).map(([paneId, paneState]) => [paneId, cloneRuntimeValue(paneState) as PaneRuntimeState]),
187
+ );
188
+ }
189
+
190
+ function reconcilePaneState(
191
+ config: AppConfig,
192
+ previous: Record<string, PaneRuntimeState>,
193
+ layout: LayoutConfig = config.layout,
194
+ ): Record<string, PaneRuntimeState> {
173
195
  const next: Record<string, PaneRuntimeState> = {};
174
- for (const instance of config.layout.instances) {
196
+ for (const instance of layout.instances) {
175
197
  const defaults = defaultPaneStateForInstance(config, instance);
176
- const paneState = { ...defaults, ...(previous[instance.instanceId] ?? {}) };
198
+ const paneState = { ...defaults, ...cloneRuntimeValue(previous[instance.instanceId] ?? {}) };
177
199
  if (
178
200
  instance.paneId === "portfolio-list"
179
201
  && !isKnownCollection(config, paneState.collectionId as string | undefined)
@@ -286,10 +308,56 @@ function nextRecentTickers(current: string[], symbol: string | null): string[] {
286
308
  return next;
287
309
  }
288
310
 
289
- function syncLayouts(layouts: SavedLayout[], activeLayoutIndex: number, layout: LayoutConfig): SavedLayout[] {
290
- return layouts.map((savedLayout, index) => (
291
- index === activeLayoutIndex ? { ...savedLayout, layout: cloneLayout(layout) } : savedLayout
292
- ));
311
+ function cloneSavedLayout(entry: SavedLayout): SavedLayout {
312
+ return {
313
+ ...entry,
314
+ layout: cloneLayout(entry.layout),
315
+ paneState: entry.paneState ? clonePaneStateMap(entry.paneState) : entry.paneState,
316
+ };
317
+ }
318
+
319
+ function buildSavedLayoutSnapshot(
320
+ entry: SavedLayout | undefined,
321
+ layout: LayoutConfig,
322
+ paneState: Record<string, PaneRuntimeState>,
323
+ focusedPaneId: string | null,
324
+ activePanel: "left" | "right",
325
+ ): SavedLayout {
326
+ return {
327
+ ...(entry ?? { name: "Default" }),
328
+ layout: cloneLayout(layout),
329
+ paneState: clonePaneStateMap(paneState),
330
+ focusedPaneId,
331
+ activePanel,
332
+ };
333
+ }
334
+
335
+ export function syncConfigActiveLayoutState(
336
+ config: AppConfig,
337
+ paneState: Record<string, PaneRuntimeState>,
338
+ focusedPaneId: string | null,
339
+ activePanel: "left" | "right",
340
+ ): AppConfig {
341
+ const activeLayoutIndex = config.activeLayoutIndex >= 0 && config.activeLayoutIndex < config.layouts.length
342
+ ? config.activeLayoutIndex
343
+ : 0;
344
+ const layouts = config.layouts.length > 0
345
+ ? config.layouts.map((savedLayout, index) => (
346
+ index === activeLayoutIndex
347
+ ? buildSavedLayoutSnapshot(savedLayout, config.layout, reconcilePaneState(config, paneState), focusedPaneId, activePanel)
348
+ : cloneSavedLayout(savedLayout)
349
+ ))
350
+ : [buildSavedLayoutSnapshot(undefined, config.layout, reconcilePaneState(config, paneState), focusedPaneId, activePanel)];
351
+ return {
352
+ ...config,
353
+ layouts,
354
+ activeLayoutIndex,
355
+ };
356
+ }
357
+
358
+ function getActiveSavedPaneState(config: AppConfig): Record<string, PaneRuntimeState> | null {
359
+ const paneState = config.layouts[config.activeLayoutIndex]?.paneState;
360
+ return paneState ? clonePaneStateMap(paneState) : null;
293
361
  }
294
362
 
295
363
  const PANEL_RESOLUTION_BOUNDS = { x: 0, y: 0, width: 120, height: 40 };
@@ -393,9 +461,7 @@ function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfi
393
461
 
394
462
  function focusPaneState(state: AppState, paneId: string): AppState {
395
463
  const layout = bringFloatingToFront(state.config.layout, paneId);
396
- const config = layout !== state.config.layout
397
- ? { ...state.config, layout, layouts: syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout) }
398
- : state.config;
464
+ const config = layout !== state.config.layout ? { ...state.config, layout } : state.config;
399
465
  const recentTickers = nextRecentTickers(
400
466
  state.recentTickers,
401
467
  resolveTickerForPane(state, paneId),
@@ -409,41 +475,44 @@ function focusPaneState(state: AppState, paneId: string): AppState {
409
475
  }
410
476
  return {
411
477
  ...state,
412
- config,
478
+ config: syncConfigActiveLayoutState(config, state.paneState, paneId, state.activePanel),
413
479
  focusedPaneId: paneId,
414
480
  recentTickers,
415
481
  };
416
482
  }
417
483
 
418
- function bringFocusedFloatingPaneToFront(config: AppConfig, focusedPaneId: string | null): AppConfig {
419
- if (!focusedPaneId) return config;
420
- const layout = bringFloatingToFront(config.layout, focusedPaneId);
421
- if (layout === config.layout) return config;
422
- return {
423
- ...config,
424
- layout,
425
- layouts: syncLayouts(config.layouts, config.activeLayoutIndex, layout),
426
- };
427
- }
428
-
429
- function withFocusedPane(state: AppState, config: AppConfig): AppState {
484
+ function withFocusedPane(
485
+ state: AppState,
486
+ config: AppConfig,
487
+ options: {
488
+ paneState?: Record<string, PaneRuntimeState>;
489
+ focusedPaneId?: string | null;
490
+ activePanel?: "left" | "right";
491
+ } = {},
492
+ ): AppState {
430
493
  const normalizedLayout = normalizePaneLayout(config.layout);
431
494
  const nextConfig = normalizedLayout === config.layout
432
495
  ? config
433
496
  : {
434
497
  ...config,
435
498
  layout: normalizedLayout,
436
- layouts: syncLayouts(config.layouts, config.activeLayoutIndex, normalizedLayout),
437
- };
438
- const nextPaneState = reconcilePaneState(nextConfig, state.paneState);
439
- const focusedPaneId = resolveFocusedPaneId(nextConfig.layout, state.focusedPaneId);
440
- const focusedConfig = bringFocusedFloatingPaneToFront(nextConfig, focusedPaneId);
499
+ };
500
+ const activePanel = options.activePanel ?? state.activePanel;
501
+ const nextPaneState = reconcilePaneState(nextConfig, options.paneState ?? state.paneState);
502
+ const requestedFocusedPaneId = Object.prototype.hasOwnProperty.call(options, "focusedPaneId")
503
+ ? (options.focusedPaneId ?? null)
504
+ : state.focusedPaneId;
505
+ const focusedPaneId = resolveFocusedPaneId(nextConfig.layout, requestedFocusedPaneId);
506
+ const focusedLayout = focusedPaneId ? bringFloatingToFront(nextConfig.layout, focusedPaneId) : nextConfig.layout;
507
+ const focusedConfig = focusedLayout === nextConfig.layout ? nextConfig : { ...nextConfig, layout: focusedLayout };
508
+ const syncedConfig = syncConfigActiveLayoutState(focusedConfig, nextPaneState, focusedPaneId, activePanel);
441
509
  return {
442
510
  ...state,
443
- config: focusedConfig,
511
+ config: syncedConfig,
444
512
  paneState: nextPaneState,
445
- brokerAccounts: reconcileBrokerAccounts(focusedConfig, state.brokerAccounts),
513
+ brokerAccounts: reconcileBrokerAccounts(syncedConfig, state.brokerAccounts),
446
514
  focusedPaneId,
515
+ activePanel,
447
516
  };
448
517
  }
449
518
 
@@ -465,7 +534,16 @@ function hydrateDesktopSnapshot(state: AppState, snapshot: DesktopSharedStateSna
465
534
  export function appReducer(state: AppState, action: AppAction): AppState {
466
535
  switch (action.type) {
467
536
  case "SET_CONFIG":
468
- return withFocusedPane({ ...state, themePreview: null, layoutHistory: {} }, action.config);
537
+ return withFocusedPane(
538
+ { ...state, themePreview: null, layoutHistory: {} },
539
+ action.config,
540
+ {
541
+ paneState: {
542
+ ...state.paneState,
543
+ ...(getActiveSavedPaneState(action.config) ?? {}),
544
+ },
545
+ },
546
+ );
469
547
 
470
548
  case "SET_TICKERS":
471
549
  return { ...state, tickers: action.tickers };
@@ -488,15 +566,13 @@ export function appReducer(state: AppState, action: AppAction): AppState {
488
566
  : currentState;
489
567
  }
490
568
  const layout = clearTickerBindings(state.config.layout, action.symbol);
491
- const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout);
492
- return {
569
+ return withFocusedPane({
493
570
  ...state,
494
571
  tickers,
495
572
  financials,
496
573
  paneState,
497
574
  recentTickers: state.recentTickers.filter((symbol) => symbol !== action.symbol),
498
- config: { ...state.config, layout, layouts },
499
- };
575
+ }, { ...state.config, layout }, { paneState });
500
576
  }
501
577
 
502
578
  case "SET_FINANCIALS": {
@@ -526,11 +602,10 @@ export function appReducer(state: AppState, action: AppAction): AppState {
526
602
 
527
603
  case "SET_ACTIVE_PANEL": {
528
604
  const firstPane = getPanelFocusTarget(state.config.layout, action.panel);
529
- return {
530
- ...state,
605
+ return withFocusedPane(state, state.config, {
531
606
  activePanel: action.panel,
532
607
  focusedPaneId: firstPane ?? state.focusedPaneId,
533
- };
608
+ });
534
609
  }
535
610
 
536
611
  case "TOGGLE_COMMAND_BAR":
@@ -658,14 +733,12 @@ export function appReducer(state: AppState, action: AppAction): AppState {
658
733
  const target = entry.past[entry.past.length - 1]!;
659
734
  entry.past = entry.past.slice(0, -1);
660
735
  entry.future = [cloneLayout(state.config.layout), ...entry.future].slice(0, MAX_LAYOUT_HISTORY);
661
- const layouts = syncLayouts(state.config.layouts, currentIndex, target);
662
736
  return withFocusedPane({
663
737
  ...state,
664
738
  layoutHistory: setHistoryForIndex(state.layoutHistory, currentIndex, entry),
665
739
  }, {
666
740
  ...state.config,
667
741
  layout: cloneLayout(target),
668
- layouts,
669
742
  });
670
743
  }
671
744
 
@@ -676,54 +749,76 @@ export function appReducer(state: AppState, action: AppAction): AppState {
676
749
  const target = entry.future[0]!;
677
750
  entry.future = entry.future.slice(1);
678
751
  entry.past = [...entry.past, cloneLayout(state.config.layout)].slice(-MAX_LAYOUT_HISTORY);
679
- const layouts = syncLayouts(state.config.layouts, currentIndex, target);
680
752
  return withFocusedPane({
681
753
  ...state,
682
754
  layoutHistory: setHistoryForIndex(state.layoutHistory, currentIndex, entry),
683
755
  }, {
684
756
  ...state.config,
685
757
  layout: cloneLayout(target),
686
- layouts,
687
758
  });
688
759
  }
689
760
 
690
761
  case "UPDATE_LAYOUT": {
691
- const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, action.layout);
692
- return withFocusedPane(state, { ...state.config, layout: action.layout, layouts });
762
+ return withFocusedPane(state, { ...state.config, layout: action.layout });
693
763
  }
694
764
 
695
765
  case "SWITCH_LAYOUT": {
696
766
  if (action.index < 0 || action.index >= state.config.layouts.length) return state;
697
- const layouts = syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout);
698
- const target = layouts[action.index]!;
767
+ if (action.index === state.config.activeLayoutIndex) return state;
768
+ const currentConfig = syncConfigActiveLayoutState(
769
+ state.config,
770
+ state.paneState,
771
+ state.focusedPaneId,
772
+ state.activePanel,
773
+ );
774
+ const target = currentConfig.layouts[action.index]!;
699
775
  return withFocusedPane(state, {
700
- ...state.config,
776
+ ...currentConfig,
701
777
  layout: cloneLayout(target.layout),
702
- layouts,
703
778
  activeLayoutIndex: action.index,
779
+ }, {
780
+ paneState: target.paneState ? clonePaneStateMap(target.paneState) : {},
781
+ focusedPaneId: target.focusedPaneId ?? null,
782
+ activePanel: target.activePanel ?? state.activePanel,
704
783
  });
705
784
  }
706
785
 
707
786
  case "NEW_LAYOUT": {
787
+ const currentConfig = syncConfigActiveLayoutState(
788
+ state.config,
789
+ state.paneState,
790
+ state.focusedPaneId,
791
+ state.activePanel,
792
+ );
708
793
  const newLayout: SavedLayout = {
709
794
  name: action.name,
710
795
  layout: cloneLayout(DEFAULT_LAYOUT),
796
+ paneState: {},
711
797
  };
712
- const layouts = [...syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout), newLayout];
798
+ const layouts = [...currentConfig.layouts, newLayout];
713
799
  return withFocusedPane({
714
800
  ...state,
715
801
  layoutHistory: setHistoryForIndex(state.layoutHistory, layouts.length - 1, { past: [], future: [] }),
716
802
  }, {
717
- ...state.config,
803
+ ...currentConfig,
718
804
  layout: cloneLayout(newLayout.layout),
719
805
  layouts,
720
806
  activeLayoutIndex: layouts.length - 1,
807
+ }, {
808
+ paneState: {},
809
+ focusedPaneId: null,
721
810
  });
722
811
  }
723
812
 
724
813
  case "DELETE_LAYOUT": {
725
814
  if (state.config.layouts.length <= 1) return state;
726
- const layouts = state.config.layouts.filter((_, index) => index !== action.index);
815
+ const currentConfig = syncConfigActiveLayoutState(
816
+ state.config,
817
+ state.paneState,
818
+ state.focusedPaneId,
819
+ state.activePanel,
820
+ );
821
+ const layouts = currentConfig.layouts.filter((_, index) => index !== action.index);
727
822
  const nextActiveLayoutIndex = action.index <= state.config.activeLayoutIndex
728
823
  ? Math.max(0, state.config.activeLayoutIndex - 1)
729
824
  : state.config.activeLayoutIndex;
@@ -732,20 +827,30 @@ export function appReducer(state: AppState, action: AppAction): AppState {
732
827
  ...state,
733
828
  layoutHistory: removeHistoryIndex(state.layoutHistory, action.index),
734
829
  }, {
735
- ...state.config,
830
+ ...currentConfig,
736
831
  layout: cloneLayout(nextLayout.layout),
737
832
  layouts,
738
833
  activeLayoutIndex: nextActiveLayoutIndex,
834
+ }, {
835
+ paneState: nextLayout.paneState ? clonePaneStateMap(nextLayout.paneState) : {},
836
+ focusedPaneId: nextLayout.focusedPaneId ?? null,
837
+ activePanel: nextLayout.activePanel ?? state.activePanel,
739
838
  });
740
839
  }
741
840
 
742
841
  case "RENAME_LAYOUT": {
743
842
  if (action.index < 0 || action.index >= state.config.layouts.length) return state;
843
+ const currentConfig = syncConfigActiveLayoutState(
844
+ state.config,
845
+ state.paneState,
846
+ state.focusedPaneId,
847
+ state.activePanel,
848
+ );
744
849
  return {
745
850
  ...state,
746
851
  config: {
747
- ...state.config,
748
- layouts: state.config.layouts.map((savedLayout, index) => (
852
+ ...currentConfig,
853
+ layouts: currentConfig.layouts.map((savedLayout, index) => (
749
854
  index === action.index ? { ...savedLayout, name: action.name } : savedLayout
750
855
  )),
751
856
  },
@@ -754,20 +859,30 @@ export function appReducer(state: AppState, action: AppAction): AppState {
754
859
 
755
860
  case "DUPLICATE_LAYOUT": {
756
861
  if (action.index < 0 || action.index >= state.config.layouts.length) return state;
757
- const source = state.config.layouts[action.index]!;
862
+ const currentConfig = syncConfigActiveLayoutState(
863
+ state.config,
864
+ state.paneState,
865
+ state.focusedPaneId,
866
+ state.activePanel,
867
+ );
868
+ const source = currentConfig.layouts[action.index]!;
758
869
  const duplicate: SavedLayout = {
870
+ ...cloneSavedLayout(source),
759
871
  name: `${source.name} Copy`,
760
- layout: cloneLayout(source.layout),
761
872
  };
762
- const layouts = [...syncLayouts(state.config.layouts, state.config.activeLayoutIndex, state.config.layout), duplicate];
873
+ const layouts = [...currentConfig.layouts, duplicate];
763
874
  return withFocusedPane({
764
875
  ...state,
765
876
  layoutHistory: setHistoryForIndex(state.layoutHistory, layouts.length - 1, { past: [], future: [] }),
766
877
  }, {
767
- ...state.config,
878
+ ...currentConfig,
768
879
  layout: cloneLayout(duplicate.layout),
769
880
  layouts,
770
881
  activeLayoutIndex: layouts.length - 1,
882
+ }, {
883
+ paneState: duplicate.paneState ? clonePaneStateMap(duplicate.paneState) : {},
884
+ focusedPaneId: duplicate.focusedPaneId ?? state.focusedPaneId,
885
+ activePanel: duplicate.activePanel ?? state.activePanel,
771
886
  });
772
887
  }
773
888
 
@@ -802,12 +917,14 @@ export function appReducer(state: AppState, action: AppAction): AppState {
802
917
  const recentTickers = Object.prototype.hasOwnProperty.call(action.patch, "cursorSymbol")
803
918
  ? nextRecentTickers(state.recentTickers, typeof nextState.cursorSymbol === "string" ? nextState.cursorSymbol : null)
804
919
  : state.recentTickers;
920
+ const paneState = {
921
+ ...state.paneState,
922
+ [action.paneId]: nextState,
923
+ };
805
924
  return {
806
925
  ...state,
807
- paneState: {
808
- ...state.paneState,
809
- [action.paneId]: nextState,
810
- },
926
+ config: syncConfigActiveLayoutState(state.config, paneState, state.focusedPaneId, state.activePanel),
927
+ paneState,
811
928
  recentTickers,
812
929
  };
813
930
  }
@@ -819,22 +936,24 @@ export function appReducer(state: AppState, action: AppAction): AppState {
819
936
  if (Object.is(currentPluginValues[action.key], action.value)) {
820
937
  return state;
821
938
  }
822
- return {
823
- ...state,
824
- paneState: {
825
- ...state.paneState,
826
- [action.paneId]: {
827
- ...current,
828
- pluginState: {
829
- ...currentPluginState,
830
- [action.pluginId]: {
831
- ...currentPluginValues,
832
- [action.key]: action.value,
833
- },
939
+ const paneState = {
940
+ ...state.paneState,
941
+ [action.paneId]: {
942
+ ...current,
943
+ pluginState: {
944
+ ...currentPluginState,
945
+ [action.pluginId]: {
946
+ ...currentPluginValues,
947
+ [action.key]: action.value,
834
948
  },
835
949
  },
836
950
  },
837
951
  };
952
+ return {
953
+ ...state,
954
+ config: syncConfigActiveLayoutState(state.config, paneState, state.focusedPaneId, state.activePanel),
955
+ paneState,
956
+ };
838
957
  }
839
958
 
840
959
  default:
@@ -843,20 +962,31 @@ export function appReducer(state: AppState, action: AppAction): AppState {
843
962
  }
844
963
 
845
964
  export function createInitialState(config: AppConfig, sessionSnapshot: AppSessionSnapshot | null = null): AppState {
846
- const paneState = reconcilePaneState(config, sessionSnapshot?.paneState ?? {});
965
+ const activeSavedLayout = config.layouts[config.activeLayoutIndex];
966
+ const savedPaneState = activeSavedLayout?.paneState ? clonePaneStateMap(activeSavedLayout.paneState) : {};
967
+ const sessionPaneState = sessionSnapshot?.paneState ? clonePaneStateMap(sessionSnapshot.paneState) : {};
968
+ const paneState = reconcilePaneState(config, { ...sessionPaneState, ...savedPaneState });
847
969
  const defaultFocusedPaneId = getTopFloatingPaneId(config.layout) ?? getPaneOrder(config.layout)[0] ?? null;
848
- const focusedPaneId = sessionSnapshot?.focusedPaneId
849
- && config.layout.instances.some((instance) => instance.instanceId === sessionSnapshot.focusedPaneId)
850
- ? sessionSnapshot.focusedPaneId
970
+ const requestedFocusedPaneId = activeSavedLayout?.focusedPaneId ?? sessionSnapshot?.focusedPaneId ?? null;
971
+ const focusedPaneId = requestedFocusedPaneId
972
+ && config.layout.instances.some((instance) => instance.instanceId === requestedFocusedPaneId)
973
+ ? requestedFocusedPaneId
851
974
  : defaultFocusedPaneId;
852
- const focusedConfig = bringFocusedFloatingPaneToFront(config, focusedPaneId);
975
+ const activePanel = activeSavedLayout?.activePanel ?? (sessionSnapshot?.activePanel === "right" ? "right" : "left");
976
+ const focusedLayout = focusedPaneId ? bringFloatingToFront(config.layout, focusedPaneId) : config.layout;
977
+ const focusedConfig = syncConfigActiveLayoutState(
978
+ focusedLayout === config.layout ? config : { ...config, layout: focusedLayout },
979
+ paneState,
980
+ focusedPaneId,
981
+ activePanel,
982
+ );
853
983
  return {
854
984
  config: focusedConfig,
855
985
  tickers: new Map(),
856
986
  financials: new Map(),
857
987
  exchangeRates: new Map([["USD", 1]]),
858
988
  brokerAccounts: {},
859
- activePanel: sessionSnapshot?.activePanel === "right" ? "right" : "left",
989
+ activePanel,
860
990
  focusedPaneId,
861
991
  paneState,
862
992
  recentTickers: config.recentTickers,