Persist bounded ideation sessions through agent tools, the Command Center, and atomic Mission convergence. - Store ideation sessions and divergent candidates in PostgreSQL with async APIs and migration support. - Expose gated ideation tools, chat routes, and agent lifecycle integration. - Add the Ideation panel, documentation, release metadata, and regression coverage. Files changed: .changeset/fn-8295-ideation-diverge-converge.md | 7 ++ docs/ideation/persisted-diverge-converge.md | 22 ++++ docs/missions.md | 4 + .../__tests__/postgres/ideation-store.pg.test.ts | 57 +++++++++ packages/core/src/async-ideation-store-queries.ts | 117 +++++++++++++++++ packages/core/src/async-ideation-store.ts | 138 +++++++++++++++++++++ packages/core/src/async-mission-store.ts | 27 ++-- packages/core/src/ideation-types.ts | 69 +++++++++++ packages/core/src/index.ts | 3 + .../core/src/postgres/migrations/0022_ideation.sql | 67 ++++++++++ packages/core/src/postgres/schema-applier.ts | 18 ++- packages/core/src/postgres/schema/project.ts | 49 +++++++- packages/core/src/store.ts | 8 +- packages/core/src/task-store/remaining-ops-8.ts | 14 +++ .../components/command-center/CommandCenter.tsx | 7 +- .../components/command-center/IdeationPanel.css | 18 +++ .../components/command-center/IdeationPanel.tsx | 58 +++++++++ .../__tests__/CommandCenter.test.tsx | 6 +- .../dashboard/src/__tests__/chat-manager.test.ts | 1 + packages/dashboard/src/__tests__/chat.test.ts | 1 + .../__tests__/ideation-tool-route-parity.test.ts | 29 +++++ packages/dashboard/src/chat.ts | 4 + packages/dashboard/src/ideation-routes.ts | 50 ++++++++ .../src/routes/register-integrated-routers.ts | 2 + .../src/__tests__/agent-ideation-tools.test.ts | 40 ++++++ .../src/__tests__/gating-classifications.test.ts | 16 +++ .../src/__tests__/permanent-agent-gating.test.ts | 2 + packages/engine/src/agent-heartbeat.ts | 4 +- packages/engine/src/agent-tools.ts | 67 ++++++++++ packages/engine/src/executor.ts | 2 + packages/engine/src/gating-classifications.ts | 8 ++ packages/engine/src/index.ts | 1 + packages/engine/src/triage.ts | 2 + 33 files changed, 897 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-8295 Fusion-Task-Lineage: 1b8b0752-22bd-4b2f-aebd-4305c63abcf9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
59 lines
5.1 KiB
TypeScript
59 lines
5.1 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
|
import { Lightbulb } from "lucide-react";
|
|
import type { IdeationCandidate, IdeationSessionWithCandidates } from "@fusion/core";
|
|
import { withProjectId } from "../../api/legacy";
|
|
import "./IdeationPanel.css";
|
|
|
|
async function ideationRequest<T>(path: string, projectId: string | undefined, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(withProjectId(`/api/ideation${path}`, projectId), {
|
|
...init,
|
|
headers: { "Content-Type": "application/json", ...init?.headers },
|
|
});
|
|
if (!response.ok) throw new Error(await response.text() || "Ideation request failed");
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
/*
|
|
FNXC:Ideation 2026-07-30-15:30:
|
|
Command Center gives humans the same bounded session → candidates → canonical
|
|
Mission convergence operation agents use. The visible Mission ID is persisted
|
|
handoff evidence, not a copied document or a separate dashboard-only roadmap.
|
|
*/
|
|
export function IdeationPanel({ projectId }: { projectId?: string }) {
|
|
const [sessions, setSessions] = useState<IdeationSessionWithCandidates[]>([]);
|
|
const [selectedId, setSelectedId] = useState<string>();
|
|
const [title, setTitle] = useState("");
|
|
const [candidate, setCandidate] = useState("");
|
|
const [error, setError] = useState<string>();
|
|
const selected = sessions.find((session) => session.id === selectedId);
|
|
const refresh = async () => {
|
|
const listed = await ideationRequest<Array<IdeationSessionWithCandidates>>("/", projectId);
|
|
const hydrated = await Promise.all(listed.map((session) => ideationRequest<IdeationSessionWithCandidates>(`/${encodeURIComponent(session.id)}`, projectId)));
|
|
setSessions(hydrated);
|
|
setSelectedId((current) => current && hydrated.some((session) => session.id === current) ? current : hydrated[0]?.id);
|
|
};
|
|
useEffect(() => { void refresh().catch((reason) => setError(reason instanceof Error ? reason.message : String(reason))); }, [projectId]);
|
|
const submit = async (event: FormEvent) => {
|
|
event.preventDefault(); setError(undefined);
|
|
try { const created = await ideationRequest<IdeationSessionWithCandidates>("/", projectId, { method: "POST", body: JSON.stringify({ title }) }); setTitle(""); await refresh(); setSelectedId(created.id); }
|
|
catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
};
|
|
const addCandidate = async (event: FormEvent) => {
|
|
event.preventDefault(); if (!selected || !candidate.trim()) return; setError(undefined);
|
|
try { await ideationRequest(`/${encodeURIComponent(selected.id)}/candidates`, projectId, { method: "POST", body: JSON.stringify({ content: candidate, origin: "human" }) }); setCandidate(""); await refresh(); }
|
|
catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
};
|
|
const converge = async (item: IdeationCandidate) => {
|
|
if (!selected) return; setError(undefined);
|
|
try { await ideationRequest(`/${encodeURIComponent(selected.id)}/converge`, projectId, { method: "POST", body: JSON.stringify({ candidateId: item.id }) }); await refresh(); }
|
|
catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
|
};
|
|
return <section className="ideation-panel" aria-label="Persisted ideation">
|
|
<header className="ideation-panel__header"><Lightbulb /><div><h2>Ideation</h2><p>Capture alternatives, then converge one into the Mission hierarchy.</p></div></header>
|
|
<form className="ideation-panel__form" onSubmit={submit}><input className="input" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="Session title" aria-label="Session title" required /><button className="btn" type="submit">Start session</button></form>
|
|
{error && <p className="ideation-panel__error" role="alert">{error}</p>}
|
|
<div className="ideation-panel__body"><aside className="ideation-panel__sessions">{sessions.length ? sessions.map((session) => <button className={`card ideation-panel__session ${session.id === selected?.id ? "is-selected" : ""}`} type="button" onClick={() => setSelectedId(session.id)} key={session.id}>{session.title}<span>{session.status}</span></button>) : <p>No sessions yet.</p>}</aside>
|
|
<div className="ideation-panel__detail">{selected ? <><h3>{selected.title}</h3>{selected.targetMissionId && <p className="ideation-panel__handoff">Converged to Mission <strong>{selected.targetMissionId}</strong></p>}{selected.status === "open" && <form className="ideation-panel__form" onSubmit={addCandidate}><input className="input" value={candidate} onChange={(event) => setCandidate(event.target.value)} placeholder="Divergent candidate" aria-label="Divergent candidate" required /><button className="btn" type="submit">Add candidate</button></form>}<ul className="ideation-panel__candidates">{selected.candidates.map((item) => <li className="card" key={item.id}><p>{item.content}</p><small>{item.origin}{item.sourceRef ? ` · ${item.sourceRef}` : ""}</small>{selected.status === "open" && <button className="btn" type="button" onClick={() => void converge(item)}>Converge</button>}</li>)}</ul></> : <p>Select or start a session.</p>}</div></div>
|
|
</section>;
|
|
}
|