diff --git a/frontend/src/views/TaskEditorView.vue b/frontend/src/views/TaskEditorView.vue index 133b0b3..cbba5da 100644 --- a/frontend/src/views/TaskEditorView.vue +++ b/frontend/src/views/TaskEditorView.vue @@ -333,7 +333,7 @@ async function save() { savedParentId = parentId.value; dirty.value = false; toast.show("Task saved"); - window.location.reload(); + router.push(`/tasks/${taskId.value}`); } else { const task = await store.createTask(data); dirty.value = false; diff --git a/src/fabledassistant/routes/tasks.py b/src/fabledassistant/routes/tasks.py index 4392d6e..ae3c974 100644 --- a/src/fabledassistant/routes/tasks.py +++ b/src/fabledassistant/routes/tasks.py @@ -6,6 +6,7 @@ from quart import Blueprint, jsonify, request from fabledassistant.auth import login_required, get_current_user_id from fabledassistant.models.note import TaskPriority, TaskStatus from fabledassistant.routes.utils import not_found, parse_iso_date, parse_pagination +from fabledassistant.services.access import can_write_note from fabledassistant.services.embeddings import upsert_note_embedding from fabledassistant.services.notes import ( create_note, @@ -154,6 +155,13 @@ async def get_task_route(task_id: int): @login_required async def update_task_route(task_id: int): uid = get_current_user_id() + result = await get_note_for_user(uid, task_id) + if result is None: + return not_found("Task") + task_note, _ = result + if not await can_write_note(uid, task_id): + return jsonify({"error": "Permission denied"}), 403 + data = await request.get_json() fields = {} for key in ("title",): @@ -198,12 +206,12 @@ async def update_task_route(task_id: int): if recurrence_rule is not _UNSET: fields["recurrence_rule"] = recurrence_rule - task = await update_note(uid, task_id, **fields) + task = await update_note(task_note.user_id, task_id, **fields) if task is None: return not_found("Task") text = f"{task.title}\n{task.body}".strip() if task.body else (task.title or "") if text: - asyncio.create_task(upsert_note_embedding(task.id, uid, text)) + asyncio.create_task(upsert_note_embedding(task.id, task_note.user_id, text)) return jsonify(task.to_dict()) @@ -211,17 +219,23 @@ async def update_task_route(task_id: int): @login_required async def patch_task_status(task_id: int): uid = get_current_user_id() + result = await get_note_for_user(uid, task_id) + if result is None: + return not_found("Task") + task_note, _ = result + if not await can_write_note(uid, task_id): + return jsonify({"error": "Permission denied"}), 403 + data = await request.get_json() status_val = data.get("status") if not status_val: return jsonify({"error": "status is required"}), 400 - try: TaskStatus(status_val) except ValueError: return jsonify({"error": f"Invalid status: {status_val}"}), 400 - task = await update_note(uid, task_id, status=status_val) + task = await update_note(task_note.user_id, task_id, status=status_val) if task is None: return not_found("Task") return jsonify(task.to_dict()) @@ -252,7 +266,13 @@ async def recurrence_preview_route(task_id: int): @login_required async def delete_task_route(task_id: int): uid = get_current_user_id() - deleted = await delete_note(uid, task_id) + result = await get_note_for_user(uid, task_id) + if result is None: + return not_found("Task") + task_note, _ = result + if not await can_write_note(uid, task_id): + return jsonify({"error": "Permission denied"}), 403 + deleted = await delete_note(task_note.user_id, task_id) if not deleted: return not_found("Task") return "", 204 diff --git a/src/fabledassistant/services/tools.py b/src/fabledassistant/services/tools.py index e5761ba..e90e8a4 100644 --- a/src/fabledassistant/services/tools.py +++ b/src/fabledassistant/services/tools.py @@ -80,6 +80,11 @@ _CORE_TOOLS = [ "type": "string", "description": "Optional task description or details", }, + "status": { + "type": "string", + "enum": ["todo", "in_progress", "done", "cancelled"], + "description": "Initial task status (default: todo)", + }, "due_date": { "type": "string", "description": "Optional due date in YYYY-MM-DD format", @@ -89,6 +94,11 @@ _CORE_TOOLS = [ "enum": ["none", "low", "medium", "high"], "description": "Task priority level", }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Tags for the task (without # prefix)", + }, "project": { "type": "string", "description": "Optional project name to assign this task to", @@ -204,6 +214,10 @@ _CORE_TOOLS = [ "type": "string", "description": "Optional project name to assign this note/task to (set to empty string to remove project)", }, + "milestone": { + "type": "string", + "description": "Optional milestone title within the project to assign this task to (set to empty string to remove milestone)", + }, "recurrence_rule": { "type": "object", "description": "Set or update recurrence (same format as create_task). Pass null to remove.", @@ -344,6 +358,10 @@ _CORE_TOOLS = [ "parameters": { "type": "object", "properties": { + "q": { + "type": "string", + "description": "Optional keyword filter — searches title and body", + }, "status": { "type": "array", "items": { @@ -1110,11 +1128,16 @@ async def execute_tool( best_score, best_note = sem_hits[0] return {"success": False, "requires_confirmation": True, "similar_note": {"id": best_note.id, "title": best_note.title}, "error": f"A task with very similar content exists: '{best_note.title}' (semantic similarity: {best_score:.0%}). Ask the user to confirm before creating a separate entry."} + task_tags = arguments.get("tags", []) + if not isinstance(task_tags, list): + task_tags = [] + task_status = arguments.get("status", "todo") note = await create_note( user_id=user_id, title=task_title, body=task_body, - status="todo", + tags=task_tags, + status=task_status, priority=arguments.get("priority", "none"), due_date=_parse_due_date(arguments.get("due_date")), project_id=pre_fields.get("project_id"), @@ -1139,6 +1162,8 @@ async def execute_tool( "priority": note.priority, "due_date": str(note.due_date) if note.due_date else None, "project_id": note.project_id, + "milestone_id": note.milestone_id, + "parent_id": note.parent_id, }, "suggested_tags": suggested, } @@ -1258,6 +1283,22 @@ async def execute_tool( update_fields["project_id"] = proj.id else: update_fields["project_id"] = None + update_fields["milestone_id"] = None # clear milestone when project is cleared + + if "milestone" in arguments: + milestone_name = arguments["milestone"] + if milestone_name: + # Resolve the project context for milestone lookup + ref_project_id = update_fields.get("project_id") or note.project_id + if ref_project_id is None: + return {"success": False, "error": "Cannot assign a milestone without a project. Set project first."} + from fabledassistant.services.milestones import get_milestone_by_title as _gmbt_upd + ms = await _gmbt_upd(user_id, ref_project_id, milestone_name) + if ms is None: + return {"success": False, "error": f"Milestone '{milestone_name}' not found. Use list_milestones to see available milestones."} + update_fields["milestone_id"] = ms.id + else: + update_fields["milestone_id"] = None updated = await update_note(user_id, note.id, **update_fields) if updated is None: @@ -1303,6 +1344,7 @@ async def execute_tool( milestone_id = ms.id notes, total = await list_notes( user_id=user_id, + q=arguments.get("q") or None, is_task=True, status=arguments.get("status"), priority=arguments.get("priority"),