@runwisp/ui 1.4.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 (70) hide show
  1. package/package.json +4 -5
  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 +30 -32
  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/Table.svelte +13 -4
  28. package/src/lib/components/TableBody.svelte +2 -1
  29. package/src/lib/components/TableCell.svelte +38 -8
  30. package/src/lib/components/TableHead.svelte +12 -4
  31. package/src/lib/components/TableRow.svelte +23 -3
  32. package/src/lib/components/TaskCard.svelte +1 -1
  33. package/src/lib/components/TimezoneSelect.svelte +2 -2
  34. package/src/lib/components/accordion.svelte.ts +19 -0
  35. package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
  36. package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
  37. package/src/lib/components/dashboard/RunsList.svelte +9 -18
  38. package/src/lib/components/dashboard/cron-format.ts +1 -1
  39. package/src/lib/components/dashboard/run-filters.test.ts +2 -58
  40. package/src/lib/components/dashboard/run-filters.ts +5 -53
  41. package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
  42. package/src/lib/components/dashboard/run-helpers.ts +12 -1
  43. package/src/lib/components/dashboard/status-config.ts +38 -83
  44. package/src/lib/components/dashboard/types.ts +0 -26
  45. package/src/lib/components/radio-group.ts +11 -0
  46. package/src/lib/index.ts +8 -43
  47. package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
  48. package/src/lib/log-console/highlight-scroll.test.ts +59 -0
  49. package/src/lib/log-console/highlight-scroll.ts +41 -0
  50. package/src/lib/theme.css +10 -4
  51. package/src/lib/utils/clipboard.svelte.test.ts +92 -0
  52. package/src/lib/utils/clipboard.svelte.ts +73 -0
  53. package/src/lib/utils/format.ts +65 -94
  54. package/src/lib/utils/id.test.ts +3 -7
  55. package/src/lib/utils/id.ts +3 -2
  56. package/src/lib/utils/theme.svelte.test.ts +0 -6
  57. package/src/lib/utils/theme.svelte.ts +3 -5
  58. package/src/lib/utils/toast.svelte.ts +3 -11
  59. package/src/stories/Accordion.stories.svelte +61 -0
  60. package/src/stories/CodeBlock.stories.svelte +55 -0
  61. package/src/stories/DataGrid.stories.svelte +0 -5
  62. package/src/stories/Heading.stories.svelte +31 -0
  63. package/src/stories/Kbd.stories.svelte +36 -0
  64. package/src/stories/Link.stories.svelte +38 -0
  65. package/src/stories/PageHeader.stories.svelte +31 -0
  66. package/src/stories/Progress.stories.svelte +26 -0
  67. package/src/stories/RadioGroup.stories.svelte +43 -0
  68. package/src/stories/Table.stories.svelte +69 -0
  69. package/src/svelte-shims.d.ts +0 -5
  70. package/src/lib/components/dashboard/status-config.test.ts +0 -18
@@ -15,8 +15,9 @@
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,10 +65,12 @@
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,
70
76
  loading = false,
@@ -100,19 +106,25 @@
100
106
  // cold-start path so a never-run task is still launchable from here.
101
107
  onRunTask?: (() => void) | undefined;
102
108
  // Service lifecycle controls. When either is set the cluster swaps the
103
- // Run control for Stop Service / Restart Service (chosen by serviceStopped).
109
+ // Run control for Stop / Restart (chosen by serviceStopped).
104
110
  onStopService?: (() => void) | undefined;
105
111
  onRestartService?: (() => void) | undefined;
106
112
  serviceStopped?: boolean;
107
113
  serviceBusy?: boolean;
108
- // Toggle the (single-instance service) history rail from the panel header,
109
- // since these tasks hide the rail by default and there is no top bar.
110
- onToggleHistory?: (() => void) | undefined;
111
- 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;
112
121
  highlightLine?: number | null;
113
122
  // Resolves a task's currently configured instance count so multi-instance
114
123
  // services render a 1-based #N suffix. Defaults to single-instance.
115
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;
116
128
  // Which runs arrived or were removed live moments ago. The panel eases in
117
129
  // when a new run takes it (triggered, or a scheduled run auto-selected)
