Export template source from 1e93145fb05ae5753b9fb268bed460f1f0e0e63f:quixos-instance/packages/agent-presentation
This commit is contained in:
+606
@@ -0,0 +1,606 @@
|
||||
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import {
|
||||
type Item,
|
||||
type Request,
|
||||
type Question,
|
||||
type ThreadController,
|
||||
type Profiles,
|
||||
itemKey,
|
||||
terminal,
|
||||
} from "./protocol.js";
|
||||
import { LocalInput, LocalTextArea, LocalSelect, Markdown } from "@quixos/camino-react/ui";
|
||||
import { draftKey } from "./http.js";
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
queued: "Queued",
|
||||
blocked: "Waiting to start",
|
||||
running: "Working",
|
||||
waiting_input: "Needs your answer",
|
||||
recovering: "Checking execution status",
|
||||
recording: "Recording result",
|
||||
pending_graph: "Awaiting graph recording",
|
||||
awaiting_graph: "Awaiting graph recording",
|
||||
completed: "Completed",
|
||||
failed: "Failed",
|
||||
cancelled: "Cancelled",
|
||||
uncertain: "Outcome uncertain",
|
||||
parked: "Needs your answer",
|
||||
};
|
||||
function ResultValue({ value }: { value: unknown }) {
|
||||
if (Array.isArray(value))
|
||||
return (
|
||||
<ol>
|
||||
{value.map((entry, index) => (
|
||||
<li key={index}>
|
||||
<ResultValue value={entry} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
if (value && typeof value === "object")
|
||||
return (
|
||||
<dl>
|
||||
{Object.entries(value).map(([key, entry]) => (
|
||||
<React.Fragment key={key}>
|
||||
<dt>{key}</dt>
|
||||
<dd>
|
||||
<ResultValue value={entry} />
|
||||
</dd>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
return <span>{value === null ? "null" : String(value)}</span>;
|
||||
}
|
||||
export function StructuredResult({ value }: { value: string }) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
return <pre>{value}</pre>;
|
||||
}
|
||||
return (
|
||||
<div className="agent-result" aria-label="Structured result">
|
||||
<ResultValue value={parsed} />
|
||||
<details>
|
||||
<summary>Raw JSON</summary>
|
||||
<pre>{value}</pre>
|
||||
<button onClick={() => void navigator.clipboard.writeText(value)}>Copy JSON</button>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function QuestionForm({
|
||||
question,
|
||||
request,
|
||||
controller,
|
||||
}: {
|
||||
question: Question;
|
||||
request: Request;
|
||||
controller: ThreadController;
|
||||
}) {
|
||||
const [answer, setAnswer] = useState(""),
|
||||
[busy, setBusy] = useState(false),
|
||||
[error, setError] = useState("");
|
||||
let options: string[] = [];
|
||||
try {
|
||||
options = JSON.parse(question.data.options);
|
||||
} catch {}
|
||||
const open =
|
||||
question.data.status === "open" &&
|
||||
!terminal.has(request.progress.status) &&
|
||||
!request.progress.cancelRequested &&
|
||||
Date.parse(question.data.expiresAt) > Date.now();
|
||||
return (
|
||||
<form
|
||||
className="agent-question"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!open || !answer.trim() || busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
void controller
|
||||
.control({
|
||||
kind: "answer",
|
||||
question: question._camino.ref.object,
|
||||
attempt: question.data.attempt,
|
||||
generation: question.data.generation,
|
||||
answer,
|
||||
})
|
||||
.catch((error) => setError(String(error)))
|
||||
.finally(() => setBusy(false));
|
||||
}}
|
||||
>
|
||||
<fieldset disabled={!open || busy}>
|
||||
<legend>{question.data.title}</legend>
|
||||
{options.map((option) => (
|
||||
<label key={option}>
|
||||
<LocalInput
|
||||
type="radio"
|
||||
name={question._camino.ref.object}
|
||||
value={option}
|
||||
checked={answer === option}
|
||||
onChange={() => setAnswer(option)}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
<label>
|
||||
Your answer
|
||||
<LocalInput aria-label="Your answer" value={answer} onChange={(event) => setAnswer(event.target.value)} />
|
||||
</label>
|
||||
<button disabled={!answer.trim()}>{busy ? "Sending answer…" : "Answer"}</button>
|
||||
</fieldset>
|
||||
{!open && (
|
||||
<p>
|
||||
{question.data.answer
|
||||
? `Answered: ${question.data.answer}`
|
||||
: question.data.status === "open"
|
||||
? "This question is no longer accepting answers."
|
||||
: question.data.status}
|
||||
</p>
|
||||
)}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
function Activity({ item }: { item: Item }) {
|
||||
let value: any;
|
||||
try {
|
||||
value = JSON.parse(item.data.activity ?? "null");
|
||||
} catch {}
|
||||
const title = value?.command ?? value?.label ?? item.data.text ?? "Activity";
|
||||
return (
|
||||
<details className="agent-activity-card">
|
||||
<summary>{String(title).slice(0, 300)}</summary>
|
||||
<pre>{value?.aggregatedOutput ?? value?.output ?? value?.detail ?? item.data.text}</pre>
|
||||
{value?.exitCode !== undefined && <p>Exit code: {value.exitCode}</p>}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
function RequestCard({
|
||||
request,
|
||||
controller,
|
||||
inspection,
|
||||
}: {
|
||||
request: Request;
|
||||
controller: ThreadController;
|
||||
inspection: boolean;
|
||||
}) {
|
||||
const [busy, setBusy] = useState(false),
|
||||
[error, setError] = useState(""),
|
||||
[details, setDetails] = useState<unknown>();
|
||||
const p = request.progress,
|
||||
cancel = () => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const action =
|
||||
request.pendingGraph && request.repair
|
||||
? import("./http.js").then(({ api }) =>
|
||||
api("/repair-control", {
|
||||
operationId: crypto.randomUUID(),
|
||||
repair: request.repair,
|
||||
attempt: p.attempt,
|
||||
generation: p.generation,
|
||||
kind: "cancel",
|
||||
}),
|
||||
)
|
||||
: controller.control({ kind: "cancel", request: request._camino.ref.object });
|
||||
void action.catch((error) => setError(String(error))).finally(() => setBusy(false));
|
||||
};
|
||||
return (
|
||||
<footer className={`agent-request-card agent-request-${p.status}`} data-request={request._camino.ref.object}>
|
||||
<div>
|
||||
{(inspection || p.status !== "completed") && <strong>{labels[p.status] ?? p.status}</strong>}
|
||||
{request.pendingGraph && <span> · Awaiting graph recording</span>}
|
||||
{p.cancelRequested && !terminal.has(p.status) && <span> · Stop requested</span>}
|
||||
{!terminal.has(p.status) && (
|
||||
<button disabled={busy || p.cancelRequested || (request.pendingGraph && !p.attempt)} onClick={cancel}>
|
||||
Stop request
|
||||
</button>
|
||||
)}
|
||||
{!request.pendingGraph && (inspection || p.status !== "completed") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
void controller
|
||||
.details(request._camino.ref.object)
|
||||
.then(setDetails)
|
||||
.catch((error) => setError(String(error)));
|
||||
}}
|
||||
>
|
||||
Request details
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{p.reason && <p>{p.reason}</p>}
|
||||
{p.status === "uncertain" && <p>Execution may have occurred. Check activity before starting another request.</p>}
|
||||
{p.status === "completed" && request.admission?.responseKind === "json" && p.result && (
|
||||
<StructuredResult value={p.result} />
|
||||
)}
|
||||
{p.status === "completed" && request.admission?.responseKind === "unit" && <p>Actions completed.</p>}
|
||||
{["failed", "cancelled", "uncertain"].includes(p.status) && request.admission?.text && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
void controller
|
||||
.submit(request.admission!.text!, { retryOf: request._camino.ref.object })
|
||||
.catch((error) => setError(String(error)))
|
||||
.finally(() => setBusy(false));
|
||||
}}
|
||||
disabled={busy}
|
||||
>
|
||||
Try again with current settings
|
||||
</button>
|
||||
)}
|
||||
{p.status === "failed" && p.rawOutput && (
|
||||
<details>
|
||||
<summary>Returned output</summary>
|
||||
<pre>{p.rawOutput}</pre>
|
||||
</details>
|
||||
)}
|
||||
{details !== undefined && (
|
||||
<details open>
|
||||
<summary>Captured request and execution</summary>
|
||||
<pre>{JSON.stringify(details, null, 2)}</pre>
|
||||
</details>
|
||||
)}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
export function ThreadView({
|
||||
controller,
|
||||
thread,
|
||||
profiles = {},
|
||||
workspace = false,
|
||||
defaultProfile = "balanced",
|
||||
allowRepair = false,
|
||||
renderWidget,
|
||||
renderItem,
|
||||
}: {
|
||||
controller: ThreadController;
|
||||
thread: string;
|
||||
profiles?: Profiles;
|
||||
workspace?: boolean;
|
||||
defaultProfile?: string;
|
||||
allowRepair?: boolean;
|
||||
renderWidget?(item: Item): React.ReactNode;
|
||||
/** Return undefined for normal rendering; null hides an item. */
|
||||
renderItem?(item: Item, request: Request): React.ReactNode | undefined;
|
||||
}) {
|
||||
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot),
|
||||
[draft, setDraft] = useState(() => sessionStorage.getItem(draftKey(thread)) ?? ""),
|
||||
[busy, setBusy] = useState(false),
|
||||
[error, setError] = useState(""),
|
||||
[profile, setProfile] = useState(() => sessionStorage.getItem(`${draftKey(thread)}:profile`) ?? ""),
|
||||
[response, setResponse] = useState(""),
|
||||
[repair, setRepair] = useState(false),
|
||||
[latest, setLatest] = useState(true),
|
||||
[now, setNow] = useState(Date.now()),
|
||||
scroll = useRef<HTMLDivElement>(null),
|
||||
previousSize = useRef(0);
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem(draftKey(thread), draft);
|
||||
}, [thread, draft]);
|
||||
useEffect(() => {
|
||||
if (latest && scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight;
|
||||
}, [state.items, latest]);
|
||||
const active =
|
||||
(state.thread?.active &&
|
||||
state.requests.find(
|
||||
(request) =>
|
||||
request._camino.ref.object === state.thread!.active!._camino.ref.object &&
|
||||
!terminal.has(request.progress.status),
|
||||
)) ??
|
||||
state.requests.find((request) =>
|
||||
["running", "waiting_input", "stopping", "parked"].includes(request.progress.status),
|
||||
),
|
||||
pending = state.requests.filter((request) => !terminal.has(request.progress.status)),
|
||||
archived = state.thread?.data.archived;
|
||||
const run = async (steer = false) => {
|
||||
if (!draft.trim() || busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const text = draft;
|
||||
try {
|
||||
if (steer && active)
|
||||
await controller.control({
|
||||
kind: "steer",
|
||||
request: active._camino.ref.object,
|
||||
attempt: active.progress.attempt,
|
||||
generation: active.progress.generation,
|
||||
text,
|
||||
});
|
||||
else
|
||||
await controller.submit(text, {
|
||||
...(profile ? { profile } : {}),
|
||||
...(response ? { response: { kind: response } } : {}),
|
||||
repair: state.maintenance && repair,
|
||||
});
|
||||
setDraft((current) => (current === text ? "" : current));
|
||||
} catch (error) {
|
||||
setError(String(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
const selected = profile || defaultProfile;
|
||||
const route = profiles[selected];
|
||||
const standard = route?.standardProfile ?? selected;
|
||||
const changeProfile = async (next: string) => {
|
||||
setProfile(next);
|
||||
sessionStorage.setItem(`${draftKey(thread)}:profile`, next);
|
||||
if (active?.progress.status !== "running" || state.maintenance) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await controller.control({
|
||||
kind: "steer",
|
||||
request: active._camino.ref.object,
|
||||
attempt: active.progress.attempt,
|
||||
generation: active.progress.generation,
|
||||
text: "",
|
||||
profile: next,
|
||||
});
|
||||
} catch (error) {
|
||||
setError(String(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
const grouped = new Map<string, { request: Request; items: Item[] }>();
|
||||
for (const item of state.items) {
|
||||
const key = item.request._camino.ref.object,
|
||||
group = grouped.get(key) ?? { request: item.request, items: [] };
|
||||
group.items.push(item);
|
||||
group.request = item.request;
|
||||
grouped.set(key, group);
|
||||
}
|
||||
for (const request of pending)
|
||||
if (!grouped.has(request._camino.ref.object)) grouped.set(request._camino.ref.object, { request, items: [] });
|
||||
return (
|
||||
<section className="agent-thread" aria-label="Agent conversation">
|
||||
<div className="agent-thread-status" role="status">
|
||||
{state.loading
|
||||
? "Loading thread…"
|
||||
: state.maintenance
|
||||
? "Workspace graph unavailable. Known activity remains visible."
|
||||
: state.connected
|
||||
? "Connected"
|
||||
: "Reconnecting…"}
|
||||
{active?.progress.startedAt && (
|
||||
<span> · Working for {Math.max(0, Math.floor((now - Date.parse(active.progress.startedAt)) / 1000))}s</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="agent-thread-history"
|
||||
ref={scroll}
|
||||
onScroll={() => {
|
||||
const element = scroll.current!;
|
||||
setLatest(element.scrollHeight - element.scrollTop - element.clientHeight < 80);
|
||||
}}
|
||||
>
|
||||
{state.pageInfo?.hasNextPage && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const element = scroll.current!,
|
||||
height = element.scrollHeight;
|
||||
previousSize.current = height;
|
||||
setLatest(false);
|
||||
void controller
|
||||
.refresh(true)
|
||||
.then(() =>
|
||||
requestAnimationFrame(() => {
|
||||
element.scrollTop += element.scrollHeight - previousSize.current;
|
||||
}),
|
||||
)
|
||||
.catch((error) => setError(String(error)));
|
||||
}}
|
||||
>
|
||||
Load earlier messages
|
||||
</button>
|
||||
)}
|
||||
{grouped.size === 0 && !state.loading && (
|
||||
<div className="agent-empty">
|
||||
<h2>A new conversation</h2>
|
||||
<p>Send a message or a task to this Agent.</p>
|
||||
</div>
|
||||
)}
|
||||
{[...grouped.values()].map(({ request, items }) => (
|
||||
<article className="agent-request" key={request._camino.ref.object}>
|
||||
<div className="agent-request-source">
|
||||
{request.admission?.sourceLabel ?? "Agent request"} ·{" "}
|
||||
{request.admission?.createdAt && (
|
||||
<time dateTime={request.admission.createdAt}>
|
||||
{new Date(request.admission.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
const custom = renderItem?.(item, request);
|
||||
return (
|
||||
<React.Fragment key={itemKey(item)}>
|
||||
{custom !== undefined ? (
|
||||
custom
|
||||
) : item.data.kind === "steering" && item.data.activity ? (
|
||||
<aside className="agent-routing-change" role="status">
|
||||
Model profile · {JSON.parse(item.data.activity).profile} ·{" "}
|
||||
{item.data.control?.status === "accepted"
|
||||
? "Applied to subsequent inference"
|
||||
: item.data.control?.status === "rejected"
|
||||
? "Change rejected by Codex"
|
||||
: item.data.control?.status === "uncertain"
|
||||
? "Change outcome uncertain"
|
||||
: "Changing…"}
|
||||
{item.data.control?.reason && <p>{item.data.control.reason}</p>}
|
||||
{item.data.text && <p>{item.data.text}</p>}
|
||||
</aside>
|
||||
) : item.data.kind === "activity" ? (
|
||||
<Activity item={item} />
|
||||
) : item.data.kind === "widget" ? (
|
||||
<div className="agent-widget">
|
||||
{renderWidget ? (
|
||||
renderWidget(item)
|
||||
) : (
|
||||
<p>
|
||||
Widget unavailable in this view.{" "}
|
||||
<a href={`/entity/${encodeURIComponent(item.subject?._camino.ref.object ?? "")}`}>
|
||||
Open object
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : item.data.kind === "status" ? (
|
||||
<aside className="agent-status-card">
|
||||
{item.data.text}
|
||||
<Activity item={item} />
|
||||
</aside>
|
||||
) : item.data.phase === "commentary" ? (
|
||||
<details className="agent-commentary">
|
||||
<summary>Agent commentary</summary>
|
||||
<Markdown text={item.data.text} />
|
||||
</details>
|
||||
) : request.admission?.responseKind === "json" && item.data.kind === "assistant" ? null : (
|
||||
<div className={`agent-message agent-message-${item.data.kind}`}>
|
||||
{item.data.kind === "assistant" ? <Markdown text={item.data.text} /> : item.data.text}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{(request.questions?.entries ?? []).map(({ node }) => (
|
||||
<QuestionForm key={node._camino.ref.object} question={node} request={request} controller={controller} />
|
||||
))}
|
||||
<RequestCard request={request} controller={controller} inspection={!workspace} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{!latest && (
|
||||
<button
|
||||
className="agent-jump"
|
||||
onClick={() => {
|
||||
setLatest(true);
|
||||
if (scroll.current) scroll.current.scrollTop = scroll.current.scrollHeight;
|
||||
}}
|
||||
>
|
||||
Jump to latest
|
||||
</button>
|
||||
)}
|
||||
<form
|
||||
className="agent-thread-composer"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void run();
|
||||
}}
|
||||
>
|
||||
{state.maintenance && allowRepair && (
|
||||
<label>
|
||||
<LocalInput type="checkbox" checked={repair} onChange={(event) => setRepair(event.target.checked)} />
|
||||
Source repair in the Workspace thread
|
||||
</label>
|
||||
)}
|
||||
<label className="agent-compose-label">
|
||||
Message
|
||||
<LocalTextArea
|
||||
aria-label="Message"
|
||||
rows={3}
|
||||
value={draft}
|
||||
disabled={archived || busy}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
void run();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="agent-compose-actions">
|
||||
{workspace && Object.keys(profiles).length > 0 && (
|
||||
<div className="agent-model-controls">
|
||||
<label>
|
||||
Model
|
||||
<LocalSelect
|
||||
aria-label="Model"
|
||||
disabled={busy}
|
||||
value={standard}
|
||||
onChange={(event) => {
|
||||
const name = event.target.value;
|
||||
void changeProfile(
|
||||
route?.standardProfile && profiles[name]?.fastProfile ? profiles[name].fastProfile! : name,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.entries(profiles)
|
||||
.filter(([, p]) => !p.standardProfile)
|
||||
.map(([name, p]) => (
|
||||
<option key={name} value={name} disabled={p.available === false}>
|
||||
{p.description ?? p.model ?? name}
|
||||
</option>
|
||||
))}
|
||||
</LocalSelect>
|
||||
</label>
|
||||
<label>
|
||||
<LocalInput
|
||||
type="checkbox"
|
||||
aria-label="Fast mode"
|
||||
checked={!!route?.standardProfile}
|
||||
disabled={busy || !(route?.fastProfile || route?.standardProfile)}
|
||||
onChange={(event) =>
|
||||
void changeProfile(event.target.checked ? route!.fastProfile! : route!.standardProfile!)
|
||||
}
|
||||
/>
|
||||
Fast
|
||||
</label>
|
||||
{active?.progress.status === "running" && <small>Changes apply between inference steps.</small>}
|
||||
</div>
|
||||
)}
|
||||
<details>
|
||||
<summary>Next request options</summary>
|
||||
<label>
|
||||
Profile
|
||||
<LocalSelect aria-label="Profile" value={profile} onChange={(event) => setProfile(event.target.value)}>
|
||||
<option value="">Agent default</option>
|
||||
{Object.keys(profiles).map((value) => (
|
||||
<option key={value} disabled={profiles[value].available === false}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</LocalSelect>
|
||||
</label>
|
||||
<label>
|
||||
Response
|
||||
<LocalSelect aria-label="Response" value={response} onChange={(event) => setResponse(event.target.value)}>
|
||||
<option value="">Agent default</option>
|
||||
<option value="text">Text follow-up</option>
|
||||
<option value="unit">Actions only</option>
|
||||
</LocalSelect>
|
||||
</label>
|
||||
</details>
|
||||
<span>{pending.length ? `${pending.length} outstanding request${pending.length === 1 ? "" : "s"}` : ""}</span>
|
||||
{active?.progress.status === "running" && !state.maintenance && (
|
||||
<button type="button" disabled={!draft.trim() || busy} onClick={() => void run(true)}>
|
||||
Steer current
|
||||
</button>
|
||||
)}
|
||||
<button disabled={!draft.trim() || busy || archived || (state.maintenance && !(allowRepair && repair))}>
|
||||
{busy ? "Sending…" : state.maintenance ? "Submit source repair" : pending.length ? "Send to queue" : "Send"}
|
||||
</button>
|
||||
</div>
|
||||
{archived && <p>This thread is archived. Reopen it to send a message.</p>}
|
||||
{(error || state.error) && (
|
||||
<p role="alert">
|
||||
{error || state.error}{" "}
|
||||
<button type="button" onClick={() => void run()}>
|
||||
Retry retained submission
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user