@flowdrop/flowdrop 1.8.0 → 1.9.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.
@@ -11,6 +11,7 @@
11
11
  import { onMount, onDestroy } from 'svelte';
12
12
  import Icon from '@iconify/svelte';
13
13
  import ChatPanel from './ChatPanel.svelte';
14
+ import ExecutionList from './ExecutionList.svelte';
14
15
  import type { Workflow } from '../../types/index.js';
15
16
  import type { EndpointConfig } from '../../config/endpoints.js';
16
17
  import type { PlaygroundMode, PlaygroundConfig } from '../../types/playground.js';
@@ -25,7 +26,10 @@
25
26
  getError,
26
27
  playgroundActions,
27
28
  getInputFields,
28
- createPollingCallback
29
+ createPollingCallback,
30
+ getActiveExecutionId,
31
+ getLatestExecutionId,
32
+ getPinnedExecutionId,
29
33
  } from '../../stores/playgroundStore.svelte.js';
30
34
  import { interruptActions } from '../../stores/interruptStore.svelte.js';
31
35
  import { logger } from '../../utils/logger.js';
@@ -49,6 +53,12 @@
49
53
  config?: PlaygroundConfig;
50
54
  /** Callback when playground is closed (for embedded mode) */
51
55
  onClose?: () => void;
56
+ /** Callback to toggle the pipeline panel (if undefined, toggle button is hidden) */
57
+ onTogglePanel?: () => void;
58
+ /** Whether the pipeline panel is currently open (for toggle button active state) */
59
+ isPipelinePanelOpen?: boolean;
60
+ /** When provided, session switches and creation navigate to a URL instead of mutating store state */
61
+ onSessionNavigate?: (sessionId: string) => void;
52
62
  }
53
63
 
54
64
  let {
@@ -58,7 +68,10 @@
58
68
  initialSessionId,
59
69
  endpointConfig,
60
70
  config = {},
61
- onClose
71
+ onClose,
72
+ onTogglePanel,
73
+ isPipelinePanelOpen = false,
74
+ onSessionNavigate,
62
75
  }: Props = $props();
63
76
 
64
77
  /** Current input values from InputCollector */
@@ -70,6 +83,9 @@
70
83
  /** Track which session's dropdown menu is open */
71
84
  let openMenuId = $state<string | null>(null);
72
85
 
86
+ /** Whether the runs sub-section is expanded under the active session */
87
+ let runsExpanded = $state(false);
88
+
73
89
  /** Track if initial session has been loaded to prevent duplicate loads */
74
90
  let initialSessionLoaded = $state(false);
75
91
 
@@ -79,6 +95,24 @@
79
95
  /** Track if auto-run has already been triggered to prevent duplicate executions */
80
96
  let autoRunTriggered = $state(false);
81
97
 
98
+ /** Whether log messages are visible in the chat panel */
99
+ let showLogs = $state(true);
100
+
101
+ /** Whether the session switcher popover is open (standalone mode) */
102
+ let sessionDropdownOpen = $state(false);
103
+
104
+ // Close session popover on outside click
105
+ $effect(() => {
106
+ if (!sessionDropdownOpen) return;
107
+ function handleOutside(e: MouseEvent) {
108
+ if (!(e.target as HTMLElement).closest('.playground__session-chip-wrap')) {
109
+ sessionDropdownOpen = false;
110
+ }
111
+ }
112
+ document.addEventListener('click', handleOutside);
113
+ return () => document.removeEventListener('click', handleOutside);
114
+ });
115
+
82
116
  /**
83
117
  * Initialize the playground on mount
84
118
  */
