@runwisp/ui 1.3.0 → 1.5.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 (73) hide show
  1. package/package.json +5 -6
  2. package/src/lib/actions/floating.ts +73 -0
  3. package/src/lib/actions/portal.ts +5 -1
  4. package/src/lib/components/Accordion.svelte +6 -2
  5. package/src/lib/components/AccordionItem.svelte +80 -23
  6. package/src/lib/components/Avatar.svelte +0 -1
  7. package/src/lib/components/CodeBlock.svelte +84 -35
  8. package/src/lib/components/CronInput.svelte +5 -2
  9. package/src/lib/components/DataGrid.svelte +95 -133
  10. package/src/lib/components/Drawer.svelte +2 -6
  11. package/src/lib/components/Dropdown.svelte +19 -57
  12. package/src/lib/components/EmptyState.svelte +2 -1
  13. package/src/lib/components/ErrorState.svelte +2 -1
  14. package/src/lib/components/Heading.svelte +19 -12
  15. package/src/lib/components/Kbd.svelte +25 -10
  16. package/src/lib/components/Link.svelte +7 -1
  17. package/src/lib/components/LogConsole.svelte +37 -34
  18. package/src/lib/components/Modal.svelte +44 -33
  19. package/src/lib/components/NotificationBell.svelte +2 -1
  20. package/src/lib/components/PageContainer.svelte +1 -10
  21. package/src/lib/components/PageHeader.svelte +8 -4
  22. package/src/lib/components/Popover.svelte +9 -51
  23. package/src/lib/components/Progress.svelte +19 -6
  24. package/src/lib/components/Radio.svelte +16 -4
  25. package/src/lib/components/RadioGroup.svelte +15 -2
  26. package/src/lib/components/Select.svelte +13 -45
  27. package/src/lib/components/Skeleton.svelte +6 -6
  28. package/src/lib/components/Table.svelte +13 -4
  29. package/src/lib/components/TableBody.svelte +2 -1
  30. package/src/lib/components/TableCell.svelte +38 -8
  31. package/src/lib/components/TableHead.svelte +12 -4
  32. package/src/lib/components/TableRow.svelte +23 -3
  33. package/src/lib/components/TaskCard.svelte +1 -1
  34. package/src/lib/components/TimezoneSelect.svelte +2 -2
  35. package/src/lib/components/accordion.svelte.ts +19 -0
  36. package/src/lib/components/dashboard/RunDetailPanel.svelte +141 -67
  37. package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
  38. package/src/lib/components/dashboard/RunsList.svelte +57 -37
  39. package/src/lib/components/dashboard/cron-format.ts +1 -1
  40. package/src/lib/components/dashboard/run-filters.test.ts +2 -58
  41. package/src/lib/components/dashboard/run-filters.ts +5 -53
  42. package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
  43. package/src/lib/components/dashboard/run-helpers.ts +12 -1
  44. package/src/lib/components/dashboard/status-config.ts +38 -83
  45. package/src/lib/components/dashboard/types.ts +0 -26
  46. package/src/lib/components/radio-group.ts +11 -0
  47. package/src/lib/index.ts +8 -43
  48. package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
  49. package/src/lib/log-console/highlight-scroll.test.ts +59 -0
  50. package/src/lib/log-console/highlight-scroll.ts +41 -0
  51. package/src/lib/theme.css +10 -4
  52. package/src/lib/utils/clipboard.svelte.test.ts +92 -0
  53. package/src/lib/utils/clipboard.svelte.ts +73 -0
  54. package/src/lib/utils/format.ts +65 -94
  55. package/src/lib/utils/id.test.ts +3 -7
  56. package/src/lib/utils/id.ts +3 -2
  57. package/src/lib/utils/theme.svelte.test.ts +0 -6
  58. package/src/lib/utils/theme.svelte.ts +3 -5
  59. package/src/lib/utils/toast.svelte.ts +3 -11
  60. package/src/stories/Accordion.stories.svelte +61 -0
  61. package/src/stories/Button.stories.svelte +2 -2
  62. package/src/stories/CodeBlock.stories.svelte +55 -0
  63. package/src/stories/DataGrid.stories.svelte +2 -7
  64. package/src/stories/Dropdown.stories.svelte +3 -3
  65. package/src/stories/Heading.stories.svelte +31 -0
  66. package/src/stories/Kbd.stories.svelte +36 -0
  67. package/src/stories/Link.stories.svelte +38 -0
  68. package/src/stories/PageHeader.stories.svelte +31 -0
  69. package/src/stories/Progress.stories.svelte +26 -0
  70. package/src/stories/RadioGroup.stories.svelte +43 -0
  71. package/src/stories/Table.stories.svelte +69 -0
  72. package/src/svelte-shims.d.ts +0 -5
  73. package/src/lib/components/dashboard/status-config.test.ts +0 -18