118
130
  // or when its run was deleted and another takes its place; picking a
@@ -148,7 +160,7 @@
148
160
 
149
161
  let canDelete = $derived.by(() => {
150
162
  if (!run || !onDelete) return false;
151
- const status = runDisplayStatus(run);
163
+ const status = displayStatus(run.status, run.endReason);
152
164
  return status !== "running" && status !== "pending";
153
165
  });
154
166
 
@@ -266,19 +278,7 @@
266
278
  let confirmDeleteOpen = $state(false);
267
279
  // Dropdown half of the Run split button (the "reuse parameters" variant).
268
280
  let runMenuOpen = $state(false);
269
- let copiedId = $state(false);
270
- let copyTimer: ReturnType<typeof setTimeout> | null = null;
271
-
272
- async function copyRunId(id: string) {
273
- try {
274
- await navigator.clipboard.writeText(id);
275
- copiedId = true;
276
- if (copyTimer) clearTimeout(copyTimer);
277
- copyTimer = setTimeout(() => (copiedId = false), 1200);
278
- } catch {
279
- // Clipboard blocked (insecure context / denied) — leave the chip as-is.
280
- }
281
- }
281
+ const runIdCopy = new CopyFeedback(1200);
282
282
 
283
283
  // The accent for a status that means "something to triage", or undefined
284
284
  // when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
@@ -362,8 +362,8 @@
362
362
  <svelte:window onkeydown={handleConsoleKeydown} />
363
363
 
