@flowdrop/flowdrop 1.9.0 → 1.11.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 (32) hide show
  1. package/dist/api/enhanced-client.js +5 -1
  2. package/dist/components/Navbar.svelte +1 -10
  3. package/dist/components/Navbar.svelte.d.ts +1 -9
  4. package/dist/components/PipelineStatus.svelte +17 -1
  5. package/dist/components/PipelineStatus.svelte.d.ts +2 -0
  6. package/dist/components/WorkflowEditor.svelte +26 -0
  7. package/dist/components/playground/ChatPanel.svelte +33 -235
  8. package/dist/components/playground/ExecutionList.svelte +2 -6
  9. package/dist/components/playground/MessageBubble.svelte +61 -4
  10. package/dist/components/playground/PipelinePanel.svelte +17 -7
  11. package/dist/components/playground/PipelinePanel.svelte.d.ts +2 -1
  12. package/dist/components/playground/Playground.svelte +123 -73
  13. package/dist/components/playground/PlaygroundApp.svelte +110 -0
  14. package/dist/components/playground/PlaygroundApp.svelte.d.ts +28 -0
  15. package/dist/components/playground/PlaygroundStudio.svelte +404 -0
  16. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +30 -0
  17. package/dist/editor/index.d.ts +1 -1
  18. package/dist/editor/index.js +1 -1
  19. package/dist/playground/index.d.ts +8 -3
  20. package/dist/playground/index.js +15 -5
  21. package/dist/playground/mount.d.ts +63 -4
  22. package/dist/playground/mount.js +173 -84
  23. package/dist/services/nodeExecutionService.js +4 -2
  24. package/dist/services/playgroundService.d.ts +11 -4
  25. package/dist/services/playgroundService.js +22 -12
  26. package/dist/stores/playgroundStore.svelte.d.ts +22 -21
  27. package/dist/stores/playgroundStore.svelte.js +79 -58
  28. package/dist/svelte-app.d.ts +2 -10
  29. package/dist/types/navbar.d.ts +14 -0
  30. package/dist/types/navbar.js +1 -0
  31. package/dist/types/playground.d.ts +3 -5
  32. package/package.json +1 -1
@@ -425,6 +425,10 @@ export class EnhancedFlowDropApiClient {
425
425
  * Fetch pipeline data including job information and status
426
426
  */
427
427
  async getPipelineData(pipelineId) {
428
- return this.request('pipelines.get', this.config.endpoints.pipelines.get, { id: pipelineId }, {}, 'get pipeline data');
428
+ const response = await this.request('pipelines.get', this.config.endpoints.pipelines.get, { id: pipelineId }, {}, 'get pipeline data');
429
+ if (!response.success || !response.data) {
430
+ throw new Error(response.error ?? 'Failed to fetch pipeline data');
431
+ }
432
+ return response.data;
429
433
  }
430
434
  }
@@ -11,18 +11,9 @@
11
11
  import Logo from './Logo.svelte';
12
12
  import SettingsModal from './SettingsModal.svelte';
13
13
  import type { SettingsCategory } from '../types/settings.js';
14
+ import type { NavbarAction } from '../types/navbar.js';
14
15
  import { m } from '../messages/index.js';
15
16
 
16
- interface NavbarAction {
17
- label: string;
18
- href: string;
19
- icon?: string;
20
- variant?: 'primary' | 'secondary' | 'outline';
21
- onclick?: (event: Event) => void;
22
- /** If true, opens link in new tab with proper security attributes */
23
- external?: boolean;
24
- }
25
-
26
17
  interface BreadcrumbItem {
27
18
  label: string;
28
19
  href?: string;
@@ -1,13 +1,5 @@
1
1
  import type { SettingsCategory } from '../types/settings.js';
2
- interface NavbarAction {
3
- label: string;
4
- href: string;
5
- icon?: string;
6
- variant?: 'primary' | 'secondary' | 'outline';
7
- onclick?: (event: Event) => void;
8
- /** If true, opens link in new tab with proper security attributes */
9
- external?: boolean;
10
- }
2
+ import type { NavbarAction } from '../types/navbar.js';
11
3
  interface BreadcrumbItem {
12
4
  label: string;
13
5
  href?: string;
@@ -24,6 +24,8 @@
24
24
  runLabel?: string;
25
25
  /** When true, suppresses breadcrumb and layout events (used inside playground panel) */
26
26
  isEmbedded?: boolean;
27
+ /** Increments when new messages arrive — triggers an immediate pipeline data refresh */
28
+ refreshTrigger?: number;
27
29
  onActionsReady?: (
28
30
  actions: Array<{
29
31
  label: string;
@@ -35,9 +37,13 @@
35
37
  ) => void;
36
38
  }
37
39
 
38
- let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false }: Props =
40
+ let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false, refreshTrigger = 0 }: Props =
39
41
  $props();
