147 lines
4.4 KiB
Svelte
147 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import GitBranchIcon from "@lucide/svelte/icons/git-branch";
|
|
import type { ApiClient } from "$lib/api/client";
|
|
import type { LiveState } from "$lib/api/live.svelte";
|
|
import type { ConversationInfo } from "$lib/api/types";
|
|
import KindBadge from "$lib/components/kind-badge.svelte";
|
|
import LiveDot from "$lib/components/live-dot.svelte";
|
|
import StatusPill from "$lib/components/status-pill.svelte";
|
|
import { Button } from "$lib/components/ui/button";
|
|
import { clip, fmtRelative, shortId } from "$lib/format";
|
|
import BranchDialog from "./branch-dialog.svelte";
|
|
import ConversationMenu from "./conversation-menu.svelte";
|
|
|
|
let {
|
|
client,
|
|
info,
|
|
liveState,
|
|
liveDetail = null,
|
|
href = null,
|
|
onChanged,
|
|
onOpen,
|
|
showTitle = true,
|
|
}: {
|
|
client: ApiClient;
|
|
info: ConversationInfo;
|
|
liveState: LiveState;
|
|
liveDetail?: string | null;
|
|
// Where another conversation lives as a link; without it, parents open in place.
|
|
href?: ((id: string) => string) | null;
|
|
onChanged: () => void;
|
|
onOpen: (id: string) => void;
|
|
showTitle?: boolean;
|
|
} = $props();
|
|
|
|
let branchOpen = $state(false);
|
|
|
|
const WINDOW_MAX = 56;
|
|
const windows = $derived(info.bindings.filter((b) => b.visible));
|
|
const queued = $derived(
|
|
info.queue.filter((item) => item.status === "queued").length
|
|
);
|
|
</script>
|
|
|
|
<header class="flex flex-col gap-1.5 border-b px-3 py-2.5 @md:px-6">
|
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
{#if showTitle}
|
|
<KindBadge kind={info.kind} />
|
|
<h1 class="min-w-0 truncate font-semibold text-base tracking-tight">
|
|
{info.title || `${info.kind} ${shortId(info.id)}`}
|
|
</h1>
|
|
{/if}
|
|
<StatusPill status={info.running_turn ? "running" : info.status} />
|
|
{#if info.pending_question}
|
|
<span class="font-medium text-link text-xs">question pending</span>
|
|
{/if}
|
|
<div class="ml-auto flex items-center gap-1">
|
|
<LiveDot
|
|
class="hidden @md:inline-flex"
|
|
detail={liveDetail}
|
|
state={liveState}
|
|
/>
|
|
<LiveDot
|
|
class="@md:hidden"
|
|
detail={liveDetail}
|
|
label={false}
|
|
state={liveState}
|
|
/>
|
|
<Button
|
|
aria-label="Branch off this conversation"
|
|
disabled={info.status !== "open"}
|
|
onclick={() => {
|
|
branchOpen = true;
|
|
}}
|
|
size="sm"
|
|
variant="outline"
|
|
>
|
|
<GitBranchIcon class="size-4" />
|
|
<span class="hidden @md:inline">Branch</span>
|
|
</Button>
|
|
<ConversationMenu
|
|
{client}
|
|
current
|
|
id={info.id}
|
|
{info}
|
|
mode="button"
|
|
{onChanged}
|
|
{onOpen}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<!-- Below @md the column is the live thread's; the rest of this lives in Meta. -->
|
|
<div
|
|
class="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-muted-foreground text-xs"
|
|
>
|
|
<span class="hidden @md:inline">{info.agent}</span>
|
|
<span class="hidden @md:inline">via {info.origin}</span>
|
|
{#if info.parent}
|
|
{#if href}
|
|
<a class="text-link hover:underline" href={href(info.parent)}>
|
|
parent {shortId(info.parent)}
|
|
</a>
|
|
{:else}
|
|
<button
|
|
class="text-link hover:underline"
|
|
onclick={() => info.parent && onOpen(info.parent)}
|
|
type="button"
|
|
>
|
|
parent {shortId(info.parent)}
|
|
</button>
|
|
{/if}
|
|
{/if}
|
|
<span class="tabular hidden @md:inline" title={info.id}>
|
|
{shortId(info.id)}
|
|
</span>
|
|
<span class="tabular hidden @md:inline">
|
|
{info.live ? "session live" : "no live session"}
|
|
{info.busy ? " · busy" : ""}
|
|
</span>
|
|
<span class="tabular">
|
|
last activity {fmtRelative(info.last_activity_at)}
|
|
</span>
|
|
{#if queued > 0}
|
|
<span
|
|
class="tabular font-medium text-note"
|
|
title="messages waiting for the next turn"
|
|
>
|
|
{queued}
|
|
queued
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{#if windows.length > 0}
|
|
<div
|
|
class="hidden flex-wrap items-center gap-x-3 gap-y-0.5 text-muted-foreground text-xs @md:flex"
|
|
>
|
|
{#each windows as window (window.frontend + window.external_id)}
|
|
<span class="truncate" title="{window.frontend}: {window.external_id}">
|
|
<span class="text-foreground/70">{window.frontend}</span>
|
|
{clip(window.external_id, WINDOW_MAX)}
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</header>
|
|
|
|
<BranchDialog {client} onOpened={onOpen} parent={info} bind:open={branchOpen} />
|