cdcaff15ab
Serve API: - POST /api/threads — run a new thread - POST /api/threads/:id/kill — kill thread - POST /api/threads/:id/pause — pause thread - POST /api/threads/:id/resume — resume thread - GET /api/threads/:id/live — SSE stream of thread records Dashboard: - Run Thread dialog (select workflow, enter prompt, set maxRounds) - Thread detail controls (pause/resume/kill buttons) - postJson API helper for write operations 262 tests pass. Refs: #118
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import { useState } from "react";
|
|
import { getThread, killThread, pauseThread, resumeThread } from "../api.ts";
|
|
import { useFetch } from "../hooks.ts";
|
|
|
|
type Props = {
|
|
threadId: string;
|
|
onBack: () => void;
|
|
};
|
|
|
|
export function ThreadDetail({ threadId, onBack }: Props) {
|
|
const { status, data, error } = useFetch(() => getThread(threadId), [threadId]);
|
|
const [actionStatus, setActionStatus] = useState<string | null>(null);
|
|
|
|
async function handleAction(action: "kill" | "pause" | "resume") {
|
|
setActionStatus(`${action}ing...`);
|
|
try {
|
|
const fn = action === "kill" ? killThread : action === "pause" ? pauseThread : resumeThread;
|
|
await fn(threadId);
|
|
setActionStatus(`${action} sent ✓`);
|
|
} catch (e) {
|
|
setActionStatus(`${action} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<button
|
|
onClick={onBack}
|
|
className="text-sm hover:underline"
|
|
style={{ color: "var(--color-accent)" }}
|
|
>
|
|
← Back to threads
|
|
</button>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleAction("pause")}
|
|
className="px-3 py-1 text-xs rounded border"
|
|
style={{ borderColor: "var(--color-warning)", color: "var(--color-warning)" }}
|
|
>
|
|
⏸ Pause
|
|
</button>
|
|
<button
|
|
onClick={() => handleAction("resume")}
|
|
className="px-3 py-1 text-xs rounded border"
|
|
style={{ borderColor: "var(--color-success)", color: "var(--color-success)" }}
|
|
>
|
|
▶ Resume
|
|
</button>
|
|
<button
|
|
onClick={() => handleAction("kill")}
|
|
className="px-3 py-1 text-xs rounded border"
|
|
style={{ borderColor: "var(--color-error)", color: "var(--color-error)" }}
|
|
>
|
|
✕ Kill
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 className="text-xl font-semibold mb-2 font-mono">{threadId}</h2>
|
|
{actionStatus && (
|
|
<p className="text-xs mb-4" style={{ color: "var(--color-text-muted)" }}>
|
|
{actionStatus}
|
|
</p>
|
|
)}
|
|
|
|
{status === "loading" && <p style={{ color: "var(--color-text-muted)" }}>Loading...</p>}
|
|
{status === "error" && <p style={{ color: "var(--color-error)" }}>Error: {error}</p>}
|
|
{status === "ok" && (
|
|
<div className="space-y-3">
|
|
{data.records.map((r, i) => (
|
|
<div
|
|
key={i}
|
|
className="p-3 rounded border text-sm"
|
|
style={{ background: "var(--color-surface)", borderColor: "var(--color-border)" }}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span
|
|
className="text-xs px-1.5 py-0.5 rounded font-mono"
|
|
style={{ background: "var(--color-border)", color: "var(--color-accent)" }}
|
|
>
|
|
{r.type}
|
|
</span>
|
|
{r.role && (
|
|
<span className="text-xs" style={{ color: "var(--color-text-muted)" }}>
|
|
{r.role}
|
|
</span>
|
|
)}
|
|
{r.timestamp && (
|
|
<span className="text-xs ml-auto" style={{ color: "var(--color-text-muted)" }}>
|
|
{new Date(r.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{r.content && (
|
|
<pre className="whitespace-pre-wrap text-xs mt-1" style={{ color: "var(--color-text)" }}>
|
|
{typeof r.content === "string" ? r.content : JSON.stringify(r.content, null, 2)}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|