Frontend: TaskCreate/TaskUpdate als aggregierte Checkliste rendern

Die CLI nutzt inzwischen TaskCreate/TaskUpdate statt TodoWrite fuers
Todo-Tracking. Diese fielen in den generischen Tool-Box-Pfad und wurden
als rohe Tool-Calls mit JSON-Input angezeigt statt als Checkliste.

Da die neuen Tools inkrementell sind (ein Call pro Anlage/Statuswechsel),
werden die Einzel-Events zu EINER Checkliste aggregiert:

- tool-ui.js: Task-Tracker (applyTaskEvent/ingestTaskEvent), Box-Aufbau
  mit wiederverwendeten todo-list-Styles + Fortschritts-Badge n/m;
  sealTaskBox/sealLiveTaskList zum Segmentieren
- chat.js: Live-Pfad (SSE) haengt Task-Events in die aggregierte Box
  statt generischer Tool-Boxen; Reset beim Stream-Start
- history.js: Replay-Pfad mit eigenem Tracker pro Chain; zugehoerige
  tool_result-Messages werden uebersprungen (kein Doppel-Render)

Segmentierung: ein zusammenhaengender Task-Lauf bleibt EINE Box. Sobald
dazwischen echter Inhalt gerendert wird (Fliesstext oder anderes Tool),
wird die Box versiegelt und das naechste Task-Event oeffnet eine neue Box
darunter - so bleibt die chronologische Reihenfolge erhalten und
Narration zwischen den Tasks sichtbar. Der Task-Stand bleibt kumulativ,
der Fortschritt zaehlt ueber die Segmente hinweg hoch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 5 committed 2026-07-27 07:27:31 +02:00
1 parent be44235ca9
commit f38f3a42d1
3 files changed
+205 -1

No files matched your search