364
364
  {#if run}
365
- {@const status = runDisplayStatus(run)}
366
- {@const config = getRunStatusConfig(status)}
365
+ {@const status = displayStatus(run.status, run.endReason)}
366
+ {@const config = RUN_STATUS_CONFIG[status]}
367
367
  {@const DetailIcon = config.icon}
368
368
  {@const duration = runDuration(
369
369
  run,
@@ -372,6 +372,8 @@
372
372
  {@const startDelay = runStartDelay(run)}
373
373
  {@const startedAt = run.startedAt ?? run.createdAt}
374
374
  {@const retry = runRetryLabel(run)}
375
+ {@const usage = runUsageLabel(run)}
376
+ {@const live = run.status === "running" ? getLiveUsage(run.id) : undefined}
375
377
  {@const paramEntries = run.params ? Object.entries(run.params) : []}
376
378
  {@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
377
379
  {@const spine = config.dot.replace(" animate-pulse", "")}
@@ -416,8 +418,36 @@
416
418
  style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
417
419
  >
418
420
  <div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
419
- <div class="flex items-start gap-4">
420
- <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">
421
451
  <!-- Identity, but only where the page around the panel isn't
422
452
  already saying it: the cross-task runs list needs the task
423
453
  name, a task's own page already has it in the breadcrumb. -->
@@ -544,16 +574,35 @@
544
574
  </div>
545
575
  </Popover>
546
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}
547
596
  <!-- Run-id chip: click to copy the full ULID. Last, because
548
597
  it is the fact you reach for least and the only one you
549
598
  interact with. -->
550
599
  <button
551
600
  type="button"
552
- onclick={() => copyRunId(run.id)}
601
+ onclick={() => void runIdCopy.copy(run.id)}
553
602
  title="Copy run ID"
554
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"
555
604
  >
556
- {#if copiedId}
605
+ {#if runIdCopy.copied}
557
606
  <Check size={11} class="text-success-surface" />Copied
558
607
  {:else}
559
608
  <Hash size={11} />{run.id}
@@ -562,27 +611,9 @@
562
611
  </div>
563
612
  </div>
564
613
 
565
- <!-- Actions: history toggle · run (split) / service lifecycle ·
614
+ <!-- Actions: run (split) / service lifecycle ·
566
615
  stop · download · delete (delete behind an inline confirm) -->
567
- <div class="flex shrink-0 items-center gap-2">
568
- {#if onToggleHistory}
569
- <button
570
- type="button"
571
- onclick={() => onToggleHistory()}
572
- 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"
573
- title={historyVisible ? "Hide run history" : "Show run history"}
574
- aria-label={historyVisible
575
- ? "Hide run history"
576
- : "Show run history"}
577
- >
578
- {#if historyVisible}
579
- <PanelLeftClose size={15} />
580
- {:else}
581
- <RotateCcwClock size={15} />
582
- {/if}
583
- </button>
584
- {/if}
585
-
616
+ <div class="flex flex-wrap items-center gap-2">
586
617
  {#if onStopService || onRestartService}
587
618
  <!-- Service lifecycle replaces the run control.
588
619
  Restart is always available (it cancels + respawns
@@ -604,7 +635,7 @@
604
635
  {:else}
605
636
  <RefreshCcw size={15} />
606
637
  {/if}
607
- <span class="@max-md:hidden"
638
+ <span class="@max-xs:hidden"
608
639
  >{serviceStopped ? "Start" : "Restart"}</span
609
640
  >
610
641
  </button>
@@ -618,7 +649,7 @@
618
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"
619
650
  >
620
651
  <Square size={15} fill="currentColor" stroke="none" />
621
- <span class="@max-md:hidden">Stop Service</span>
652
+ <span class="@max-xs:hidden">Stop</span>
622
653
  </button>
623
654
  {/if}
624
655
  {:else}
@@ -635,7 +666,7 @@
635
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"
636
667
  >
637
668
  <Play size={15} fill="currentColor" stroke="none" />
638
- <span class="@max-md:hidden">Run</span>
669
+ <span class="@max-xs:hidden">Run</span>
639
670
  </button>
640
671
  {#if onRunAgain}
641
672
  <Popover bind:open={runMenuOpen} placement="bottom-end">
@@ -681,7 +712,7 @@
681
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"
682
713
  >
683
714
  <Square size={15} fill="currentColor" stroke="none" />
684
- <span class="@max-md:hidden">Stop</span>
715
+ <span class="@max-xs:hidden">Stop</span>
685
716
  </button>
686
717
  {/if}
687
718
  {/if}
@@ -758,7 +789,7 @@
758
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)]"}
759
790
  >
760
791
  <div
761
- 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)]"
762
793
  >
763
794
  <div class="flex min-w-0 items-center gap-3">
764
795
  {#if consoleMaximized}
@@ -773,12 +804,12 @@
773
804
  </span>
774
805
  {:else}
775
806
  <span
776
- 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)]"
777
808
  >
778
809
  <TerminalIcon size={14} class="opacity-70" />
779
810
  Console output
780
811
  </span>
781
- <span class="hidden text-[var(--rw-con-gutter)] sm:inline"
812
+ <span class="hidden text-[var(--rw-con-gutter)] @lg:inline"
782
813
  >stdout + stderr</span
783
814
  >
784
815
  {/if}
@@ -795,7 +826,7 @@
795
826
  aria-label="Toggle line wrapping"
796
827
  >
797
828
  <TextWrap size={13} />
798
- <span class="hidden sm:inline">Wrap</span>
829
+ <span class="hidden @sm:inline">Wrap</span>
799
830
  </button>
800
831
  <button
801
832
  type="button"
@@ -806,10 +837,7 @@
806
837
  >
807
838
  {#if consoleMaximized}
808
839
  <Minimize2 size={13} />
809
- <span
810
- class="rounded-[3px] border border-[var(--rw-con-gutter)] px-1.5 text-[10px] tracking-wide"
811
- >Esc</span
812
- >
840
+ <Kbd keys="Esc" size="xs" tone="console" />
813
841
  {:else}
814
842
  <Maximize2 size={13} />
815
843
  Expand
@@ -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}
@@ -19,9 +19,8 @@
19
19
  import type { RunMotion } from "../../utils/run-motion.js";
20
20
  import EmptyState from "../EmptyState.svelte";
21
21
  import RunFilterPopover from "./RunFilterPopover.svelte";
22
- import type { Run } from "./types.js";
23
- import type { RunSelector, RunStatus } from "@runwisp/common";
24
- import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
22
+ import { displayStatus, type Run, type RunSelector, type RunStatus } from "@runwisp/common";
23
+ import { RUN_STATUS_CONFIG } from "./status-config.js";
25
24
  import {
26
25
  activeDimensions,
27
26
  clearDimension,
@@ -71,7 +70,6 @@
71
70
  onBulkRerun,
72
71
  getInstanceCount = () => 1,
73
72
  motion,
74
- flush = false,
75
73
  outputSearch = false,
76
74
  outputQuery = "",
77
75
  outputMatches = null,
@@ -105,11 +103,6 @@
105
103
  // Which runs arrived or were removed live moments ago: their rows animate
106
104
  // in or out. Page loads, scrolling and filtering never do.
107
105
  motion?: RunMotion;
108
- // Flush rail mode (task detail page): render as a borderless rail that
109
- // fills its column and is divided from the detail panel by a single right
110
- // border — no card chrome of its own. Default renders the standalone card
111
- // the cross-task /runs grid expects.
112
- flush?: boolean;
113
106
  // Output search (history rail): filters runs by what they printed. The
114
107
  // search box lives in the app header now; the parent owns the box, the
115
108
  // query, and the async log search. This component just renders the
@@ -392,17 +385,15 @@
392
385
  });
393
386
  </script>
394
387
 
388
+ <!-- A borderless rail that fills its column, divided from the detail panel by
389
+ a single right border. -->
395
390
  <div
396
- class={flush
397
- ? "flex h-full w-full flex-col overflow-hidden border-b border-outline bg-surface md:w-[300px] md:shrink-0 md:border-r md:border-b-0"
398
- : "flex flex-col overflow-hidden rounded-[4px] border border-outline bg-surface-raised shadow-sm md:col-span-4 lg:col-span-3"}
391
+ class="flex h-full w-full flex-col overflow-hidden border-b border-outline bg-surface md:w-[300px] md:shrink-0 md:border-r md:border-b-0"
399
392
  >
400
393
  <div
401
394
  class="flex shrink-0 items-center gap-2 border-b px-3 py-2 {hasSelection
402
395
  ? 'border-outline-faint bg-primary-soft/40'
403
- : flush
404
- ? 'border-transparent bg-surface'
405
- : 'border-outline-faint bg-surface-sunken'}"
396
+ : 'border-transparent bg-surface'}"
406
397
  >
407
398
  {#if bulkActions}
408
399
  <label
@@ -483,7 +474,7 @@
483
474
  </span>
484
475
  <div class="ml-auto flex items-center gap-1">
485
476
  {#if showFilters}
486
- <RunFilterPopover bind:filters {showTask} {tasks} compact={flush} />
477
+ <RunFilterPopover bind:filters {showTask} {tasks} />
487
478
  {/if}
488
479
  <Button
489
480
  variant="ghost"
@@ -688,8 +679,8 @@
688
679
  {/snippet}
689
680
 
690
681
  {#snippet runRowButton(run: Run, isActive: boolean, match: RunOutputMatch | undefined)}
691
- {@const dstatus = runDisplayStatus(run)}
692
- {@const config = getRunStatusConfig(dstatus)}
682
+ {@const dstatus = displayStatus(run.status, run.endReason)}
683
+ {@const config = RUN_STATUS_CONFIG[dstatus]}
693
684
  {@const running = run.status === "running"}
694
685
  {@const spine = config.dot.replace(" animate-pulse", "")}
695
686
  {@const startedAt = run.startedAt ?? run.createdAt}
@@ -2,7 +2,7 @@
2
2
 
3
3
  import cronstrue from "cronstrue";
4
4
 
5
- export interface HumanizedCron {
5
+ interface HumanizedCron {
6
6
  /** Display text — humanized when possible, otherwise the raw expression. */
7
7
  humanized: string;
8
8
  /** The original cron expression, for tooltips. */
@@ -6,13 +6,10 @@ import { END_REASONS } from "@runwisp/common";
6
6
  import {
7
7
  emptyRunFilters,
8
8
  FAILURE_STATUS_TOKEN,
9
- NEEDS_ATTENTION_STATUSES,
10
- isNeedsAttention,
11
9
  dimensionActive,
12
10
  activeDimensions,
13
11
  activeFilterCount,
14
12
  clearDimension,
15
- clearPopoverFilters,
16
13
  humanizeStatus,
17
14
  triggerDescription,
18
15
  FILTERABLE_TRIGGERS,
@@ -29,7 +26,6 @@ import {
29
26
  exitCodeRangeActive,
30
27
  isExitCodeExprValid,
31
28
  exitCodeChipLabel,
32
- exitCodePropChanged,
33
29
  type StatusBucket,
34
30
  type RunsListFilters,
35
31
  } from "./run-filters.js";
@@ -54,23 +50,6 @@ describe("emptyRunFilters", () => {
54
50
  });
55
51
  });
56
52
 
57
- describe("isNeedsAttention", () => {
58
- it("is true for exactly the attention set, order-insensitive", () => {
59
- expect(isNeedsAttention([...NEEDS_ATTENTION_STATUSES])).toBe(true);
60
- expect(isNeedsAttention([...NEEDS_ATTENTION_STATUSES].reverse())).toBe(true);
61
- });
62
-
63
- it("is exactly the failure sentinel (server resolves it to is_failure)", () => {
64
- expect(NEEDS_ATTENTION_STATUSES).toEqual([FAILURE_STATUS_TOKEN]);
65
- });
66
-
67
- it("is false for a raw end reason or a different set", () => {
68
- expect(isNeedsAttention(["failed"])).toBe(false);
69
- expect(isNeedsAttention(["succeeded"])).toBe(false);
70
- expect(isNeedsAttention([])).toBe(false);
71
- });
72
- });
73
-
74
53
  describe("dimensionActive / activeDimensions", () => {
75
54
  it("detects each dimension independently", () => {
76
55
  expect(dimensionActive(base({ statuses: ["failed"] }), "status")).toBe(true);
@@ -125,25 +104,6 @@ describe("clearDimension", () => {
125
104
  });
126
105
  });
127
106
 
128
- describe("clearPopoverFilters", () => {
129
- it("clears every popover dimension but keeps search and sort", () => {
130
- const f = base({
131
- search: "nightly",
132
- sortDirection: "asc",
133
- statuses: ["failed"],
134
- taskName: "backup",
135
- triggeredBy: "cron",
136
- exitCode: "1",
137
- retriesOnly: true,
138
- createdAfter: "2026-01-01T00:00:00Z",
139
- });
140
- const cleared = clearPopoverFilters(f);
141
- expect(activeFilterCount(cleared)).toBe(0);
142
- expect(cleared.search).toBe("nightly");
143
- expect(cleared.sortDirection).toBe("asc");
144
- });
145
- });
146
-
147
107
  describe("humanizeStatus", () => {
148
108
  it("title-cases and de-snakes a status token", () => {
149
109
  expect(humanizeStatus("log_overflow")).toBe("Log overflow");
@@ -154,8 +114,8 @@ describe("humanizeStatus", () => {
154
114
 
155
115
  describe("statusChipLabel", () => {
156
116
  it("names a bucket when the selection matches one exactly", () => {
157
- // The Failed bucket is the needs-attention set.
158
- expect(statusChipLabel([...NEEDS_ATTENTION_STATUSES])).toBe("Failed");
117
+ // The Failed bucket is the failure sentinel.
118
+ expect(statusChipLabel([FAILURE_STATUS_TOKEN])).toBe("Failed");
159
119
  expect(statusChipLabel(["pending", "running"])).toBe("Running");
160
120
  expect(statusChipLabel(["succeeded"])).toBe("Succeeded");
161
121
  });
@@ -332,19 +292,3 @@ describe("exit-code expression", () => {
332
292
  expect(exitCodeChipLabel(" 137 ")).toBe("Exit 137");
333
293
  });
334
294
  });
335
-
336
- describe("exitCodePropChanged", () => {
337
- it("is false when the committed value is unchanged (unrelated filters update)", () => {
338
- expect(exitCodePropChanged("137", "137")).toBe(false);
339
- expect(exitCodePropChanged(undefined, undefined)).toBe(false);
340
- });
341
-
342
- it("is true when the exit-code filter was cleared externally (chip removed)", () => {
343
- expect(exitCodePropChanged("137", undefined)).toBe(true);
344
- });
345
-
346
- it("is true when the exit-code filter was set to a new committed value", () => {
347
- expect(exitCodePropChanged(undefined, "200")).toBe(true);
348
- expect(exitCodePropChanged("137", "200")).toBe(true);
349
- });
350
- });