@@ -8,15 +8,16 @@
8
8
  Check,
9
9
  Terminal as TerminalIcon,
10
10
  Download,
11
- Trash2,
11
+ Trash,
12
12
  Play,
13
13
  Square,
14
14
  MousePointerClick,
15
15
  RotateCw,
16
16
  RefreshCcw,
17
17
  ChevronDown,
18
- RotateCcwClock,
18
+ ArrowLeft,
19
19
  PanelLeftClose,
20
+ PanelLeftOpen,
20
21
  SlidersHorizontal,
21
22
  Maximize2,
22
23
  Minimize2,
@@ -25,24 +26,27 @@
25
26
  } from "@lucide/svelte";
26
27
  import Button from "../Button.svelte";
27
28
  import EmptyState from "../EmptyState.svelte";
29
+ import Kbd from "../Kbd.svelte";
28
30
  import LogConsole from "../LogConsole.svelte";
29
31
  import Popover from "../Popover.svelte";
30
32
  import Tooltip from "../Tooltip.svelte";
31
33
  import { portal } from "../../actions/portal.js";
32
34
  import { prefersReducedMotion } from "../../actions/row-motion.js";
33
35
  import type { RunMotion } from "../../utils/run-motion.js";
34
- import type { Run } from "./types.js";
35
36
  import { isLogEvent, type LogEvent, type LogSlice } from "../../log-console/types.js";
36
- import { formatClockTime, formatCalendarDate } from "../../utils/format.js";
37
+ import { formatBytes, formatClockTime, formatCalendarDate } from "../../utils/format.js";
37
38
  import { formatShortId } from "../../utils/id.js";
38
39
  import { TickingNow } from "../../utils/ticking-now.svelte.js";
39
- import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
40
+ import { CopyFeedback } from "../../utils/clipboard.svelte.js";
41
+ import { displayStatus, type ResourceUsage, type Run } from "@runwisp/common";
42
+ import { RUN_STATUS_CONFIG } from "./status-config.js";
40
43
  import {
41
44
  runDuration,
42
45
  runStartDelay,
43
46
  runVerdict,
44
47
  formatTriggeredByLabel,
45
48
  runRetryLabel,
49
+ runUsageLabel,
46
50
  instanceSuffix,
47
51
  } from "./run-helpers.js";
48
52
 
@@ -61,12 +65,15 @@
61
65
  onRestartService,
62
66
  serviceStopped = false,
63
67
  serviceBusy = false,
64
- onToggleHistory,
65
- historyVisible = false,
68
+ onBack,
69
+ onToggleList,
70
+ listVisible = true,
66
71
  highlightLine = null,
67
72
  getInstanceCount = () => 1,
73
+ getLiveUsage = () => undefined,
68
74
  motion,
69
75
  notFound = false,
