110 lines
3.4 KiB
Svelte
110 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import StatusPill from "$lib/components/status-pill.svelte";
|
|
import {
|
|
elapsedMs,
|
|
fmtDuration,
|
|
fmtMoney,
|
|
fmtTime,
|
|
fmtTokens,
|
|
} from "$lib/format";
|
|
import { cn } from "$lib/utils";
|
|
import { isLive, ORIGIN_LABELS, type Turn } from "./activity.svelte";
|
|
import Markdown from "./markdown.svelte";
|
|
import Reasoning from "./reasoning.svelte";
|
|
import SayBubble from "./say-bubble.svelte";
|
|
import ToolNodeView from "./tool-node.svelte";
|
|
|
|
let { turn, now }: { turn: Turn; now: number } = $props();
|
|
|
|
const duration = $derived(
|
|
turn.usage?.duration_ms ?? elapsedMs(turn.startedAt, turn.endedAt, now)
|
|
);
|
|
const origin = $derived(ORIGIN_LABELS[turn.origin] ?? turn.origin);
|
|
const originLabel = $derived(
|
|
turn.itemOrigin && turn.itemOrigin !== turn.origin
|
|
? `${origin} · ${turn.itemOrigin}`
|
|
: origin
|
|
);
|
|
const pill = $derived(
|
|
turn.status !== "running" && isLive(turn) ? "running" : turn.status
|
|
);
|
|
const toolCount = $derived(Object.keys(turn.nodes).length);
|
|
</script>
|
|
|
|
<article
|
|
class={cn(
|
|
"flex flex-col gap-2 border-b py-3",
|
|
pill === "running" && "bg-signal/[0.03]"
|
|
)}
|
|
>
|
|
<header class="flex flex-wrap items-center gap-x-3 gap-y-1 px-1 text-xs">
|
|
{#if turn.origin !== "agent"}
|
|
<StatusPill status={pill} />
|
|
{/if}
|
|
<span
|
|
class={cn(
|
|
"font-medium",
|
|
turn.origin === "inject" && "text-note",
|
|
turn.origin === "agent" || turn.origin === "task"
|
|
? "text-kind-deep"
|
|
: "text-foreground"
|
|
)}
|
|
>
|
|
{originLabel}
|
|
</span>
|
|
<span class="tabular text-muted-foreground">{fmtTime(turn.startedAt)}</span>
|
|
<span class="tabular text-muted-foreground">{fmtDuration(duration)}</span>
|
|
{#if toolCount > 0}
|
|
<span class="tabular text-muted-foreground">
|
|
{toolCount}
|
|
tool {toolCount === 1 ? "call" : "calls"}
|
|
</span>
|
|
{/if}
|
|
{#if turn.thinking > 0 && turn.status === "running" && !turn.reasoning}
|
|
<span class="text-kind-deep">thinking…</span>
|
|
{/if}
|
|
{#if turn.usage}
|
|
<span class="tabular ml-auto text-muted-foreground">
|
|
in {fmtTokens(turn.usage.input)} · out {fmtTokens(turn.usage.output)} ·
|
|
cache {fmtTokens(turn.usage.cache_read)}/{fmtTokens(
|
|
turn.usage.cache_creation
|
|
)}
|
|
{#if typeof turn.usage.cost_usd === "number"}
|
|
· {fmtMoney(turn.usage.cost_usd)}
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</header>
|
|
{#if turn.userText && turn.origin === "task"}
|
|
<p class="mx-1 px-1 text-muted-foreground text-xs">{turn.userText}</p>
|
|
{:else if turn.userText}
|
|
<Markdown
|
|
class="mx-1 rounded-md bg-muted/50 px-2.5 py-1.5"
|
|
inert
|
|
text={turn.userText}
|
|
/>
|
|
{/if}
|
|
{#if turn.reasoning.trim()}
|
|
<Reasoning live={turn.status === "running"} text={turn.reasoning} />
|
|
{/if}
|
|
{#if turn.roots.length > 0}
|
|
<div class="flex flex-col">
|
|
{#each turn.roots as rootId (rootId)}
|
|
{#if turn.nodes[rootId]}
|
|
<ToolNodeView node={turn.nodes[rootId]} nodes={turn.nodes} {now} />
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{#each turn.says as text, index (index)}
|
|
<SayBubble {text} />
|
|
{/each}
|
|
{#if turn.text}
|
|
<Markdown class="mx-1 px-1" text={turn.text} />
|
|
{:else if turn.status === "running" && turn.origin !== "agent" && turn.roots.length === 0 && turn.thinking === 0}
|
|
<p class="mx-1 px-1 text-muted-foreground text-sm">
|
|
Waiting for the model…
|
|
</p>
|
|
{/if}
|
|
</article>
|