research canvas: refresh claws + make Description collapsible
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 39s
ci / rust (push) Successful in 3m16s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m35s

Two UX cleanups on the research canvas:

1. Agent cards were showing "(missing agent)" for every slot after an
   in-wizard auto-provision because the Dashboard's `claws` prop is SSR-
   rendered and doesn't include agents created during the client session.
   Refetch /api/team/claws on canvas mount + refreshKey change, merge
   with the prop (fresh wins) so newly-provisioned agents resolve
   correctly without a page reload.

2. The Description block was always fully expanded. Wrap it in <details
   open> so the user can collapse it once they've read it, matching the
   existing "Original prompt" pattern.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-18 18:21:37 -07:00
co-authored by Claude Opus 4.7
parent 3444859e11
commit 82b5cf4385
@@ -108,6 +108,24 @@ export function ResearchCanvas({
// every SSE step event; a 250ms tick clears expired entries so the // every SSE step event; a 250ms tick clears expired entries so the
// card glow fades naturally. Plan = reading (cyan), Work/Synth/ // card glow fades naturally. Plan = reading (cyan), Work/Synth/
// Aggregate = writing (green). // Aggregate = writing (green).
// Top up the passed-in `agents` prop with any claws the workspace has
// acquired since SSR — e.g. auto-provisioned team agents created inside
// the wizard would otherwise show as "(missing agent)" on the roster
// cards until the next full page reload.
const [freshAgents, setFreshAgents] = useState<Agent[]>([]);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const r = await fetch("/api/team/claws");
if (!r.ok) return;
const claws = (await r.json()) as Agent[];
if (!cancelled) setFreshAgents(claws);
} catch { /* keep prop value */ }
})();
return () => { cancelled = true; };
}, [selectedId, refreshKey]);
const [activeAgents, setActiveAgents] = useState< const [activeAgents, setActiveAgents] = useState<
Map<string, { phase: StepPulse["phase"]; expiresAt: number }> Map<string, { phase: StepPulse["phase"]; expiresAt: number }>
>(new Map()); >(new Map());
@@ -273,7 +291,13 @@ export function ResearchCanvas({
const pipelineRunning = const pipelineRunning =
(topic.status === "processing" || topic.status === "publishing") && (topic.status === "processing" || topic.status === "publishing") &&
topic.runs_in_flight > 0; topic.runs_in_flight > 0;
const agentById = new Map(agents.map((a) => [a.id, a])); // Merge — fresh claws win when both include the same id so recent
// display-name/model edits show up without a page reload.
const mergedAgents = [
...agents.filter((a) => !freshAgents.some((f) => f.id === a.id)),
...freshAgents,
];
const agentById = new Map(mergedAgents.map((a) => [a.id, a]));
// Guard against a stale approval from a previously-selected topic: only // Guard against a stale approval from a previously-selected topic: only
// consider one that actually belongs to the current topic. // consider one that actually belongs to the current topic.
const boundApproval = const boundApproval =
@@ -704,8 +728,17 @@ export function ResearchCanvas({
</details> </details>
</> </>
) : ( ) : (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}> <details open style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<div style={sectionHeader}>Description</div> <summary
style={{
...sectionHeader,
cursor: "pointer",
listStyle: "revert",
userSelect: "none",
}}
>
Description
</summary>
<pre <pre
style={{ style={{
padding: 16, padding: 16,
@@ -723,7 +756,7 @@ export function ResearchCanvas({
> >
{topic.description} {topic.description}
</pre> </pre>
</div> </details>
)} )}
{/* Stage explainer + primary action */} {/* Stage explainer + primary action */}