@runwisp/ui 1.5.1 → 1.7.1
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.
- package/package.json +5 -7
- package/src/lib/actions/fold-to-fit.test.ts +36 -0
- package/src/lib/actions/fold-to-fit.ts +89 -0
- package/src/lib/actions/modal-dialog.test.ts +47 -0
- package/src/lib/actions/modal-dialog.ts +48 -0
- package/src/lib/components/AccordionItem.svelte +16 -5
- package/src/lib/components/Badge.svelte +3 -24
- package/src/lib/components/Button.svelte +2 -9
- package/src/lib/components/CodeBlock.svelte +7 -6
- package/src/lib/components/Drawer.svelte +22 -30
- package/src/lib/components/DurationInput.svelte +6 -25
- package/src/lib/components/FilterBar.svelte +2 -6
- package/src/lib/components/Link.svelte +9 -4
- package/src/lib/components/LogConsole.svelte +95 -418
- package/src/lib/components/Modal.svelte +7 -30
- package/src/lib/components/Spinner.svelte +10 -5
- package/src/lib/components/StatusIndicator.svelte +37 -104
- package/src/lib/components/Tabs.svelte +15 -1
- package/src/lib/components/dashboard/RunActions.svelte +156 -0
- package/src/lib/components/dashboard/RunConsoleFrame.svelte +319 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +130 -717
- package/src/lib/components/dashboard/RunFacts.svelte +119 -0
- package/src/lib/components/dashboard/RunFilterChips.svelte +52 -0
- package/src/lib/components/dashboard/RunFilterPopover.svelte +4 -5
- package/src/lib/components/dashboard/RunHeaderLine.svelte +295 -0
- package/src/lib/components/dashboard/RunListSkeleton.svelte +43 -0
- package/src/lib/components/dashboard/RunRow.svelte +202 -0
- package/src/lib/components/dashboard/RunsList.svelte +265 -543
- package/src/lib/components/dashboard/bulk-selection.svelte.test.ts +42 -0
- package/src/lib/components/dashboard/bulk-selection.svelte.ts +50 -0
- package/src/lib/components/dashboard/cron-format.test.ts +9 -0
- package/src/lib/components/dashboard/cron-format.ts +5 -1
- package/src/lib/components/dashboard/run-filters.test.ts +60 -0
- package/src/lib/components/dashboard/run-filters.ts +78 -6
- package/src/lib/components/dashboard/run-helpers.test.ts +122 -12
- package/src/lib/components/dashboard/run-helpers.ts +122 -6
- package/src/lib/components/dashboard/status-config.ts +18 -0
- package/src/lib/components/dashboard/types.ts +6 -0
- package/src/lib/index.ts +13 -2
- package/src/lib/log-console/AnsiLine.svelte +12 -0
- package/src/lib/log-console/FrameHistoryBlock.svelte +57 -0
- package/src/lib/log-console/LogCache.svelte.test.ts +135 -0
- package/src/lib/log-console/LogConsoleFooter.svelte +88 -0
- package/src/lib/log-console/LogFetcher.svelte.test.ts +159 -0
- package/src/lib/log-console/frame-history.svelte.test.ts +47 -0
- package/src/lib/log-console/frame-history.svelte.ts +71 -0
- package/src/lib/log-console/wrap-layout.svelte.test.ts +92 -0
- package/src/lib/log-console/wrap-layout.svelte.ts +188 -0
- package/src/lib/utils/format.test.ts +122 -0
- package/src/lib/utils/format.ts +97 -4
- package/src/lib/utils/toast.svelte.test.ts +46 -0
- package/src/lib/utils/typing-target.ts +10 -0
- package/src/stories/Accordion.stories.svelte +60 -1
- package/src/stories/CodeBlock.stories.svelte +13 -0
- package/src/stories/Link.stories.svelte +25 -1
- package/src/stories/Tabs.stories.svelte +17 -0
|
@@ -2,54 +2,27 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
-
import { untrack } from "svelte";
|
|
6
5
|
import {
|
|
7
|
-
Hash,
|
|
8
|
-
Check,
|
|
9
6
|
Terminal as TerminalIcon,
|
|
10
|
-
Download,
|
|
11
|
-
Trash,
|
|
12
7
|
Play,
|
|
13
|
-
Square,
|
|
14
8
|
MousePointerClick,
|
|
15
|
-
RotateCw,
|
|
16
|
-
RefreshCcw,
|
|
17
|
-
ChevronDown,
|
|
18
9
|
ArrowLeft,
|
|
19
|
-
PanelLeftClose,
|
|
20
|
-
PanelLeftOpen,
|
|
21
|
-
SlidersHorizontal,
|
|
22
|
-
Maximize2,
|
|
23
|
-
Minimize2,
|
|
24
|
-
TextWrap,
|
|
25
10
|
SearchX,
|
|
26
11
|
} from "@lucide/svelte";
|
|
27
|
-
import Button from "../Button.svelte";
|
|
28
12
|
import EmptyState from "../EmptyState.svelte";
|
|
29
|
-
import Kbd from "../Kbd.svelte";
|
|
30
13
|
import LogConsole from "../LogConsole.svelte";
|
|
31
|
-
import Popover from "../Popover.svelte";
|
|
32
14
|
import Tooltip from "../Tooltip.svelte";
|
|
33
|
-
import { portal } from "../../actions/portal.js";
|
|
34
15
|
import { prefersReducedMotion } from "../../actions/row-motion.js";
|
|
35
16
|
import type { RunMotion } from "../../utils/run-motion.js";
|
|
36
|
-
import {
|
|
37
|
-
import { formatBytes, formatClockTime, formatCalendarDate } from "../../utils/format.js";
|
|
38
|
-
import { formatShortId } from "../../utils/id.js";
|
|
17
|
+
import type { LogEvent, LogSlice } from "../../log-console/types.js";
|
|
39
18
|
import { TickingNow } from "../../utils/ticking-now.svelte.js";
|
|
40
|
-
import { CopyFeedback } from "../../utils/clipboard.svelte.js";
|
|
41
19
|
import { displayStatus, type ResourceUsage, type Run } from "@runwisp/common";
|
|
20
|
+
import RunActions from "./RunActions.svelte";
|
|
21
|
+
import RunConsoleFrame from "./RunConsoleFrame.svelte";
|
|
22
|
+
import RunFacts from "./RunFacts.svelte";
|
|
23
|
+
import RunHeaderLine from "./RunHeaderLine.svelte";
|
|
42
24
|
import { RUN_STATUS_CONFIG } from "./status-config.js";
|
|
43
|
-
import {
|
|
44
|
-
runDuration,
|
|
45
|
-
runStartDelay,
|
|
46
|
-
runVerdict,
|
|
47
|
-
formatTriggeredByLabel,
|
|
48
|
-
runRetryLabel,
|
|
49
|
-
runUsageLabel,
|
|
50
|
-
runLogDownloadUrl,
|
|
51
|
-
instanceSuffix,
|
|
52
|
-
} from "./run-helpers.js";
|
|
25
|
+
import { runDuration, RUN_VERDICTS, instanceSuffix } from "./run-helpers.js";
|
|
53
26
|
|
|
54
27
|
let {
|
|
55
28
|
run,
|
|
@@ -62,19 +35,17 @@
|
|
|
62
35
|
onRunAgain,
|
|
63
36
|
onStop,
|
|
64
37
|
onRunTask,
|
|
65
|
-
onStopService,
|
|
66
|
-
onRestartService,
|
|
67
|
-
serviceStopped = false,
|
|
68
|
-
serviceBusy = false,
|
|
69
38
|
onBack,
|
|
70
|
-
onToggleList,
|
|
71
|
-
listVisible = true,
|
|
72
39
|
highlightLine = null,
|
|
73
40
|
getInstanceCount = () => 1,
|
|
74
41
|
getLiveUsage = () => undefined,
|
|
75
42
|
motion,
|
|
76
43
|
notFound = false,
|
|
77
44
|
loading = false,
|
|
45
|
+
headerLayout = "stacked",
|
|
46
|
+
stopLabel,
|
|
47
|
+
emptyTitle,
|
|
48
|
+
emptyDescription,
|
|
78
49
|
}: {
|
|
79
50
|
run: Run | undefined;
|
|
80
51
|
fetchLogs: (
|
|
@@ -91,7 +62,7 @@
|
|
|
91
62
|
// redraw line so the log console can offer inline rewind. Optional.
|
|
92
63
|
fetchLineHistory?: (runId: string, lineNum: number) => Promise<string[][]>;
|
|
93
64
|
showTaskName?: boolean;
|
|
94
|
-
onDelete?: (runId: string) => void;
|
|
65
|
+
onDelete?: ((runId: string) => void) | undefined;
|
|
95
66
|
// Start a new run with default options. When set, the action cluster's
|
|
96
67
|
// primary control is a Run button (the cold-start / re-trigger path).
|
|
97
68
|
onRun?: (() => void) | undefined;
|
|
@@ -106,19 +77,9 @@
|
|
|
106
77
|
// Trigger the task from the empty state (no run selected yet), the
|
|
107
78
|
// cold-start path so a never-run task is still launchable from here.
|
|
108
79
|
onRunTask?: (() => void) | undefined;
|
|
109
|
-
// Service lifecycle controls. When either is set the cluster swaps the
|
|
110
|
-
// Run control for Stop / Restart (chosen by serviceStopped).
|
|
111
|
-
onStopService?: (() => void) | undefined;
|
|
112
|
-
onRestartService?: (() => void) | undefined;
|
|
113
|
-
serviceStopped?: boolean;
|
|
114
|
-
serviceBusy?: boolean;
|
|
115
80
|
// Return to the run list when the panel replaces it (a phone), shown as
|
|
116
81
|
// a back arrow in the header's top-left corner.
|
|
117
82
|
onBack?: (() => void) | undefined;
|
|
118
|
-
// Fold the run list away (or back) when it sits beside the panel on a
|
|
119
|
-
// screen too narrow to keep both comfortably, in the same corner.
|
|
120
|
-
onToggleList?: (() => void) | undefined;
|
|
121
|
-
listVisible?: boolean;
|
|
122
83
|
highlightLine?: number | null;
|
|
123
84
|
// Resolves a task's currently configured instance count so multi-instance
|
|
124
85
|
// services render a 1-based #N suffix. Defaults to single-instance.
|
|
@@ -138,6 +99,15 @@
|
|
|
138
99
|
// True while there is no run to show *yet* (the run list or a deep-linked
|
|
139
100
|
// run is still loading), so the empty state doesn't claim "No runs yet".
|
|
140
101
|
loading?: boolean;
|
|
102
|
+
// "stacked": verdict over a fact line, with room for the task name.
|
|
103
|
+
// "line": one line whose facts fold into ⋯ when it runs out of room,
|
|
104
|
+
// for a page that already names the task.
|
|
105
|
+
headerLayout?: "stacked" | "line";
|
|
106
|
+
// Label of the stop button, "Stop" by default.
|
|
107
|
+
stopLabel?: string;
|
|
108
|
+
// Empty-state copy for when no run is selected, replacing the default.
|
|
109
|
+
emptyTitle?: string;
|
|
110
|
+
emptyDescription?: string;
|
|
141
111
|
} = $props();
|
|
142
112
|
|
|
143
113
|
function easeInFresh(node: HTMLElement, runId: string) {
|
|
@@ -173,195 +143,8 @@
|
|
|
173
143
|
if (run?.status !== "running") return;
|
|
174
144
|
return durationTicker.start();
|
|
175
145
|
});
|
|
176
|
-
|
|
177
|
-
const TAIL_LINES = 1000;
|
|
178
|
-
|
|
179
|
-
let logConsole = $state<{ onStream: (event: LogEvent) => void } | null>(null);
|
|
180
|
-
|
|
181
|
-
// Message from the most recent seed-fetch failure (session expiry, daemon
|
|
182
|
-
// restart mid-request, ...), surfaced to LogConsole instead of letting a
|
|
183
|
-
// failed fetch render as an indistinguishable blank "no output" pane.
|
|
184
|
-
let logFetchError = $state<string | null>(null);
|
|
185
|
-
|
|
186
|
-
// Derive a stable scalar so the effect only re-runs when the id actually
|
|
187
|
-
// changes, not on every run object reference swap from SSE array updates.
|
|
188
|
-
let runId = $derived(run?.id);
|
|
189
|
-
|
|
190
|
-
// The run whose seed fetch has settled. Until it matches the shown run the
|
|
191
|
-
// console says it's loading, not "No output yet".
|
|
192
|
-
let seededRunId = $state<string | null>(null);
|
|
193
|
-
|
|
194
|
-
// Fetches the batched tail page a run's console opens on (see the effect
|
|
195
|
-
// below) and paints it into the console. Resolves the line the live
|
|
196
|
-
// stream should resume from, or null when there's nothing left to stream
|
|
197
|
-
// (the run already ended, or the caller cancelled while we were waiting).
|
|
198
|
-
// A failed fetch still falls back to an SSE tail backfill from scratch,
|
|
199
|
-
// it does not stop the console from live-tailing, but records the
|
|
200
|
-
// failure so LogConsole can show it instead of looking like a quiet run.
|
|
201
|
-
async function seedConsoleTail(
|
|
202
|
-
id: string,
|
|
203
|
-
isCancelled: () => boolean,
|
|
204
|
-
): Promise<{ fromLine: number } | null> {
|
|
205
|
-
try {
|
|
206
|
-
const seed = await fetchLogs(id, -TAIL_LINES, -1);
|
|
207
|
-
if (isCancelled()) return null;
|
|
208
|
-
if (!isLogEvent(seed)) return { fromLine: -TAIL_LINES };
|
|
209
|
-
if (logConsole) logConsole.onStream(seed);
|
|
210
|
-
return seed.finished // ended run: nothing live to follow
|
|
211
|
-
? null
|
|
212
|
-
: { fromLine: seed.sizeLines > 0 ? seed.sizeLines : -TAIL_LINES };
|
|
213
|
-
} catch (err) {
|
|
214
|
-
if (isCancelled()) return null;
|
|
215
|
-
logFetchError = err instanceof Error ? err.message : "Failed to load logs";
|
|
216
|
-
return { fromLine: -TAIL_LINES };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
$effect(() => {
|
|
221
|
-
const id = runId;
|
|
222
|
-
if (!id) return;
|
|
223
|
-
|
|
224
|
-
const stream = streamLogs;
|
|
225
|
-
if (!stream) {
|
|
226
|
-
seededRunId = id;
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
logFetchError = null;
|
|
231
|
-
|
|
232
|
-
let cleanup: (() => void) | undefined;
|
|
233
|
-
let cancelled = false;
|
|
234
|
-
|
|
235
|
-
untrack(() => {
|
|
236
|
-
void (async () => {
|
|
237
|
-
// Paint the tail in one batched page so the console opens
|
|
238
|
-
// already at the newest output, instead of replaying 1000
|
|
239
|
-
// backfill lines over SSE and visibly scrolling down to reach
|
|
240
|
-
// the end. The live stream then picks up after the seeded tail
|
|
241
|
-
// (its disk backfill from that anchor closes the fetch↔stream
|
|
242
|
-
// race, and the daemon dedupes anything already shown).
|
|
243
|
-
const seeded = await seedConsoleTail(id, () => cancelled);
|
|
244
|
-
if (cancelled) return;
|
|
245
|
-
seededRunId = id;
|
|
246
|
-
if (!seeded) return;
|
|
247
|
-
cleanup = stream(
|
|
248
|
-
id,
|
|
249
|
-
(event: LogEvent) => {
|
|
250
|
-
if (logConsole) logConsole.onStream(event);
|
|
251
|
-
},
|
|
252
|
-
{ fromLine: seeded.fromLine },
|
|
253
|
-
);
|
|
254
|
-
})();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
return () => {
|
|
258
|
-
cancelled = true;
|
|
259
|
-
if (cleanup) cleanup();
|
|
260
|
-
};
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Maximize the console to a full-bleed overlay. The console wrapper is
|
|
264
|
-
// never remounted, `maximizePortal` relocates the *live* node to <body>
|
|
265
|
-
// (escaping any transformed/blurred ancestor that would otherwise trap a
|
|
266
|
-
// position:fixed child) and moves it back on restore, so the SSE stream,
|
|
267
|
-
// scroll position, and the {#key run.id} mount all survive the toggle.
|
|
268
|
-
let consoleMaximized = $state(false);
|
|
269
|
-
// Wrap long console lines instead of horizontally scrolling them. Owned
|
|
270
|
-
// here so the toolbar toggle and the <LogConsole> share one source of truth.
|
|
271
|
-
let consoleWrap = $state(false);
|
|
272
|
-
|
|
273
|
-
// A run going away (deselect) tears down the console; drop the overlay too.
|
|
274
|
-
$effect(() => {
|
|
275
|
-
if (!run) consoleMaximized = false;
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Inline-confirm popover for delete, and a transient "Copied" on the run-id chip.
|
|
279
|
-
let confirmDeleteOpen = $state(false);
|
|
280
|
-
// Dropdown half of the Run split button (the "reuse parameters" variant).
|
|
281
|
-
let runMenuOpen = $state(false);
|
|
282
|
-
const runIdCopy = new CopyFeedback(1200);
|
|
283
|
-
|
|
284
|
-
// The accent for a status that means "something to triage", or undefined
|
|
285
|
-
// when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
|
|
286
|
-
// failure is the one lit thing on the page, a deliberate stop or a skip is
|
|
287
|
-
// not a failure, and success least of all. Doubles as the switch for the
|
|
288
|
-
// verdict phrase's colour, so wash and wording light up together.
|
|
289
|
-
function alarmAccent(displayed: string): string | undefined {
|
|
290
|
-
if (
|
|
291
|
-
displayed === "failed" ||
|
|
292
|
-
displayed === "crashed" ||
|
|
293
|
-
displayed === "log_overflow" ||
|
|
294
|
-
displayed === "missed" ||
|
|
295
|
-
displayed === "start_failed" ||
|
|
296
|
-
displayed === "unhealthy"
|
|
297
|
-
)
|
|
298
|
-
return "var(--color-danger-surface)";
|
|
299
|
-
if (displayed === "timeout" || displayed === "daemon_stopped" || displayed === "queue_full")
|
|
300
|
-
return "var(--color-warning-surface)";
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function handleConsoleKeydown(event: KeyboardEvent) {
|
|
305
|
-
if (!run) return;
|
|
306
|
-
if (event.key === "Escape" && consoleMaximized) {
|
|
307
|
-
consoleMaximized = false;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// `F` toggles focus mode, the way a media player does, but never while
|
|
311
|
-
// the operator is typing into a field (search box, etc.).
|
|
312
|
-
if (
|
|
313
|
-
(event.key === "f" || event.key === "F") &&
|
|
314
|
-
!event.metaKey &&
|
|
315
|
-
!event.ctrlKey &&
|
|
316
|
-
!event.altKey
|
|
317
|
-
) {
|
|
318
|
-
const target = event.target;
|
|
319
|
-
if (
|
|
320
|
-
target instanceof HTMLElement &&
|
|
321
|
-
(target.tagName === "INPUT" ||
|
|
322
|
-
target.tagName === "TEXTAREA" ||
|
|
323
|
-
target.isContentEditable)
|
|
324
|
-
)
|
|
325
|
-
return;
|
|
326
|
-
event.preventDefault();
|
|
327
|
-
consoleMaximized = !consoleMaximized;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function maximizePortal(node: HTMLElement, active: boolean) {
|
|
332
|
-
const anchor = document.createComment("maximized-console");
|
|
333
|
-
function moveOut() {
|
|
334
|
-
const parent = node.parentNode;
|
|
335
|
-
if (parent && parent !== document.body) {
|
|
336
|
-
parent.insertBefore(anchor, node);
|
|
337
|
-
document.body.appendChild(node);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function moveBack() {
|
|
341
|
-
const parent = anchor.parentNode;
|
|
342
|
-
if (parent) {
|
|
343
|
-
parent.insertBefore(node, anchor);
|
|
344
|
-
anchor.remove();
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
if (active) moveOut();
|
|
348
|
-
return {
|
|
349
|
-
update(next: boolean) {
|
|
350
|
-
if (next) moveOut();
|
|
351
|
-
else moveBack();
|
|
352
|
-
},
|
|
353
|
-
destroy() {
|
|
354
|
-
// Mirror the `portal` action: if still parked on <body>, remove
|
|
355
|
-
// the node so Svelte's own teardown doesn't leak it there.
|
|
356
|
-
if (node.parentNode === document.body) node.remove();
|
|
357
|
-
anchor.remove();
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
146
|
</script>
|
|
362
147
|
|
|
363
|
-
<svelte:window onkeydown={handleConsoleKeydown} />
|
|
364
|
-
|
|
365
148
|
{#if run}
|
|
366
149
|
{@const status = displayStatus(run.status, run.endReason)}
|
|
367
150
|
{@const config = RUN_STATUS_CONFIG[status]}
|
|
@@ -370,32 +153,14 @@
|
|
|
370
153
|
run,
|
|
371
154
|
run.status === "running" ? durationTicker.now.getTime() : undefined,
|
|
372
155
|
)}
|
|
373
|
-
{@const startDelay = runStartDelay(run)}
|
|
374
|
-
{@const startedAt = run.startedAt ?? run.createdAt}
|
|
375
|
-
{@const retry = runRetryLabel(run)}
|
|
376
|
-
{@const usage = runUsageLabel(run)}
|
|
377
|
-
{@const live = run.status === "running" ? getLiveUsage(run.id) : undefined}
|
|
378
|
-
{@const paramEntries = run.params ? Object.entries(run.params) : []}
|
|
379
156
|
{@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
|
|
380
|
-
{@const spine = config.
|
|
157
|
+
{@const spine = config.solidDot}
|
|
381
158
|
{@const isRunning = run.status === "running"}
|
|
382
159
|
<!-- A code is worth the ink only when it is news: `exit 0` restates
|
|
383
160
|
"succeeded", while a non-zero code is the first thing to triage on. -->
|
|
384
161
|
{@const showCode = status === "failed"}
|
|
385
|
-
{@const verdict =
|
|
386
|
-
{@const
|
|
387
|
-
status === "stopped"
|
|
388
|
-
? "run stopped by operator"
|
|
389
|
-
: status === "daemon_stopped"
|
|
390
|
-
? "daemon stopped mid-run"
|
|
391
|
-
: status === "timeout"
|
|
392
|
-
? "run timed out"
|
|
393
|
-
: "end of output"}
|
|
394
|
-
{@const endTone =
|
|
395
|
-
status === "stopped" || status === "daemon_stopped" || status === "timeout"
|
|
396
|
-
? "warn"
|
|
397
|
-
: "muted"}
|
|
398
|
-
{@const alarm = alarmAccent(status)}
|
|
162
|
+
{@const verdict = RUN_VERDICTS[status]}
|
|
163
|
+
{@const alarm = config.alarm}
|
|
399
164
|
<!-- The panel: a status spine runs the full left edge across both the header
|
|
400
165
|
readout and the console below, hugging the rail divider (artifact
|
|
401
166
|
".detail .spine"). -->
|
|
@@ -409,458 +174,133 @@
|
|
|
409
174
|
class="absolute inset-y-0 left-0 z-[3] w-1 {spine} {isRunning ? 'spine-flow' : ''}"
|
|
410
175
|
aria-hidden="true"
|
|
411
176
|
></div>
|
|
412
|
-
|
|
177
|
+
{#snippet actions()}
|
|
178
|
+
<RunActions
|
|
179
|
+
runId={run.id}
|
|
180
|
+
taskName={run.taskName}
|
|
181
|
+
{isRunning}
|
|
182
|
+
{canDelete}
|
|
183
|
+
{onRun}
|
|
184
|
+
{onRunAgain}
|
|
185
|
+
{onStop}
|
|
186
|
+
{onDelete}
|
|
187
|
+
{stopLabel}
|
|
188
|
+
/>
|
|
189
|
+
{/snippet}
|
|
190
|
+
{#if headerLayout === "line"}
|
|
191
|
+
<RunHeaderLine
|
|
192
|
+
{run}
|
|
193
|
+
{status}
|
|
194
|
+
{duration}
|
|
195
|
+
{suffix}
|
|
196
|
+
live={isRunning ? getLiveUsage(run.id) : undefined}
|
|
197
|
+
{onBack}
|
|
198
|
+
{actions}
|
|
199
|
+
/>
|
|
200
|
+
{:else}
|
|
201
|
+
<!-- Detailed header: a readout whose ink scales with how much went wrong.
|
|
413
202
|
Every run states its outcome in one phrase over one quiet fact line;
|
|
414
203
|
only a run worth triaging colours the phrase and tints the surface.
|
|
415
204
|
Nothing here is a fixed slot, every fact renders only when it is
|
|
416
205
|
true, so the header's height is itself a signal. -->
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
206
|
+
<div
|
|
207
|
+
class="head-region @container relative shrink-0 border-b border-outline-faint"
|
|
208
|
+
style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
|
|
209
|
+
>
|
|
210
|
+
<div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
|
|
211
|
+
<!-- The actions wrap onto their own row rather than squeeze the
|
|
423
212
|
readout below a legible width on a narrow panel. -->
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
>
|
|
433
|
-
<ArrowLeft size={20} />
|
|
434
|
-
</button>
|
|
435
|
-
{:else if onToggleList}
|
|
436
|
-
<button
|
|
437
|
-
type="button"
|
|
438
|
-
onclick={() => onToggleList()}
|
|
439
|
-
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
440
|
-
title={listVisible ? "Hide run list" : "Show run list"}
|
|
441
|
-
aria-label={listVisible ? "Hide run list" : "Show run list"}
|
|
442
|
-
aria-expanded={listVisible}
|
|
443
|
-
>
|
|
444
|
-
{#if listVisible}
|
|
445
|
-
<PanelLeftClose size={20} />
|
|
446
|
-
{:else}
|
|
447
|
-
<PanelLeftOpen size={20} />
|
|
448
|
-
{/if}
|
|
449
|
-
</button>
|
|
450
|
-
{/if}
|
|
451
|
-
<div class="min-w-60 flex-1">
|
|
452
|
-
<!-- Identity, but only where the page around the panel isn't
|
|
453
|
-
already saying it: the cross-task runs list needs the task
|
|
454
|
-
name, a task's own page already has it in the breadcrumb. -->
|
|
455
|
-
{#if showTaskName || suffix}
|
|
456
|
-
<div
|
|
457
|
-
class="mb-1.5 font-mono text-[11.5px] font-medium tracking-[0.06em] text-on-surface-muted"
|
|
213
|
+
<div class="flex flex-wrap items-start gap-x-3 gap-y-3">
|
|
214
|
+
{#if onBack}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onclick={() => onBack()}
|
|
218
|
+
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
219
|
+
title="Back to runs"
|
|
220
|
+
aria-label="Back to runs"
|
|
458
221
|
>
|
|
459
|
-
|
|
460
|
-
</
|
|
222
|
+
<ArrowLeft size={20} />
|
|
223
|
+
</button>
|
|
461
224
|
{/if}
|
|
225
|
+
<div class="min-w-60 flex-1">
|
|
226
|
+
<!-- Identity, but only where the page around the panel isn't
|
|
227
|
+
already saying it: the cross-task runs list needs the task
|
|
228
|
+
name, a task's own page already has it in the breadcrumb. -->
|
|
229
|
+
{#if showTaskName || suffix}
|
|
230
|
+
<div
|
|
231
|
+
class="mb-1.5 font-mono text-[11.5px] font-medium tracking-[0.06em] text-on-surface-muted"
|
|
232
|
+
>
|
|
233
|
+
{showTaskName
|
|
234
|
+
? `${run.taskName}${suffix}`
|
|
235
|
+
: `instance ${suffix}`}
|
|
236
|
+
</div>
|
|
237
|
+
{/if}
|
|
462
238
|
|
|
463
|
-
|
|
239
|
+
<!-- Verdict: the outcome as one sentence, and the only large
|
|
464
240
|
type in the panel. It is prose, so the phrase is sans and
|
|
465
241
|
only the tokens inside it, the duration, a failing exit,
|
|
466
242
|
are mono (DESIGN.md's mono-vs-sans rule). The glyph is bare
|
|
467
243
|
(no plate, no ring) so it reads as part of the sentence,
|
|
468
244
|
and it alone carries the outcome colour on a healthy run:
|
|
469
245
|
green words are ink spent on nothing being wrong. -->
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
>
|
|
476
|
-
<DetailIcon
|
|
477
|
-
size={18}
|
|
478
|
-
strokeWidth={2.25}
|
|
479
|
-
class="shrink-0 {config.color} {isRunning
|
|
480
|
-
? 'animate-spin'
|
|
481
|
-
: ''}"
|
|
482
|
-
/>
|
|
483
|
-
<span
|
|
484
|
-
class="font-sans font-semibold tracking-[-0.01em] {alarm
|
|
485
|
-
? config.color
|
|
486
|
-
: 'text-on-surface'}">{verdict.verb}</span
|
|
246
|
+
<Tooltip content={config.description} position="right" wide>
|
|
247
|
+
<h2
|
|
248
|
+
class="flex flex-wrap items-center gap-x-2.5 text-[22px] @max-2xl:text-[18px] @max-xs:text-[16px]"
|
|
249
|
+
data-testid="run-verdict"
|
|
250
|
+
data-status={status}
|
|
487
251
|
>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
<!-- Grouped with its own separator so a wrap can
|
|
496
|
-
never leave the dot dangling at a line end. -->
|
|
252
|
+
<DetailIcon
|
|
253
|
+
size={18}
|
|
254
|
+
strokeWidth={2.25}
|
|
255
|
+
class="shrink-0 {config.color} {isRunning
|
|
256
|
+
? 'animate-spin'
|
|
257
|
+
: ''}"
|
|
258
|
+
/>
|
|
497
259
|
<span
|
|
498
|
-
class="
|
|
260
|
+
class="font-sans font-semibold tracking-[-0.01em] {alarm
|
|
261
|
+
? config.color
|
|
262
|
+
: 'text-on-surface'}">{verdict.verb}</span
|
|
499
263
|
>
|
|
500
|
-
|
|
501
|
-
>·</span
|
|
502
|
-
>
|
|
503
|
-
<span class="text-danger-surface" data-testid="run-exit"
|
|
504
|
-
>exit {run.exitCode}</span
|
|
505
|
-
>
|
|
506
|
-
</span>
|
|
507
|
-
{/if}
|
|
508
|
-
</h2>
|
|
509
|
-
</Tooltip>
|
|
510
|
-
|
|
511
|
-
<!-- Facts: when it happened and what shaped it. Queue wait,
|
|
512
|
-
retries and parameters are absent on an ordinary run and
|
|
513
|
-
appear inline the moment they exist. -->
|
|
514
|
-
<div
|
|
515
|
-
class="mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
516
|
-
>
|
|
517
|
-
<!-- No hover-for-the-full-timestamp here: the date sits
|
|
518
|
-
right beside the clock time, so a tooltip could only
|
|
519
|
-
repeat it. The line wraps at narrow widths instead of
|
|
520
|
-
dropping the date and hiding it behind a hover. -->
|
|
521
|
-
<span data-testid="run-started" class="text-on-surface-muted"
|
|
522
|
-
>{run.startedAt ? formatClockTime(run.startedAt) : "—"}</span
|
|
523
|
-
>
|
|
524
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
525
|
-
<span>{formatCalendarDate(startedAt)}</span>
|
|
526
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
527
|
-
<span>via {formatTriggeredByLabel(run.triggeredBy).toLowerCase()}</span>
|
|
528
|
-
{#if startDelay}
|
|
529
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
530
|
-
<span
|
|
531
|
-
class="text-warning-soft-text"
|
|
532
|
-
title="Waited between its scheduled tick and actually starting"
|
|
533
|
-
>{startDelay} queued</span
|
|
534
|
-
>
|
|
535
|
-
{/if}
|
|
536
|
-
{#if retry}
|
|
537
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
538
|
-
<span class="inline-flex items-center gap-1">
|
|
539
|
-
<RotateCw size={11} />
|
|
540
|
-
retry #{run.retryAttempt}{#if run.retryOfRunId} of
|
|
541
|
-
{formatShortId(run.retryOfRunId)}{/if}
|
|
542
|
-
</span>
|
|
543
|
-
{/if}
|
|
544
|
-
{#if paramEntries.length > 0}
|
|
545
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
546
|
-
<Popover placement="bottom-start">
|
|
547
|
-
{#snippet trigger()}
|
|
264
|
+
{#if verdict.timed && duration}
|
|
548
265
|
<span
|
|
549
|
-
class="
|
|
266
|
+
class="font-mono text-[0.86em] font-medium text-on-surface tabular-nums"
|
|
267
|
+
data-testid="run-duration">{duration}</span
|
|
550
268
|
>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
{/snippet}
|
|
556
|
-
<div class="max-w-md min-w-48">
|
|
269
|
+
{/if}
|
|
270
|
+
{#if showCode}
|
|
271
|
+
<!-- Grouped with its own separator so a wrap can
|
|
272
|
+
never leave the dot dangling at a line end. -->
|
|
557
273
|
<span
|
|
558
|
-
class="
|
|
559
|
-
>Parameters</span
|
|
274
|
+
class="inline-flex items-center gap-x-2.5 font-mono text-[0.86em] font-medium tabular-nums"
|
|
560
275
|
>
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
{key}
|
|
567
|
-
</dt>
|
|
568
|
-
<dd
|
|
569
|
-
class="font-mono text-xs font-medium break-all text-on-surface"
|
|
570
|
-
>
|
|
571
|
-
{value}
|
|
572
|
-
</dd>
|
|
573
|
-
{/each}
|
|
574
|
-
</dl>
|
|
575
|
-
</div>
|
|
576
|
-
</Popover>
|
|
577
|
-
{/if}
|
|
578
|
-
{#if live}
|
|
579
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
580
|
-
<span
|
|
581
|
-
data-testid="run-live-usage"
|
|
582
|
-
class="text-on-surface-muted"
|
|
583
|
-
title="Live CPU (100% = one core) and memory of the run's processes"
|
|
584
|
-
>CPU {Math.round(live.cpuPercent)}% · {formatBytes(
|
|
585
|
-
live.memoryBytes,
|
|
586
|
-
)}</span
|
|
587
|
-
>
|
|
588
|
-
{/if}
|
|
589
|
-
{#if usage}
|
|
590
|
-
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
591
|
-
<span
|
|
592
|
-
data-testid="run-usage"
|
|
593
|
-
title="Peak memory and CPU time of the run's processes"
|
|
594
|
-
>{usage}</span
|
|
595
|
-
>
|
|
596
|
-
{/if}
|
|
597
|
-
<!-- Run-id chip: click to copy the full ULID. Last, because
|
|
598
|
-
it is the fact you reach for least and the only one you
|
|
599
|
-
interact with. -->
|
|
600
|
-
<button
|
|
601
|
-
type="button"
|
|
602
|
-
onclick={() => void runIdCopy.copy(run.id)}
|
|
603
|
-
title="Copy run ID"
|
|
604
|
-
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"
|
|
605
|
-
>
|
|
606
|
-
{#if runIdCopy.copied}
|
|
607
|
-
<Check size={11} class="text-success-surface" />Copied
|
|
608
|
-
{:else}
|
|
609
|
-
<Hash size={11} />{run.id}
|
|
610
|
-
{/if}
|
|
611
|
-
</button>
|
|
612
|
-
</div>
|
|
613
|
-
</div>
|
|
614
|
-
|
|
615
|
-
<!-- Actions: run (split) / service lifecycle ·
|
|
616
|
-
stop · download · delete (delete behind an inline confirm) -->
|
|
617
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
618
|
-
{#if onStopService || onRestartService}
|
|
619
|
-
<!-- Service lifecycle replaces the run control.
|
|
620
|
-
Restart is always available (it cancels + respawns
|
|
621
|
-
all instances in one call); Stop appears only while
|
|
622
|
-
the service is running. Each still confirms, so
|
|
623
|
-
neither fires accidentally. -->
|
|
624
|
-
{#if onRestartService}
|
|
625
|
-
<button
|
|
626
|
-
type="button"
|
|
627
|
-
onclick={() => onRestartService()}
|
|
628
|
-
disabled={serviceBusy}
|
|
629
|
-
title={serviceStopped
|
|
630
|
-
? "Starts the service"
|
|
631
|
-
: "Cancels and respawns every instance"}
|
|
632
|
-
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-primary-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-primary hover:bg-primary-soft active:translate-y-px disabled:cursor-not-allowed disabled:opacity-60"
|
|
633
|
-
>
|
|
634
|
-
{#if serviceStopped}
|
|
635
|
-
<Play size={15} fill="currentColor" stroke="none" />
|
|
636
|
-
{:else}
|
|
637
|
-
<RefreshCcw size={15} />
|
|
638
|
-
{/if}
|
|
639
|
-
<span class="@max-xs:hidden"
|
|
640
|
-
>{serviceStopped ? "Start" : "Restart"}</span
|
|
641
|
-
>
|
|
642
|
-
</button>
|
|
643
|
-
{/if}
|
|
644
|
-
{#if !serviceStopped && onStopService}
|
|
645
|
-
<button
|
|
646
|
-
type="button"
|
|
647
|
-
onclick={() => onStopService()}
|
|
648
|
-
disabled={serviceBusy}
|
|
649
|
-
title="Stops the service for the rest of the daemon's lifetime"
|
|
650
|
-
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"
|
|
651
|
-
>
|
|
652
|
-
<Square size={15} fill="currentColor" stroke="none" />
|
|
653
|
-
<span class="@max-xs:hidden">Stop</span>
|
|
654
|
-
</button>
|
|
655
|
-
{/if}
|
|
656
|
-
{:else}
|
|
657
|
-
{#if onRun}
|
|
658
|
-
<!-- Split button: Run (defaults) + an arrow for the
|
|
659
|
-
reuse-parameters variant, shown only when there is one. -->
|
|
660
|
-
<div
|
|
661
|
-
class="inline-flex overflow-hidden rounded-[3px] border border-primary-soft-border"
|
|
662
|
-
>
|
|
663
|
-
<button
|
|
664
|
-
type="button"
|
|
665
|
-
onclick={() => onRun()}
|
|
666
|
-
title="Run this task now with default options"
|
|
667
|
-
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"
|
|
668
|
-
>
|
|
669
|
-
<Play size={15} fill="currentColor" stroke="none" />
|
|
670
|
-
<span class="@max-xs:hidden">Run</span>
|
|
671
|
-
</button>
|
|
672
|
-
{#if onRunAgain}
|
|
673
|
-
<Popover bind:open={runMenuOpen} placement="bottom-end">
|
|
674
|
-
{#snippet trigger()}
|
|
675
|
-
<span
|
|
676
|
-
class="flex h-9 cursor-pointer items-center border-l border-primary-soft-border bg-surface-raised px-2 text-primary hover:bg-primary-soft"
|
|
677
|
-
title="More run options"
|
|
678
|
-
aria-label="More run options"
|
|
679
|
-
>
|
|
680
|
-
<ChevronDown size={15} />
|
|
681
|
-
</span>
|
|
682
|
-
{/snippet}
|
|
683
|
-
<button
|
|
684
|
-
type="button"
|
|
685
|
-
onclick={() => {
|
|
686
|
-
runMenuOpen = false;
|
|
687
|
-
onRunAgain?.();
|
|
688
|
-
}}
|
|
689
|
-
class="flex w-56 items-start gap-2.5 text-left"
|
|
276
|
+
<span class="text-on-surface-faint" aria-hidden="true"
|
|
277
|
+
>·</span
|
|
278
|
+
>
|
|
279
|
+
<span class="text-danger-surface" data-testid="run-exit"
|
|
280
|
+
>exit {run.exitCode}</span
|
|
690
281
|
>
|
|
691
|
-
|
|
692
|
-
size={15}
|
|
693
|
-
class="mt-0.5 shrink-0 text-on-surface-muted"
|
|
694
|
-
/>
|
|
695
|
-
<span class="flex flex-col">
|
|
696
|
-
<span
|
|
697
|
-
class="font-mono text-sm font-medium text-on-surface"
|
|
698
|
-
>Run again</span
|
|
699
|
-
>
|
|
700
|
-
<span class="text-xs text-on-surface-muted"
|
|
701
|
-
>Reuse this run's parameters</span
|
|
702
|
-
>
|
|
703
|
-
</span>
|
|
704
|
-
</button>
|
|
705
|
-
</Popover>
|
|
282
|
+
</span>
|
|
706
283
|
{/if}
|
|
707
|
-
</
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
>
|
|
715
|
-
<Square size={15} fill="currentColor" stroke="none" />
|
|
716
|
-
<span class="@max-xs:hidden">Stop</span>
|
|
717
|
-
</button>
|
|
718
|
-
{/if}
|
|
719
|
-
{/if}
|
|
720
|
-
<a
|
|
721
|
-
href={runLogDownloadUrl(run.id)}
|
|
722
|
-
download="{run.taskName}-{run.id}.log"
|
|
723
|
-
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"
|
|
724
|
-
title="Download the full log (rotated and current parts as one file)"
|
|
725
|
-
aria-label="Download log"
|
|
726
|
-
>
|
|
727
|
-
<Download size={15} />
|
|
728
|
-
</a>
|
|
729
|
-
{#if canDelete}
|
|
730
|
-
<Popover bind:open={confirmDeleteOpen} placement="bottom-end">
|
|
731
|
-
{#snippet trigger()}
|
|
732
|
-
<span
|
|
733
|
-
class="flex cursor-pointer items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-danger-soft-border hover:bg-danger-soft hover:text-danger-surface"
|
|
734
|
-
title="Delete this run"
|
|
735
|
-
aria-label="Delete run"
|
|
736
|
-
>
|
|
737
|
-
<Trash size={15} />
|
|
738
|
-
</span>
|
|
739
|
-
{/snippet}
|
|
740
|
-
<div class="w-60">
|
|
741
|
-
<div class="font-mono text-sm font-semibold text-on-surface">
|
|
742
|
-
Delete this run?
|
|
743
|
-
</div>
|
|
744
|
-
<div class="mt-1 text-xs leading-relaxed text-on-surface-muted">
|
|
745
|
-
Its captured output is removed from disk. This can't be
|
|
746
|
-
undone.
|
|
747
|
-
</div>
|
|
748
|
-
<div class="mt-3 flex justify-end gap-2">
|
|
749
|
-
<Button
|
|
750
|
-
variant="ghost"
|
|
751
|
-
size="sm"
|
|
752
|
-
onclick={() => (confirmDeleteOpen = false)}
|
|
753
|
-
>
|
|
754
|
-
Cancel
|
|
755
|
-
</Button>
|
|
756
|
-
<Button
|
|
757
|
-
variant="danger"
|
|
758
|
-
size="sm"
|
|
759
|
-
onclick={() => {
|
|
760
|
-
confirmDeleteOpen = false;
|
|
761
|
-
onDelete?.(run.id);
|
|
762
|
-
}}
|
|
763
|
-
>
|
|
764
|
-
Delete run
|
|
765
|
-
</Button>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
</Popover>
|
|
769
|
-
{/if}
|
|
284
|
+
</h2>
|
|
285
|
+
</Tooltip>
|
|
286
|
+
|
|
287
|
+
<RunFacts {run} live={isRunning ? getLiveUsage(run.id) : undefined} />
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{@render actions()}
|
|
770
291
|
</div>
|
|
771
292
|
</div>
|
|
772
293
|
</div>
|
|
773
|
-
</div>
|
|
774
|
-
|
|
775
|
-
<!-- Console hero, maximizes to a full-bleed overlay via portal -->
|
|
776
|
-
{#if consoleMaximized}
|
|
777
|
-
<div
|
|
778
|
-
use:portal
|
|
779
|
-
class="console-backdrop fixed inset-0 z-40 bg-backdrop backdrop-blur-sm"
|
|
780
|
-
onclick={() => (consoleMaximized = false)}
|
|
781
|
-
role="presentation"
|
|
782
|
-
></div>
|
|
783
294
|
{/if}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
<div class="flex min-w-0 items-center gap-3">
|
|
794
|
-
{#if consoleMaximized}
|
|
795
|
-
<!-- Focus-mode identity: which run is filling the screen -->
|
|
796
|
-
<span class="flex min-w-0 items-center gap-2">
|
|
797
|
-
<span class="h-2 w-2 shrink-0 rounded-full {spine}"></span>
|
|
798
|
-
<span class="truncate font-medium text-[var(--rw-con-text)]">
|
|
799
|
-
{run.taskName}{suffix}
|
|
800
|
-
</span>
|
|
801
|
-
<span class="text-[var(--rw-con-gutter)]">·</span>
|
|
802
|
-
<span class="font-semibold capitalize {config.color}">{status}</span>
|
|
803
|
-
</span>
|
|
804
|
-
{:else}
|
|
805
|
-
<span
|
|
806
|
-
class="flex items-center gap-2 font-semibold whitespace-nowrap text-[var(--rw-con-text)]"
|
|
807
|
-
>
|
|
808
|
-
<TerminalIcon size={14} class="opacity-70" />
|
|
809
|
-
Console output
|
|
810
|
-
</span>
|
|
811
|
-
<span class="hidden text-[var(--rw-con-gutter)] @lg:inline"
|
|
812
|
-
>stdout + stderr</span
|
|
813
|
-
>
|
|
814
|
-
{/if}
|
|
815
|
-
</div>
|
|
816
|
-
<div class="flex shrink-0 items-center gap-3">
|
|
817
|
-
<button
|
|
818
|
-
type="button"
|
|
819
|
-
onclick={() => (consoleWrap = !consoleWrap)}
|
|
820
|
-
class="flex cursor-pointer items-center gap-1.5 rounded-[3px] px-2 py-1 hover:bg-[var(--rw-con-gutter)]/30 hover:text-[var(--rw-con-text)] {consoleWrap
|
|
821
|
-
? 'text-[var(--rw-con-text)]'
|
|
822
|
-
: 'text-[var(--rw-con-dim)]'}"
|
|
823
|
-
title={consoleWrap ? "Unwrap lines" : "Wrap long lines"}
|
|
824
|
-
aria-pressed={consoleWrap}
|
|
825
|
-
aria-label="Toggle line wrapping"
|
|
826
|
-
>
|
|
827
|
-
<TextWrap size={13} />
|
|
828
|
-
<span class="hidden @sm:inline">Wrap</span>
|
|
829
|
-
</button>
|
|
830
|
-
<button
|
|
831
|
-
type="button"
|
|
832
|
-
onclick={() => (consoleMaximized = !consoleMaximized)}
|
|
833
|
-
class="flex cursor-pointer items-center gap-1.5 rounded-[3px] px-2 py-1 text-[var(--rw-con-dim)] hover:bg-[var(--rw-con-gutter)]/30 hover:text-[var(--rw-con-text)]"
|
|
834
|
-
title={consoleMaximized ? "Restore console (Esc)" : "Maximize console · F"}
|
|
835
|
-
aria-label={consoleMaximized ? "Restore console" : "Maximize console"}
|
|
836
|
-
>
|
|
837
|
-
{#if consoleMaximized}
|
|
838
|
-
<Minimize2 size={13} />
|
|
839
|
-
<Kbd keys="Esc" size="xs" tone="console" />
|
|
840
|
-
{:else}
|
|
841
|
-
<Maximize2 size={13} />
|
|
842
|
-
Expand
|
|
843
|
-
{/if}
|
|
844
|
-
</button>
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
847
|
-
{#key run.id}
|
|
848
|
-
<LogConsole
|
|
849
|
-
bind:this={logConsole}
|
|
850
|
-
fetchLogs={(f: number, t: number) => fetchLogs(run.id, f, t)}
|
|
851
|
-
fetchLineHistory={fetchLineHistory
|
|
852
|
-
? (n: number) => fetchLineHistory(run.id, n)
|
|
853
|
-
: undefined}
|
|
854
|
-
bind:wrap={consoleWrap}
|
|
855
|
-
class="min-h-0 flex-1"
|
|
856
|
-
{highlightLine}
|
|
857
|
-
{endLabel}
|
|
858
|
-
{endTone}
|
|
859
|
-
error={logFetchError}
|
|
860
|
-
loading={seededRunId !== run.id}
|
|
861
|
-
/>
|
|
862
|
-
{/key}
|
|
863
|
-
</div>
|
|
295
|
+
|
|
296
|
+
<RunConsoleFrame
|
|
297
|
+
{run}
|
|
298
|
+
{suffix}
|
|
299
|
+
{fetchLogs}
|
|
300
|
+
{streamLogs}
|
|
301
|
+
{fetchLineHistory}
|
|
302
|
+
{highlightLine}
|
|
303
|
+
/>
|
|
864
304
|
</div>
|
|
865
305
|
{:else if loading && !notFound}
|
|
866
306
|
<!-- Loading: the panel's own frame (verdict header over the console) with
|
|
@@ -899,12 +339,15 @@
|
|
|
899
339
|
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-4 bg-surface-sunken/30"
|
|
900
340
|
>
|
|
901
341
|
<EmptyState
|
|
902
|
-
title={notFound
|
|
342
|
+
title={notFound
|
|
343
|
+
? "Run not found"
|
|
344
|
+
: (emptyTitle ?? (onRunTask ? "No runs yet" : "Select a run"))}
|
|
903
345
|
description={notFound
|
|
904
346
|
? "This run doesn't exist. It may have been deleted by retention, or the link is wrong. Pick a run from the list to continue."
|
|
905
|
-
:
|
|
906
|
-
|
|
907
|
-
|
|
347
|
+
: (emptyDescription ??
|
|
348
|
+
(onRunTask
|
|
349
|
+
? "This task hasn't run yet. Trigger it to see its output here."
|
|
350
|
+
: "Pick a run from the list to view details and logs."))}
|
|
908
351
|
icon={notFound ? SearchX : MousePointerClick}
|
|
909
352
|
/>
|
|
910
353
|
{#if onRunTask && !notFound}
|
|
@@ -929,34 +372,6 @@
|
|
|
929
372
|
background: color-mix(in srgb, var(--rw-oc) 7%, var(--color-surface-raised));
|
|
930
373
|
}
|
|
931
374
|
|
|
932
|
-
/* Gentle fade for the maximize backdrop; disabled under reduced motion. */
|
|
933
|
-
.console-backdrop {
|
|
934
|
-
animation: console-backdrop-in 150ms;
|
|
935
|
-
}
|
|
936
|
-
@keyframes console-backdrop-in {
|
|
937
|
-
from {
|
|
938
|
-
opacity: 0;
|
|
939
|
-
}
|
|
940
|
-
to {
|
|
941
|
-
opacity: 1;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/* The console zooms up as it takes over the viewport. */
|
|
946
|
-
.console-zoom {
|
|
947
|
-
animation: console-zoom-in 260ms cubic-bezier(0.33, 0.05, 0.2, 1);
|
|
948
|
-
}
|
|
949
|
-
@keyframes console-zoom-in {
|
|
950
|
-
from {
|
|
951
|
-
opacity: 0.4;
|
|
952
|
-
transform: scale(0.96);
|
|
953
|
-
}
|
|
954
|
-
to {
|
|
955
|
-
opacity: 1;
|
|
956
|
-
transform: none;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
375
|
/* Living spine: a light sweep travels down the edge while a run is in flight. */
|
|
961
376
|
.spine-flow {
|
|
962
377
|
overflow: hidden;
|
|
@@ -984,8 +399,6 @@
|
|
|
984
399
|
}
|
|
985
400
|
|
|
986
401
|
@media (prefers-reduced-motion: reduce) {
|
|
987
|
-
.console-backdrop,
|
|
988
|
-
.console-zoom,
|
|
989
402
|
.spine-flow::after {
|
|
990
403
|
animation: none;
|
|
991
404
|
}
|