40
42
 
43
+ // Track previous trigger value so the $effect only fires on increments, not on initial mount.
44
+ // svelte-ignore state_referenced_locally
45
+ let _prevRefreshTrigger = refreshTrigger;
46
+
41
47
  // Initialize API client if not provided
42
48
  // svelte-ignore state_referenced_locally — client created once from props
43
49
  const client =
@@ -285,6 +291,16 @@
285
291
 
286
292
  // Note: Interval cleanup is handled by the $effect above.
287
293
  // In Svelte 5, $effect cleanup runs both on re-execution and component destroy.
294
+
295
+ // Refresh pipeline data whenever new messages arrive (e.g. log messages during execution).
296
+ // Debounced so burst arrivals collapse into one fetch.
297
+ $effect(() => {
298
+ const t = refreshTrigger;
299
+ if (t <= 0 || t === _prevRefreshTrigger) return;
300
+ _prevRefreshTrigger = t;
301
+ const timer = setTimeout(fetchPipelineData, 300);
302
+ return () => clearTimeout(timer);
303
+ });
288
304
  </script>
289
305
 
290
306
  <div class="pipeline-status-container" class:pipeline-status-container--embedded={isEmbedded}>
@@ -10,6 +10,8 @@ interface Props {
10
10
  runLabel?: string;
11
11
  /** When true, suppresses breadcrumb and layout events (used inside playground panel) */
12
12
  isEmbedded?: boolean;
13
+ /** Increments when new messages arrive — triggers an immediate pipeline data refresh */
14
+ refreshTrigger?: number;
13
15
  onActionsReady?: (actions: Array<{
14
16
  label: string;
15
17
  href: string;
@@ -266,6 +266,32 @@
266
266
  }
267
267
  });
268
268
 
269
+ // Apply nodeStatuses from the parent (PipelineStatus embedded mode) to flowNodes
270
+ // whenever they change. loadNodeExecutionInfo() only fires on pipelineId change,
271
+ // so this is the update path for subsequent refreshes (e.g. after HITL resolution).
272
+ $effect(() => {
273
+ const statuses = props.nodeStatuses;
274
+ if (!statuses || Object.keys(statuses).length === 0) return;
275
+
276
+ flowNodes = untrack(() => flowNodes).map((node) => {
277
+ const rawStatus = statuses[node.id];
278
+ if (!rawStatus) return node;
279
+
280
+ const existing = node.data.executionInfo ?? { status: 'idle' as const, executionCount: 0, isExecuting: false };
281
+ return {
282
+ ...node,
283
+ data: {
284
+ ...node.data,
285
+ executionInfo: {
286
+ ...existing,
287
+ status: rawStatus === 'error' ? ('failed' as const) : rawStatus,
288
+ isExecuting: rawStatus === 'running'
289
+ }
290
+ }
291
+ };
292
+ });
293
+ });
294
+
269
295
  // ---------------------------------------------------------------------------
270
296
  // History restore callback
271
297
  // ---------------------------------------------------------------------------
@@ -22,10 +22,10 @@
22
22
  getMessages,
23
23
  getChatMessages,
24
24
  getIsExecuting,
25
+ getCanSendMessage,
25
26
  getSessionStatus,
26
27
  getCurrentSession
27
28
  } from '../../stores/playgroundStore.svelte.js';