@@ -262,6 +296,13 @@
262
296
  try {
263
297
  const sessionName = `Session ${getSessions().length + 1}`;
264
298
  const session = await playgroundService.createSession(workflowId, sessionName);
299
+
300
+ if (onSessionNavigate) {
301
+ // URL-based routing: navigate to the new session; page remount handles store init
302
+ onSessionNavigate(session.id);
303
+ return;
304
+ }
305
+
265
306
  playgroundActions.addSession(session);
266
307
  playgroundActions.setCurrentSession(session);
267
308
  playgroundActions.clearMessages();
@@ -278,6 +319,8 @@
278
319
  * Select a session
279
320
  */
280
321
  async function handleSelectSession(sessionId: string): Promise<void> {
322
+ playgroundActions.pinExecution(null);
323
+ runsExpanded = false;
281
324
  const currentSessionId = getCurrentSession()?.id;
282
325
  if (currentSessionId === sessionId) {
283
326
  return;
@@ -488,8 +531,8 @@
488
531
  class:playground--no-sidebar={config.showSidebar === false}
489
532
  >
490
533
  <div class="playground__container">
491
- <!-- Sidebar (conditionally rendered based on config.showSidebar) -->
492
- {#if config.showSidebar !== false}
534
+ <!-- Sidebar — hidden in standalone mode (session switcher lives in the header chip instead) -->
535
+ {#if config.showSidebar === true || (config.showSidebar !== false && mode !== 'standalone')}
493
536
  <aside
494
537
  class="playground__sidebar"
495
538
  style={config.sidebarWidth ? `--fd-playground-sidebar-width: ${config.sidebarWidth}` : ''}
@@ -499,6 +542,13 @@
499
542
  <div class="playground__sidebar-title">
500
543
  <span>Playground</span>
501
544
  </div>
545
+ <a
546
+ href="/workflow/{workflowId}/edit"
547
+ class="playground__edit-link"
548
+ title="Edit workflow"
549
+ >
550
+ <Icon icon="mdi:pencil-outline" />
551
+ </a>
502
552
  {#if (mode === 'embedded' || mode === 'modal') && onClose}
503
553
  <button
504
554
  type="button"
@@ -515,24 +565,24 @@
515
565
  {/if}
516
566
  </div>
517
567
 
518
- <!-- New Session Section -->
568
+ <!-- Sessions Section -->
519
569
  <div class="playground__section">
520
- <button
521
- type="button"
522
- class="playground__new-session-btn"
523
- onclick={handleCreateSession}
524
- disabled={getIsLoading()}
525
- title="Start a new session"
526
- >
527
- <Icon icon="mdi:plus" />
528
- <span>New Session</span>
529
- </button>
570
+ <!-- Section header with inline add button -->
571
+ <div class="playground__section-header">
572
+ <span class="playground__section-label">Sessions</span>
573
+ <button
574
+ type="button"
575
+ class="playground__section-add"
576
+ onclick={handleCreateSession}
577
+ disabled={getIsLoading()}
578
+ title="New session"
579
+ >
580
+ <Icon icon="mdi:plus" />
581
+ </button>
582
+ </div>
530
583
 
531
- <!-- Sessions List - click a session to load it -->
584
+ <!-- Sessions List -->
532
585
  <div class="playground__sessions-wrap">
533
- {#if getSessions().length > 0}
534
- <p class="playground__sessions-hint">Click a session to load it</p>
535
- {/if}
536
586
  <div class="playground__sessions">
537
587
  {#if getSessions().length === 0 && !getIsLoading()}
538
588
  <div class="playground__sessions-empty">
@@ -540,42 +590,75 @@
540
590
  </div>
541
591
  {:else}
542
592
  {#each getSessions() as session (session.id)}
543
- <div
544
- class="playground__session"
545
- class:playground__session--active={getCurrentSession()?.id === session.id}
546
- role="button"
547
- tabindex="0"
548
- title="Click to load this session"
549
- aria-label={m().layout.loadSession({ name: session.name })}
550
- onclick={() => handleSelectSession(session.id)}
551
- onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
552
- >
553
- <span class="playground__session-name" title={session.name}>
554
- {session.name}
555
- </span>
556
- <div class="playground__session-actions">
557
- <button
558
- type="button"
559
- class="playground__session-menu"
560
- class:playground__session-menu--open={openMenuId === session.id}
561
- onclick={(e) => handleMenuToggle(e, session.id)}
562
- title="Session options"
563
- >
564
- <Icon icon="mdi:dots-vertical" />
565
- </button>
566
- {#if openMenuId === session.id}
567
- <div class="playground__session-dropdown">
568
- <button
569
- type="button"
570
- class="playground__session-dropdown-item playground__session-dropdown-item--danger"
571
- onclick={(e) => handleMenuDelete(e, session.id)}
572
- >
573
- <Icon icon="mdi:delete-outline" />
574
- <span>Delete</span>
575
- </button>
576
- </div>
577
- {/if}
593
+ {@const isActive = getCurrentSession()?.id === session.id}
594
+ <div class="playground__session-group">
595
+ <div
596
+ class="playground__session"
597
+ class:playground__session--active={isActive}
598
+ role="button"
599
+ tabindex="0"
600
+ title="Click to load this session"
601
+ aria-label={m().layout.loadSession({ name: session.name })}
602
+ onclick={() => handleSelectSession(session.id)}
603
+ onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
604
+ >
605
+ <span class="playground__session-name" title={session.name}>
606
+ {session.name}
607
+ </span>
608
+ <div class="playground__session-actions">
609
+ <button
610
+ type="button"
611
+ class="playground__session-menu"
612
+ class:playground__session-menu--open={openMenuId === session.id}
613
+ onclick={(e) => handleMenuToggle(e, session.id)}
614
+ title="Session options"
615
+ >
616
+ <Icon icon="mdi:dots-vertical" />
617
+ </button>
618
+ {#if openMenuId === session.id}
619
+ <div class="playground__session-dropdown">
620
+ <button
621
+ type="button"
622
+ class="playground__session-dropdown-item playground__session-dropdown-item--danger"
623
+ onclick={(e) => handleMenuDelete(e, session.id)}
624
+ >
625
+ <Icon icon="mdi:delete-outline" />
626
+ <span>Delete</span>
627
+ </button>
628
+ </div>
629
+ {/if}
630
+ </div>
578
631
  </div>
632
+ <!-- Collapsible runs sub-section under active session -->
633
+ {#if isActive && getCurrentSession()?.executions?.length}
634
+ <div class="playground__runs-section">
635
+ <button
636
+ type="button"
637
+ class="playground__runs-toggle"
638
+ onclick={() => (runsExpanded = !runsExpanded)}
639
+ >
640
+ <Icon icon={runsExpanded ? 'mdi:chevron-down' : 'mdi:chevron-right'} />
641
+ <span>Runs</span>
642
+ <span class="playground__runs-count">{getCurrentSession()!.executions!.length}</span>
643
+ </button>
644
+ {#if runsExpanded}
645
+ <div class="playground__executions-inline">
646
+ <ExecutionList
647
+ executions={getCurrentSession()!.executions!}
648
+ activeExecutionId={getActiveExecutionId()}
649
+ latestExecutionId={getLatestExecutionId()}
650
+ onSelect={(id) => {
651
+ if (id === getLatestExecutionId()) {
652
+ playgroundActions.pinExecution(null);
653
+ } else {
654
+ playgroundActions.pinExecution(id);
655
+ }
656
+ }}
657
+ />
658
+ </div>
659
+ {/if}
660
+ </div>
661
+ {/if}
579
662
  </div>
580
663
  {/each}
581
664
  {/if}
@@ -587,18 +670,118 @@
587
670
 
588
671
  <!-- Main Content -->
589
672
  <main class="playground__main">
590
- <!-- Session Header (conditionally rendered based on config.showSessionHeader) -->
591
- {#if getCurrentSession() && config.showSessionHeader !== false}
673
+ <!-- Session Header -->
674
+ {#if mode === 'standalone' || (getCurrentSession() && config.showSessionHeader !== false)}
592
675
  <header class="playground__header">
593
- <h2 class="playground__header-title">{getCurrentSession()?.name}</h2>
594
- <button
595
- type="button"
596
- class="playground__header-close"
597
- onclick={handleCloseSession}
598
- title="Close session"
599
- >
600
- <Icon icon="mdi:close" />
601
- </button>
676
+ {#if mode === 'standalone'}
677
+ <!-- Panel icon + label (mirrors PipelinePanel header) -->
678
+ <Icon icon="mdi:message-text-outline" class="playground__header-icon" />
679
+ <span class="playground__header-label">Sessions</span>
680
+
681
+ <!-- Session chip — switches sessions via popover -->
682
+ <div class="playground__session-chip-wrap">
683
+ <button
684
+ type="button"
685
+ class="playground__session-chip"
686
+ class:playground__session-chip--open={sessionDropdownOpen}
687
+ onclick={() => (sessionDropdownOpen = !sessionDropdownOpen)}
688
+ title="Switch session"
689
+ >
690
+ <span class="playground__session-chip-name">
691
+ {getCurrentSession()?.name ?? 'No session'}
692
+ </span>
693
+ <Icon icon={sessionDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'} class="playground__session-chip-chevron" />
694
+ </button>
695
+
696
+ {#if sessionDropdownOpen}
697
+ <div class="playground__session-popover">
698
+ <button
699
+ type="button"
700
+ class="playground__session-popover-item playground__session-popover-item--new"
701
+ disabled={getIsLoading()}
702
+ onclick={() => { sessionDropdownOpen = false; void handleCreateSession(); }}
703
+ >
704
+ <Icon icon="mdi:plus" />
705
+ <span>New session</span>
706
+ </button>
707
+ {#if getSessions().length > 0}
708
+ <div class="playground__session-popover-divider"></div>
709
+ {#each getSessions() as session (session.id)}
710
+ {@const isActive = getCurrentSession()?.id === session.id}
711
+ <div class="playground__session-popover-row">
712
+ <button
713
+ type="button"
714
+ class="playground__session-popover-item"
715
+ class:playground__session-popover-item--active={isActive}
716
+ onclick={() => {
717
+ sessionDropdownOpen = false;
718
+ if (onSessionNavigate) {
719
+ onSessionNavigate(session.id);
720
+ } else {
721
+ void handleSelectSession(session.id);
722
+ }
723
+ }}
724
+ >
725
+ {#if isActive}
726
+ <Icon icon="mdi:check" class="playground__session-popover-check" />
727
+ {:else}
728
+ <Icon icon="mdi:message-outline" />
729
+ {/if}
730
+ <span>{session.name}</span>
731
+ </button>
732
+ <button
733
+ type="button"
734
+ class="playground__session-popover-delete"
735
+ onclick={(e) => { handleMenuDelete(e, session.id); sessionDropdownOpen = false; }}
736
+ title="Delete session"
737
+ >
738
+ <Icon icon="mdi:delete-outline" />
739
+ </button>
740
+ </div>
741
+ {/each}
742
+ {/if}
743
+ </div>
744
+ {/if}
745
+ </div>
746
+ {:else}
747
+ <!-- Embedded / modal: original title + close -->
748
+ <div class="playground__header-group">
749
+ <h2 class="playground__header-title">{getCurrentSession()?.name}</h2>
750
+ <button
751
+ type="button"
752
+ class="playground__header-close"
753
+ onclick={handleCloseSession}
754
+ title="Close session"
755
+ >
756
+ <Icon icon="mdi:close" />
757
+ </button>
758
+ </div>
759
+ {/if}
760
+
761
+ <div class="playground__header-actions">
762
+ {#if mode === 'standalone' && onTogglePanel}
763
+ <button
764
+ type="button"
765
+ class="playground__log-toggle"
766
+ class:playground__log-toggle--active={isPipelinePanelOpen}
767
+ onclick={onTogglePanel}
768
+ title={isPipelinePanelOpen ? 'Hide pipeline' : 'Show pipeline'}
769
+ >
770
+ <Icon icon="mdi:source-branch" />
771
+ Pipeline
772
+ </button>
773
+ {/if}
774
+ <button
775
+ type="button"
776
+ class="playground__log-toggle"
777
+ class:playground__log-toggle--active={showLogs}
778
+ onclick={() => (showLogs = !showLogs)}
779
+ title={showLogs ? 'Hide log messages' : 'Show log messages'}
780
+ >
781
+ <Icon icon="mdi:console" />
782
+ Logs
783
+ </button>
784
+ </div>
602
785
  </header>
603
786
  {/if}
604
787
 
@@ -636,6 +819,7 @@
636
819
  onSendMessage={handleSendMessage}
637
820
  onStopExecution={handleStopExecution}
638
821
  onInterruptResolved={handleInterruptResolved}
822
+ bind:showLogs
639
823
  />
640
824
  {/if}
641
825
  </div>
@@ -691,6 +875,7 @@
691
875
  width: var(--fd-playground-sidebar-width);
692
876
  background-color: var(--fd-background);
693
877
  border-right: 1px solid var(--fd-border);
878
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
694
879
  display: flex;
695
880
  flex-direction: column;
696
881
  }
@@ -717,6 +902,24 @@
717
902
  color: var(--fd-foreground);
718
903
  }
719
904
 
905
+ .playground__edit-link {
906
+ display: flex;
907
+ align-items: center;
908
+ justify-content: center;
909
+ width: var(--fd-playground-icon-btn-size);
910
+ height: var(--fd-playground-icon-btn-size);
911
+ border-radius: var(--fd-radius-md);
912
+ color: var(--fd-muted-foreground);
913
+ text-decoration: none;
914
+ transition: all var(--fd-transition-fast);
915
+ flex-shrink: 0;
916
+ }
917
+
918
+ .playground__edit-link:hover {
919
+ background-color: var(--fd-muted);
920
+ color: var(--fd-foreground);
921
+ }
922
+
720
923
  .playground__sidebar-close {
721
924
  display: flex;
722
925
  align-items: center;
@@ -742,51 +945,119 @@
742
945
  display: flex;
743
946
  flex-direction: column;
744
947
  min-height: 0;
745
- padding: var(--fd-space-md) var(--fd-space-xs) 0;
948
+ padding: 0 var(--fd-space-md);
746
949
  }
747
950
 
748
- /* New Session neutral full-width button with icon */
749
- .playground__new-session-btn {
951
+ /* Section header: label + add icon */
952
+ .playground__section-header {
953
+ display: flex;
954
+ align-items: center;
955
+ justify-content: space-between;
956
+ padding: var(--fd-space-md) var(--fd-space-xs) var(--fd-space-xs);
957
+ }
958
+
959
+ .playground__section-label {
960
+ font-size: var(--fd-text-xs);
961
+ font-weight: 600;
962
+ color: var(--fd-muted-foreground);
963
+ text-transform: uppercase;
964
+ letter-spacing: 0.06em;
965
+ }
966
+
967
+ .playground__section-add {
750
968
  display: flex;
751
969
  align-items: center;
752
970
  justify-content: center;
753
- gap: var(--fd-space-xs);
754
- width: 100%;
755
- padding: var(--fd-space-sm) var(--fd-space-xl);
756
- border: 1px solid var(--fd-border);
971
+ width: var(--fd-playground-icon-btn-size);
972
+ height: var(--fd-playground-icon-btn-size);
973
+ border: none;
757
974
  border-radius: var(--fd-radius-md);
758
- background-color: var(--fd-background);
975
+ background: transparent;
976
+ color: var(--fd-muted-foreground);
977
+ cursor: pointer;
978
+ transition: all var(--fd-transition-fast);
979
+ }
980
+
981
+ .playground__section-add:hover:not(:disabled) {
982
+ background-color: var(--fd-muted);
759
983
  color: var(--fd-foreground);
760
- font-size: var(--fd-text-sm);
984
+ }
985
+
986
+ .playground__section-add:disabled {
987
+ opacity: 0.4;
988
+ cursor: not-allowed;
989
+ }
990
+
991
+ /* Session group wraps session row + its inline runs */
992
+ .playground__session-group {
993
+ margin-bottom: var(--fd-space-3xs);
994
+ }
995
+
996
+ /* Collapsible runs sub-section under active session */
997
+ .playground__runs-section {
998
+ margin-bottom: var(--fd-space-3xs);
999
+ }
1000
+
1001
+ .playground__runs-toggle {
1002
+ display: flex;
1003
+ align-items: center;
1004
+ gap: var(--fd-space-3xs);
1005
+ width: 100%;
1006
+ padding: var(--fd-space-3xs) var(--fd-space-sm);
1007
+ padding-left: calc(var(--fd-space-md) + var(--fd-space-3xs));
1008
+ border: none;
1009
+ border-radius: var(--fd-radius-sm);
1010
+ background: transparent;
1011
+ color: var(--fd-muted-foreground);
1012
+ font-size: var(--fd-text-xs);
761
1013
  font-weight: 500;
762
1014
  cursor: pointer;
763
- transition:
764
- background-color var(--fd-transition-fast),
765
- border-color var(--fd-transition-fast),
766
- transform 0.1s ease;
767
- box-sizing: border-box;
1015
+ text-align: left;
1016
+ transition: all var(--fd-transition-fast);
768
1017
  }
769
1018
 
770
- .playground__new-session-btn:hover:not(:disabled) {
1019
+ .playground__runs-toggle:hover {
771
1020
  background-color: var(--fd-muted);
772
- border-color: var(--fd-border);
773
- transform: translateY(-1px);
1021
+ color: var(--fd-foreground);
1022
+ }
1023
+
1024
+ .playground__runs-toggle :global(svg) {
1025
+ width: 0.875rem;
1026
+ height: 0.875rem;
1027
+ flex-shrink: 0;
774
1028
  }
775
1029
 
776
- .playground__new-session-btn:focus {
777
- outline: none;
778
- box-shadow: 0 0 0 2px var(--fd-ring);
1030
+ .playground__runs-count {
1031
+ margin-left: auto;
1032
+ font-size: var(--fd-text-2xs);
1033
+ font-weight: 600;
1034
+ color: var(--fd-muted-foreground);
1035
+ background: var(--fd-muted);
1036
+ border-radius: 999px;
1037
+ padding: 1px var(--fd-space-xs);
1038
+ min-width: 1.4em;
1039
+ text-align: center;
1040
+ line-height: 1.4;
779
1041
  }
780
1042
 
781
- .playground__new-session-btn:disabled {
782
- opacity: 0.5;
783
- cursor: not-allowed;
784
- transform: none;
1043
+ /* Inline runs tree under active session */
1044
+ .playground__executions-inline {
1045
+ margin-left: calc(var(--fd-space-md) + var(--fd-space-xs));
1046
+ margin-bottom: var(--fd-space-xs);
1047
+ border-left: 2px solid var(--fd-border);
1048
+ padding-left: var(--fd-space-xs);
1049
+ }
1050
+
1051
+ .playground__executions-inline :global(.execution-list__item) {
1052
+ padding: var(--fd-space-xs) var(--fd-space-sm);
1053
+ font-size: var(--fd-text-xs);
1054
+ border-radius: var(--fd-radius-sm);
1055
+ border-left-width: 2px;
785
1056
  }
786
1057
 
787
- .playground__new-session-btn :global(svg) {
788
- width: 1.125rem;
789
- height: 1.125rem;
1058
+ .playground__executions-inline :global(.execution-list) {
1059
+ gap: 1px;
1060
+ padding: var(--fd-space-3xs) 0;
790
1061
  }
791
1062
 
792
1063
  /* Sessions */
@@ -797,17 +1068,10 @@
797
1068
  min-height: 0;
798
1069
  }
799
1070
 
800
- .playground__sessions-hint {
801
- font-size: var(--fd-text-2xs);
802
- color: var(--fd-muted-foreground);
803
- margin: var(--fd-space-md) 0 var(--fd-space-2xs) var(--fd-space-md);
804
- line-height: 1.3;
805
- }
806
-
807
1071
  .playground__sessions {
808
1072
  flex: 1;
809
1073
  overflow-y: auto;
810
- padding: 0 var(--fd-space-xs) var(--fd-space-xl);
1074
+ padding: 0 var(--fd-space-3xs) var(--fd-space-xl);
811
1075
  min-height: 0;
812
1076
  }
813
1077
 
@@ -824,7 +1088,6 @@
824
1088
  align-items: center;
825
1089
  justify-content: space-between;
826
1090
  padding: var(--fd-space-sm) var(--fd-space-md);
827
- margin-bottom: var(--fd-space-3xs);
828
1091
  border-radius: var(--fd-radius-md);
829
1092
  border-left: 3px solid transparent;
830
1093
  cursor: pointer;
@@ -956,15 +1219,34 @@
956
1219
  .playground__header {
957
1220
  display: flex;
958
1221
  align-items: center;
959
- justify-content: space-between;
1222
+ gap: var(--fd-space-xs);
960
1223
  height: var(--fd-playground-header-height);
961
- padding: 0 var(--fd-space-2xl);
1224
+ padding: 0 var(--fd-space-xl);
962
1225
  border-bottom: 1px solid var(--fd-border);
963
1226
  background-color: var(--fd-background);
964
1227
  box-sizing: border-box;
965
1228
  flex-shrink: 0;
966
1229
  }
967
1230
 
1231
+ :global(.playground__header-icon) {
1232
+ font-size: var(--fd-text-base);
1233
+ color: var(--fd-muted-foreground);
1234
+ flex-shrink: 0;
1235
+ }
1236
+
1237
+ .playground__header-label {
1238
+ font-size: var(--fd-text-sm);
1239
+ font-weight: 600;
1240
+ color: var(--fd-foreground);
1241
+ flex-shrink: 0;
1242
+ }
1243
+
1244
+ .playground__header-group {
1245
+ display: flex;
1246
+ align-items: center;
1247
+ gap: var(--fd-space-xs);
1248
+ }
1249
+
968
1250
  .playground__header-title {
969
1251
  font-size: var(--fd-text-md);
970
1252
  font-weight: 600;
@@ -992,6 +1274,207 @@
992
1274
  color: var(--fd-foreground);
993
1275
  }
994
1276
 
1277
+ .playground__header-actions {
1278
+ display: flex;
1279
+ align-items: center;
1280
+ gap: var(--fd-space-xs);
1281
+ margin-left: auto;
1282
+ }
1283
+
1284
+ .playground__log-toggle {
1285
+ display: inline-flex;
1286
+ align-items: center;
1287
+ gap: var(--fd-space-3xs);
1288
+ padding: var(--fd-space-3xs) var(--fd-space-sm);
1289
+ border: 1px solid var(--fd-border);
1290
+ border-radius: var(--fd-radius-md);
1291
+ background: transparent;
1292
+ color: var(--fd-muted-foreground);
1293
+ font-size: var(--fd-text-xs);
1294
+ font-weight: 500;
1295
+ cursor: pointer;
1296
+ transition: all var(--fd-transition-fast);
1297
+ line-height: 1;
1298
+ }
1299
+
1300
+ .playground__log-toggle :global(svg) {
1301
+ font-size: var(--fd-text-xs);
1302
+ }
1303
+
1304
+ .playground__log-toggle:hover {
1305
+ background-color: var(--fd-muted);
1306
+ color: var(--fd-foreground);
1307
+ border-color: var(--fd-border-strong);
1308
+ }
1309
+
1310
+ .playground__log-toggle--active {
1311
+ background-color: var(--fd-primary-muted);
1312
+ border-color: var(--fd-primary);
1313
+ color: var(--fd-primary);
1314
+ }
1315
+
1316
+ /* Session chip (standalone mode) */
1317
+ .playground__session-chip-wrap {
1318
+ position: relative;
1319
+ flex-shrink: 0;
1320
+ }
1321
+
1322
+ .playground__session-chip {
1323
+ display: inline-flex;
1324
+ align-items: center;
1325
+ gap: var(--fd-space-xs);
1326
+ padding: var(--fd-space-3xs) var(--fd-space-sm) var(--fd-space-3xs) var(--fd-space-xs);
1327
+ border: 1px solid var(--fd-border);
1328
+ border-radius: var(--fd-radius-md);
1329
+ background: var(--fd-background);
1330
+ color: var(--fd-foreground);
1331
+ font-size: var(--fd-text-sm);
1332
+ font-weight: 500;
1333
+ cursor: pointer;
1334
+ transition: all var(--fd-transition-fast);
1335
+ max-width: 220px;
1336
+ line-height: 1;
1337
+ }
1338
+
1339
+ .playground__session-chip :global(svg) {
1340
+ flex-shrink: 0;
1341
+ font-size: var(--fd-text-sm);
1342
+ color: var(--fd-muted-foreground);
1343
+ }
1344
+
1345
+ .playground__session-chip:hover {
1346
+ background-color: var(--fd-muted);
1347
+ border-color: var(--fd-border-strong);
1348
+ }
1349
+
1350
+ .playground__session-chip--open {
1351
+ background-color: var(--fd-muted);
1352
+ border-color: var(--fd-border-strong);
1353
+ }
1354
+
1355
+ .playground__session-chip-name {
1356
+ flex: 1;
1357
+ white-space: nowrap;
1358
+ overflow: hidden;
1359
+ text-overflow: ellipsis;
1360
+ min-width: 0;
1361
+ }
1362
+
1363
+ :global(.playground__session-chip-chevron) {
1364
+ color: var(--fd-muted-foreground);
1365
+ flex-shrink: 0;
1366
+ }
1367
+
1368
+ /* Session switcher popover */
1369
+ .playground__session-popover {
1370
+ position: absolute;
1371
+ top: calc(100% + var(--fd-space-xs));
1372
+ left: 0;
1373
+ z-index: 50;
1374
+ min-width: 220px;
1375
+ max-width: 300px;
1376
+ padding: var(--fd-space-xs);
1377
+ background-color: var(--fd-background);
1378
+ border: 1px solid var(--fd-border);
1379
+ border-radius: var(--fd-radius-lg);
1380
+ box-shadow: var(--fd-shadow-lg);
1381
+ }
1382
+
1383
+ .playground__session-popover-divider {
1384
+ height: 1px;
1385
+ background-color: var(--fd-border-muted);
1386
+ margin: var(--fd-space-xs) 0;
1387
+ }
1388
+
1389
+ .playground__session-popover-row {
1390
+ display: flex;
1391
+ align-items: center;
1392
+ gap: 2px;
1393
+ }
1394
+
1395
+ .playground__session-popover-item {
1396
+ display: flex;
1397
+ align-items: center;
1398
+ gap: var(--fd-space-sm);
1399
+ flex: 1;
1400
+ min-width: 0;
1401
+ padding: var(--fd-space-sm) var(--fd-space-sm);
1402
+ border: none;
1403
+ border-radius: var(--fd-radius-sm);
1404
+ background: transparent;
1405
+ color: var(--fd-foreground);
1406
+ font-size: var(--fd-text-sm);
1407
+ text-align: left;
1408
+ cursor: pointer;
1409
+ transition: background-color var(--fd-transition-fast);
1410
+ white-space: nowrap;
1411
+ overflow: hidden;
1412
+ text-overflow: ellipsis;
1413
+ }
1414
+
1415
+ .playground__session-popover-item :global(svg) {
1416
+ flex-shrink: 0;
1417
+ color: var(--fd-muted-foreground);
1418
+ font-size: var(--fd-text-sm);
1419
+ }
1420
+
1421
+ .playground__session-popover-item span {
1422
+ overflow: hidden;
1423
+ text-overflow: ellipsis;
1424
+ }
1425
+
1426
+ .playground__session-popover-item:hover {
1427
+ background-color: var(--fd-muted);
1428
+ }
1429
+
1430
+ .playground__session-popover-item:disabled {
1431
+ opacity: 0.4;
1432
+ cursor: not-allowed;
1433
+ }
1434
+
1435
+ .playground__session-popover-item--new {
1436
+ color: var(--fd-primary);
1437
+ font-weight: 500;
1438
+ }
1439
+
1440
+ .playground__session-popover-item--new :global(svg) {
1441
+ color: var(--fd-primary);
1442
+ }
1443
+
1444
+ .playground__session-popover-item--active {
1445
+ font-weight: 500;
1446
+ }
1447
+
1448
+ :global(.playground__session-popover-check) {
1449
+ color: var(--fd-primary) !important;
1450
+ }
1451
+
1452
+ .playground__session-popover-delete {
1453
+ display: flex;
1454
+ align-items: center;
1455
+ justify-content: center;
1456
+ flex-shrink: 0;
1457
+ width: var(--fd-size-icon-btn);
1458
+ height: var(--fd-size-icon-btn);
1459
+ border: none;
1460
+ border-radius: var(--fd-radius-sm);
1461
+ background: transparent;
1462
+ color: var(--fd-muted-foreground);
1463
+ cursor: pointer;
1464
+ opacity: 0;
1465
+ transition: all var(--fd-transition-fast);
1466
+ }
1467
+
1468
+ .playground__session-popover-row:hover .playground__session-popover-delete {
1469
+ opacity: 1;
1470
+ }
1471
+
1472
+ .playground__session-popover-delete:hover {
1473
+ background-color: var(--fd-error-muted);
1474
+ color: var(--fd-error);
1475
+ opacity: 1;
1476
+ }
1477
+
995
1478
  /* Error */
996
1479
  .playground__error {
997
1480
  display: flex;