@flowdrop/flowdrop 1.10.0 → 1.12.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 (53) hide show
  1. package/dist/api/enhanced-client.d.ts +29 -16
  2. package/dist/api/enhanced-client.js +0 -14
  3. package/dist/components/Navbar.svelte +1 -10
  4. package/dist/components/Navbar.svelte.d.ts +1 -9
  5. package/dist/components/PipelineStatus.svelte +9 -12
  6. package/dist/components/WorkflowEditor.svelte +3 -0
  7. package/dist/components/interrupt/ChoicePrompt.svelte +24 -5
  8. package/dist/components/interrupt/ConfirmationPrompt.svelte +5 -0
  9. package/dist/components/interrupt/InterruptBubble.svelte +12 -0
  10. package/dist/components/interrupt/ReviewPrompt.svelte +20 -0
  11. package/dist/components/interrupt/TextInputPrompt.svelte +5 -0
  12. package/dist/components/nodes/GatewayNode.svelte +2 -6
  13. package/dist/components/nodes/WorkflowNode.svelte +2 -6
  14. package/dist/components/playground/ChatInput.svelte +359 -0
  15. package/dist/components/playground/ChatInput.svelte.d.ts +14 -0
  16. package/dist/components/playground/ChatPanel.svelte +100 -724
  17. package/dist/components/playground/ChatPanel.svelte.d.ts +9 -26
  18. package/dist/components/playground/ControlPanel.svelte +496 -0
  19. package/dist/components/playground/ControlPanel.svelte.d.ts +20 -0
  20. package/dist/components/playground/ExecutionConsole.svelte +163 -0
  21. package/dist/components/playground/ExecutionConsole.svelte.d.ts +14 -0
  22. package/dist/components/playground/MessageStream.svelte +283 -0
  23. package/dist/components/playground/MessageStream.svelte.d.ts +27 -0
  24. package/dist/components/playground/PipelineKanbanView.svelte +284 -0
  25. package/dist/components/playground/PipelineKanbanView.svelte.d.ts +11 -0
  26. package/dist/components/playground/PipelinePanel.svelte +204 -65
  27. package/dist/components/playground/PipelinePanel.svelte.d.ts +3 -1
  28. package/dist/components/playground/PipelineTableView.svelte +376 -0
  29. package/dist/components/playground/PipelineTableView.svelte.d.ts +11 -0
  30. package/dist/components/playground/Playground.svelte +262 -1200
  31. package/dist/components/playground/Playground.svelte.d.ts +0 -13
  32. package/dist/components/playground/PlaygroundApp.svelte +110 -0
  33. package/dist/components/playground/PlaygroundApp.svelte.d.ts +28 -0
  34. package/dist/components/playground/PlaygroundStudio.svelte +35 -61
  35. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -1
  36. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +22 -0
  37. package/dist/components/playground/pipelineViewUtils.svelte.js +77 -0
  38. package/dist/messages/defaults.d.ts +24 -0
  39. package/dist/messages/defaults.js +24 -0
  40. package/dist/playground/index.d.ts +8 -2
  41. package/dist/playground/index.js +8 -1
  42. package/dist/playground/mount.d.ts +59 -4
  43. package/dist/playground/mount.js +102 -9
  44. package/dist/stores/playgroundStore.svelte.d.ts +6 -0
  45. package/dist/stores/playgroundStore.svelte.js +21 -1
  46. package/dist/svelte-app.d.ts +2 -10
  47. package/dist/types/index.d.ts +28 -2
  48. package/dist/types/navbar.d.ts +14 -0
  49. package/dist/types/navbar.js +1 -0
  50. package/dist/types/playground.d.ts +5 -2
  51. package/dist/types/playground.js +5 -7
  52. package/dist/utils/nodeStatus.js +15 -5
  53. package/package.json +1 -1
@@ -1,20 +1,27 @@
1
1
  <!--
2
2
  Playground Component
3
-
4
- Main component for the Playground feature.
5
- Clean, conversational interface similar to Langflow.
6
- Supports both embedded (panel) and standalone (page) modes.
7
- Styled with BEM syntax.
3
+
4
+ Three-pane interactive workflow runtime. Hosts session and execution logic
5
+ for the playground feature; delegates rendering to ExecutionConsole (top
6
+ right) and ControlPanel (bottom right) with a draggable vertical resizer
7
+ between them.
8
+
9
+ Used by PlaygroundStudio (standalone), PlaygroundModal (modal), and the
10
+ /workflow/[id]/playground/[sessionId] route.
8
11
  -->
9
12
 
10
13
  <script lang="ts">
11
- import { onMount, onDestroy } from 'svelte';
14
+ import { onMount, onDestroy, untrack } from 'svelte';
12
15
  import Icon from '@iconify/svelte';
13
- import ChatPanel from './ChatPanel.svelte';
14
- import ExecutionList from './ExecutionList.svelte';
16
+ import ExecutionConsole from './ExecutionConsole.svelte';
17
+ import ControlPanel from './ControlPanel.svelte';
15
18
  import type { Workflow } from '../../types/index.js';
16
19
  import type { EndpointConfig } from '../../config/endpoints.js';
17
- import type { PlaygroundMode, PlaygroundConfig } from '../../types/playground.js';
20
+ import type {
21
+ PlaygroundMode,
22
+ PlaygroundConfig,
23
+ PlaygroundSessionStatus
24
+ } from '../../types/playground.js';
18
25
  import { playgroundService } from '../../services/playgroundService.js';
19
26
  import { interruptService } from '../../services/interruptService.js';
20
27
  import { setEndpointConfig } from '../../services/api.js';
@@ -25,41 +32,22 @@
25
32
  getIsLoading,
26
33
  getError,
27
34
  playgroundActions,
28
- getInputFields,
29
35
  applyServerResponse,
30
- getCanSendMessage,
31
- getLatestSequenceNumber,
32
- getActiveExecutionId,
33
- getLatestExecutionId,
34
- getPinnedExecutionId,
36
+ getLatestSequenceNumber
35
37
  } from '../../stores/playgroundStore.svelte.js';
36
38
  import { interruptActions } from '../../stores/interruptStore.svelte.js';
37
39
  import { logger } from '../../utils/logger.js';
38
- import { m } from '../../messages/index.js';
39
40
 
40
- /**
41
- * Component props
42
- */
43
41
  interface Props {
44
- /** Target workflow ID */
45
42
  workflowId: string;
46
- /** Pre-loaded workflow (optional, will be fetched if not provided) */
47
43
  workflow?: Workflow;
48
- /** Display mode: embedded (panel) or standalone (page) */
49
44
  mode?: PlaygroundMode;
50
- /** Resume a specific session */
51
45
  initialSessionId?: string;
52
- /** API endpoint configuration */
53
46
  endpointConfig?: EndpointConfig;
54
- /** Playground configuration options */
55
47
  config?: PlaygroundConfig;
56
- /** Callback when playground is closed (for embedded mode) */
57
48
  onClose?: () => void;
58
- /** Callback to toggle the pipeline panel (if undefined, toggle button is hidden) */
59
49
  onTogglePanel?: () => void;
60
- /** Whether the pipeline panel is currently open (for toggle button active state) */
61
50
  isPipelinePanelOpen?: boolean;
62
- /** When provided, session switches and creation navigate to a URL instead of mutating store state */
63
51
  onSessionNavigate?: (sessionId: string) => void;
64
52
  }
65
53
 
@@ -73,54 +61,69 @@
73
61
  onClose,
74
62
  onTogglePanel,
75
63
  isPipelinePanelOpen = false,
76
- onSessionNavigate,
64
+ onSessionNavigate
77
65
  }: Props = $props();
78
66
 
79
- /** Current input values from InputCollector */
80
- let inputValues = $state<Record<string, unknown>>({});
81
-
82
- /** Track session being edited for rename */
83
- let editingSessionId = $state<string | null>(null);
67
+ let loadedInitialSessionId = $state<string | undefined>(undefined);
68
+ let autoRunTriggered = $state(false);
69
+ let isRefreshing = $state(false);
84
70
 
85
- /** Track which session's dropdown menu is open */
86
- let openMenuId = $state<string | null>(null);
71
+ // Vertical resizer state for the ExecutionConsole ControlPanel split.
72
+ let playgroundContentEl = $state<HTMLElement | null>(null);
73
+ let controlPanelHeight = $state(280);
74
+ let isVerticalResizing = $state(false);
75
+ let containerHeight = $state(0);
76
+ let dragContainerBottom = 0;
87
77
 
