feat(backends,api,ui): process trees with cpu and stderr per session, interrupt, close and kill from the system page
This commit is contained in:
1 parent
9875f08016
commit
712c24de4e
12 files changed
+996
-127
No files matched your search
@@ -313,6 +313,22 @@ export class ApiClient {
|
||||
return this.get("/api/sessions");
|
||||
}
|
||||
|
||||
sessionStderr(key: string): Promise<{ key: string; lines: string[] }> {
|
||||
return this.get(`/api/sessions/${encodeURIComponent(key)}/stderr`);
|
||||
}
|
||||
|
||||
interruptSession(key: string): Promise<{ interrupted: boolean }> {
|
||||
return this.post(`/api/sessions/${encodeURIComponent(key)}/interrupt`);
|
||||
}
|
||||
|
||||
closeSession(key: string): Promise<{ closed: boolean }> {
|
||||
return this.post(`/api/sessions/${encodeURIComponent(key)}/close`);
|
||||
}
|
||||
|
||||
killProcess(pid: number): Promise<{ killed: boolean }> {
|
||||
return this.post(`/api/processes/${pid}/kill`);
|
||||
}
|
||||
|
||||
jobs(): Promise<JobsResponse> {
|
||||
return this.get("/api/jobs");
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@ export interface ContentBlock {
|
||||
|
||||
export interface HistoryMessage {
|
||||
content: string | ContentBlock[];
|
||||
id?: string | null;
|
||||
role: "user" | "assistant";
|
||||
service?: string;
|
||||
// When the first block of this message was written; absent for old rows.
|
||||
ts?: string | null;
|
||||
}
|
||||
@@ -144,17 +146,31 @@ export interface AgentsResponse {
|
||||
mcps: { name: string; kind: string }[];
|
||||
}
|
||||
|
||||
export interface ProcessRow {
|
||||
age_seconds: number;
|
||||
command: string;
|
||||
cpu: number | null;
|
||||
depth: number;
|
||||
name: string;
|
||||
parent: number;
|
||||
pid: number;
|
||||
rss: number;
|
||||
}
|
||||
|
||||
export interface SessionRow {
|
||||
age_seconds: number;
|
||||
agent: string;
|
||||
busy: boolean;
|
||||
cpu: number | null;
|
||||
dirty: boolean;
|
||||
idle_seconds: number;
|
||||
key: string;
|
||||
kind: string;
|
||||
pending: number;
|
||||
pending_question: boolean;
|
||||
pid: number | null;
|
||||
pinned: boolean;
|
||||
processes: ProcessRow[];
|
||||
rss: number | null;
|
||||
running_turn: string | null;
|
||||
session_id: string | null;
|
||||
@@ -162,6 +178,8 @@ export interface SessionRow {
|
||||
}
|
||||
|
||||
export interface SessionsResponse {
|
||||
cpu: number | null;
|
||||
others: ProcessRow[];
|
||||
rss: number | null;
|
||||
rss_limit: number | null;
|
||||
sessions: SessionRow[];
|
||||
|
||||
@@ -68,6 +68,12 @@ export function fmtSeconds(seconds: number | null | undefined): string {
|
||||
: fmtDuration(seconds * THOUSAND);
|
||||
}
|
||||
|
||||
export function fmtCpu(percent: number | null | undefined): string {
|
||||
return percent === null || percent === undefined
|
||||
? "–"
|
||||
: `${Math.round(percent)}%`;
|
||||
}
|
||||
|
||||
export function fmtBytes(bytes: number | null | undefined): string {
|
||||
if (bytes === null || bytes === undefined) {
|
||||
return "–";
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import type { ProcessRow } from "$lib/api/types";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { fmtBytes, fmtCpu, fmtSeconds } from "$lib/format";
|
||||
|
||||
let {
|
||||
rows,
|
||||
onKill,
|
||||
}: { rows: ProcessRow[]; onKill: (row: ProcessRow) => void } = $props();
|
||||
|
||||
const INDENT_REM = 0.875;
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-muted-foreground text-xs">
|
||||
<tr class="border-b text-left">
|
||||
<th class="py-1.5 pr-3 font-medium">pid</th>
|
||||
<th class="w-full py-1.5 pr-3 font-medium">command</th>
|
||||
<th class="py-1.5 pr-3 text-right font-medium">cpu</th>
|
||||
<th class="py-1.5 pr-3 text-right font-medium">rss</th>
|
||||
<th class="hidden py-1.5 pr-3 text-right font-medium sm:table-cell">
|
||||
age
|
||||
</th>
|
||||
<th class="py-1.5"><span class="sr-only">actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row (row.pid)}
|
||||
<tr class="border-b last:border-b-0">
|
||||
<td class="tabular py-1 pr-3 text-muted-foreground text-xs">
|
||||
{row.pid}
|
||||
</td>
|
||||
<td class="max-w-0 py-1 pr-3">
|
||||
<span
|
||||
class="block truncate font-mono text-xs"
|
||||
title={row.command}
|
||||
style:padding-left="{row.depth * INDENT_REM}rem"
|
||||
>
|
||||
{row.command || row.name}
|
||||
</span>
|
||||
</td>
|
||||
<td class="tabular py-1 pr-3 text-right text-xs">
|
||||
{fmtCpu(row.cpu)}
|
||||
</td>
|
||||
<td class="tabular whitespace-nowrap py-1 pr-3 text-right text-xs">
|
||||
{fmtBytes(row.rss)}
|
||||
</td>
|
||||
<td
|
||||
class="tabular hidden whitespace-nowrap py-1 pr-3 text-right text-muted-foreground text-xs sm:table-cell"
|
||||
>
|
||||
{fmtSeconds(row.age_seconds)}
|
||||
</td>
|
||||
<td class="py-0.5 text-right">
|
||||
<Button
|
||||
class="text-muted-foreground hover:text-destructive max-sm:h-10"
|
||||
onclick={() => onKill(row)}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
>
|
||||
Kill
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import type { ApiClient } from "$lib/api/client";
|
||||
import type { ProcessRow, SessionRow } from "$lib/api/types";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { shortId } from "$lib/format";
|
||||
import { ConversationFeed } from "$lib/panel/conversation.svelte";
|
||||
import TurnCard from "$lib/panel/turn-card.svelte";
|
||||
import ProcessTable from "./process-table.svelte";
|
||||
|
||||
let {
|
||||
client,
|
||||
row,
|
||||
busy,
|
||||
note,
|
||||
onInterrupt,
|
||||
onClose,
|
||||
onKill,
|
||||
}: {
|
||||
client: ApiClient;
|
||||
row: SessionRow;
|
||||
busy: boolean;
|
||||
note: string;
|
||||
onInterrupt: (row: SessionRow) => void;
|
||||
onClose: (row: SessionRow) => void;
|
||||
onKill: (process: ProcessRow) => void;
|
||||
} = $props();
|
||||
|
||||
const TICK_MS = 1000;
|
||||
const feed = new ConversationFeed(
|
||||
() => client,
|
||||
untrack(() => row.key)
|
||||
);
|
||||
let stderr = $state<string[] | null>(null);
|
||||
let now = $state(Date.now());
|
||||
|
||||
const turn = $derived(feed.model.running);
|
||||
|
||||
onMount(() => {
|
||||
feed.start();
|
||||
const timer = setInterval(() => {
|
||||
now = Date.now();
|
||||
}, TICK_MS);
|
||||
return () => {
|
||||
feed.stop();
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const { key, idle_seconds: polled } = row;
|
||||
if (polled >= 0) {
|
||||
client
|
||||
.sessionStderr(key)
|
||||
.then((tail) => {
|
||||
stderr = tail.lines;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 px-3 pt-2 pb-4">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<p class="tabular min-w-0 flex-1 text-muted-foreground text-xs">
|
||||
session
|
||||
{shortId(row.session_id)}
|
||||
{#if row.pid}
|
||||
· pid {row.pid}
|
||||
{/if}
|
||||
{#if row.pending > 0}
|
||||
· {row.pending}
|
||||
buffered
|
||||
{/if}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if note}
|
||||
<span class="text-muted-foreground text-xs" role="status">{note}</span>
|
||||
{/if}
|
||||
{#if !feed.error}
|
||||
<Button
|
||||
class="max-sm:h-10"
|
||||
href="{base}/conversations/{row.key}"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
Open thread
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
class="max-sm:h-10"
|
||||
disabled={busy || !row.busy}
|
||||
onclick={() => onInterrupt(row)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Interrupt turn
|
||||
</Button>
|
||||
<Button
|
||||
class="max-sm:h-10"
|
||||
disabled={busy}
|
||||
onclick={() => onClose(row)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Close session
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="flex flex-col gap-1">
|
||||
<h4 class="label-quiet">Now</h4>
|
||||
{#if turn}
|
||||
<TurnCard {now} {turn} />
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{row.busy
|
||||
? "A turn is running; its events have not reached this page yet."
|
||||
: "Nothing in flight."}
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-1">
|
||||
<h4 class="label-quiet">Process tree</h4>
|
||||
{#if row.processes.length === 0}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
The process is not visible to the gateway.
|
||||
</p>
|
||||
{:else}
|
||||
<ProcessTable {onKill} rows={row.processes} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-1">
|
||||
<h4 class="label-quiet">stderr</h4>
|
||||
{#if stderr?.length === 0}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Nothing on stderr since this process started.
|
||||
</p>
|
||||
{:else if stderr}
|
||||
<pre
|
||||
class="max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted/40 p-2 text-xs"
|
||||
>{stderr.join(
|
||||
"\n"
|
||||
)}</pre>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,16 +1,39 @@
|
||||
<script lang="ts">
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import type { SessionsResponse } from "$lib/api/types";
|
||||
import type {
|
||||
ProcessRow,
|
||||
SessionRow,
|
||||
SessionsResponse,
|
||||
} from "$lib/api/types";
|
||||
import ErrorNote from "$lib/components/error-note.svelte";
|
||||
import { fmtBytes, fmtSeconds, shortId } from "$lib/format";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { fmtBytes, fmtCpu, fmtSeconds, shortId } from "$lib/format";
|
||||
import { gateway } from "$lib/gateway.svelte";
|
||||
import { session } from "$lib/session.svelte";
|
||||
import KindMark from "$lib/shell/kind-mark.svelte";
|
||||
import ProcessTable from "$lib/system/process-table.svelte";
|
||||
import SessionDetail from "$lib/system/session-detail.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
const POLL_MS = 10_000;
|
||||
interface Order {
|
||||
confirm: string;
|
||||
lines: string[];
|
||||
run: () => Promise<unknown>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const POLL_MS = 3000;
|
||||
const SECOND_LOOK_FOR_CPU_MS = 1000;
|
||||
const COLUMNS = 8;
|
||||
let sessions = $state<SessionsResponse | null>(null);
|
||||
let failure = $state<string | null>(null);
|
||||
let unfolded = $state<Set<string>>(new Set());
|
||||
let order = $state<Order | null>(null);
|
||||
let orderFailure = $state<string | null>(null);
|
||||
let notes = $state<Record<string, string>>({});
|
||||
let busy = $state(false);
|
||||
|
||||
async function load() {
|
||||
const { client } = session;
|
||||
@@ -25,90 +48,315 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(key: string) {
|
||||
const next = new Set(unfolded);
|
||||
if (!next.delete(key)) {
|
||||
next.add(key);
|
||||
}
|
||||
unfolded = next;
|
||||
}
|
||||
|
||||
function ask(next: Order) {
|
||||
orderFailure = null;
|
||||
order = next;
|
||||
}
|
||||
|
||||
async function carryOut(pending: Order) {
|
||||
busy = true;
|
||||
try {
|
||||
await pending.run();
|
||||
order = null;
|
||||
await load();
|
||||
} catch (cause) {
|
||||
orderFailure = cause instanceof Error ? cause.message : String(cause);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function interrupt(row: SessionRow) {
|
||||
const { client } = session;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const { interrupted } = await client.interruptSession(row.key);
|
||||
notes = {
|
||||
...notes,
|
||||
[row.key]: interrupted ? "" : "Nothing was running.",
|
||||
};
|
||||
await load();
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
notes = { ...notes, [row.key]: message };
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function askClose(row: SessionRow) {
|
||||
const { client } = session;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
const lines = [
|
||||
"The claude process exits. The conversation stays and resumes from its transcript on the next message.",
|
||||
];
|
||||
if (row.busy) {
|
||||
lines.push("A turn is running: it is cut and its reply is lost.");
|
||||
}
|
||||
if (row.pinned) {
|
||||
lines.push("This session is pinned: the gateway never closes it itself.");
|
||||
}
|
||||
ask({
|
||||
confirm: "Close session",
|
||||
lines,
|
||||
run: () => client.closeSession(row.key),
|
||||
title: `Close the ${row.kind} session?`,
|
||||
});
|
||||
}
|
||||
|
||||
function askKill(process: ProcessRow) {
|
||||
const { client } = session;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
const owner = sessions?.sessions.find((row) => row.pid === process.pid);
|
||||
const lines = [
|
||||
`SIGKILL to ${process.name} and everything it started. Nothing gets to clean up.`,
|
||||
];
|
||||
if (owner) {
|
||||
lines.push(
|
||||
"It is a session's claude: the session is dropped, and the conversation resumes from its transcript on the next message."
|
||||
);
|
||||
}
|
||||
ask({
|
||||
confirm: "Kill",
|
||||
lines,
|
||||
run: () => client.killProcess(process.pid),
|
||||
title: `Kill process ${process.pid}?`,
|
||||
});
|
||||
}
|
||||
|
||||
const titles = $derived(
|
||||
new Map(gateway.conversations.map((row) => [row.id, row.title]))
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
const second = setTimeout(load, SECOND_LOOK_FOR_CPU_MS);
|
||||
const poll = setInterval(load, POLL_MS);
|
||||
return () => clearInterval(poll);
|
||||
return () => {
|
||||
clearTimeout(second);
|
||||
clearInterval(poll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<h2 class="font-medium text-sm">Live claude processes</h2>
|
||||
{#if sessions}
|
||||
<span class="tabular text-muted-foreground text-xs">
|
||||
{sessions.sessions.length}
|
||||
· {fmtBytes(sessions.rss)}
|
||||
{#if sessions.rss_limit}
|
||||
of {fmtBytes(sessions.rss_limit)}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#snippet state(s: SessionRow)}
|
||||
<span class={cn(s.busy ? "text-signal" : "text-muted-foreground")}>
|
||||
{s.busy ? "busy" : "idle"}
|
||||
</span>
|
||||
{#if s.pinned}
|
||||
<span class="ml-1 text-muted-foreground">pinned</span>
|
||||
{/if}
|
||||
{#if s.dirty}
|
||||
<span class="ml-1 text-warn">dirty</span>
|
||||
{/if}
|
||||
{#if s.pending_question}
|
||||
<span class="ml-1 text-attention-foreground">question</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-wrap items-baseline gap-3">
|
||||
<h2 class="font-medium text-sm">Live claude processes</h2>
|
||||
{#if sessions}
|
||||
<span class="tabular text-muted-foreground text-xs">
|
||||
{sessions.sessions.length}
|
||||
{sessions.sessions.length === 1 ? "session" : "sessions"}
|
||||
· {fmtBytes(sessions.rss)}
|
||||
{#if sessions.rss_limit}
|
||||
of {fmtBytes(sessions.rss_limit)}
|
||||
{/if}
|
||||
· cpu {fmtCpu(sessions.cpu)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
{#if failure}
|
||||
<ErrorNote message={failure} retry={load} />
|
||||
{:else if !sessions}
|
||||
<p class="text-muted-foreground text-sm">Loading…</p>
|
||||
{:else if sessions.sessions.length === 0}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
No claude processes alive. Sessions spawn on the first turn and are reaped
|
||||
by idle time or memory pressure.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border bg-strip">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-muted-foreground text-xs">
|
||||
<tr class="border-b text-left">
|
||||
<th class="px-3 py-2 font-medium">conversation</th>
|
||||
<th class="px-3 py-2 font-medium">agent</th>
|
||||
<th class="px-3 py-2 font-medium">state</th>
|
||||
<th class="px-3 py-2 text-right font-medium">rss</th>
|
||||
<th class="px-3 py-2 text-right font-medium">idle</th>
|
||||
<th class="px-3 py-2 text-right font-medium">age</th>
|
||||
<th class="px-3 py-2 text-right font-medium">turns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sessions.sessions as s (s.key)}
|
||||
<tr class="row-hover border-b last:border-b-0">
|
||||
<td class="px-3 py-1.5">
|
||||
<a
|
||||
class="flex items-center gap-2 hover:underline"
|
||||
href="{base}/conversations/{s.key}"
|
||||
<section class="flex flex-col gap-2">
|
||||
<h3 class="label-quiet">Sessions</h3>
|
||||
{#if sessions.sessions.length === 0}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
No claude processes alive. Sessions spawn on the first turn and are
|
||||
reaped by idle time or memory pressure.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border bg-strip">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-muted-foreground text-xs">
|
||||
<tr class="border-b text-left">
|
||||
<th class="px-3 py-2 font-medium">conversation</th>
|
||||
<th class="hidden px-3 py-2 font-medium sm:table-cell">
|
||||
agent
|
||||
</th>
|
||||
<th class="hidden px-3 py-2 font-medium sm:table-cell">
|
||||
state
|
||||
</th>
|
||||
<th class="px-3 py-2 text-right font-medium">cpu</th>
|
||||
<th class="px-3 py-2 text-right font-medium">rss</th>
|
||||
<th
|
||||
class="hidden px-3 py-2 text-right font-medium sm:table-cell"
|
||||
>
|
||||
<KindMark kind={s.kind} />
|
||||
<span class="tabular text-xs">{shortId(s.key)}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-3 py-1.5">{s.agent}</td>
|
||||
<td class="px-3 py-1.5 text-xs">
|
||||
<span
|
||||
class={cn(s.busy ? "text-signal" : "text-muted-foreground")}
|
||||
idle
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-3 py-2 text-right font-medium sm:table-cell"
|
||||
>
|
||||
{s.busy ? "busy" : "idle"}
|
||||
</span>
|
||||
{#if s.pinned}
|
||||
<span class="ml-1 text-muted-foreground">pinned</span>
|
||||
age
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-3 py-2 text-right font-medium sm:table-cell"
|
||||
>
|
||||
turns
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sessions.sessions as s (s.key)}
|
||||
{@const expanded = unfolded.has(s.key)}
|
||||
<tr class={cn("border-b", !expanded && "last:border-b-0")}>
|
||||
<td class="max-w-0 px-1.5 py-1 sm:w-2/5">
|
||||
<button
|
||||
aria-expanded={expanded}
|
||||
class="row-hover flex max-w-full items-center gap-2 rounded-md px-1.5 py-1 text-left max-sm:min-h-10"
|
||||
onclick={() => toggle(s.key)}
|
||||
type="button"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
class={cn(
|
||||
"size-3.5 text-icon transition-transform duration-150",
|
||||
expanded && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<KindMark kind={s.kind} />
|
||||
<span class="flex min-w-0 flex-col">
|
||||
<span class="truncate font-medium">
|
||||
{titles.get(s.key) ?? shortId(s.key)}
|
||||
</span>
|
||||
<span class="text-xs sm:hidden">
|
||||
{@render state(s)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="hidden px-3 py-1.5 sm:table-cell">{s.agent}</td>
|
||||
<td
|
||||
class="hidden whitespace-nowrap px-3 py-1.5 text-xs sm:table-cell"
|
||||
>
|
||||
{@render state(s)}
|
||||
</td>
|
||||
<td class="tabular px-3 py-1.5 text-right">
|
||||
{fmtCpu(s.cpu)}
|
||||
</td>
|
||||
<td class="tabular whitespace-nowrap px-3 py-1.5 text-right">
|
||||
{fmtBytes(s.rss)}
|
||||
</td>
|
||||
<td
|
||||
class="tabular hidden px-3 py-1.5 text-right sm:table-cell"
|
||||
>
|
||||
{fmtSeconds(s.idle_seconds)}
|
||||
</td>
|
||||
<td
|
||||
class="tabular hidden px-3 py-1.5 text-right sm:table-cell"
|
||||
>
|
||||
{fmtSeconds(s.age_seconds)}
|
||||
</td>
|
||||
<td
|
||||
class="tabular hidden px-3 py-1.5 text-right sm:table-cell"
|
||||
>
|
||||
{s.turns}
|
||||
</td>
|
||||
</tr>
|
||||
{#if expanded && session.client}
|
||||
<tr class="border-b last:border-b-0">
|
||||
<td class="bg-background/60" colspan={COLUMNS}>
|
||||
<SessionDetail
|
||||
{busy}
|
||||
client={session.client}
|
||||
note={notes[s.key] ?? ""}
|
||||
onClose={askClose}
|
||||
onInterrupt={interrupt}
|
||||
onKill={askKill}
|
||||
row={s}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if s.dirty}
|
||||
<span class="ml-1 text-warn">dirty</span>
|
||||
{/if}
|
||||
{#if s.pending_question}
|
||||
<span class="ml-1 text-attention-foreground">question</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="tabular px-3 py-1.5 text-right">{fmtBytes(s.rss)}</td>
|
||||
<td class="tabular px-3 py-1.5 text-right">
|
||||
{fmtSeconds(s.idle_seconds)}
|
||||
</td>
|
||||
<td class="tabular px-3 py-1.5 text-right">
|
||||
{fmtSeconds(s.age_seconds)}
|
||||
</td>
|
||||
<td class="tabular px-3 py-1.5 text-right">{s.turns}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-2">
|
||||
<h3 class="label-quiet">Outside any session</h3>
|
||||
{#if sessions.others.length === 0}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Nothing else runs under the gateway.
|
||||
</p>
|
||||
{:else}
|
||||
<ProcessTable onKill={askKill} rows={sessions.others} />
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Dialog.Root
|
||||
onOpenChange={(next) => {
|
||||
if (!next) {
|
||||
order = null;
|
||||
}
|
||||
}}
|
||||
open={order !== null}
|
||||
>
|
||||
<Dialog.Content>
|
||||
{#if order}
|
||||
{@const pending = order}
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{pending.title}</Dialog.Title>
|
||||
{#each pending.lines as line (line)}
|
||||
<Dialog.Description>{line}</Dialog.Description>
|
||||
{/each}
|
||||
{#if orderFailure}
|
||||
<p class="text-destructive text-sm" role="alert">{orderFailure}</p>
|
||||
{/if}
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
disabled={busy}
|
||||
onclick={() => {
|
||||
order = null;
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busy}
|
||||
onclick={() => carryOut(pending)}
|
||||
variant="destructive"
|
||||
>
|
||||
{pending.confirm}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in new issue
Block a user