+157
View File
@@ -377,6 +377,163 @@ function buildTodoListFromInput(todos, isError) {
return container;
}
// ── Task-Liste (CLI Task*-Tools) ──
// Die CLI nutzt inzwischen TaskCreate/TaskUpdate (statt des früheren TodoWrite) für das
// Todo-Tracking. Diese Tools sind inkrementell (ein Call pro Task-Anlage/Statuswechsel).
// Damit die frühere Checklisten-Visualisierung erhalten bleibt, aggregieren wir sie zu
// EINER Box, die in-place aktualisiert wird – statt viele generische Tool-Boxen zu zeigen.
/** True für die Task-Tracking-Tools, die zur aggregierten Checkliste gehören. */
function isTaskListTool(rawName) {
return rawName === 'TaskCreate' || rawName === 'TaskUpdate';
}
/** Neuer, leerer Task-Tracker (pro Chain bzw. pro Live-Stream). */
function makeTaskTracker() {
return { tasks: [], byId: {}, box: null };
}
/**
* Wendet ein TaskCreate/TaskUpdate-Event auf den Tracker an.
* - TaskCreate: neuer Task; ID sequentiell (die CLI vergibt #1..#N in Erstell-Reihenfolge),
* ein evtl. vorhandenes Result "Task #N ..." bestätigt/überschreibt die ID.
* - TaskUpdate: setzt den Status des per taskId referenzierten Tasks.
*/
function applyTaskEvent(tracker, rawName, inputJson, resultText) {
var input = null;
try { input = JSON.parse(inputJson); } catch (e) {}
if (!input || typeof input !== 'object') input = {};
if (rawName === 'TaskCreate') {
var id = tracker.tasks.length + 1;
var m = resultText && /Task #(\d+)/.exec(resultText);
if (m) id = parseInt(m[1], 10);
var task = {
id: String(id),
subject: input.subject || input.description || ('Aufgabe ' + id),
status: 'pending'
};
if (!tracker.byId[task.id]) tracker.tasks.push(task);
tracker.byId[task.id] = task;
} else if (rawName === 'TaskUpdate') {
var tid = String(input.taskId != null ? input.taskId : (input.id != null ? input.id : ''));
if (!tid) return tracker;
var existing = tracker.byId[tid];
if (existing) {
if (input.status) existing.status = input.status;
} else {
// Update ohne vorheriges Create (z.B. paginierter History-Ausschnitt) → Platzhalter
var placeholder = { id: tid, subject: 'Aufgabe ' + tid, status: input.status || 'pending' };
tracker.byId[tid] = placeholder;
tracker.tasks.push(placeholder);
}
}
return tracker;
}
/** Rendert die Task-Items (Checkboxen) in den Listen-Container (in-place, leert vorher). */
function renderTaskListItems(list, tracker) {
list.innerHTML = '';
var done = 0;
for (var i = 0; i < tracker.tasks.length; i++) {
var task = tracker.tasks[i];
var status = task.status || 'pending';
if (status === 'completed') done++;
var icon = status === 'completed' ? '✅' : status === 'in_progress' ? '🔄' : '⬜';
var statusClass = status === 'completed' ? 'todo-done' : status === 'in_progress' ? 'todo-progress' : 'todo-pending';
var row = document.createElement('div');
row.className = 'todo-item ' + statusClass;
var iconSpan = document.createElement('span');
iconSpan.className = 'todo-icon';
iconSpan.textContent = icon;
var idSpan = document.createElement('span');
idSpan.className = 'todo-id';
idSpan.textContent = task.id + '.';
var textSpan = document.createElement('span');
textSpan.className = 'todo-text';
textSpan.textContent = task.subject;
row.appendChild(iconSpan);
row.appendChild(idSpan);
row.appendChild(textSpan);
list.appendChild(row);
}
// Fortschritts-Badge im Header aktualisieren
var box = list.parentElement;
var badge = box && box.querySelector('.task-progress');
if (badge) badge.textContent = done + '/' + tracker.tasks.length;
parseFluentEmojis(list);
}
/** Baut die komplette Task-Listen-Box (Header + Checkliste) für den aktuellen Tracker-Stand. */
function buildTaskListBox(tracker) {
var box = document.createElement('div');
box.className = 'tool-box task-list-box';
var header = document.createElement('div');
header.className = 'tool-box-header';
var nameSpan = document.createElement('span');
nameSpan.className = 'tool-name';
nameSpan.textContent = 'Aufgabenliste';
header.appendChild(nameSpan);
var progress = document.createElement('span');
progress.className = 'tool-write-meta task-progress';
header.appendChild(progress);
box.appendChild(header);
var list = document.createElement('div');
list.className = 'todo-list task-list';
box.appendChild(list);
renderTaskListItems(list, tracker);
return box;
}
/**
* Verarbeitet ein Task-Event für einen Tracker und stellt die (einzige) Box sicher:
* beim ersten Event wird sie erzeugt und via appendBox() eingehängt, danach in-place aktualisiert.
*/
function ingestTaskEvent(tracker, rawName, inputJson, resultText, appendBox) {
applyTaskEvent(tracker, rawName, inputJson, resultText);
if (!tracker.box) {
tracker.box = buildTaskListBox(tracker);
appendBox(tracker.box);
} else {
renderTaskListItems(tracker.box.querySelector('.task-list'), tracker);
}
return tracker.box;
}
/**
* "Versiegelt" die aktuelle Task-Box: der Tracker behält seinen Task-Stand (kumulativ),
* aber die nächste Task-Event erzeugt eine NEUE Box statt die alte in-place zu aktualisieren.
* Wird aufgerufen, sobald zwischen zwei Task-Events echter Inhalt (Fließtext, anderes Tool)
* gerendert wurde → so bleibt die chronologische Reihenfolge (Text zwischen den Tasks sichtbar).
* Ein zusammenhängender Task-Lauf ohne Zwischentext bleibt weiterhin EINE Box.
*/
function sealTaskBox(tracker) {
if (tracker) tracker.box = null;
}
// Live-Stream-Tracker (ein aktiver Chain zur Zeit; Reset beim Stream-Start).
var _liveTaskTracker = null;
function resetLiveTaskList() { _liveTaskTracker = null; }
/** Versiegelt die Live-Task-Box (nächstes Task-Event → neue Box). No-op wenn keine offen. */
function sealLiveTaskList() { if (_liveTaskTracker) _liveTaskTracker.box = null; }
/** Live-Handler: nimmt ein TaskCreate/TaskUpdate aus dem SSE-Stream entgegen. */
function handleLiveTaskEvent(rawName, inputJson, resultText) {
if (!_liveTaskTracker) _liveTaskTracker = makeTaskTracker();
ingestTaskEvent(_liveTaskTracker, rawName, inputJson, resultText, function(box) {
chatEl().appendChild(box);
});
scrollDown();
}
function finishToolBox(div, data) {
if (!div) return;
renderToolBoxContent(div, data.name, data.input, data.result, data.isError, data.sources);