88
- /** Whether the runs sub-section is expanded under the active session */
89
- let runsExpanded = $state(false);
78
+ $effect(() => {
79
+ if (!playgroundContentEl) return;
80
+ const observer = new ResizeObserver(([entry]) => {
81
+ containerHeight = entry.contentRect.height;
82
+ });
83
+ observer.observe(playgroundContentEl);
84
+ return () => observer.disconnect();
85
+ });
90
86
 
91
- /** Track if initial session has been loaded to prevent duplicate loads */
92
- let initialSessionLoaded = $state(false);
87
+ $effect(() => {
88
+ if (containerHeight > 0) {
89
+ controlPanelHeight = clampControlPanelHeight(untrack(() => controlPanelHeight));
90
+ }
91
+ });
93
92
 
94
- /** Track the session ID that was loaded to detect prop changes */
95
- let loadedInitialSessionId = $state<string | undefined>(undefined);
93
+ const maxControlPanelHeight = $derived(
94
+ containerHeight ? Math.round(containerHeight * 0.6) : 600
95
+ );
96
96
 
97
- /** Track if auto-run has already been triggered to prevent duplicate executions */
98
- let autoRunTriggered = $state(false);
97
+ function clampControlPanelHeight(h: number): number {
98
+ return Math.min(Math.max(h, 140), maxControlPanelHeight);
99
+ }
99
100
 
100
- /** Whether log messages are visible in the chat panel */
101
- let showLogs = $state(true);
101
+ function handleVerticalResizerPointerDown(e: PointerEvent) {
102
+ if (playgroundContentEl) dragContainerBottom = playgroundContentEl.getBoundingClientRect().bottom;
103
+ isVerticalResizing = true;
104
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
105
+ }
102
106
 
103
- /** Whether a manual refresh is in flight */
104
- let isRefreshing = $state(false);
107
+ function handleVerticalResizerPointerMove(e: PointerEvent) {
108
+ if (!isVerticalResizing) return;
109
+ controlPanelHeight = clampControlPanelHeight(dragContainerBottom - e.clientY);
110
+ }
105
111
 
106
- /** Whether the session switcher popover is open (standalone mode) */
107
- let sessionDropdownOpen = $state(false);
112
+ function handleVerticalResizerPointerUp() {
113
+ isVerticalResizing = false;
114
+ }
108
115
 
