feat: show RSS headlines as clickable links in the transcript

read_news bisher zeigte nur Klartext-Titel in der Karte an — man
konnte im Chat selbst nicht auf den Artikel klicken (nur das Modell
kannte den Link, für ein mögliches read_article danach). Jetzt trägt
der tool_result-Event zusätzlich strukturierte 'items' (source, title,
link), das Frontend rendert daraus eine Liste mit echten <a>-Links
statt reinem Text. read_article bleibt unverändert bei reinem Text
(items bleibt leer, kein Link zum Verlinken vorhanden).

Verifiziert: die Rendering-Logik direkt per simuliertem Datenkanal-
Event getestet (unabhängig von der Modell-Zuverlässigkeit, die bei
read_news aktuell bei ~15-20% liegt) — zwei Links korrekt mit href,
target=_blank, rel=noopener und Quelle gerendert.

NICHT gepusht — weiterhin nur lokal, wie angewiesen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
This commit is contained in:
Jeuner 2026-09-07 18:36:09 +02:00
parent 0d2e826dd3
commit b0282900d2
4 changed files with 46 additions and 9 deletions

View file

@ -62,7 +62,7 @@ function addToolError(text) {
}
showError(text);
}
function addToolResult(text) {
function addToolResult(text, items) {
const pending = document.getElementById("pending-tool");
const article = pending || document.createElement("article");
if (pending) {
@ -76,10 +76,36 @@ function addToolResult(text) {
const speaker = document.createElement("span");
speaker.className = "speaker";
speaker.textContent = "Astra";
const body = document.createElement("p");
body.className = "tool-result";
body.textContent = text;
article.append(speaker, body);
article.append(speaker);
if (items?.length) {
const list = document.createElement("ul");
list.className = "tool-result-list";
for (const item of items) {
const li = document.createElement("li");
if (item.link) {
const link = document.createElement("a");
link.href = item.link;
link.target = "_blank";
link.rel = "noopener";
link.textContent = item.title;
li.append(link);
} else {
li.textContent = item.title;
}
if (item.source) {
const source = document.createElement("small");
source.textContent = ` (${item.source})`;
li.append(source);
}
list.append(li);
}
article.append(list);
} else {
const body = document.createElement("p");
body.className = "tool-result";
body.textContent = text;
article.append(body);
}
if (!pending) $("messages").append(article);
$("messages").scrollTop = $("messages").scrollHeight;
}
@ -148,7 +174,7 @@ function receive(event) {
if (message.type === "activity" && !muted) $("status").textContent = message.text;
if (message.type === "tool_start") addToolStart(message.text);
if (message.type === "tool_error") addToolError(message.text);
if (message.type === "tool_result") addToolResult(message.text);
if (message.type === "tool_result") addToolResult(message.text, message.items);
if (message.type === "partial") $("partial").textContent = message.text;
if (message.type === "transcript") addMessage(message);
if (message.type === "image") addImage(message.url);