28
- import type { PlaygroundExecution } from '../../types/playground.js';
29
29
  import {
30
30
  getInterruptsMap,
31
31
  interruptActions,
@@ -135,74 +135,15 @@
135
135
  const displayMessages = $derived(showLogs ? getMessages() : getChatMessages());
136
136
 
137
137
  // ---------------------------------------------------------------------------
138
- // Execution separators
139
- // ---------------------------------------------------------------------------
140
-
141
- type ChatItem =
142
- | { type: 'message'; message: PlaygroundMessage; msgIndex: number }
143
- | { type: 'separator'; key: string; label: string; status: PlaygroundExecution['status'] };
144
-
145
- /** Map executionId → { label, status } derived from the current session */
146
- const executionMeta = $derived(
147
- new Map(
148
- (getCurrentSession()?.executions ?? []).map((e, i) => [
149
- e.id,
150
- { label: `Run #${i + 1}`, status: e.status }
151
- ])
152
- )
153
- );
154
-
155
- /**
156
- * Interleave execution-boundary separators into the message list.
157
- * A separator is inserted before the first message of each new execution.
158
- */
159
- const chatItems = $derived(
160
- (() => {
161
- const items: ChatItem[] = [];
162
- let lastExecId: string | null = null;
163
-
164
- displayMessages.forEach((msg, i) => {
165
- const execId = msg.executionId ?? null;
166
- if (execId !== null && execId !== lastExecId) {
167
- const meta = executionMeta.get(execId);
168
- if (meta) {
169
- items.push({ type: 'separator', key: `sep-${execId}`, label: meta.label, status: meta.status });
170
- lastExecId = execId;
171
- }
172
- }
173
- items.push({ type: 'message', message: msg, msgIndex: i });
174
- });
175
-
176
- return items;
177
- })()
178
- );
179
-
180
- /**
181
- * Track previous message count for detecting new messages.
182
- * We only want to auto-scroll when NEW messages are added,
183
- * not when existing messages are updated.
184
- */
185
138
  let previousMessageCount = $state(0);
139
+ let userScrolledUp = $state(false);
186
140
 
187
- /**
188
- * Check if user is near the bottom of the scroll container.
189
- * Used to determine if we should auto-scroll when new messages arrive.
190
- * If user has scrolled up to read previous messages, we don't interrupt them.
191
- *
192
- * @param threshold - Pixels from bottom to consider "near bottom"
193
- * @returns True if user is within threshold of the bottom
194
- */
195
- function isNearBottom(threshold: number = 100): boolean {
196
- if (!messagesContainer) return true;
141
+ function handleScroll() {
142
+ if (!messagesContainer) return;
197
143
  const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
198
- return scrollHeight - scrollTop - clientHeight <= threshold;
144
+ userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
199
145
  }
200
146
 
201
- /**
202
- * Check if a form element inside the messages container has focus.
203
- * When user is interacting with a form (e.g., interrupt prompt),
204
- * we should not auto-scroll as it disrupts their input.
205
- */
206
147
  function isFormFocused(): boolean {
207
148
  if (!messagesContainer) return false;
208
149
  const activeElement = document.activeElement;
@@ -297,7 +238,7 @@
297
238
  */
298
239
  function handleSend(): void {
299
240
  const trimmedValue = inputValue.trim();
300
- if (!trimmedValue || getIsExecuting()) {
241
+ if (!trimmedValue || !getCanSendMessage()) {
301
242
  return;
302
243
  }
303
244
 
@@ -380,58 +321,25 @@
380
321
  $effect(() => {
381
322
  const session = getCurrentSession();
382
323
  if (session) {
383
- // Reset to enabled state for new/changed sessions
384
324
  runEnabled = true;
385
- // Clear processed IDs for the new session
386
325
  processedEnableRunIds = new Set();
326
+ userScrolledUp = false;
387
327
  }
388
328
  });
389
329
 
390
- /**
391
- * Smart auto-scroll to bottom when NEW messages are added.
392
- *
393
- * Only scrolls if:
394
- * 1. autoScroll prop is enabled
395
- * 2. New messages were actually added (not just updates)
396
- * 3. User is already near the bottom (hasn't scrolled up to read)
397
- * 4. User is not interacting with a form inside the chat
398
- *
399
- * This prevents disruptive scrolling when:
400
- * - User is reading previous messages
401
- * - User is filling out an interrupt form
402
- * - Messages are being updated (e.g., status changes)
403
- */
404
330
  $effect(() => {
405
331
  const currentCount = displayMessages.length;
406
332
 
407
- // Skip if auto-scroll is disabled or no container
408
333
  if (!autoScroll || !messagesContainer) {
409
334
  previousMessageCount = currentCount;
410
335
  return;
411
336
  }
412
337
 
413
- // Check if this is a NEW message (count increased)
414
338
  const hasNewMessage = currentCount > previousMessageCount;
415
-
416
- // Update the tracked count
417
339
  previousMessageCount = currentCount;
418
340
 
419
- // Only scroll if there's a new message
420
- if (!hasNewMessage) {
421
- return;
422
- }
423
-
424
- // Don't scroll if user has scrolled up to read previous messages
425
- if (!isNearBottom()) {
426
- return;
427
- }
428
-
429
- // Don't scroll if user is interacting with a form
430
- if (isFormFocused()) {
431
- return;
432
- }
341
+ if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
433
342
 
434
- // Safe to scroll to bottom
435
343
  tick().then(() => {
436
344
  if (messagesContainer) {
437
345
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
@@ -439,50 +347,17 @@
439
347
  });
440
348
  });
441
349
 
442
- /**
443
- * Track previous executing state to detect when execution completes
444
- */
445
- let wasExecuting = $state(false);
350
+ let wasExecuting = false;
446
351
 
447
352
  /**
448
- * Auto-focus input when execution completes or session becomes ready
353
+ * Auto-focus input when execution completes
449
354
  */
450
355
  $effect(() => {
451
- const currentlyExecuting = getIsExecuting();
452
-
453
- // Focus input when execution completes (was executing, now not)
454
- if (wasExecuting && !currentlyExecuting && inputField) {
455
- tick().then(() => {
456
- inputField?.focus();
457
- });
458
- }
459
-
460
- // Update tracking state
461
- wasExecuting = currentlyExecuting;
462
- });
463
-
464
- /**
465
- * Focus input when session status changes to idle or completed
466
- */
467
- $effect(() => {
468
- const status = getSessionStatus();
469
- if ((status === 'idle' || status === 'completed') && inputField && !getIsExecuting()) {
470
- tick().then(() => {
471
- inputField?.focus();
472
- });
473
- }
474
- });
475
-
476
- /**
477
- * Focus input when a new session is created/loaded
478
- */
479
- $effect(() => {
480
- const session = getCurrentSession();
481
- if (session && inputField && !getIsExecuting()) {
482
- tick().then(() => {
483
- inputField?.focus();
484
- });
356
+ const nowExecuting = getIsExecuting();
357
+ if (wasExecuting && !nowExecuting && inputField) {
358
+ tick().then(() => inputField?.focus());
485
359
  }
360
+ wasExecuting = nowExecuting;
486
361
  });
487
362
 
488
363
  /**
@@ -498,7 +373,7 @@
498
373
 
499
374
  <div class="chat-panel">
500
375
  <!-- Messages Container -->
501
- <div class="chat-panel__messages" bind:this={messagesContainer}>
376
+ <div class="chat-panel__messages" bind:this={messagesContainer} onscroll={handleScroll}>
502
377
  {#if showWelcome}
503
378
  <!-- Welcome State (no session) -->
504
379
  <div class="chat-panel__welcome">
@@ -582,50 +457,25 @@
582
457
  {/if}
583
458
  </div>
584
459
  {:else}
585
- <!-- Messages with execution separators -->
586
- {#each chatItems as item (item.type === 'message' ? item.message.id : item.key)}
587
- {#if item.type === 'separator'}
588
- <div
589
- class="chat-panel__exec-sep"
590
- class:chat-panel__exec-sep--completed={item.status === 'completed'}
591
- class:chat-panel__exec-sep--failed={item.status === 'failed'}
592
- class:chat-panel__exec-sep--running={item.status === 'running'}
593
- >
594
- <span class="chat-panel__exec-sep-line"></span>
595
- <span class="chat-panel__exec-sep-label">
596
- {#if item.status === 'completed'}
597
- <Icon icon="mdi:check-circle" class="chat-panel__exec-sep-icon" />
598
- {:else if item.status === 'failed'}
599
- <Icon icon="mdi:alert-circle" class="chat-panel__exec-sep-icon" />
600
- {:else}
601
- <Icon icon="mdi:play-circle" class="chat-panel__exec-sep-icon" />
602
- {/if}
603
- {item.label}
604
- <span class="chat-panel__exec-sep-status">{item.status}</span>
605
- </span>
606
- <span class="chat-panel__exec-sep-line"></span>
607
- </div>
608
- {:else}
609
- {@const message = item.message}
610
- {@const index = item.msgIndex}
611
- {#if isInterruptMessage(message)}
612
- {@const interrupt = getInterruptForMessage(message)}
613
- {#if interrupt}
614
- <InterruptBubble
615
- {interrupt}
616
- showTimestamp={showTimestamps}
617
- onResolved={onInterruptResolved}
618
- />
619
- {/if}
620
- {:else}
621
- <MessageBubble
622
- {message}
460
+ <!-- Messages -->
461
+ {#each displayMessages as message, index (message.id)}
462
+ {#if isInterruptMessage(message)}
463
+ {@const interrupt = getInterruptForMessage(message)}
464
+ {#if interrupt}
465
+ <InterruptBubble
466
+ {interrupt}
623
467
  showTimestamp={showTimestamps}
624
- isLast={index === displayMessages.length - 1}
625
- {enableMarkdown}
626
- {compactSystemMessages}
468
+ onResolved={onInterruptResolved}
627
469
  />
628
470
  {/if}
471
+ {:else}
472
+ <MessageBubble
473
+ {message}
474
+ showTimestamp={showTimestamps}
475
+ isLast={index === displayMessages.length - 1}
476
+ {enableMarkdown}
477
+ {compactSystemMessages}
478
+ />
629
479
  {/if}
630
480
  {/each}
631
481
 
@@ -670,7 +520,7 @@
670
520
  </div>
671
521
  {/if}
672
522
 
673
- {#if getSessionStatus() === 'running' || getIsExecuting()}
523
+ {#if getIsExecuting()}
674
524
  <button
675
525
  type="button"
676
526
  class="chat-panel__stop-btn"
@@ -685,7 +535,7 @@
685
535
  type="button"
686
536
  class="chat-panel__send-btn"
687
537
  onclick={handleSend}
688
- disabled={!inputValue.trim()}
538
+ disabled={!inputValue.trim() || !getCanSendMessage()}
689
539
  title={actions.sendTitle}
690
540
  >
691
541
  {actions.send}
@@ -716,57 +566,6 @@
716
566
  background-color: var(--fd-background);
717
567
  }
718
568
 
719
- /* Execution separator */
720
- .chat-panel__exec-sep {
721
- display: flex;
722
- align-items: center;
723
- gap: var(--fd-space-sm);
724
- padding: var(--fd-space-md) var(--fd-space-3xl);
725
- margin: var(--fd-space-sm) 0;
726
- }
727
-
728
- .chat-panel__exec-sep-line {
729
- flex: 1;
730
- height: 1px;
731
- background-color: var(--fd-border);
732
- }
733
-
734
- .chat-panel__exec-sep-label {
735
- display: flex;
736
- align-items: center;
737
- gap: var(--fd-space-3xs);
738
- font-size: var(--fd-text-xs);
739
- font-weight: 600;
740
- white-space: nowrap;
741
- color: var(--fd-muted-foreground);
742
- }
743
-
744
- :global(.chat-panel__exec-sep-icon) {
745
- font-size: 0.875rem;
746
- }
747
-
748
- .chat-panel__exec-sep--completed .chat-panel__exec-sep-label {
749
- color: var(--fd-success, #16a34a);
750
- }
751
-
752
- .chat-panel__exec-sep--completed .chat-panel__exec-sep-line {
753
- background-color: var(--fd-success-muted, rgba(22, 163, 74, 0.2));
754
- }
755
-
756
- .chat-panel__exec-sep--failed .chat-panel__exec-sep-label {
757
- color: var(--fd-error);
758
- }
759
-
760
- .chat-panel__exec-sep--failed .chat-panel__exec-sep-line {
761
- background-color: var(--fd-error-muted);
762
- }
763
-
764
- .chat-panel__exec-sep-status {
765
- text-transform: uppercase;
766
- letter-spacing: 0.04em;
767
- opacity: 0.8;
768
- font-size: var(--fd-text-2xs);
769
- }
770
569
 
771
570
  /* Messages Container - Scrollable area that takes remaining space */
772
571
  .chat-panel__messages {
@@ -774,7 +573,6 @@
774
573
  min-height: 0; /* Critical: allows overflow to work in flex container */
775
574
  overflow-y: auto;
776
575
  padding: var(--fd-space-3xl);
777
- scroll-behavior: smooth;
778
576
  }
779
577
 
780
578
  /* Welcome State */
@@ -11,10 +11,6 @@
11
11
 
12
12
  let { executions, activeExecutionId, latestExecutionId, onSelect }: Props = $props();
13
13
 
14
- function label(index: number): string {
15
- return `Run #${index + 1}`;
16
- }
17
-
18
14
  function statusIcon(status: PlaygroundExecution['status']): string {
19
15
  if (status === 'completed') return 'mdi:check-circle';
20
16
  if (status === 'failed') return 'mdi:alert-circle';
@@ -23,7 +19,7 @@
23
19
  </script>
24
20
 
25
21
  <div class="execution-list">
26
- {#each executions as execution, i (execution.id)}
22
+ {#each executions as execution (execution.id)}
27
23
  <div
28
24
  class="execution-list__item"
29
25
  class:execution-list__item--active={execution.id === activeExecutionId}
@@ -43,7 +39,7 @@
43
39
  class="execution-list__status-icon execution-list__status-icon--{execution.status}"
44
40
  />
45
41
  {/if}
46
- <span class="execution-list__label">{label(i)}</span>
42
+ <span class="execution-list__label">{execution.id}</span>
47
43
  {#if execution.id === latestExecutionId}
48
44
  <span class="execution-list__badge">latest</span>
49
45
  {/if}
@@ -153,8 +153,17 @@
153
153
 
154
154
  {#if useCompactMode}
155
155
  <!-- Compact system message: minimal inline text without bubble -->
156
- <div class="system-notice" class:system-notice--last={isLast}>
157
- <Icon icon="mdi:information-outline" class="system-notice__icon" />
156
+ <div
157
+ class="system-notice"
158
+ class:system-notice--last={isLast}
159
+ class:system-notice--warning={message.metadata?.level === 'warning'}
160
+ class:system-notice--error={message.metadata?.level === 'error'}
161
+ class:system-notice--debug={message.metadata?.level === 'debug'}
162
+ >
163
+ <Icon icon={getLogLevelIcon()} class="system-notice__icon" />
164
+ {#if message.metadata?.source}
165
+ <span class="system-notice__source">{message.metadata.source}</span>
166
+ {/if}
158
167
  <span class="system-notice__text">{message.content}</span>
159
168
  {#if showTimestamp}
160
169
  <span class="system-notice__timestamp">{formatTimestamp(message.timestamp)}</span>
@@ -172,6 +181,9 @@
172
181
  <Icon icon={getLogLevelIcon()} />
173
182
  </div>
174
183
  <div class="log-row__body">
184
+ {#if message.metadata?.source}
185
+ <span class="log-row__source">{message.metadata.source}</span>
186
+ {/if}
175
187
  <span class="log-row__node">{message.metadata?.nodeLabel ?? message.nodeId ?? 'log'}</span>
176
188
  <span class="log-row__text">{message.content}</span>
177
189
  </div>
@@ -578,7 +590,7 @@
578
590
 
579
591
  .log-row {
580
592
  display: flex;
581
- align-items: baseline;
593
+ align-items: center;
582
594
  gap: var(--fd-space-sm);
583
595
  padding: 0.1875rem var(--fd-space-xl);
584
596
  border-left: 2px solid var(--fd-info);
@@ -636,6 +648,20 @@
636
648
  overflow: hidden;
637
649
  }
638
650
 
651
+ .log-row__source {
652
+ flex-shrink: 0;
653
+ font-size: 0.6rem;
654
+ text-transform: uppercase;
655
+ letter-spacing: 0.06em;
656
+ color: var(--fd-muted-foreground);
657
+ opacity: 0.6;
658
+ background-color: var(--fd-muted);
659
+ border: 1px solid var(--fd-border);
660
+ border-radius: var(--fd-radius-sm);
661
+ padding: 0 0.25rem;
662
+ line-height: 1.4;
663
+ }
664
+
639
665
  .log-row__node {
640
666
  flex-shrink: 0;
641
667
  font-weight: 600;
@@ -688,11 +714,42 @@
688
714
  color: var(--fd-border-strong);
689
715
  }
690
716
 
691
- .system-notice__text {
717
+ .system-notice__source {
718
+ flex-shrink: 0;
719
+ font-size: 0.6rem;
720
+ text-transform: uppercase;
721
+ letter-spacing: 0.06em;
692
722
  color: var(--fd-muted-foreground);
723
+ background-color: var(--fd-muted);
724
+ border: 1px solid var(--fd-border);
725
+ border-radius: var(--fd-radius-sm);
726
+ padding: 0 0.25rem;
727
+ line-height: 1.4;
728
+ }
729
+
730
+ .system-notice__text {
693
731
  line-height: var(--fd-leading-tight);
694
732
  }
695
733
 
734
+ .system-notice--warning {
735
+ color: var(--fd-warning);
736
+ }
737
+ .system-notice--warning :global(.system-notice__icon) {
738
+ color: var(--fd-warning);
739
+ }
740
+
741
+ .system-notice--error {
742
+ color: var(--fd-error);
743
+ }
744
+ .system-notice--error :global(.system-notice__icon) {
745
+ color: var(--fd-error);
746
+ }
747
+
748
+ .system-notice--debug {
749
+ color: var(--fd-border-strong);
750
+ opacity: 0.6;
751
+ }
752
+
696
753
  .system-notice__timestamp {
697
754
  flex-shrink: 0;
698
755
  font-size: 0.625rem;