feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -59,24 +59,49 @@ export const investigationDiagramPlugins = {
|
||||
return {
|
||||
initialize: (nextConfig: MermaidConfig) => renderer.initialize(nextConfig),
|
||||
async render(id: string, source: string) {
|
||||
const result = await renderer.render(id, source);
|
||||
const svg = new DOMParser().parseFromString(result.svg, "image/svg+xml");
|
||||
svg.querySelectorAll(".cluster").forEach((cluster) => {
|
||||
const rect = Array.from(cluster.children).find((child) => child.tagName === "rect");
|
||||
const caption = cluster.querySelector(".cluster-label");
|
||||
if (!rect || !caption) return;
|
||||
const x = Number(rect.getAttribute("x"));
|
||||
const y = Number(rect.getAttribute("y"));
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
caption.setAttribute(
|
||||
"transform",
|
||||
`translate(${x + 18}, ${y + (config?.flowchart?.subGraphTitleMargin?.top ?? 12)})`
|
||||
);
|
||||
// Mermaid paints crossing edges after clusters; keep captions above those edges.
|
||||
const clusters = cluster.parentElement;
|
||||
if (clusters?.classList.contains("clusters")) clusters.parentElement?.append(caption);
|
||||
});
|
||||
return { ...result, svg: new XMLSerializer().serializeToString(svg.documentElement) };
|
||||
// Streamdown's two-argument renderer uses Mermaid's temporary #d{id}
|
||||
// under body. It must remain measurable, but never enter page flow.
|
||||
// Scope the rule to this render, including fullscreen and error paths.
|
||||
const measurementStyle = document.createElement("style");
|
||||
measurementStyle.textContent = `body > [id=${JSON.stringify(`d${id}`)}] {
|
||||
position: fixed !important; inset: 0 auto auto 0;
|
||||
width: 100%; visibility: hidden; pointer-events: none;
|
||||
contain: layout paint;
|
||||
}`;
|
||||
document.head.append(measurementStyle);
|
||||
const result = await renderer.render(id, source).finally(() => measurementStyle.remove());
|
||||
// Mermaid's strict renderer emits HTML labels inside foreignObject,
|
||||
// including HTML void tags such as <br>. Parse its sanitized output in
|
||||
// an inert HTML fragment, not as a standalone XML document.
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = result.svg;
|
||||
const svg = template.content.firstElementChild;
|
||||
if (template.content.childElementCount !== 1 || svg?.localName !== "svg"
|
||||
|| svg.namespaceURI !== "http://www.w3.org/2000/svg" || svg.querySelector("parsererror")) {
|
||||
throw new Error("Mermaid did not return a valid SVG diagram");
|
||||
}
|
||||
try {
|
||||
svg.querySelectorAll(".cluster").forEach((cluster) => {
|
||||
const rect = Array.from(cluster.children).find((child) => child.tagName === "rect");
|
||||
const caption = cluster.querySelector(".cluster-label");
|
||||
if (!rect || !caption) return;
|
||||
const x = Number(rect.getAttribute("x"));
|
||||
const y = Number(rect.getAttribute("y"));
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
caption.setAttribute(
|
||||
"transform",
|
||||
`translate(${x + 18}, ${y + (config?.flowchart?.subGraphTitleMargin?.top ?? 12)})`
|
||||
);
|
||||
// Mermaid paints crossing edges after clusters; keep captions above those edges.
|
||||
const clusters = cluster.parentElement;
|
||||
if (clusters?.classList.contains("clusters")) clusters.parentElement?.append(caption);
|
||||
});
|
||||
// Streamdown embeds this as HTML, just like Mermaid's original output.
|
||||
return { ...result, svg: svg.outerHTML };
|
||||
} catch {
|
||||
// Caption placement is cosmetic; never discard a usable original graph.
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -84,26 +109,29 @@ export const investigationDiagramPlugins = {
|
||||
};
|
||||
|
||||
const roleStyles = [
|
||||
"classDef tjAgent fill:#EEF3FF,stroke:#7A93CB,color:#284574,stroke-width:1.4px;",
|
||||
"classDef tjAgent fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.4px;",
|
||||
"classDef tjHuman fill:#FFF6E8,stroke:#C4A168,color:#735018,stroke-width:1.4px;",
|
||||
"classDef tjDecision fill:#4263C7,stroke:#4263C7,color:#FFFFFF,stroke-width:1.5px;",
|
||||
"classDef tjDecision fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.5px;",
|
||||
"classDef tjEvidence fill:#EDF7F4,stroke:#6B9E8B,color:#2D5E4E,stroke-width:1.6px;",
|
||||
"classDef tjAction fill:#FFF6E8,stroke:#C4A168,color:#735018,stroke-width:1.6px;"
|
||||
];
|
||||
|
||||
/** Style supplied relationships; never create nodes from a dangling display reference. */
|
||||
export function styleInvestigationDiagram(diagram: InvestigationDiagram) {
|
||||
export function styleInvestigationDiagram(diagram: InvestigationDiagram, running = false) {
|
||||
const assignments = diagram.node_refs.flatMap((node) => {
|
||||
const escaped = node.node_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const inSource = new RegExp(`\\b${escaped}(?=\\s*(?:[\\[({@;]|--|==|-\\.|$))`, "m").test(
|
||||
diagram.mermaid
|
||||
);
|
||||
if (!inSource) return [];
|
||||
if (running && node.status === "running") return [`class ${node.node_id} tjRunning;`];
|
||||
return node.action_refs.length
|
||||
? [`class ${node.node_id} tjAction;`]
|
||||
: node.evidence_refs.length
|
||||
? [`class ${node.node_id} tjEvidence;`]
|
||||
: [];
|
||||
});
|
||||
return [diagram.mermaid, ...roleStyles, ...assignments].join("\n");
|
||||
return [diagram.mermaid, ...roleStyles,
|
||||
running ? "classDef tjRunning fill:#E8F0FF,stroke:#4263C7,color:#284574,stroke-width:2.5px;" : "classDef tjRunning fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.4px;",
|
||||
...assignments].join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user