109
- // Close session popover on outside click
110
- $effect(() => {
111
- if (!sessionDropdownOpen) return;
112
- function handleOutside(e: MouseEvent) {
113
- if (!(e.target as HTMLElement).closest('.playground__session-chip-wrap')) {
114
- sessionDropdownOpen = false;
115
- }
116
+ function handleVerticalResizerKeyDown(e: KeyboardEvent) {
117
+ const step = e.shiftKey ? 50 : 20;
118
+ if (e.key === 'ArrowUp') {
119
+ e.preventDefault();
120
+ controlPanelHeight = clampControlPanelHeight(controlPanelHeight + step);
121
+ } else if (e.key === 'ArrowDown') {
122
+ e.preventDefault();
123
+ controlPanelHeight = clampControlPanelHeight(controlPanelHeight - step);
116
124
  }
117
- document.addEventListener('click', handleOutside);
118
- return () => document.removeEventListener('click', handleOutside);
119
- });
125
+ }
120
126
 
121
- /**
122
- * Initialize the playground on mount
123
- */
124
127
  onMount(() => {
125
128
  if (endpointConfig) setEndpointConfig(endpointConfig);
126
129
  if (workflow) playgroundActions.setWorkflow(workflow);
@@ -151,36 +154,17 @@
151
154
 
152
155
  /**
153
156
  * Handle reactive changes to initialSessionId prop
154
- * This allows the initial session to be set after mount
155
157
  */
156
158
  $effect(() => {
157
- // Skip if no initialSessionId provided
158
- if (!initialSessionId) {
159
- return;
160
- }
161
-
162
- // Skip if this session was already loaded or is currently loading.
163
- // loadedInitialSessionId is set synchronously at the start of loadInitialSession,
164
- // so this prevents the effect from spawning concurrent loads when isLoading changes.
165
- if (loadedInitialSessionId === initialSessionId) {
166
- return;
167
- }
159
+ if (!initialSessionId) return;
160
+ if (loadedInitialSessionId === initialSessionId) return;
168
161
 
169
- // Skip if sessions haven't been loaded yet (will be handled by onMount)
170
162
  const sessionList = getSessions();
171
- if (sessionList.length === 0 && getIsLoading()) {
172
- return;
173
- }
163
+ if (sessionList.length === 0) return;
174
164
 
175
- // Load the initial session if sessions are available
176
- if (sessionList.length > 0) {
177
- void loadInitialSession(initialSessionId);
178
- }
165
+ void loadInitialSession(initialSessionId);
179
166
  });
180
167
 
181
- /**
182
- * Initialize the playground: load sessions, load initial session, handle auto-run
183
- */
184
168
  async function initializePlayground(): Promise<void> {
185
169
  try {
186
170
  await loadSessions();
@@ -200,42 +184,27 @@
200
184
  }
201
185
  }
202
186
 
203
- /**
204
- * Load the initial session with validation and error handling
205
- *
206
- * @param sessionId - The session ID to load
207
- */
208
187
  async function loadInitialSession(sessionId: string): Promise<void> {
209
- // Validate session exists in loaded sessions before setting the guard.
210
- // If not found yet, we skip setting loadedInitialSessionId so the $effect
211
- // can retry when _sessions updates (e.g. after a new session is created).
212
188
  const sessionList = getSessions();
213
189
  const sessionExists = sessionList.some((s) => s.id === sessionId);
214
190
 
215
191
  if (!sessionExists) {
216
192
  logger.warn(
217
- `[Playground] Initial session "${sessionId}" not found in available sessions. ` +
218
- `Available sessions: ${sessionList.map((s) => s.id).join(', ') || 'none'}`
193
+ `[Playground] Initial session "${sessionId}" not found. ` +
194
+ `Available: ${sessionList.map((s) => s.id).join(', ') || 'none'}`
219
195
  );
220
- initialSessionLoaded = true;
221
196
  return;
222
197
  }
223
198
 
224
- // Set guard BEFORE the first await to prevent concurrent loads.
225
199
  loadedInitialSessionId = sessionId;
226
200
 
227
201
  try {
228
202
  await loadSession(sessionId);
229
- initialSessionLoaded = true;
230
203
  } catch (err) {
231
204
  logger.error('[Playground] Failed to load initial session:', err);
232
- initialSessionLoaded = true;
233
205
  }
234
206
  }
235
207
 
236
- /**
237
- * Cleanup on destroy
238
- */
239
208
  onDestroy(() => {
240
209
  playgroundService.stopPolling();
241
210
  interruptService.stopPolling();
@@ -243,23 +212,6 @@
243
212
  interruptActions.reset();
244
213
  });
245
214
 
246
- /**
247
- * Close dropdown menu when clicking outside
248
- */
249
- $effect(() => {
250
- if (!openMenuId) return;
251
-
252
- function onDocumentClick() {
253
- openMenuId = null;
254
- }
255
-
256
- document.addEventListener('click', onDocumentClick);
257
- return () => document.removeEventListener('click', onDocumentClick);
258
- });
259
-
260
- /**
261
- * Load sessions for the workflow
262
- */
263
215
  async function loadSessions(): Promise<void> {
264
216
  playgroundActions.setLoading(true);
265
217
  playgroundActions.setError(null);
@@ -276,9 +228,6 @@
276
228
  }
277
229
  }
278
230
 
279
- /**
280
- * Load a specific session and its messages
281
- */
282
231
  async function loadSession(sessionId: string): Promise<void> {
283
232
  playgroundActions.setLoading(true);
284
233
  playgroundActions.setError(null);
@@ -290,7 +239,7 @@
290
239
  const response = await playgroundService.getMessages(sessionId);
291
240
  applyServerResponse(response);
292
241
 
293
- if (session.status === 'running') {
242
+ if (session.status !== 'idle') {
294
243
  startPolling(sessionId, true);
295
244
  }
296
245
  } catch (err) {
@@ -302,9 +251,6 @@
302
251
  }
303
252
  }
304
253
 
305
- /**
306
- * Create a new session
307
- */
308
254
  async function handleCreateSession(): Promise<void> {
309
255
  playgroundActions.setLoading(true);
310
256
  playgroundActions.setError(null);
@@ -314,7 +260,6 @@
314
260
  const session = await playgroundService.createSession(workflowId, sessionName);
315
261
 
316
262
  if (onSessionNavigate) {
317
- // URL-based routing: navigate to the new session; page remount handles store init
318
263
  onSessionNavigate(session.id);
319
264
  return;
320
265
  }
@@ -331,31 +276,21 @@
331
276
  }
332
277
  }
333
278
 
334
- /**
335
- * Select a session
336
- */
337
279
  async function handleSelectSession(sessionId: string): Promise<void> {
338
280
  playgroundActions.pinExecution(null);
339
- runsExpanded = false;
340
281
  const currentSessionId = getCurrentSession()?.id;
341
- if (currentSessionId === sessionId) {
342
- return;
343
- }
282
+ if (currentSessionId === sessionId) return;
344
283
 
345
284
  playgroundService.stopPolling();
346
285
  playgroundActions.updateSessionStatus('idle');
347
286
  await loadSession(sessionId);
348
287
  }
349
288
 
350
- /**
351
- * Delete a session
352
- */
353
289
  async function handleDeleteSession(sessionId: string): Promise<void> {
354
290
  try {
355
291
  await playgroundService.deleteSession(sessionId);
356
292
  playgroundActions.removeSession(sessionId);
357
293
 
358
- // If we deleted the current session, clear it
359
294
  if (getCurrentSession()?.id === sessionId) {
360
295
  playgroundService.stopPolling();
361
296
  }
@@ -366,51 +301,12 @@
366
301
  }
367
302
  }
368
303
 
369
- /**
370
- * Toggle session dropdown menu
371
- */
372
- function handleMenuToggle(event: Event, sessionId: string): void {
373
- event.stopPropagation();
374
- openMenuId = openMenuId === sessionId ? null : sessionId;
375
- }
376
-
377
- /**
378
- * Handle delete from dropdown menu
379
- */
380
- function handleMenuDelete(event: Event, sessionId: string): void {
381
- event.stopPropagation();
382
- openMenuId = null;
383
- void handleDeleteSession(sessionId);
384
- }
385
-
386
- /**
387
- * Close current session (go back to welcome)
388
- */
389
- function handleCloseSession(): void {
390
- playgroundService.stopPolling();
391
- interruptService.stopPolling();
392
- playgroundActions.setCurrentSession(null);
393
- playgroundActions.clearMessages();
394
- // Clear interrupts for this session
395
- const sessionId = getCurrentSession()?.id;
396
- if (sessionId) {
397
- interruptActions.clearSessionInterrupts(sessionId);
398
- }
399
- }
400
-
401
- /**
402
- * Send a message
403
- */
404
304
  async function handleSendMessage(content: string): Promise<void> {
405
305
  if (getIsExecuting()) return;
406
306
 
407
- const session = getCurrentSession();
408
- if (!session) {
307
+ if (!getCurrentSession()) {
409
308
  await handleCreateSession();
410
- const newSession = getCurrentSession();
411
- if (!newSession) {
412
- return;
413
- }
309
+ if (!getCurrentSession()) return;
414
310
  }
415
311
 
416
312
  const sessionId = getCurrentSession()!.id;
@@ -420,22 +316,13 @@
420
316
  playgroundActions.setError(null);
421
317
 
422
318
  try {
423
- const inputs: Record<string, unknown> = {};
424
- const fields = getInputFields();
425
-
426
- fields.forEach((field) => {
427
- const key = `${field.nodeId}:${field.fieldId}`;
428
- if (inputValues[key] !== undefined) {
429
- if (!inputs[field.nodeId]) {
430
- inputs[field.nodeId] = {};
431
- }
432
- (inputs[field.nodeId] as Record<string, unknown>)[field.fieldId] = inputValues[key];
433
- }
434
- });
435
-
436
- const message = await playgroundService.sendMessage(sessionId, content, inputs);
319
+ const message = await playgroundService.sendMessage(sessionId, content, {});
437
320
  playgroundActions.addMessage(message);
438
- startPolling(sessionId);
321
+ // Only start polling if not already active — avoids resetting the cursor
322
+ // mid-session and re-fetching messages that are already in the store.
323
+ if (!playgroundService.isPolling()) {
324
+ startPolling(sessionId);
325
+ }
439
326
  } catch (err) {
440
327
  const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
441
328
  playgroundActions.setError(errorMessage);
@@ -444,14 +331,9 @@
444
331
  }
445
332
  }
446
333
 
447
- /**
448
- * Stop execution
449
- */
450
334
  async function handleStopExecution(): Promise<void> {
451
335
  const sessionId = getCurrentSession()?.id;
452
- if (!sessionId) {
453
- return;
454
- }
336
+ if (!sessionId) return;
455
337
 
456
338
  try {
457
339
  await playgroundService.stopExecution(sessionId);
@@ -466,10 +348,11 @@
466
348
  }
467
349
  }
468
350
 
469
- /**
470
- * Start polling for messages
471
- */
472
- function startPolling(sessionId: string, seedSequence = false): void {
351
+ function startPolling(
352
+ sessionId: string,
353
+ seedSequence = false,
354
+ overrideShouldStopPolling?: (status: PlaygroundSessionStatus) => boolean
355
+ ): void {
473
356
  const pollingInterval = config.pollingInterval ?? 1500;
474
357
  const initialSequenceNumber = seedSequence ? getLatestSequenceNumber() : null;
475
358
 
@@ -477,15 +360,11 @@
477
360
  sessionId,
478
361
  (response) => applyServerResponse(response),
479
362
  pollingInterval,
480
- config.shouldStopPolling,
363
+ overrideShouldStopPolling ?? config.shouldStopPolling,
481
364
  initialSequenceNumber
482
365
  );
483
366
  }
484
367
 
485
- /**
486
- * Fetch the latest messages and session status from the server.
487
- * Resumes polling if the session is running but polling had stopped.
488
- */
489
368
  async function refreshFromServer(): Promise<void> {
490
369
  const sessionId = getCurrentSession()?.id;
491
370
  if (!sessionId || isRefreshing) return;
@@ -506,53 +385,27 @@
506
385
  }
507
386
  }
508
387
 
509
- /**
510
- * Refresh messages for the current session
511
- * Called after interrupt resolution when polling has stopped
512
- */
513
388
  async function handleInterruptResolved(): Promise<void> {
514
389
  const sessionId = getCurrentSession()?.id;
515
390
  if (!sessionId) return;
516
391
 
517
392
  try {
518
- const response = await playgroundService.getMessages(sessionId);
393
+ // Catch up immediately rather than waiting for the next poll interval.
394
+ // Use the service's sequence cursor so we only fetch new messages.
395
+ const response = await playgroundService.getMessages(
396
+ sessionId,
397
+ playgroundService.getLastSequenceNumber() ?? undefined
398
+ );
519
399
  applyServerResponse(response);
520
-
521
- if (response.sessionStatus === 'running') {
522
- startPolling(sessionId, true);
523
- }
524
400
  } catch (err) {
525
401
  logger.error('[Playground] Failed to refresh after interrupt:', err);
526
402
  }
527
- }
528
403
 
529
- /**
530
- * Format date for display
531
- */
532
- function formatDate(dateString: string): string {
533
- const date = new Date(dateString);
534
- const now = new Date();
535
- const diffMs = now.getTime() - date.getTime();
536
- const diffMins = Math.floor(diffMs / 60000);
537
- const diffHours = Math.floor(diffMs / 3600000);
538
- const diffDays = Math.floor(diffMs / 86400000);
539
-
540
- if (diffMins < 1) {
541
- return 'Just now';
542
- }
543
- if (diffMins < 60) {
544
- return `${diffMins}m ago`;
545
- }
546
- if (diffHours < 24) {
547
- return `${diffHours}h ago`;
404
+ // Polling continues through awaiting_input now, but restart defensively
405
+ // in case it stopped for any reason (e.g. component re-mount).
406
+ if (!playgroundService.isPolling()) {
407
+ startPolling(sessionId, true);
548
408
  }
549
- if (diffDays < 7) {
550
- return `${diffDays}d ago`;
551
- }
552
- return date.toLocaleDateString('en-US', {
553
- month: 'short',
554
- day: 'numeric'
555
- });
556
409
  }
557
410
  </script>
558
411
 
@@ -561,324 +414,100 @@
561
414
  class:playground--embedded={mode === 'embedded'}
562
415
  class:playground--standalone={mode === 'standalone'}
563
416
  class:playground--modal={mode === 'modal'}
564
- class:playground--no-sidebar={config.showSidebar === false}
565
417
  >
566
- <div class="playground__container">
567
- <!-- Sidebar — hidden in standalone mode (session switcher lives in the header chip instead) -->
568
- {#if config.showSidebar === true || (config.showSidebar !== false && mode !== 'standalone')}
569
- <aside
570
- class="playground__sidebar"
571
- style={config.sidebarWidth ? `--fd-playground-sidebar-width: ${config.sidebarWidth}` : ''}
572
- >
573
- <!-- Sidebar Header -->
574
- <div class="playground__sidebar-header">
575
- <div class="playground__sidebar-title">
576
- <span>Playground</span>
577
- </div>
578
- <a
579
- href="/workflow/{workflowId}/edit"
580
- class="playground__edit-link"
581
- title="Edit workflow"
582
- >
583
- <Icon icon="mdi:pencil-outline" />
584
- </a>
585
- {#if (mode === 'embedded' || mode === 'modal') && onClose}
586
- <button
587
- type="button"
588
- class="playground__sidebar-close"
589
- onclick={onClose}
590
- title="Close playground"
591
- >
592
- {#if mode === 'modal'}
593
- <Icon icon="mdi:close" />
594
- {:else}
595
- <Icon icon="mdi:dock-right" />
596
- {/if}
597
- </button>
598
- {/if}
599
- </div>
600
-
601
- <!-- Sessions Section -->
602
- <div class="playground__section">
603
- <!-- Section header with inline add button -->
604
- <div class="playground__section-header">
605
- <span class="playground__section-label">Sessions</span>
606
- <button
607
- type="button"
608
- class="playground__section-add"
609
- onclick={handleCreateSession}
610
- disabled={getIsLoading()}
611
- title="New session"
612
- >
613
- <Icon icon="mdi:plus" />
614
- </button>
615
- </div>
616
-
617
- <!-- Sessions List -->
618
- <div class="playground__sessions-wrap">
619
- <div class="playground__sessions">
620
- {#if getSessions().length === 0 && !getIsLoading()}
621
- <div class="playground__sessions-empty">
622
- <span>No sessions yet</span>
623
- </div>
624
- {:else}
625
- {#each getSessions() as session (session.id)}
626
- {@const isActive = getCurrentSession()?.id === session.id}
627
- <div class="playground__session-group">
628
- <div
629
- class="playground__session"
630
- class:playground__session--active={isActive}
631
- role="button"
632
- tabindex="0"
633
- title="Click to load this session"
634
- aria-label={m().layout.loadSession({ name: session.name })}
635
- onclick={() => handleSelectSession(session.id)}
636
- onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
637
- >
638
- <span class="playground__session-name" title={session.name}>
639
- {session.name}
640
- </span>
641
- <div class="playground__session-actions">
642
- <button
643
- type="button"
644
- class="playground__session-menu"
645
- class:playground__session-menu--open={openMenuId === session.id}
646
- onclick={(e) => handleMenuToggle(e, session.id)}
647
- title="Session options"
648
- >
649
- <Icon icon="mdi:dots-vertical" />
650
- </button>
651
- {#if openMenuId === session.id}
652
- <div class="playground__session-dropdown">
653
- <button
654
- type="button"
655
- class="playground__session-dropdown-item playground__session-dropdown-item--danger"
656
- onclick={(e) => handleMenuDelete(e, session.id)}
657
- >
658
- <Icon icon="mdi:delete-outline" />
659
- <span>Delete</span>
660
- </button>
661
- </div>
662
- {/if}
663
- </div>
664
- </div>
665
- <!-- Collapsible runs sub-section under active session -->
666
- {#if isActive && getCurrentSession()?.executions?.length}
667
- <div class="playground__runs-section">
668
- <button
669
- type="button"
670
- class="playground__runs-toggle"
671
- onclick={() => (runsExpanded = !runsExpanded)}
672
- >
673
- <Icon icon={runsExpanded ? 'mdi:chevron-down' : 'mdi:chevron-right'} />
674
- <span>Runs</span>
675
- <span class="playground__runs-count">{getCurrentSession()!.executions!.length}</span>
676
- </button>
677
- {#if runsExpanded}
678
- <div class="playground__executions-inline">
679
- <ExecutionList
680
- executions={getCurrentSession()!.executions!}
681
- activeExecutionId={getActiveExecutionId()}
682
- latestExecutionId={getLatestExecutionId()}
683
- onSelect={(id) => {
684
- if (id === getLatestExecutionId()) {
685
- playgroundActions.pinExecution(null);
686
- } else {
687
- playgroundActions.pinExecution(id);
688
- }
689
- }}
690
- />
691
- </div>
692
- {/if}
693
- </div>
694
- {/if}
695
- </div>
696
- {/each}
697
- {/if}
698
- </div>
699
- </div>
700
- </div>
701
- </aside>
418
+ <main class="playground__main">
419
+ {#if getError()}
420
+ <div class="playground__error">
421
+ <Icon icon="mdi:alert-circle" />
422
+ <span>{getError()}</span>
423
+ <button
424
+ type="button"
425
+ class="playground__error-dismiss"
426
+ onclick={() => playgroundActions.setError(null)}
427
+ >
428
+ <Icon icon="mdi:close" />
429
+ </button>
430
+ </div>
702
431
  {/if}
703
432
 
704
- <!-- Main Content -->
705
- <main class="playground__main">
706
- <!-- Session Header -->
707
- {#if mode === 'standalone' || (getCurrentSession() && config.showSessionHeader !== false)}
708
- <header class="playground__header">
709
- {#if mode === 'standalone'}
710
- <!-- Panel icon + label (mirrors PipelinePanel header) -->
711
- <Icon icon="mdi:message-text-outline" class="playground__header-icon" />
712
- <span class="playground__header-label">Sessions</span>
713
-
714
- <!-- Session chip — switches sessions via popover -->
715
- <div class="playground__session-chip-wrap">
716
- <button
717
- type="button"
718
- class="playground__session-chip"
719
- class:playground__session-chip--open={sessionDropdownOpen}
720
- onclick={() => (sessionDropdownOpen = !sessionDropdownOpen)}
721
- title="Switch session"
722
- >
723
- <span class="playground__session-chip-name">
724
- {getCurrentSession()?.name ?? 'No session'}
725
- </span>
726
- <Icon icon={sessionDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'} class="playground__session-chip-chevron" />
727
- </button>
728
-
729
- {#if sessionDropdownOpen}
730
- <div class="playground__session-popover">
731
- <button
732
- type="button"
733
- class="playground__session-popover-item playground__session-popover-item--new"
734
- disabled={getIsLoading()}
735
- onclick={() => { sessionDropdownOpen = false; void handleCreateSession(); }}
736
- >
737
- <Icon icon="mdi:plus" />
738
- <span>New session</span>
739
- </button>
740
- {#if getSessions().length > 0}
741
- <div class="playground__session-popover-divider"></div>
742
- {#each getSessions() as session (session.id)}
743
- {@const isActive = getCurrentSession()?.id === session.id}
744
- <div class="playground__session-popover-row">
745
- <button
746
- type="button"
747
- class="playground__session-popover-item"
748
- class:playground__session-popover-item--active={isActive}
749
- onclick={() => {
750
- sessionDropdownOpen = false;
751
- if (onSessionNavigate) {
752
- onSessionNavigate(session.id);
753
- } else {
754
- void handleSelectSession(session.id);
755
- }
756
- }}
757
- >
758
- {#if isActive}
759
- <Icon icon="mdi:check" class="playground__session-popover-check" />
760
- {:else}
761
- <Icon icon="mdi:message-outline" />
762
- {/if}
763
- <span>{session.name}</span>
764
- </button>
765
- <button
766
- type="button"
767
- class="playground__session-popover-delete"
768
- onclick={(e) => { handleMenuDelete(e, session.id); sessionDropdownOpen = false; }}
769
- title="Delete session"
770
- >
771
- <Icon icon="mdi:delete-outline" />
772
- </button>
773
- </div>
774
- {/each}
775
- {/if}
776
- </div>
777
- {/if}
778
- </div>
779
- {:else}
780
- <!-- Embedded / modal: original title + close -->
781
- <div class="playground__header-group">
782
- <h2 class="playground__header-title">{getCurrentSession()?.name}</h2>
783
- <button
784
- type="button"
785
- class="playground__header-close"
786
- onclick={handleCloseSession}
787
- title="Close session"
788
- >
789
- <Icon icon="mdi:close" />
790
- </button>
791
- </div>
792
- {/if}
793
-
794
- <div class="playground__header-actions">
795
- {#if mode === 'standalone' && onTogglePanel}
796
- <button
797
- type="button"
798
- class="playground__log-toggle"
799
- class:playground__log-toggle--active={isPipelinePanelOpen}
800
- onclick={onTogglePanel}
801
- title={isPipelinePanelOpen ? 'Hide pipeline' : 'Show pipeline'}
802
- >
803
- <Icon icon="mdi:source-branch" />
804
- Pipeline
805
- </button>
806
- {/if}
807
- {#if getCurrentSession()}
808
- <button
809
- type="button"
810
- class="playground__log-toggle"
811
- class:playground__refresh--spinning={isRefreshing}
812
- onclick={() => void refreshFromServer()}
813
- disabled={isRefreshing}
814
- title="Refresh status"
815
- >
816
- <Icon icon="mdi:refresh" />
817
- Refresh
818
- </button>
819
- {/if}
820
- <button
821
- type="button"
822
- class="playground__log-toggle"
823
- class:playground__log-toggle--active={showLogs}
824
- onclick={() => (showLogs = !showLogs)}
825
- title={showLogs ? 'Hide log messages' : 'Show log messages'}
826
- >
827
- <Icon icon="mdi:console" />
828
- Logs
829
- </button>
830
- </div>
831
- </header>
832
- {/if}
833
-
834
- <!-- Error Banner -->
835
- {#if getError()}
836
- <div class="playground__error">
837
- <Icon icon="mdi:alert-circle" />
838
- <span>{getError()}</span>
839
- <button
840
- type="button"
841
- class="playground__error-dismiss"
842
- onclick={() => playgroundActions.setError(null)}
843
- >
844
- <Icon icon="mdi:close" />
845
- </button>
433
+ <div
434
+ class="playground__content"
435
+ bind:this={playgroundContentEl}
436
+ >
437
+ {#if getIsLoading() && !getCurrentSession()}
438
+ <div class="playground__loading">
439
+ <Icon icon="mdi:loading" class="playground__loading-icon" />
440
+ <span>Loading...</span>
441
+ </div>
442
+ {:else}
443
+ <ExecutionConsole
444
+ showTimestamps={config.showTimestamps ?? true}
445
+ autoScroll={config.autoScroll ?? true}
446
+ enableMarkdown={config.enableMarkdown ?? true}
447
+ showLogsInline={config.logDisplayMode === 'inline'}
448
+ onInterruptResolved={handleInterruptResolved}
449
+ onCreateSession={getSessions().length === 0 ? handleCreateSession : undefined}
450
+ />
451
+
452
+ <div
453
+ class="playground__vertical-resizer"
454
+ class:playground__vertical-resizer--active={isVerticalResizing}
455
+ role="separator"
456
+ aria-orientation="horizontal"
457
+ aria-valuenow={Math.round(controlPanelHeight)}
458
+ aria-valuemin={140}
459
+ aria-valuemax={maxControlPanelHeight}
460
+ aria-label="Resize execution console"
461
+ tabindex="0"
462
+ onpointerdown={handleVerticalResizerPointerDown}
463
+ onpointermove={handleVerticalResizerPointerMove}
464
+ onpointerup={handleVerticalResizerPointerUp}
465
+ onpointercancel={handleVerticalResizerPointerUp}
466
+ onkeydown={handleVerticalResizerKeyDown}
467
+ >
468
+ <div class="playground__vertical-resizer-handle"></div>
846
469
  </div>
847
- {/if}
848
470
 
849
- <!-- Chat Content -->
850
- <div class="playground__content">
851
- {#if getIsLoading() && !getCurrentSession()}
852
- <div class="playground__loading">
853
- <Icon icon="mdi:loading" class="playground__loading-icon" />
854
- <span>Loading...</span>
855
- </div>
856
- {:else}
857
- <ChatPanel
858
- showTimestamps={config.showTimestamps ?? true}
859
- autoScroll={config.autoScroll ?? true}
860
- showLogsInline={config.logDisplayMode === 'inline'}
861
- enableMarkdown={config.enableMarkdown ?? true}
862
- showChatInput={config.showChatInput ?? true}
863
- showRunButton={config.showRunButton ?? true}
864
- predefinedMessage={config.predefinedMessage ?? 'Run workflow'}
865
- onSendMessage={handleSendMessage}
866
- onStopExecution={handleStopExecution}
867
- onInterruptResolved={handleInterruptResolved}
868
- bind:showLogs
869
- />
870
- {/if}
871
- </div>
872
- </main>
873
- </div>
471
+ <ControlPanel
472
+ style="height: {controlPanelHeight}px; flex-shrink: 0;"
473
+ {isPipelinePanelOpen}
474
+ {onTogglePanel}
475
+ {isRefreshing}
476
+ {onSessionNavigate}
477
+ onCreateSession={handleCreateSession}
478
+ onSelectSession={handleSelectSession}
479
+ onDeleteSession={handleDeleteSession}
480
+ onSendMessage={handleSendMessage}
481
+ onStopExecution={handleStopExecution}
482
+ onRefresh={refreshFromServer}
483
+ showChatInput={config.showChatInput ?? true}
484
+ showRunButton={config.showRunButton ?? true}
485
+ predefinedMessage={config.predefinedMessage}
486
+ />
487
+ {/if}
488
+ </div>
489
+ </main>
490
+
491
+ {#if (mode === 'embedded' || mode === 'modal') && onClose}
492
+ <button
493
+ type="button"
494
+ class="playground__floating-close"
495
+ onclick={onClose}
496
+ title="Close playground"
497
+ aria-label="Close playground"
498
+ >
499
+ <Icon icon={mode === 'modal' ? 'mdi:close' : 'mdi:dock-right'} />
500
+ </button>
501
+ {/if}
874
502
  </div>
875
503
 
876
504
  <style>
877
505
  .playground {
506
+ position: relative;
878
507
  display: flex;
879
508
  flex-direction: column;
880
509
  height: 100%;
881
- overflow: hidden; /* Prevent playground-level scrolling */
510
+ overflow: clip; /* clip avoids the BFC that overflow:hidden creates, which breaks position:sticky inside */
882
511
  background-color: var(--fd-muted);
883
512
  font-family:
884
513
  -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@@ -890,720 +519,153 @@
890
519
  }
891
520
 
892
521
  .playground--standalone {
893
- height: 100vh;
894
522
  background: var(--fd-layout-background, var(--fd-muted));
895
523
  }
896
524
 
897
- /* Dark mode override for standalone */
898
525
  :global([data-theme='dark']) .playground--standalone {
899
526
  background: linear-gradient(135deg, #141418 0%, #1a1a2e 50%, #16162a 100%);
900
527
  }
901
528
 
902
529
  .playground--modal {
903
- height: 100%;
904
530
  width: 100%;
905
531
  }
906
532
 
907
- /* No sidebar mode - minimal chat widget experience */
908
- .playground--no-sidebar .playground__main {
909
- border-left: none;
910
- }
911
-
912
- /* Container */
913
- .playground__container {
914
- display: flex;
533
+ .playground__main {
915
534
  flex: 1;
916
- min-height: 0;
917
- }
918
-
919
- /* Sidebar */
920
- .playground__sidebar {
921
- width: var(--fd-playground-sidebar-width);
922
- background-color: var(--fd-background);
923
- border-right: 1px solid var(--fd-border);
924
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
925
535
  display: flex;
926
536
  flex-direction: column;
537
+ min-width: 0;
538
+ min-height: 0;
539
+ overflow: clip; /* clip avoids the BFC that overflow:hidden creates, which breaks position:sticky inside */
540
+ background-color: var(--fd-background);
927
541
  }
928
542
 
929
- /* Fixed height so sidebar and main session header align on same horizontal line */
930
- .playground__sidebar-header {
931
- display: flex;
932
- align-items: center;
933
- justify-content: space-between;
934
- height: var(--fd-playground-header-height);
935
- padding: 0 var(--fd-space-xl);
936
- border-bottom: 1px solid var(--fd-border);
937
- box-sizing: border-box;
938
- flex-shrink: 0;
939
- }
940
-
941
- .playground__sidebar-title {
543
+ .playground__error {
942
544
  display: flex;
943
545
  align-items: center;
944
546
  gap: var(--fd-space-xs);
945
- font-size: var(--fd-text-md);
946
- font-weight: 600;
947
- line-height: 1.25;
948
- color: var(--fd-foreground);
949
- }
950
-
951
- .playground__edit-link {
952
- display: flex;
953
- align-items: center;
954
- justify-content: center;
955
- width: var(--fd-playground-icon-btn-size);
956
- height: var(--fd-playground-icon-btn-size);
957
- border-radius: var(--fd-radius-md);
958
- color: var(--fd-muted-foreground);
959
- text-decoration: none;
960
- transition: all var(--fd-transition-fast);
547
+ padding: var(--fd-space-md) var(--fd-space-xl);
548
+ background-color: var(--fd-error-muted);
549
+ border-bottom: 1px solid var(--fd-error);
550
+ color: var(--fd-error);
551
+ font-size: var(--fd-text-sm);
961
552
  flex-shrink: 0;
962
553
  }
963
554
 
964
- .playground__edit-link:hover {
965
- background-color: var(--fd-muted);
966
- color: var(--fd-foreground);
967
- }
968
-
969
- .playground__sidebar-close {
555
+ .playground__error-dismiss {
556
+ margin-left: auto;
970
557
  display: flex;
971
558
  align-items: center;
972
559
  justify-content: center;
973
- width: var(--fd-playground-icon-btn-size);
974
- height: var(--fd-playground-icon-btn-size);
560
+ width: var(--fd-space-3xl);
561
+ height: var(--fd-space-3xl);
975
562
  border: none;
976
- border-radius: var(--fd-radius-md);
563
+ border-radius: var(--fd-radius-sm);
977
564
  background: transparent;
978
- color: var(--fd-muted-foreground);
565
+ color: var(--fd-error);
979
566
  cursor: pointer;
980
- transition: all var(--fd-transition-fast);
567
+ transition: background-color var(--fd-transition-fast);
981
568
  }
982
569
 
983
- .playground__sidebar-close:hover {
984
- background-color: var(--fd-muted);
985
- color: var(--fd-foreground);
570
+ .playground__error-dismiss:hover {
571
+ background-color: var(--fd-error-muted);
986
572
  }
987
573
 
988
- /* Section */
989
- .playground__section {
574
+ .playground__content {
990
575
  flex: 1;
991
- display: flex;
992
- flex-direction: column;
993
576
  min-height: 0;
994
- padding: 0 var(--fd-space-md);
995
- }
996
-
997
- /* Section header: label + add icon */
998
- .playground__section-header {
999
577
  display: flex;
1000
- align-items: center;
1001
- justify-content: space-between;
1002
- padding: var(--fd-space-md) var(--fd-space-xs) var(--fd-space-xs);
1003
- }
1004
-
1005
- .playground__section-label {
1006
- font-size: var(--fd-text-xs);
1007
- font-weight: 600;
1008
- color: var(--fd-muted-foreground);
1009
- text-transform: uppercase;
1010
- letter-spacing: 0.06em;
578
+ flex-direction: column;
1011
579
  }
1012
580
 
1013
- .playground__section-add {
581
+ .playground__vertical-resizer {
582
+ height: 8px;
583
+ flex-shrink: 0;
584
+ cursor: row-resize;
585
+ background-color: var(--fd-background);
586
+ border-top: 1px solid var(--fd-border);
587
+ border-bottom: 1px solid var(--fd-border);
1014
588
  display: flex;
1015
589
  align-items: center;
1016
590
  justify-content: center;
1017
- width: var(--fd-playground-icon-btn-size);
1018
- height: var(--fd-playground-icon-btn-size);
1019
- border: none;
1020
- border-radius: var(--fd-radius-md);
1021
- background: transparent;
1022
- color: var(--fd-muted-foreground);
1023
- cursor: pointer;
1024
- transition: all var(--fd-transition-fast);
1025
- }
1026
-
1027
- .playground__section-add:hover:not(:disabled) {
1028
- background-color: var(--fd-muted);
1029
- color: var(--fd-foreground);
1030
- }
1031
-
1032
- .playground__section-add:disabled {
1033
- opacity: 0.4;
1034
- cursor: not-allowed;
1035
- }
1036
-
1037
- /* Session group wraps session row + its inline runs */
1038
- .playground__session-group {
1039
- margin-bottom: var(--fd-space-3xs);
591
+ touch-action: none;
592
+ z-index: 1;
593
+ transition: background-color var(--fd-transition-normal);
1040
594
  }
1041
595
 
1042
- /* Collapsible runs sub-section under active session */
1043
- .playground__runs-section {
1044
- margin-bottom: var(--fd-space-3xs);
596
+ .playground__vertical-resizer:hover,
597
+ .playground__vertical-resizer--active {
598
+ background-color: var(--fd-primary-muted);
1045
599
  }
1046
600
 
1047
- .playground__runs-toggle {
1048
- display: flex;
1049
- align-items: center;
1050
- gap: var(--fd-space-3xs);
1051
- width: 100%;
1052
- padding: var(--fd-space-3xs) var(--fd-space-sm);
1053
- padding-left: calc(var(--fd-space-md) + var(--fd-space-3xs));
1054
- border: none;
601
+ .playground__vertical-resizer-handle {
602
+ width: 48px;
603
+ height: 4px;
604
+ background-color: var(--fd-border-strong);
1055
605
  border-radius: var(--fd-radius-sm);
1056
- background: transparent;
1057
- color: var(--fd-muted-foreground);
1058
- font-size: var(--fd-text-xs);
1059
- font-weight: 500;
1060
- cursor: pointer;
1061
- text-align: left;
1062
- transition: all var(--fd-transition-fast);
1063
- }
1064
-
1065
- .playground__runs-toggle:hover {
1066
- background-color: var(--fd-muted);
1067
- color: var(--fd-foreground);
1068
- }
1069
-
1070
- .playground__runs-toggle :global(svg) {
1071
- width: 0.875rem;
1072
- height: 0.875rem;
1073
- flex-shrink: 0;
1074
- }
1075
-
1076
- .playground__runs-count {
1077
- margin-left: auto;
1078
- font-size: var(--fd-text-2xs);
1079
- font-weight: 600;
1080
- color: var(--fd-muted-foreground);
1081
- background: var(--fd-muted);
1082
- border-radius: 999px;
1083
- padding: 1px var(--fd-space-xs);
1084
- min-width: 1.4em;
1085
- text-align: center;
1086
- line-height: 1.4;
1087
- }
1088
-
1089
- /* Inline runs tree under active session */
1090
- .playground__executions-inline {
1091
- margin-left: calc(var(--fd-space-md) + var(--fd-space-xs));
1092
- margin-bottom: var(--fd-space-xs);
1093
- border-left: 2px solid var(--fd-border);
1094
- padding-left: var(--fd-space-xs);
606
+ transition:
607
+ background-color var(--fd-transition-normal),
608
+ transform var(--fd-transition-normal);
1095
609
  }
1096
610
 
1097
- .playground__executions-inline :global(.execution-list__item) {
1098
- padding: var(--fd-space-xs) var(--fd-space-sm);
1099
- font-size: var(--fd-text-xs);
1100
- border-radius: var(--fd-radius-sm);
1101
- border-left-width: 2px;
611
+ .playground__vertical-resizer:hover .playground__vertical-resizer-handle {
612
+ background-color: var(--fd-primary);
613
+ transform: scaleX(1.1);
1102
614
  }
1103
615
 
1104
- .playground__executions-inline :global(.execution-list) {
1105
- gap: 1px;
1106
- padding: var(--fd-space-3xs) 0;
616
+ .playground__vertical-resizer--active .playground__vertical-resizer-handle {
617
+ background-color: var(--fd-primary-hover);
618
+ transform: scaleX(1.2);
1107
619
  }
1108
620
 
1109
- /* Sessions */
1110
- .playground__sessions-wrap {
1111
- flex: 1;
621
+ .playground__loading {
1112
622
  display: flex;
1113
623
  flex-direction: column;
1114
- min-height: 0;
624
+ align-items: center;
625
+ justify-content: center;
626
+ flex: 1;
627
+ gap: var(--fd-space-xl);
628
+ color: var(--fd-muted-foreground);
1115
629
  }
1116
630
 
1117
- .playground__sessions {
1118
- flex: 1;
1119
- overflow-y: auto;
1120
- padding: 0 var(--fd-space-3xs) var(--fd-space-xl);
1121
- min-height: 0;
631
+ :global(.playground__loading-icon) {
632
+ font-size: var(--fd-text-2xl);
633
+ animation: playground-spin 1s linear infinite;
1122
634
  }
1123
635
 
1124
- .playground__sessions-empty {
1125
- padding: var(--fd-space-xl);
1126
- text-align: center;
1127
- font-size: var(--fd-text-xsm);
1128
- color: var(--fd-muted-foreground);
636
+ @keyframes playground-spin {
637
+ from {
638
+ transform: rotate(0deg);
639
+ }
640
+ to {
641
+ transform: rotate(360deg);
642
+ }
1129
643
  }
1130
644
 
1131
- /* Session row - clickable to load session; clear hover/active affordance */
1132
- .playground__session {
645
+ .playground__floating-close {
646
+ position: absolute;
647
+ top: var(--fd-space-md);
648
+ right: var(--fd-space-md);
649
+ z-index: 10;
1133
650
  display: flex;
1134
651
  align-items: center;
1135
- justify-content: space-between;
1136
- padding: var(--fd-space-sm) var(--fd-space-md);
652
+ justify-content: center;
653
+ width: var(--fd-playground-icon-btn-size, 2rem);
654
+ height: var(--fd-playground-icon-btn-size, 2rem);
655
+ border: 1px solid var(--fd-border);
1137
656
  border-radius: var(--fd-radius-md);
1138
- border-left: 3px solid transparent;
657
+ background-color: var(--fd-background);
658
+ color: var(--fd-muted-foreground);
1139
659
  cursor: pointer;
1140
- transition:
1141
- background-color var(--fd-transition-fast),
1142
- border-left-color var(--fd-transition-fast);
660
+ transition: all var(--fd-transition-fast);
1143
661
  }
1144
662
 
1145
- .playground__session:hover {
663
+ .playground__floating-close:hover {
1146
664
  background-color: var(--fd-muted);
1147
- border-left-color: var(--fd-border);
1148
- }
1149
-
1150
- .playground__session--active {
1151
- background-color: var(--fd-primary-muted);
1152
- border-left-color: var(--fd-primary);
1153
- }
1154
-
1155
- .playground__session--active:hover {
1156
- background-color: var(--fd-primary-muted);
1157
- border-left-color: var(--fd-primary);
665
+ color: var(--fd-foreground);
1158
666
  }
1159
667
 
1160
- .playground__session-name {
1161
- flex: 1;
1162
- font-size: var(--fd-text-sm);
1163
- color: var(--fd-foreground);
1164
- white-space: nowrap;
1165
- overflow: hidden;
1166
- text-overflow: ellipsis;
1167
- }
1168
-
1169
- .playground__session--active .playground__session-name {
1170
- color: var(--fd-primary);
1171
- font-weight: 500;
1172
- }
1173
-
1174
- .playground__session-menu {
1175
- display: flex;
1176
- align-items: center;
1177
- justify-content: center;
1178
- width: var(--fd-space-3xl);
1179
- height: var(--fd-space-3xl);
1180
- border: none;
1181
- border-radius: var(--fd-radius-sm);
1182
- background: transparent;
1183
- color: var(--fd-muted-foreground);
1184
- cursor: pointer;
1185
- opacity: 0;
1186
- transition: all var(--fd-transition-fast);
1187
- }
1188
-
1189
- .playground__session:hover .playground__session-menu {
1190
- opacity: 1;
1191
- }
1192
-
1193
- .playground__session-menu:hover {
1194
- background-color: var(--fd-muted);
1195
- color: var(--fd-foreground);
1196
- }
1197
-
1198
- .playground__session-menu--open {
1199
- opacity: 1;
1200
- background-color: var(--fd-muted);
1201
- color: var(--fd-foreground);
1202
- }
1203
-
1204
- .playground__session-actions {
1205
- position: relative;
1206
- display: flex;
1207
- align-items: center;
1208
- flex-shrink: 0;
1209
- }
1210
-
1211
- .playground__session-dropdown {
1212
- position: absolute;
1213
- top: 100%;
1214
- right: 0;
1215
- z-index: 50;
1216
- min-width: 140px;
1217
- padding: var(--fd-space-xs);
1218
- background-color: var(--fd-background);
1219
- border: 1px solid var(--fd-border);
1220
- border-radius: var(--fd-radius-md);
1221
- box-shadow: var(--fd-shadow-lg);
1222
- }
1223
-
1224
- .playground__session-dropdown-item {
1225
- display: flex;
1226
- align-items: center;
1227
- gap: var(--fd-space-sm);
1228
- width: 100%;
1229
- padding: var(--fd-space-sm) var(--fd-space-md);
1230
- border: none;
1231
- border-radius: var(--fd-radius-sm);
1232
- background: transparent;
1233
- color: var(--fd-foreground);
1234
- font-size: var(--fd-text-sm);
1235
- cursor: pointer;
1236
- transition: all var(--fd-transition-fast);
1237
- white-space: nowrap;
1238
- }
1239
-
1240
- .playground__session-dropdown-item:hover {
1241
- background-color: var(--fd-muted);
1242
- }
1243
-
1244
- .playground__session-dropdown-item--danger {
1245
- color: var(--fd-error);
1246
- }
1247
-
1248
- .playground__session-dropdown-item--danger:hover {
1249
- background-color: var(--fd-error-muted);
1250
- color: var(--fd-error);
1251
- }
1252
-
1253
- /* Main Content */
1254
- .playground__main {
1255
- flex: 1;
1256
- display: flex;
1257
- flex-direction: column;
1258
- min-width: 0;
1259
- min-height: 0; /* Allow proper flex shrinking */
1260
- overflow: hidden; /* Prevent scrolling - ChatPanel handles it */
1261
- background-color: var(--fd-background);
1262
- }
1263
-
1264
- /* Header - exact same height as playground__sidebar-header for alignment */
1265
- .playground__header {
1266
- display: flex;
1267
- align-items: center;
1268
- gap: var(--fd-space-xs);
1269
- height: var(--fd-playground-header-height);
1270
- padding: 0 var(--fd-space-xl);
1271
- border-bottom: 1px solid var(--fd-border);
1272
- background-color: var(--fd-background);
1273
- box-sizing: border-box;
1274
- flex-shrink: 0;
1275
- }
1276
-
1277
- :global(.playground__header-icon) {
1278
- font-size: var(--fd-text-base);
1279
- color: var(--fd-muted-foreground);
1280
- flex-shrink: 0;
1281
- }
1282
-
1283
- .playground__header-label {
1284
- font-size: var(--fd-text-sm);
1285
- font-weight: 600;
1286
- color: var(--fd-foreground);
1287
- flex-shrink: 0;
1288
- }
1289
-
1290
- .playground__header-group {
1291
- display: flex;
1292
- align-items: center;
1293
- gap: var(--fd-space-xs);
1294
- }
1295
-
1296
- .playground__header-title {
1297
- font-size: var(--fd-text-md);
1298
- font-weight: 600;
1299
- line-height: 1.25;
1300
- color: var(--fd-foreground);
1301
- margin: 0;
1302
- }
1303
-
1304
- .playground__header-close {
1305
- display: flex;
1306
- align-items: center;
1307
- justify-content: center;
1308
- width: var(--fd-playground-icon-btn-size);
1309
- height: var(--fd-playground-icon-btn-size);
1310
- border: none;
1311
- border-radius: var(--fd-radius-md);
1312
- background: transparent;
1313
- color: var(--fd-muted-foreground);
1314
- cursor: pointer;
1315
- transition: all var(--fd-transition-fast);
1316
- }
1317
-
1318
- .playground__header-close:hover {
1319
- background-color: var(--fd-muted);
1320
- color: var(--fd-foreground);
1321
- }
1322
-
1323
- .playground__header-actions {
1324
- display: flex;
1325
- align-items: center;
1326
- gap: var(--fd-space-xs);
1327
- margin-left: auto;
1328
- }
1329
-
1330
- .playground__log-toggle {
1331
- display: inline-flex;
1332
- align-items: center;
1333
- gap: var(--fd-space-3xs);
1334
- padding: var(--fd-space-3xs) var(--fd-space-sm);
1335
- border: 1px solid var(--fd-border);
1336
- border-radius: var(--fd-radius-md);
1337
- background: transparent;
1338
- color: var(--fd-muted-foreground);
1339
- font-size: var(--fd-text-xs);
1340
- font-weight: 500;
1341
- cursor: pointer;
1342
- transition: all var(--fd-transition-fast);
1343
- line-height: 1;
1344
- }
1345
-
1346
- .playground__log-toggle :global(svg) {
1347
- font-size: var(--fd-text-xs);
1348
- }
1349
-
1350
- .playground__log-toggle:hover {
1351
- background-color: var(--fd-muted);
1352
- color: var(--fd-foreground);
1353
- border-color: var(--fd-border-strong);
1354
- }
1355
-
1356
- .playground__log-toggle--active {
1357
- background-color: var(--fd-primary-muted);
1358
- border-color: var(--fd-primary);
1359
- color: var(--fd-primary);
1360
- }
1361
-
1362
- .playground__refresh--spinning :global(svg) {
1363
- animation: spin 0.8s linear infinite;
1364
- }
1365
-
1366
- /* Session chip (standalone mode) */
1367
- .playground__session-chip-wrap {
1368
- position: relative;
1369
- flex-shrink: 0;
1370
- }
1371
-
1372
- .playground__session-chip {
1373
- display: inline-flex;
1374
- align-items: center;
1375
- gap: var(--fd-space-xs);
1376
- padding: var(--fd-space-3xs) var(--fd-space-sm) var(--fd-space-3xs) var(--fd-space-xs);
1377
- border: 1px solid var(--fd-border);
1378
- border-radius: var(--fd-radius-md);
1379
- background: var(--fd-background);
1380
- color: var(--fd-foreground);
1381
- font-size: var(--fd-text-sm);
1382
- font-weight: 500;
1383
- cursor: pointer;
1384
- transition: all var(--fd-transition-fast);
1385
- max-width: 220px;
1386
- line-height: 1;
1387
- }
1388
-
1389
- .playground__session-chip :global(svg) {
1390
- flex-shrink: 0;
1391
- font-size: var(--fd-text-sm);
1392
- color: var(--fd-muted-foreground);
1393
- }
1394
-
1395
- .playground__session-chip:hover {
1396
- background-color: var(--fd-muted);
1397
- border-color: var(--fd-border-strong);
1398
- }
1399
-
1400
- .playground__session-chip--open {
1401
- background-color: var(--fd-muted);
1402
- border-color: var(--fd-border-strong);
1403
- }
1404
-
1405
- .playground__session-chip-name {
1406
- flex: 1;
1407
- white-space: nowrap;
1408
- overflow: hidden;
1409
- text-overflow: ellipsis;
1410
- min-width: 0;
1411
- }
1412
-
1413
- :global(.playground__session-chip-chevron) {
1414
- color: var(--fd-muted-foreground);
1415
- flex-shrink: 0;
1416
- }
1417
-
1418
- /* Session switcher popover */
1419
- .playground__session-popover {
1420
- position: absolute;
1421
- top: calc(100% + var(--fd-space-xs));
1422
- left: 0;
1423
- z-index: 50;
1424
- min-width: 220px;
1425
- max-width: 300px;
1426
- padding: var(--fd-space-xs);
1427
- background-color: var(--fd-background);
1428
- border: 1px solid var(--fd-border);
1429
- border-radius: var(--fd-radius-lg);
1430
- box-shadow: var(--fd-shadow-lg);
1431
- }
1432
-
1433
- .playground__session-popover-divider {
1434
- height: 1px;
1435
- background-color: var(--fd-border-muted);
1436
- margin: var(--fd-space-xs) 0;
1437
- }
1438
-
1439
- .playground__session-popover-row {
1440
- display: flex;
1441
- align-items: center;
1442
- gap: 2px;
1443
- }
1444
-
1445
- .playground__session-popover-item {
1446
- display: flex;
1447
- align-items: center;
1448
- gap: var(--fd-space-sm);
1449
- flex: 1;
1450
- min-width: 0;
1451
- padding: var(--fd-space-sm) var(--fd-space-sm);
1452
- border: none;
1453
- border-radius: var(--fd-radius-sm);
1454
- background: transparent;
1455
- color: var(--fd-foreground);
1456
- font-size: var(--fd-text-sm);
1457
- text-align: left;
1458
- cursor: pointer;
1459
- transition: background-color var(--fd-transition-fast);
1460
- white-space: nowrap;
1461
- overflow: hidden;
1462
- text-overflow: ellipsis;
1463
- }
1464
-
1465
- .playground__session-popover-item :global(svg) {
1466
- flex-shrink: 0;
1467
- color: var(--fd-muted-foreground);
1468
- font-size: var(--fd-text-sm);
1469
- }
1470
-
1471
- .playground__session-popover-item span {
1472
- overflow: hidden;
1473
- text-overflow: ellipsis;
1474
- }
1475
-
1476
- .playground__session-popover-item:hover {
1477
- background-color: var(--fd-muted);
1478
- }
1479
-
1480
- .playground__session-popover-item:disabled {
1481
- opacity: 0.4;
1482
- cursor: not-allowed;
1483
- }
1484
-
1485
- .playground__session-popover-item--new {
1486
- color: var(--fd-primary);
1487
- font-weight: 500;
1488
- }
1489
-
1490
- .playground__session-popover-item--new :global(svg) {
1491
- color: var(--fd-primary);
1492
- }
1493
-
1494
- .playground__session-popover-item--active {
1495
- font-weight: 500;
1496
- }
1497
-
1498
- :global(.playground__session-popover-check) {
1499
- color: var(--fd-primary) !important;
1500
- }
1501
-
1502
- .playground__session-popover-delete {
1503
- display: flex;
1504
- align-items: center;
1505
- justify-content: center;
1506
- flex-shrink: 0;
1507
- width: var(--fd-size-icon-btn);
1508
- height: var(--fd-size-icon-btn);
1509
- border: none;
1510
- border-radius: var(--fd-radius-sm);
1511
- background: transparent;
1512
- color: var(--fd-muted-foreground);
1513
- cursor: pointer;
1514
- opacity: 0;
1515
- transition: all var(--fd-transition-fast);
1516
- }
1517
-
1518
- .playground__session-popover-row:hover .playground__session-popover-delete {
1519
- opacity: 1;
1520
- }
1521
-
1522
- .playground__session-popover-delete:hover {
1523
- background-color: var(--fd-error-muted);
1524
- color: var(--fd-error);
1525
- opacity: 1;
1526
- }
1527
-
1528
- /* Error */
1529
- .playground__error {
1530
- display: flex;
1531
- align-items: center;
1532
- gap: var(--fd-space-xs);
1533
- padding: var(--fd-space-md) var(--fd-space-xl);
1534
- background-color: var(--fd-error-muted);
1535
- border-bottom: 1px solid var(--fd-error);
1536
- color: var(--fd-error);
1537
- font-size: var(--fd-text-sm);
1538
- }
1539
-
1540
- .playground__error-dismiss {
1541
- margin-left: auto;
1542
- display: flex;
1543
- align-items: center;
1544
- justify-content: center;
1545
- width: var(--fd-space-3xl);
1546
- height: var(--fd-space-3xl);
1547
- border: none;
1548
- border-radius: var(--fd-radius-sm);
1549
- background: transparent;
1550
- color: var(--fd-error);
1551
- cursor: pointer;
1552
- transition: background-color var(--fd-transition-fast);
1553
- }
1554
-
1555
- .playground__error-dismiss:hover {
1556
- background-color: var(--fd-error-muted);
1557
- }
1558
-
1559
- /* Content */
1560
- .playground__content {
1561
- flex: 1;
1562
- min-height: 0;
1563
- display: flex;
1564
- flex-direction: column;
1565
- }
1566
-
1567
- /* Loading */
1568
- .playground__loading {
1569
- display: flex;
1570
- flex-direction: column;
1571
- align-items: center;
1572
- justify-content: center;
1573
- flex: 1;
1574
- gap: var(--fd-space-xl);
1575
- color: var(--fd-muted-foreground);
1576
- }
1577
-
1578
- :global(.playground__loading-icon) {
1579
- font-size: var(--fd-text-2xl);
1580
- animation: spin 1s linear infinite;
1581
- }
1582
-
1583
- @keyframes spin {
1584
- from {
1585
- transform: rotate(0deg);
1586
- }
1587
- to {
1588
- transform: rotate(360deg);
1589
- }
1590
- }
1591
-
1592
- /* Responsive */
1593
- @media (max-width: 768px) {
1594
- .playground__sidebar {
1595
- width: 180px;
1596
- }
1597
- }
1598
-
1599
- @media (max-width: 640px) {
1600
- .playground__sidebar {
1601
- position: absolute;
1602
- left: 0;
1603
- top: 0;
1604
- bottom: 0;
1605
- z-index: 20;
1606
- box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
1607
- }
668
+ .playground--modal .playground__floating-close {
669
+ display: none;
1608
670
  }
1609
671
  </style>