{{ eventCount }} found
@@ -341,6 +369,17 @@ async function applyTag(tag: string) {
text-decoration: line-through;
opacity: 0.7;
}
+.tool-task-priority {
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0.1rem 0.3rem;
+ border-radius: 3px;
+}
+.priority-high { color: var(--color-danger, #e74c3c); }
+.priority-medium { color: var(--color-warning, #f59e0b); }
+.priority-low { color: var(--color-text-muted); }
.tool-completed {
text-decoration: line-through;
color: var(--color-success, #2ecc71);
diff --git a/src/fabledassistant/services/caldav.py b/src/fabledassistant/services/caldav.py
index 3afb75b..ce8ac80 100644
--- a/src/fabledassistant/services/caldav.py
+++ b/src/fabledassistant/services/caldav.py
@@ -575,6 +575,81 @@ async def complete_todo(
return await asyncio.to_thread(_complete)
+async def update_todo(
+ user_id: int,
+ query: str,
+ summary: str | None = None,
+ due: str | None = None,
+ description: str | None = None,
+ priority: int | None = None,
+ timezone: str | None = None,
+ calendar_name: str | None = None,
+) -> dict:
+ """Update a CalDAV todo matching the query."""
+ config = await get_caldav_config(user_id)
+ _check_config(config)
+ tz = timezone or config.get("caldav_timezone") or None
+
+ def _do_update():
+ client = _make_client(config)
+ cal_name = calendar_name or config.get("caldav_calendar_name", "")
+ calendar = _get_calendar(client, cal_name)
+ todos = calendar.todos(include_completed=True)
+
+ q = query.lower()
+ matches = []
+ for t in todos:
+ cal_obj = icalendar.Calendar.from_ical(t.data)
+ for component in cal_obj.walk():
+ if component.name == "VTODO":
+ s = str(component.get("SUMMARY", ""))
+ if q in s.lower():
+ matches.append((t, component))
+
+ if not matches:
+ raise ValueError(f"No todo found matching '{query}'.")
+ if len(matches) > 3:
+ titles = [str(m[1].get("SUMMARY", "")) for m in matches]
+ raise ValueError(
+ f"Too many matches ({len(matches)}) for '{query}'. "
+ f"Be more specific. Found: {', '.join(titles[:10])}"
+ )
+
+ todo_obj, component = matches[0]
+
+ if summary:
+ component["SUMMARY"] = summary
+ if description is not None:
+ if "DESCRIPTION" in component:
+ del component["DESCRIPTION"]
+ component.add("description", description)
+ if priority is not None:
+ if "PRIORITY" in component:
+ del component["PRIORITY"]
+ component.add("priority", priority)
+ if due:
+ if "DUE" in component:
+ del component["DUE"]
+ try:
+ dt = datetime.fromisoformat(due)
+ dt = _apply_timezone(dt, tz)
+ component.add("due", dt)
+ except ValueError:
+ component.add("due", date_type.fromisoformat(due))
+
+ # Rebuild ical data and save
+ cal_data = icalendar.Calendar()
+ cal_data.add("prodid", "-//FabledAssistant//EN")
+ cal_data.add("version", "2.0")
+ cal_data.add_component(component)
+ todo_obj.data = cal_data.to_ical().decode("utf-8")
+ todo_obj.save()
+
+ return _parse_vtodo(component)
+
+ return await asyncio.to_thread(_do_update)
+
+
async def delete_todo(
user_id: int,
query: str,
diff --git a/src/fabledassistant/services/intent.py b/src/fabledassistant/services/intent.py
index 8788ed6..d52f3fc 100644
--- a/src/fabledassistant/services/intent.py
+++ b/src/fabledassistant/services/intent.py
@@ -77,8 +77,13 @@ Rules:
- Infer reasonable defaults: birthdays and holidays are all-day + yearly recurring; "weekly meeting" is weekly recurring.
- Use descriptive titles: "My Birthday" not just "Birthday", "Team Standup" not just "Meeting".
- "add to", "update", "edit", "expand", "flesh out", "modify", "append to", "continue writing" a note → use update_note with query= and mode="append" for additions or mode="replace" for full rewrites.
+- "mark as done", "complete", "finish", "mark in progress", "start" a task → use update_note with status field.
+- "set priority", "change priority", "make it high priority" → use update_note with priority field.
+- "set due date", "move due date", "due on Friday" for a task → use update_note with due_date field.
+- "what are my overdue tasks", "show overdue", "tasks due today", "high priority tasks", "in progress tasks", "what's due this week" → use list_tasks with appropriate status/priority/due_before/due_after filters. For overdue, set due_before to today's date.
- If a note was created earlier in the conversation and the user provides more content for it, use update_note (not create_note).
- Use create_note ONLY when the user explicitly wants a brand new note that doesn't already exist.
+- "update", "change", "rename", "set due date on" a CalDAV todo → use update_todo.
- "update", "change", "move", "reschedule" an event → use update_event.
- "delete", "cancel", "remove" an event → use delete_event.
- "which calendars", "list calendars", "my calendars" → use list_calendars.
diff --git a/src/fabledassistant/services/llm.py b/src/fabledassistant/services/llm.py
index 54049ed..b705c03 100644
--- a/src/fabledassistant/services/llm.py
+++ b/src/fabledassistant/services/llm.py
@@ -250,21 +250,22 @@ async def build_context(
tool_lines = [
"You have access to tool functions. You MUST use them when the user asks you to create, add, find, schedule, or search for anything.",
"CRITICAL: Call the tool functions directly. NEVER write out function calls as text or code. NEVER describe what you would do — just do it.",
- "Available actions: create_task, create_note, update_note, search_notes.",
+ "Available actions: create_task, create_note, update_note, list_tasks, search_notes.",
]
if has_caldav:
tool_lines[-1] = (
- "Available actions: create_task, create_note, update_note, search_notes, "
+ "Available actions: create_task, create_note, update_note, list_tasks, search_notes, "
"create_event, list_events, search_events, update_event, delete_event, "
- "list_calendars, create_todo, list_todos, complete_todo, delete_todo."
+ "list_calendars, create_todo, list_todos, update_todo, complete_todo, delete_todo."
)
tool_lines.append("For calendar events, use ISO 8601 datetime format (e.g. 2026-09-30T00:00:00).")
tool_lines.append("CalDAV todos are separate from app tasks. Use create_task for app tasks, create_todo for CalDAV todos.")
tool_lines.append("When the user says 'remind me' with a time before an event, use the reminder_minutes parameter.")
tool_lines.append("For relative dates like 'Friday' or 'next week', resolve them to YYYY-MM-DD format.")
tool_lines.append(
- "Use update_note to edit/expand an existing note. "
+ "Use update_note to edit/expand an existing note OR to update a task's status/priority/due_date. "
"Use create_note ONLY for genuinely new notes with a different title. "
+ "Use list_tasks to find tasks by status, priority, or due date (e.g. overdue, high priority, in progress). "
"If a note was created earlier in the conversation and the user provides more content for it, use update_note."
)
tool_guidance = "\n".join(tool_lines)
diff --git a/src/fabledassistant/services/tools.py b/src/fabledassistant/services/tools.py
index eefa12e..a5d1ba1 100644
--- a/src/fabledassistant/services/tools.py
+++ b/src/fabledassistant/services/tools.py
@@ -15,6 +15,7 @@ from fabledassistant.services.caldav import (
list_todos,
search_events,
update_event,
+ update_todo,
)
from fabledassistant.services.notes import create_note, get_note_by_title, list_notes, update_note
from fabledassistant.services.tag_suggestions import suggest_tags
@@ -79,8 +80,9 @@ _CORE_TOOLS = [
"function": {
"name": "update_note",
"description": (
- "Update an existing note's content or title. "
- "Use this when the user asks to add to, edit, expand, flesh out, or modify a note that already exists. "
+ "Update an existing note or task — content, title, status, priority, or due date. "
+ "Use this when the user asks to add to, edit, expand, or modify a note, "
+ "OR to mark a task done/in-progress, change its priority, or set a due date. "
"NEVER use create_note when updating an existing note — always use update_note."
),
"parameters": {
@@ -88,26 +90,40 @@ _CORE_TOOLS = [
"properties": {
"query": {
"type": "string",
- "description": "Title or keyword to find the note to update",
+ "description": "Title or keyword to find the note or task to update",
},
"body": {
"type": "string",
- "description": "New note content in markdown",
+ "description": "New note content in markdown (omit if only updating task fields)",
},
"title": {
"type": "string",
- "description": "Optional new title for the note",
+ "description": "Optional new title",
},
"mode": {
"type": "string",
"enum": ["replace", "append"],
"description": (
- "How to apply the new body: 'replace' overwrites the existing content (default), "
- "'append' adds the new content after the existing content"
+ "How to apply the new body: 'replace' overwrites existing content (default), "
+ "'append' adds after existing content"
),
},
+ "status": {
+ "type": "string",
+ "enum": ["todo", "in_progress", "done"],
+ "description": "New task status. Use to mark a task done, start it, etc.",
+ },
+ "priority": {
+ "type": "string",
+ "enum": ["none", "low", "medium", "high"],
+ "description": "New task priority",
+ },
+ "due_date": {
+ "type": "string",
+ "description": "New due date in YYYY-MM-DD format",
+ },
},
- "required": ["query", "body"],
+ "required": ["query"],
},
},
},
@@ -128,6 +144,45 @@ _CORE_TOOLS = [
},
},
},
+ {
+ "type": "function",
+ "function": {
+ "name": "list_tasks",
+ "description": (
+ "List the user's tasks with optional filters. Use this when the user asks about their tasks "
+ "by status (e.g. 'what's in progress'), priority (e.g. 'high priority tasks'), "
+ "or due date (e.g. 'overdue tasks', 'due this week'). "
+ "For 'overdue', set due_before to today's date. For 'due today', set due_after to today and due_before to tomorrow."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["todo", "in_progress", "done"],
+ "description": "Filter by task status",
+ },
+ "priority": {
+ "type": "string",
+ "enum": ["none", "low", "medium", "high"],
+ "description": "Filter by priority level",
+ },
+ "due_before": {
+ "type": "string",
+ "description": "Return tasks due before this date (YYYY-MM-DD). Use today's date to find overdue tasks.",
+ },
+ "due_after": {
+ "type": "string",
+ "description": "Return tasks due on or after this date (YYYY-MM-DD)",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum number of tasks to return (default 10)",
+ },
+ },
+ },
+ },
+ },
]
# CalDAV tools — only included when user has CalDAV configured
@@ -370,6 +425,47 @@ _CALDAV_TOOLS = [
},
},
},
+ {
+ "type": "function",
+ "function": {
+ "name": "update_todo",
+ "description": "Update a CalDAV todo's summary, due date, description, or priority.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Search term to find the todo to update (matches against summary)",
+ },
+ "summary": {
+ "type": "string",
+ "description": "New summary/title for the todo",
+ },
+ "due": {
+ "type": "string",
+ "description": "New due date or datetime in ISO 8601 format",
+ },
+ "description": {
+ "type": "string",
+ "description": "New description",
+ },
+ "priority": {
+ "type": "integer",
+ "description": "New priority (1=highest, 9=lowest)",
+ },
+ "timezone": {
+ "type": "string",
+ "description": "Optional IANA timezone for due datetime",
+ },
+ "calendar_name": {
+ "type": "string",
+ "description": "Optional calendar name to search in",
+ },
+ },
+ "required": ["query"],
+ },
+ },
+ },
{
"type": "function",
"function": {
@@ -508,6 +604,12 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
update_fields["body"] = note.body + "\n\n" + new_body
else:
update_fields["body"] = new_body
+ if "status" in arguments:
+ update_fields["status"] = arguments["status"]
+ if "priority" in arguments:
+ update_fields["priority"] = arguments["priority"]
+ if "due_date" in arguments:
+ update_fields["due_date"] = _parse_due_date(arguments["due_date"])
updated = await update_note(user_id, note.id, **update_fields)
if updated is None:
@@ -524,6 +626,38 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
"suggested_tags": suggested,
}
+ elif tool_name == "list_tasks":
+ notes, total = await list_notes(
+ user_id=user_id,
+ is_task=True,
+ status=arguments.get("status"),
+ priority=arguments.get("priority"),
+ due_before=_parse_due_date(arguments.get("due_before")),
+ due_after=_parse_due_date(arguments.get("due_after")),
+ limit=int(arguments.get("limit", 10)),
+ sort="due_date",
+ order="asc",
+ )
+ results = []
+ for n in notes:
+ results.append({
+ "id": n.id,
+ "title": n.title,
+ "status": n.status,
+ "priority": n.priority,
+ "due_date": str(n.due_date) if n.due_date else None,
+ "preview": (n.body[:120] + "...") if n.body and len(n.body) > 120 else (n.body or ""),
+ })
+ return {
+ "success": True,
+ "type": "tasks",
+ "data": {
+ "total": total,
+ "count": len(results),
+ "results": results,
+ },
+ }
+
elif tool_name == "search_notes":
query = arguments.get("query", "")
notes, total = await list_notes(user_id=user_id, q=query, limit=5)
@@ -671,6 +805,23 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
},
}
+ elif tool_name == "update_todo":
+ result = await update_todo(
+ user_id=user_id,
+ query=arguments["query"],
+ summary=arguments.get("summary"),
+ due=arguments.get("due"),
+ description=arguments.get("description"),
+ priority=arguments.get("priority"),
+ timezone=arguments.get("timezone"),
+ calendar_name=arguments.get("calendar_name"),
+ )
+ return {
+ "success": True,
+ "type": "todo_updated",
+ "data": result,
+ }
+
elif tool_name == "complete_todo":
result = await complete_todo(
user_id=user_id,