feat(mcp): delete tasks, create task lists and lists, agent badge; export/import settings category
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m21s

New MCP tools delete_task (tasks:write), create_task_list (tasks:write) and create_list
(lists:write); list_task_lists now returns the board columns so agents know the valid
kanbanStatus values. Task deletion is a shared helper (deleteTask) for REST and MCP.

Tasks and list items get agent_mark (migration 12): "created" when an agent adds them via MCP,
"edited" when an agent changes them; a change by the user clears it. The new AgentBadge shows
"KI" / "KI geändert" in task rows, Kanban cards, the task detail, the inbox and lists.

Settings: export and import move out of "Webhooks, KI-Agenten & Export" into their own
category "Export & Import". Docs, help, README and changelog updated; v0.29.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 08:49:10 +01:00
1 parent 8faed7a160
commit ac068248d7
28 files changed
+441 -65

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mindsift-frontend",
"version": "0.28.0",
"version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindsift-frontend",
"version": "0.28.0",
"version": "0.29.0",
"license": "Unlicense",
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.28.0",
"version": "0.29.0",
"license": "Unlicense",
"type": "module",
"scripts": {
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import { Bot } from 'lucide-react';
import { AgentMark } from '../lib/types';
interface AgentBadgeProps {
mark: AgentMark | undefined;
// sm: task rows and the task detail; xs: Kanban cards and list items
size?: 'sm' | 'xs';
}
const TITLES: Record<AgentMark, string> = {
created: 'Von einem KI-Agenten per MCP angelegt. Das Zeichen verschwindet, sobald du den Eintrag selbst änderst.',
edited: 'Zuletzt von einem KI-Agenten per MCP geändert. Das Zeichen verschwindet, sobald du den Eintrag selbst änderst.',
};
// Marks entries an AI agent created or changed via MCP that the user has not touched since
// (Doku/ui-patterns.md)
export const AgentBadge: React.FC<AgentBadgeProps> = ({ mark, size = 'sm' }) => {
if (mark !== 'created' && mark !== 'edited') return null;
const label = mark === 'created' ? 'KI' : 'KI geändert';
return (
<span
className={`px-1.5 border border-accent/40 bg-accent/10 text-accent rounded-btn font-medium inline-flex items-center gap-1 flex-shrink-0 ${
size === 'xs' ? 'py-0.2 text-[9px]' : 'py-0.5 text-[10px]'
}`}
title={TITLES[mark]}
aria-label={TITLES[mark]}
>
<Bot className="w-2.5 h-2.5" />
<span>{label}</span>
</span>
);
};
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { AgentBadge } from '../AgentBadge';
describe('AgentBadge', () => {
it('shows nothing for entries of the user', () => {
expect(render(<AgentBadge mark={undefined} />).container.firstChild).toBeNull();
});
it('tells created and edited entries apart', () => {
const created = render(<AgentBadge mark="created" />).container.firstChild as HTMLElement;
expect(created.textContent).toBe('KI');
expect(created.getAttribute('title')).toContain('angelegt');
const edited = render(<AgentBadge mark="edited" />).container.firstChild as HTMLElement;
expect(edited.textContent).toBe('KI geändert');
expect(edited.getAttribute('title')).toContain('geändert');
});
});
+3 -3
View File
@@ -513,10 +513,10 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
content: (
<div className="space-y-2 text-muted leading-relaxed">
<p>
Unter <em className="text-main">Kontoeinstellungen → Webhooks, KI-Agenten &amp; Export</em> kannst du persönliche API-Schlüssel erzeugen. Pro Schlüssel legst du mit Schaltern fest, was er darf: Gedanken in die Inbox, Aufgaben oder Listeneinträge einliefern (Webhooks) oder für KI-Agenten Aufgaben und Listen lesen und ändern. Notizen erreicht ein Schlüssel nie.
Unter <em className="text-main">Kontoeinstellungen → Webhooks &amp; KI-Agenten</em> kannst du persönliche API-Schlüssel erzeugen. Pro Schlüssel legst du mit Schaltern fest, was er darf: Gedanken in die Inbox, Aufgaben oder Listeneinträge einliefern (Webhooks) oder für KI-Agenten Aufgaben und Listen lesen und ändern. Notizen erreicht ein Schlüssel nie.
</p>
<p>
<strong className="text-main">KI-Agenten:</strong> MindSift ist ein MCP-Server (<code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">POST /api/v1/mcp</code>). Claude Code, OpenAI Codex, Claude Desktop, Cursor und eigene Agenten können damit offene Aufgaben auslesen, abhaken und anlegen sowie Einkaufs- und Checklisten führen. Die Einstellungen zeigen den fertigen Befehl bzw. die Konfiguration für den jeweiligen Agenten; der Agent sieht nur die Werkzeuge, für die der Schlüssel freigegeben ist.
<strong className="text-main">KI-Agenten:</strong> MindSift ist ein MCP-Server (<code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">POST /api/v1/mcp</code>). Claude Code, OpenAI Codex, Claude Desktop, Cursor und eigene Agenten können damit offene Aufgaben auslesen, anlegen, abhaken und löschen, neue Aufgabenlisten und Listen anlegen sowie Einkaufs- und Checklisten führen. Die Einstellungen zeigen den fertigen Befehl bzw. die Konfiguration für den jeweiligen Agenten; der Agent sieht nur die Werkzeuge, für die der Schlüssel freigegeben ist. Was ein Agent angelegt oder geändert hat, trägt das Zeichen <strong className="text-main">KI</strong> bzw. <strong className="text-main">KI geändert</strong>, bis du den Eintrag selbst änderst.
</p>
<p>
Mit dem Feld <code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">"target"</code> wählst du das Ziel: <code className="font-mono">triage</code> (Standard), <code className="font-mono">task</code> (optional mit <code className="font-mono">taskListId</code>) oder <code className="font-mono">list_item</code> (mit <code className="font-mono">listId</code>). Der Simulator in den Einstellungen zeigt passende Beispiele.
@@ -664,7 +664,7 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
<strong className="text-main">Eigenes Theme:</strong> Vorlage herunterladen oder einfügen, Farben und Rundungen anpassen, als <code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">.css</code>-Datei hochladen oder einfügen und „Speichern &amp; aktivieren“. Das Theme greift sofort und steht dir auf allen Geräten zur Verfügung.
</li>
<li>
<strong className="text-main">Server-Backups:</strong> Hat die Administration automatische Backups eingeschaltet, findest du unter <em>Kontoeinstellungen → Webhooks, KI-Agenten &amp; Export</em> die täglichen Sicherungen deiner Daten. Du kannst sie herunterladen oder wieder einspielen, auch nur für einzelne Bereiche wie Listen oder Aufgaben.
<strong className="text-main">Server-Backups:</strong> Hat die Administration automatische Backups eingeschaltet, findest du unter <em>Kontoeinstellungen → Export &amp; Import</em> die täglichen Sicherungen deiner Daten. Du kannst sie herunterladen oder wieder einspielen, auch nur für einzelne Bereiche wie Listen oder Aufgaben.
</li>
<li>
<strong className="text-main">Ohne Internet:</strong> Als App installiert (oder im Browser) startet MindSift auch offline mit dem zuletzt geladenen Stand. Gedanken erfassen sowie Aufgaben und Listeneinträge abhaken geht weiter; ein Hinweis unten zeigt, wie viele Änderungen warten, und sie werden automatisch übertragen, sobald die Verbindung zurück ist.
@@ -16,6 +16,7 @@ import { OFFLINE_ITEM_PREFIX, pendingListItems } from '../../lib/offline';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge';
import { AgentBadge } from '../../components/AgentBadge';
import { FormSheet } from '../../components/FormSheet';
import { useMobileActions } from '../../context/MobileActionContext';
@@ -1390,6 +1391,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
)}
</div>
<AgentBadge mark={item.agentMark} size="xs" />
{renderItemAuthor(item)}
<button
type="button"
@@ -1499,6 +1501,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</div>
<div className="flex items-center gap-2">
<AgentBadge mark={item.agentMark} size="xs" />
{renderItemAuthor(item)}
{item.completedByName && (
<span className="text-xs text-faint">✓ {item.completedByName}</span>
@@ -35,7 +35,7 @@ type SimTarget = 'triage' | 'task' | 'list_item';
type McpClient = 'claude' | 'codex' | 'json';
// Personal API keys with scopes (switches), MCP connection snippets, a webhook simulator and copyable
// examples (settings → Webhooks, KI-Agenten & Export)
// examples (settings → Webhooks & KI-Agenten)
export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
const [keys, setKeys] = useState<APIKey[]>([]);
const [newKeyName, setNewKeyName] = useState('');
@@ -17,6 +17,7 @@ import {
Upload,
Pencil,
Copy,
Archive,
} from 'lucide-react';
import { AuthStatus, NotificationEvents, NotificationSettings, PushDevice, User, UserTheme } from '../../lib/types';
import { ApiKeysPanel } from './ApiKeysPanel';
@@ -65,7 +66,7 @@ interface UserSettingsViewProps {
onBack: () => void;
}
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'webhooks';
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'webhooks' | 'data';
export function buildNotificationSettings(user: User): NotificationSettings {
const hasLegacy = Array.isArray(user.alarmChannels);
@@ -582,7 +583,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
<div>
<h1 className="text-base font-bold text-main tracking-tight">Kontoeinstellungen</h1>
<p className="text-xs text-muted">
Persönliches Profil, Design-Themes, Aufgaben-Alarme, Webhooks, KI-Agenten & Daten-Backup
Persönliches Profil, Design-Themes, Aufgaben-Alarme, Webhooks, KI-Agenten, Export & Import
</p>
</div>
</div>
@@ -651,8 +652,23 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
>
<Key className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Webhooks, KI-Agenten & Export</p>
<p className="text-[10px] text-faint">API-Keys, MCP, Curl & JSON-Backup</p>
<p className="font-semibold">Webhooks & KI-Agenten</p>
<p className="text-[10px] text-faint">API-Keys, MCP & Curl</p>
</div>
</button>
<button
onClick={() => setSection('data')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'data'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-panel-hover'
}`}
>
<Archive className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Export & Import</p>
<p className="text-[10px] text-faint">Backups, Obsidian & Wiederherstellen</p>
</div>
</button>
</aside>
@@ -1849,14 +1865,12 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
)}
{/* ================================================================= */}
{/* SECTION 4: PRIVATE WEBHOOKS & EXPORT */}
{/* SECTION 4: PRIVATE WEBHOOKS & AI AGENTS */}
{/* ================================================================= */}
{section === 'webhooks' && (
<div className="space-y-5">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">
Webhooks, KI-Agenten & Daten-Export
</h2>
<h2 className="text-sm font-bold text-main">Webhooks & KI-Agenten</h2>
<p className="text-muted mt-0.5">
Erstelle persönliche API-Schlüssel, um aus Skripten, Smarthome oder Bots Aufgaben und Gedanken
einzuspeisen oder KI-Agenten wie Claude Code und Codex an deine Aufgaben und Listen zu lassen.
@@ -1864,11 +1878,24 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div>
<ApiKeysPanel />
</div>
)}
<div className="pt-4 border-t border-border">
<ServerBackupsCard />
{/* ================================================================= */}
{/* SECTION 5: EXPORT & IMPORT */}
{/* ================================================================= */}
{section === 'data' && (
<div className="space-y-5">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">Export & Import</h2>
<p className="text-muted mt-0.5">
Lade deine Daten als Backup oder Markdown-Archiv herunter, spiele Server-Backups zurück oder importiere
MindSift-Daten und Obsidian-Vaults.
</p>
</div>
<ServerBackupsCard />
{/* Full Data Backup */}
<div className="pt-4 border-t border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
@@ -20,6 +20,7 @@ import {
} from 'lucide-react';
import { Thought, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types';
import { UserAvatar } from '../../components/UserAvatar';
import { AgentBadge } from '../../components/AgentBadge';
import { UserAutocomplete } from '../UserAutocomplete';
import { api, triggerHaptic } from '../../lib/api';
import { MarkdownViewer } from '../../lib/markdown';
@@ -244,6 +245,7 @@ const TaskDetailModalContent: React.FC<TaskDetailModalProps & { task: Thought }>
<span>Webhook</span>
</span>
)}
<AgentBadge mark={task.agentMark} />
</div>
<div className="flex items-center gap-1.5">
+4 -1
View File
@@ -35,6 +35,7 @@ import { useKanbanTouchDrag } from './useKanbanTouchDrag';
import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { TaskDetailModal } from './TaskDetailModal';
import { AgentBadge } from '../../components/AgentBadge';
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge';
@@ -287,7 +288,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
}
} else if (msg.event === 'TASK_REORDERED') {
loadData();
} else if (msg.event === 'TASK_LIST_UPDATED' || msg.event === 'TASK_LIST_DELETED') {
} else if (msg.event === 'TASK_LIST_CREATED' || msg.event === 'TASK_LIST_UPDATED' || msg.event === 'TASK_LIST_DELETED') {
// Payload only carries the list ID; reload so permissions reflect this user's view
api.getTaskLists().then((lists) => setTaskLists(Array.isArray(lists) ? lists : [])).catch(() => {});
}
@@ -1352,6 +1353,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<span>Webhook</span>
</span>
)}
<AgentBadge mark={task.agentMark} />
{/* Note indicator */}
{task.notes && (
@@ -2435,6 +2437,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<span>Webhook</span>
</span>
)}
<AgentBadge mark={task.agentMark} size="xs" />
{task.dueDate && (
<span
className={`inline-flex items-center gap-0.5 text-[9px] ${!task.isCompleted && isOverdue(task.dueDate) ? 'text-status-err font-semibold' : 'text-muted'}`}
@@ -25,6 +25,7 @@ import {
import { SharedList, Thought } from '../../lib/types';
import { addDays, formatDue } from '../../lib/due';
import { api, triggerHaptic } from '../../lib/api';
import { AgentBadge } from '../../components/AgentBadge';
interface TriageDeckProps {
deck: Thought[];
@@ -158,6 +159,7 @@ const SwipeableQueueItem: React.FC<SwipeableQueueItemProps> = ({
<Mic className="w-2.5 h-2.5" /> Diktat
</span>
)}
<AgentBadge mark={item.agentMark} />
</div>
{/* Desktop quick buttons (hidden on mobile so queue items are clean & easy to tap/swipe) */}
@@ -521,6 +523,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
<div className="flex items-center gap-1.5">
{priorityBadge(nextCard.priority)}
{sourceBadge(nextCard.source)}
<AgentBadge mark={nextCard.agentMark} />
</div>
<p className="text-sm text-muted line-clamp-2">{nextCard.content}</p>
<div className="text-[11px] text-faint">Nächster Quick Dump…</div>
@@ -590,6 +593,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
<div className="flex items-center gap-1.5">
{priorityBadge(activeCard.priority)}
{sourceBadge(activeCard.source)}
<AgentBadge mark={activeCard.agentMark} />
</div>
<div
className="flex items-center gap-2"
+6 -1
View File
@@ -148,13 +148,16 @@ export const RECURRENCE_LABELS: Record<Exclude<Recurrence, ''>, string> = {
yearly: 'jährlich',
};
// Set while the last change came from an AI agent via MCP and the user has not touched the entry since
export type AgentMark = 'created' | 'edited';
export interface Thought {
id: string;
userId: string;
content: string;
tags: string[];
priority: PriorityLevel;
source: 'manual' | 'voice' | 'webhook';
source: 'manual' | 'voice' | 'webhook' | 'agent';
status: ThoughtStatus;
isCompleted: boolean;
convertedType?: 'task' | 'document' | 'list_item' | 'document_block';
@@ -173,6 +176,7 @@ export interface Thought {
sortOrder?: number;
createdAt: string;
triagedAt?: string;
agentMark?: AgentMark;
}
export interface KanbanColumn {
@@ -538,6 +542,7 @@ export interface ListItem {
sortOrder: number;
createdAt: string;
completedAt?: string;
agentMark?: AgentMark;
}
export interface ListShare {