151 lines
3.7 KiB
Svelte
151 lines
3.7 KiB
Svelte
<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>
|