76
+ loading = false,
70
77
  }: {
71
78
  run: Run | undefined;
72
79
  fetchLogs: (
@@ -99,19 +106,25 @@
99
106
  // cold-start path so a never-run task is still launchable from here.
100
107
  onRunTask?: (() => void) | undefined;
101
108
  // Service lifecycle controls. When either is set the cluster swaps the
102
- // Run control for Stop Service / Restart Service (chosen by serviceStopped).
109
+ // Run control for Stop / Restart (chosen by serviceStopped).
103
110
  onStopService?: (() => void) | undefined;
104
111
  onRestartService?: (() => void) | undefined;
105
112
  serviceStopped?: boolean;
106
113
  serviceBusy?: boolean;
107
- // Toggle the (single-instance service) history rail from the panel header,
108
- // since these tasks hide the rail by default and there is no top bar.
109
- onToggleHistory?: (() => void) | undefined;
110
- historyVisible?: boolean;
114
+ // Return to the run list when the panel replaces it (a phone), shown as
115
+ // a back arrow in the header's top-left corner.
116
+ onBack?: (() => void) | undefined;
117
+ // Fold the run list away (or back) when it sits beside the panel on a
118
+ // screen too narrow to keep both comfortably, in the same corner.
119
+ onToggleList?: (() => void) | undefined;
120
+ listVisible?: boolean;
111
121
  highlightLine?: number | null;
112
122
  // Resolves a task's currently configured instance count so multi-instance
113
123
  // services render a 1-based #N suffix. Defaults to single-instance.
114
124
  getInstanceCount?: (taskName: string) => number;
125
+ // Live CPU and memory of a running run, by run ID; undefined when it
126
+ // isn't measured. Shown in the header while the run is running.
127
+ getLiveUsage?: (runId: string) => ResourceUsage | undefined;
115
128
  // Which runs arrived or were removed live moments ago. The panel eases in
116
129
  // when a new run takes it (triggered, or a scheduled run auto-selected)
117
130
  // or when its run was deleted and another takes its place; picking a
@@ -121,6 +134,9 @@
121
134
  // or never existed). The empty state then says so plainly instead of the
122
135
  // generic "Select a run" — the caller must not silently substitute another.
123
136
  notFound?: boolean;
137
+ // True while there is no run to show *yet* (the run list or a deep-linked
138
+ // run is still loading), so the empty state doesn't claim "No runs yet".
139
+ loading?: boolean;
124
140
  } = $props();
125
141
 
126
142
  function easeInFresh(node: HTMLElement, runId: string) {
@@ -144,7 +160,7 @@
144
160
 
145
161
  let canDelete = $derived.by(() => {
146
162
  if (!run || !onDelete) return false;
147
- const status = runDisplayStatus(run);
163
+ const status = displayStatus(run.status, run.endReason);
148
164
  return status !== "running" && status !== "pending";
149
165
  });
150
166
 
@@ -170,6 +186,10 @@
170
186
  // changes, not on every run object reference swap from SSE array updates.
171
187
  let runId = $derived(run?.id);
172
188
 
189
+ // The run whose seed fetch has settled. Until it matches the shown run the
190
+ // console says it's loading, not "No output yet".
191
+ let seededRunId = $state<string | null>(null);
192
+
173
193
  // Fetches the batched tail page a run's console opens on (see the effect
174
194
  // below) and paints it into the console. Resolves the line the live
175
195
  // stream should resume from, or null when there's nothing left to stream
@@ -201,7 +221,10 @@
201
221
  if (!id) return;
202
222
 
203
223
  const stream = streamLogs;
204
- if (!stream) return;
224
+ if (!stream) {
225
+ seededRunId = id;
226
+ return;
227
+ }
205
228
 
206
229
  logFetchError = null;
207
230
 
@@ -217,7 +240,9 @@
217
240
  // (its disk backfill from that anchor closes the fetch↔stream
218
241
  // race, and the daemon dedupes anything already shown).
219
242
  const seeded = await seedConsoleTail(id, () => cancelled);
220
- if (cancelled || !seeded) return;
243
+ if (cancelled) return;
244
+ seededRunId = id;
245
+ if (!seeded) return;
221
246
  cleanup = stream(
222
247
  id,
223
248
  (event: LogEvent) => {
@@ -253,19 +278,7 @@
253
278
  let confirmDeleteOpen = $state(false);
254
279
  // Dropdown half of the Run split button (the "reuse parameters" variant).
255
280
  let runMenuOpen = $state(false);
256
- let copiedId = $state(false);
257
- let copyTimer: ReturnType<typeof setTimeout> | null = null;
258
-
259
- async function copyRunId(id: string) {
260
- try {
261
- await navigator.clipboard.writeText(id);
262
- copiedId = true;
263
- if (copyTimer) clearTimeout(copyTimer);
264
- copyTimer = setTimeout(() => (copiedId = false), 1200);
265
- } catch {
266
- // Clipboard blocked (insecure context / denied) — leave the chip as-is.
267
- }
268
- }
281
+ const runIdCopy = new CopyFeedback(1200);
269
282
 
270
283
  // The accent for a status that means "something to triage", or undefined
271
284
  // when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
@@ -349,8 +362,8 @@
349
362
  <svelte:window onkeydown={handleConsoleKeydown} />
350
363
 
351
364
  {#if run}
352
- {@const status = runDisplayStatus(run)}
353
- {@const config = getRunStatusConfig(status)}
365
+ {@const status = displayStatus(run.status, run.endReason)}
366
+ {@const config = RUN_STATUS_CONFIG[status]}
354
367
  {@const DetailIcon = config.icon}
355
368
  {@const duration = runDuration(
356
369
  run,
@@ -359,6 +372,8 @@
359
372
  {@const startDelay = runStartDelay(run)}
360
373
  {@const startedAt = run.startedAt ?? run.createdAt}
361
374
  {@const retry = runRetryLabel(run)}
375
+ {@const usage = runUsageLabel(run)}
376
+ {@const live = run.status === "running" ? getLiveUsage(run.id) : undefined}
362
377
  {@const paramEntries = run.params ? Object.entries(run.params) : []}
363
378
  {@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
364
379
  {@const spine = config.dot.replace(" animate-pulse", "")}
@@ -403,8 +418,36 @@
403
418
  style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
404
419
  >
405
420
  <div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
406
- <div class="flex items-start gap-4">
407
- <div class="min-w-0 flex-1">
421
+ <!-- The actions wrap onto their own row rather than squeeze the
422
+ readout below a legible width on a narrow panel. -->
423
+ <div class="flex flex-wrap items-start gap-x-3 gap-y-3">
424
+ {#if onBack}
425
+ <button
426
+ type="button"
427
+ onclick={() => onBack()}
428
+ class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
429
+ title="Back to runs"
430
+ aria-label="Back to runs"
431
+ >
432
+ <ArrowLeft size={20} />
433
+ </button>
434
+ {:else if onToggleList}
435
+ <button
436
+ type="button"
437
+ onclick={() => onToggleList()}
438
+ class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
439
+ title={listVisible ? "Hide run list" : "Show run list"}
440
+ aria-label={listVisible ? "Hide run list" : "Show run list"}
441
+ aria-expanded={listVisible}
442
+ >
443
+ {#if listVisible}
444
+ <PanelLeftClose size={20} />
445
+ {:else}
446
+ <PanelLeftOpen size={20} />
447
+ {/if}
448
+ </button>
449
+ {/if}
450
+ <div class="min-w-60 flex-1">
408
451
  <!-- Identity, but only where the page around the panel isn't
409
452
  already saying it: the cross-task runs list needs the task
410
453
  name, a task's own page already has it in the breadcrumb. -->
@@ -531,16 +574,35 @@
531
574
  </div>
532
575
  </Popover>
533
576
  {/if}
577
+ {#if live}
578
+ <span class="text-outline-hover" aria-hidden="true">·</span>
579
+ <span
580
+ data-testid="run-live-usage"
581
+ class="text-on-surface-muted"
582
+ title="Live CPU (100% = one core) and memory of the run's processes"
583
+ >CPU {Math.round(live.cpuPercent)}% · {formatBytes(
584
+ live.memoryBytes,
585
+ )}</span
586
+ >
587
+ {/if}
588
+ {#if usage}
589
+ <span class="text-outline-hover" aria-hidden="true">·</span>
590
+ <span
591
+ data-testid="run-usage"
592
+ title="Peak memory and CPU time of the run's processes"
593
+ >{usage}</span
594
+ >
595
+ {/if}
534
596
  <!-- Run-id chip: click to copy the full ULID. Last, because
535
597
  it is the fact you reach for least and the only one you
536
598
  interact with. -->
537
599
  <button
538
600
  type="button"
539
- onclick={() => copyRunId(run.id)}
601
+ onclick={() => void runIdCopy.copy(run.id)}
540
602
  title="Copy run ID"
541
603
  class="inline-flex items-center gap-1 rounded-[3px] border border-outline-faint bg-surface-sunken px-1.5 text-on-surface-faint hover:border-outline-hover hover:text-primary"
542
604
  >
543
- {#if copiedId}
605
+ {#if runIdCopy.copied}
544
606
  <Check size={11} class="text-success-surface" />Copied
545
607
  {:else}
546
608
  <Hash size={11} />{run.id}
@@ -549,27 +611,9 @@
549
611
  </div>
550
612
  </div>
551
613
 
552
- <!-- Actions: history toggle · run (split) / service lifecycle ·
614
+ <!-- Actions: run (split) / service lifecycle ·
553
615
  stop · download · delete (delete behind an inline confirm) -->
554
- <div class="flex shrink-0 items-center gap-2">
555
- {#if onToggleHistory}
556
- <button
557
- type="button"
558
- onclick={() => onToggleHistory()}
559
- class="inline-flex items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-outline-hover hover:bg-surface-sunken hover:text-primary"
560
- title={historyVisible ? "Hide run history" : "Show run history"}
561
- aria-label={historyVisible
562
- ? "Hide run history"
563
- : "Show run history"}
564
- >
565
- {#if historyVisible}
566
- <PanelLeftClose size={15} />
567
- {:else}
568
- <RotateCcwClock size={15} />
569
- {/if}
570
- </button>
571
- {/if}
572
-
616
+ <div class="flex flex-wrap items-center gap-2">
573
617
  {#if onStopService || onRestartService}
574
618
  <!-- Service lifecycle replaces the run control.
575
619
  Restart is always available (it cancels + respawns
@@ -591,7 +635,7 @@
591
635
  {:else}
592
636
  <RefreshCcw size={15} />
593
637
  {/if}
594
- <span class="@max-md:hidden"
638
+ <span class="@max-xs:hidden"
595
639
  >{serviceStopped ? "Start" : "Restart"}</span
596
640
  >
597
641
  </button>
@@ -605,7 +649,7 @@
605
649
  class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-danger-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-danger-surface hover:bg-danger-soft active:translate-y-px disabled:cursor-not-allowed disabled:opacity-60"
606
650
  >
607
651
  <Square size={15} fill="currentColor" stroke="none" />
608
- <span class="@max-md:hidden">Stop Service</span>
652
+ <span class="@max-xs:hidden">Stop</span>
609
653
  </button>
610
654
  {/if}
611
655
  {:else}
@@ -622,7 +666,7 @@
622
666
  class="inline-flex h-9 cursor-pointer items-center gap-1.5 bg-surface-raised px-3 font-mono text-sm font-medium text-primary hover:bg-primary-soft active:translate-y-px"
623
667
  >
624
668
  <Play size={15} fill="currentColor" stroke="none" />
625
- <span class="@max-md:hidden">Run</span>
669
+ <span class="@max-xs:hidden">Run</span>
626
670
  </button>
627
671
  {#if onRunAgain}
628
672
  <Popover bind:open={runMenuOpen} placement="bottom-end">
@@ -668,7 +712,7 @@
668
712
  class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-danger-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-danger-surface hover:bg-danger-soft active:translate-y-px"
669
713
  >
670
714
  <Square size={15} fill="currentColor" stroke="none" />
671
- <span class="@max-md:hidden">Stop</span>
715
+ <span class="@max-xs:hidden">Stop</span>
672
716
  </button>
673
717
  {/if}
674
718
  {/if}
@@ -691,7 +735,7 @@
691
735
  title="Delete this run"
692
736
  aria-label="Delete run"
693
737
  >
694
- <Trash2 size={15} />
738
+ <Trash size={15} />
695
739
  </span>
696
740
  {/snippet}
697
741
  <div class="w-60">
@@ -745,7 +789,7 @@
745
789
  : "ml-1 flex min-h-[300px] flex-1 flex-col overflow-hidden border-t border-[var(--rw-con-gutter)] bg-[var(--rw-con-bg)]"}
746
790
  >
747
791
  <div
748
- class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] text-[var(--rw-con-dim)]"
792
+ class="@container flex shrink-0 items-center justify-between gap-3 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] text-[var(--rw-con-dim)]"
749
793
  >
750
794
  <div class="flex min-w-0 items-center gap-3">
751
795
  {#if consoleMaximized}
@@ -760,12 +804,12 @@
760
804
  </span>
761
805
  {:else}
762
806
  <span
763
- class="flex items-center gap-2 font-semibold text-[var(--rw-con-text)]"
807
+ class="flex items-center gap-2 font-semibold whitespace-nowrap text-[var(--rw-con-text)]"
764
808
  >
765
809
  <TerminalIcon size={14} class="opacity-70" />
766
810
  Console output
767
811
  </span>
768
- <span class="hidden text-[var(--rw-con-gutter)] sm:inline"
812
+ <span class="hidden text-[var(--rw-con-gutter)] @lg:inline"
769
813
  >stdout + stderr</span
770
814
  >
771
815
  {/if}
@@ -782,7 +826,7 @@
782
826
  aria-label="Toggle line wrapping"
783
827
  >
784
828
  <TextWrap size={13} />
785
- <span class="hidden sm:inline">Wrap</span>
829
+ <span class="hidden @sm:inline">Wrap</span>
786
830
  </button>
787
831
  <button
788
832
  type="button"
@@ -793,10 +837,7 @@
793
837
  >
794
838
  {#if consoleMaximized}
795
839
  <Minimize2 size={13} />
796
- <span
797
- class="rounded-[3px] border border-[var(--rw-con-gutter)] px-1.5 text-[10px] tracking-wide"
798
- >Esc</span
799
- >
840
+ <Kbd keys="Esc" size="xs" tone="console" />
800
841
  {:else}
801
842
  <Maximize2 size={13} />
802
843
  Expand
@@ -817,10 +858,43 @@
817
858
  {endLabel}
818
859
  {endTone}
819
860
  error={logFetchError}
861
+ loading={seededRunId !== run.id}
820
862
  />
821
863
  {/key}
822
864
  </div>
823
865
  </div>
866
+ {:else if loading && !notFound}
867
+ <!-- Loading: the panel's own frame (verdict header over the console) with
868
+ placeholders where the run's facts go, so nothing jumps when it lands. -->
869
+ <div
870
+ class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
871
+ role="status"
872
+ aria-label="Loading run"
873
+ >
874
+ <div class="shrink-0 border-b border-outline-faint bg-surface-raised">
875
+ <div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
876
+ <div class="flex h-[33px] items-center gap-2.5">
877
+ <span class="size-[18px] animate-pulse rounded-full bg-outline-hover"></span>
878
+ <span class="h-[18px] w-40 animate-pulse rounded-[3px] bg-outline-hover"></span>
879
+ </div>
880
+ <div class="mt-2.5 flex h-[17px] items-center">
881
+ <span class="h-3 w-64 max-w-full animate-pulse rounded-[3px] bg-outline-hover"
882
+ ></span>
883
+ </div>
884
+ </div>
885
+ </div>
886
+ <div
887
+ class="ml-1 flex min-h-[300px] flex-1 flex-col overflow-hidden border-t border-[var(--rw-con-gutter)] bg-[var(--rw-con-bg)]"
888
+ >
889
+ <div
890
+ class="flex shrink-0 items-center gap-2 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] font-semibold text-[var(--rw-con-text)]"
891
+ >
892
+ <TerminalIcon size={14} class="opacity-70" />
893
+ Console output
894
+ </div>
895
+ <LogConsole fetchLogs={() => undefined} loading class="min-h-0 flex-1" />
896
+ </div>
897
+ </div>
824
898
  {:else}
825
899
  <div
826
900
  class="flex min-w-0 flex-1 flex-col items-center justify-center gap-4 bg-surface-sunken/30"
@@ -40,7 +40,6 @@
40
40
  dayStartIso,
41
41
  dayEndIso,
42
42
  isExitCodeExprValid,
43
- exitCodePropChanged,
44
43
  type RunsListFilters,
45
44
  } from "./run-filters.js";
46
45
 
@@ -48,13 +47,10 @@
48
47
  filters = $bindable(),
49
48
  showTask = false,
50
49
  tasks = [],
51
- compact = false,
52
50
  }: {
53
51
  filters: RunsListFilters;
54
52
  showTask?: boolean;
55
53
  tasks?: { name: string }[];
56
- // Icon-only trigger for narrow rails where a "Filter" label wouldn't fit.
57
- compact?: boolean;
58
54
  } = $props();
59
55
 
60
56
  // The individual statuses behind the "Advanced" expander, grouped the way
@@ -121,18 +117,14 @@
121
117
  filters = { ...filters, triggeredBy: value || undefined };
122
118
  }
123
119
 
124
- // Exit code: a free-form expression (`137`, `>100`, `>100 <150`) normalized
125
- // to an inclusive range at the wire. Edited in a local buffer for smooth
126
- // typing and committed on change. `RunFilterPopover` itself is long-lived
127
- // (its trigger button stays mounted while the popover is closed), so the
128
- // buffer does NOT re-seed on its own — it must explicitly resync whenever
129
- // `filters.exitCode` changes from outside (a chip removal, "clear
130
- // filters"), without clobbering in-progress typing when some unrelated
131
- // filter dimension changes instead.
120
+ // Exit-code text is edited in a local buffer and committed on change. The
121
+ // component outlives the popover, so the buffer resyncs only when the
122
+ // committed `exitCode` itself moves (chip removed, filters cleared); an
123
+ // update to another dimension must not clobber in-progress typing.
132
124
  let exitCodeInput = $state(filters.exitCode ?? "");
133
125
  let lastExitCode = filters.exitCode;
134
126
  $effect(() => {
135
- if (exitCodePropChanged(lastExitCode, filters.exitCode)) {
127
+ if (lastExitCode !== filters.exitCode) {
136
128
  exitCodeInput = filters.exitCode ?? "";
137
129
  }
138
130
  lastExitCode = filters.exitCode;
@@ -152,8 +144,8 @@
152
144
  <Popover placement="right-start" mobileSheet>
153
145
  {#snippet trigger()}
154
146
  <span
155
- class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-[3px] border font-mono text-xs font-medium {compact &&
156
- count === 0
147
+ class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-[3px] border font-mono text-xs font-medium {count ===
148
+ 0
157
149
  ? 'w-7 justify-center px-0'
158
150
  : 'px-2'} {count > 0
159
151
  ? 'border-primary-soft-border bg-primary-soft text-primary-soft-text'
@@ -161,7 +153,6 @@
161
153
  title="Filter runs"
162
154
  >
163
155
  <Funnel size={13} />
164
- {#if !compact}Filter{/if}
165
156
  {#if count > 0}
166
157
  <Badge variant="primary" size="sm" class="px-1.5 py-0">{count}</Badge>
167
158
  {/if}