diff --git a/TODO.md b/TODO.md
index 24ce4a0..85ccf2f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,7 +13,7 @@
- [x] Refactoring: Agent/Session-Einstellungen, die den System-Prompt bzw. das Tool Use bearbeiten
- [ ] Cronjobs: Planung, ob cron von Debian/Linux verwendet werden soll, oder ein eigener Worker/Service in der App selbst, der das übernimmt
- [x] Weiteres Modell hinzufügen: "Haiku 4.5"
-- [ ] Weitere Tools: Glob? Search? Todo List?
+- [x] Weitere Tools: Glob? Search? Todo List?
- [ ] Settings in Datenbank speichern (users table) anstelle von localStorage
- [ ] Einzelne Nachicht als Link teilen
@@ -27,7 +27,7 @@
## Frontend
- [x] Chat: Es scrollt beim Laden nicht ganz nach unten, ich denke weil im DOM noch etwas nachgeladen wird (ich denke es ist die Schriftart), und die Höhe sich beim Laden nochmal verändert
-- [x] Chat: User-Nachrichten mit dem Tag [Datei: pfad/zur/datei.xyz:1-10] wird nicht als HTML geparsed
+- [x] Chat: User-Nachrichten mit dem Tag `[Datei: pfad/zur/datei.xyz:1-10]` wird nicht als HTML geparsed
- [x] Chat: Wird zwar erkannt und ist klickbar, aber es öffnet sich einfach nur der File Explorer, ohne dass sich die Datei öffnet und die Darstellung in Monospace ist auch nicht schön
- [x] Chat: Es werden momentan alle Tool-Calls angezeigt - die Anzeige soll individuell in den Einstellungen gemacht werden können. Auswahl aus drei Stufen: "Alle anzeigen" / "Nur Modifikationen" (file änderungen, memory änderungen, etc.) / "Verstecken"
- [x] Browser: "Zurück"-Taste soll auf den vorherigen location.hash wechseln
@@ -35,6 +35,7 @@
- [ ] Chat: Datum und Uhrzeit bei den Nachrichten anzeigen (Format: dd.MM.yyyy HH:mm:ss)
- [x] Dashboard: Menu-Dropdown bei Cards sind abgeschnitten wenn die Card-Höhe nicht ausreicht (logisch, weil das Menu-Dropdown ein Child von der Card ist)
- [ ] Auto-scrolling deaktivieren, wenn man weiterhoch gescrollt hat, und er wieder fortsetzen wenn man ganz nach unten gescrollt hat
+- [ ] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen
## Ideas / Backlog
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css
index d66a812..33a402e 100644
--- a/wwwroot/css/chat.css
+++ b/wwwroot/css/chat.css
@@ -141,7 +141,7 @@
padding: 0;
}
-.tool-box:has([hidden]) {
+.tool-box:has([hidden]):not(:has(.todo-list)) {
opacity: var(--semi-transparent);
}
@@ -206,6 +206,77 @@
overflow: auto;
}
+.tool-result-markdown {
+ white-space: normal;
+}
+
+.tool-result-markdown p,
+.tool-result-markdown ul,
+.tool-result-markdown ol {
+ margin: 0 0 .4em 0;
+}
+
+.tool-result-markdown p:last-child,
+.tool-result-markdown ul:last-child,
+.tool-result-markdown ol:last-child {
+ margin-bottom: 0;
+}
+
+.tool-result-markdown li {
+ margin-left: 1.2em;
+}
+
+/* Todo-Liste */
+
+.todo-list {
+ padding: var(--box-padding);
+ padding-top: 0;
+ display: flex;
+ flex-direction: column;
+ font-size: .85rem;
+ gap: var(--badge-spacing);
+}
+
+.todo-item {
+ display: flex;
+ align-items: center;
+ gap: var(--badge-spacing);
+}
+
+.todo-icon {
+ flex-shrink: 0;
+}
+
+.todo-id {
+ flex-shrink: 0;
+}
+
+.todo-prio {
+ flex-shrink: 0;
+ border-radius: var(--border-radius);
+ border: var(--border-width) solid currentcolor;
+ font-size: var(--font-size);
+ padding: var(--badge-padding);
+}
+
+.todo-prio-high { color: var(--dangerous); }
+.todo-prio-medium { color: var(--warning); }
+.todo-prio-low { opacity: var(--semi-transparent); }
+
+.todo-text {
+ flex: 1;
+ word-break: break-word;
+}
+
+.todo-done .todo-text {
+ text-decoration: line-through;
+ opacity: var(--semi-transparent);
+}
+
+.todo-progress .todo-text {
+ font-style: italic;
+}
+
.tool-write-meta {
opacity: var(--semi-transparent);
margin-left: auto;
diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js
index 3ead21a..4b69380 100644
--- a/wwwroot/js/ui.js
+++ b/wwwroot/js/ui.js
@@ -422,6 +422,7 @@ function isChainMetaVisible(chainData) {
function addToolBoxLoading(name) {
const div = document.createElement('div');
div.className = 'tool-box streamed';
+ div.dataset.toolName = name;
const displayName = TOOL_DISPLAY_NAMES[name] || name;
div.innerHTML = '' + esc(displayName) + ' ';
chatEl().appendChild(div);
@@ -485,6 +486,14 @@ function renderToolBoxContent(div, name, inputJson, result, isError) {
div.appendChild(preview);
foldTarget = preview;
+ } else if (name === 'todo_write') {
+ // Todo-Liste: kein tool-input, standardmäßig sichtbar, custom parser
+ if (result) {
+ const todoList = buildTodoList(result, isError);
+ div.appendChild(todoList);
+ foldTarget = todoList;
+ }
+
} else {
// Standard: path/command als input anzeigen
let inputLabel = '';
@@ -508,7 +517,7 @@ function renderToolBoxContent(div, name, inputJson, result, isError) {
foldTarget = resultDiv;
}
- if (foldTarget) {
+ if (foldTarget && name !== 'todo_write') {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
foldTarget.hidden = !foldTarget.hidden;
@@ -579,6 +588,63 @@ function buildWritePreview(content) {
return container;
}
+function buildTodoList(result, isError) {
+ const container = document.createElement('div');
+ container.className = 'todo-list' + (isError ? ' tool-error' : '');
+
+ const lines = result.split('\n').filter(l => l.trim());
+ for (const line of lines) {
+ // Format: ICON [id] [priority] content
+ const m = line.match(/^(\S+)\s+\[([^\]]+)\]\s+(?:\[([^\]]+)\]\s+)?(.+)$/);
+ const row = document.createElement('div');
+ row.className = 'todo-item';
+
+ if (m) {
+ const icon = m[1]; // ✅ / 🔄 / ⬜
+ const id = m[2];
+ const prio = m[3] || null;
+ const text = m[4];
+
+ const statusClass =
+ icon === '✅' ? 'todo-done' :
+ icon === '🔄' ? 'todo-progress' :
+ 'todo-pending';
+ row.classList.add(statusClass);
+
+ const iconSpan = document.createElement('span');
+ iconSpan.className = 'todo-icon';
+ iconSpan.textContent = icon;
+
+ const idSpan = document.createElement('span');
+ idSpan.className = 'todo-id';
+ idSpan.textContent = id + '.';
+
+ const textSpan = document.createElement('span');
+ textSpan.className = 'todo-text';
+ textSpan.textContent = text;
+
+ row.appendChild(iconSpan);
+ row.appendChild(idSpan);
+ row.appendChild(textSpan);
+
+ if (prio) {
+ const prioSpan = document.createElement('span');
+ prioSpan.className = 'todo-prio todo-prio-' + prio.toLowerCase();
+ prioSpan.textContent = prio;
+ row.appendChild(prioSpan);
+ }
+ } else {
+ row.textContent = line;
+ }
+
+ container.appendChild(row);
+ }
+
+ parseFluentEmojis(container);
+
+ return container;
+}
+
function finishToolBox(div, data) {
if (!div) return;
renderToolBoxContent(div, data.name, data.input, data.result, data.isError);
@@ -588,6 +654,7 @@ function finishToolBox(div, data) {
function addToolBoxStatic(name, input, result, isError) {
const div = document.createElement('div');
div.className = 'tool-box';
+ div.dataset.toolName = name;
renderToolBoxContent(div, name, input, result, isError);
chatEl().appendChild(div);
scrollDown();
@@ -602,6 +669,20 @@ function appendToolResult(result, isError) {
// Wenn Diff vorhanden (edit_file/write_file) → kein Result anhängen
if (lastBox.querySelector('.tool-diff')) return;
+ const toolName = lastBox.dataset.toolName || '';
+ const isTodoWrite = toolName === 'todo_write';
+
+ if (isTodoWrite) {
+ // Vorhandene todo-list ersetzen oder neu erstellen
+ const existing = lastBox.querySelector('.todo-list');
+ if (existing) existing.remove();
+ if (result && !isError) {
+ const todoList = buildTodoList(result, isError);
+ lastBox.appendChild(todoList);
+ }
+ return;
+ }
+
const existing = lastBox.querySelector('.tool-result');
if (existing) {
existing.textContent = result || '';