22 KiB
Knowledge View Task Consolidation — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Consolidate tasks into the Knowledge view as a fifth card type, deprecate /notes and /tasks list routes, and simplify navigation down to a single Knowledge hub.
Architecture: The backend knowledge service (services/knowledge.py) stops excluding tasks from queries and adds type=task filtering via the is_task property (Note.status IS NOT NULL). The knowledge route validation gains "task" as a valid type. The frontend KnowledgeView gains task card rendering with status/priority/due-date badges. Router redirects replace the deleted list views.
Tech Stack: Python/Quart backend (SQLAlchemy), Vue 3 + TypeScript frontend, Pinia stores, Vue Router.
File Map
| Action | Path |
|---|---|
| Modify | src/fabledassistant/services/knowledge.py |
| Modify | src/fabledassistant/routes/knowledge.py |
| Modify | frontend/src/views/KnowledgeView.vue |
| Modify | frontend/src/router/index.ts |
| Modify | frontend/src/components/AppHeader.vue |
| Modify | frontend/src/App.vue |
| Delete | frontend/src/views/NotesListView.vue |
| Delete | frontend/src/views/TasksListView.vue |
Task 1: Backend — Include tasks in knowledge queries
Files:
- Modify:
src/fabledassistant/services/knowledge.py - Modify:
src/fabledassistant/routes/knowledge.py
Context: The knowledge service currently excludes tasks by filtering Note.status.is_(None). Every query function (query_knowledge, query_knowledge_ids, _semantic_knowledge_search, get_knowledge_tags, get_knowledge_counts) has this exclusion. Adding task support means: (1) removing the task exclusion from the "all types" queries, (2) adding type=task as a filter option that maps to Note.status.isnot(None), (3) enriching _note_to_item with task-specific fields, (4) updating counts to include tasks.
- Step 1: Add
"task"to_VALID_TYPESin the route file
In src/fabledassistant/routes/knowledge.py, change:
_VALID_TYPES = {"note", "person", "place", "list"}
to:
_VALID_TYPES = {"note", "person", "place", "list", "task"}
- Step 2: Update
_note_to_itemto include task fields
In src/fabledassistant/services/knowledge.py, the _note_to_item function builds the item dict. After the existing elif note.entity_type == "list": block (which ends around line 48), add a task branch. Find:
elif note.entity_type == "list":
# Parse markdown task list syntax into structured items
body = note.body or ""
list_items = []
for line in body.split("\n"):
stripped = line.strip()
if stripped.startswith("- [ ] ") or stripped.startswith("- [x] ") or stripped.startswith("- [X] "):
checked_item = not stripped.startswith("- [ ] ")
list_items.append({"text": stripped[6:], "checked": checked_item})
item["list_items"] = list_items
item["item_count"] = len(list_items)
item["checked_count"] = sum(1 for i in list_items if i["checked"])
item["body"] = body
return item
Replace with:
elif note.entity_type == "list":
# Parse markdown task list syntax into structured items
body = note.body or ""
list_items = []
for line in body.split("\n"):
stripped = line.strip()
if stripped.startswith("- [ ] ") or stripped.startswith("- [x] ") or stripped.startswith("- [X] "):
checked_item = not stripped.startswith("- [ ] ")
list_items.append({"text": stripped[6:], "checked": checked_item})
item["list_items"] = list_items
item["item_count"] = len(list_items)
item["checked_count"] = sum(1 for i in list_items if i["checked"])
item["body"] = body
# Task fields — included for all items but only meaningful when is_task
if note.is_task:
item["note_type"] = "task"
item["status"] = note.status
item["priority"] = note.priority
item["due_date"] = note.due_date.isoformat() if note.due_date else None
return item
This overrides note_type to "task" for task items (since entity_type returns the note_type column which is "note" for tasks) and adds status/priority/due_date fields.
- Step 3: Update
query_knowledgeto include tasks
In the query_knowledge function, the "all types" filter currently excludes tasks. Change the base query and the else branch.
Find:
base = (
select(Note)
.where(Note.user_id == user_id)
.where(Note.status.is_(None)) # exclude tasks
)
if note_type:
base = base.where(Note.note_type == note_type)
else:
# Exclude tasks — already done above; also exclude any legacy nulls
base = base.where(Note.note_type.in_(["note", "person", "place", "list"]))
Replace with:
base = select(Note).where(Note.user_id == user_id)
if note_type == "task":
base = base.where(Note.status.isnot(None))
elif note_type:
base = base.where(Note.note_type == note_type).where(Note.status.is_(None))
else:
# All types including tasks
pass
- Step 4: Update
_semantic_knowledge_searchto include tasks
Find:
candidates = await semantic_search_notes(
user_id=user_id,
query=q,
limit=min(200, limit * 8),
threshold=0.3,
is_task=False,
)
Replace with:
is_task_filter = True if note_type == "task" else (False if note_type else None)
candidates = await semantic_search_notes(
user_id=user_id,
query=q,
limit=min(200, limit * 8),
threshold=0.3,
is_task=is_task_filter,
)
Also update the type matching in the filter loop — find:
for _score, note in candidates:
if note_type and note.entity_type != note_type:
continue
Replace with:
for _score, note in candidates:
if note_type == "task" and not note.is_task:
continue
elif note_type and note_type != "task" and note.entity_type != note_type:
continue
- Step 5: Update
query_knowledge_idsto include tasks
Find:
base = (
select(Note.id)
.where(Note.user_id == user_id)
.where(Note.status.is_(None))
)
if note_type:
base = base.where(Note.note_type == note_type)
else:
base = base.where(Note.note_type.in_(["note", "person", "place", "list"]))
Replace with:
base = select(Note.id).where(Note.user_id == user_id)
if note_type == "task":
base = base.where(Note.status.isnot(None))
elif note_type:
base = base.where(Note.note_type == note_type).where(Note.status.is_(None))
else:
pass
- Step 6: Update
get_knowledge_tagsto include task tags
Find:
base = (
select(func.unnest(Note.tags).label("tag"))
.where(Note.user_id == user_id)
.where(Note.status.is_(None))
)
if note_type:
base = base.where(Note.note_type == note_type)
else:
base = base.where(Note.note_type.in_(["note", "person", "place", "list"]))
Replace with:
base = (
select(func.unnest(Note.tags).label("tag"))
.where(Note.user_id == user_id)
)
if note_type == "task":
base = base.where(Note.status.isnot(None))
elif note_type:
base = base.where(Note.note_type == note_type).where(Note.status.is_(None))
else:
pass
- Step 7: Update
get_knowledge_countsto include tasks
Find:
async with async_session() as session:
stmt = (
select(Note.note_type, func.count(Note.id))
.where(Note.user_id == user_id)
.where(Note.status.is_(None))
.where(Note.note_type.in_(["note", "person", "place", "list"]))
.group_by(Note.note_type)
)
if tags:
for tag in tags:
stmt = stmt.where(Note.tags.contains([tag]))
rows = list((await session.execute(stmt)).all())
counts = {row[0]: row[1] for row in rows}
# Ensure all types present even if zero
for t in ("note", "person", "place", "list"):
counts.setdefault(t, 0)
counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list"))
return counts
Replace with:
async with async_session() as session:
# Count non-task types
stmt = (
select(Note.note_type, func.count(Note.id))
.where(Note.user_id == user_id)
.where(Note.status.is_(None))
.where(Note.note_type.in_(["note", "person", "place", "list"]))
.group_by(Note.note_type)
)
if tags:
for tag in tags:
stmt = stmt.where(Note.tags.contains([tag]))
rows = list((await session.execute(stmt)).all())
counts = {row[0]: row[1] for row in rows}
# Count tasks separately (is_task = status IS NOT NULL)
task_stmt = (
select(func.count(Note.id))
.where(Note.user_id == user_id)
.where(Note.status.isnot(None))
)
if tags:
for tag in tags:
task_stmt = task_stmt.where(Note.tags.contains([tag]))
task_count: int = (await session.execute(task_stmt)).scalar_one()
counts["task"] = task_count
for t in ("note", "person", "place", "list", "task"):
counts.setdefault(t, 0)
counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list", "task"))
return counts
- Step 8: Verify backend changes
cd /path/to/fabledassistant
make typecheck
make test
Expected: no errors.
- Step 9: Commit
git add src/fabledassistant/services/knowledge.py src/fabledassistant/routes/knowledge.py
git commit -m "feat(knowledge): include tasks in knowledge queries and counts"
Task 2: Frontend — Task card rendering in KnowledgeView
Files:
- Modify:
frontend/src/views/KnowledgeView.vue
Context: KnowledgeView.vue has a KnowledgeItem interface and renders cards in a grid. Each card type has type-specific content (person shows relationship/email, list shows checkboxes, etc.). Task cards need status, priority, and due date display. The activeType ref controls filtering; it needs "task" as a valid value. The type filter sidebar needs a "Tasks" button. The new-note button interaction changes from split-button to toggle.
- Step 1: Add
"task"to the KnowledgeItem interface and filter type
In the <script setup> section, find the KnowledgeItem interface and add task fields:
interface KnowledgeItem {
id: number;
note_type: "note" | "person" | "place" | "list";
// ... existing fields
Change to:
interface KnowledgeItem {
id: number;
note_type: "note" | "person" | "place" | "list" | "task";
// ... existing fields
Also add the task-specific fields at the end of the interface (before the closing }):
// Task-specific
status?: string;
priority?: string;
due_date?: string;
Update the activeType ref type:
const activeType = ref<"" | "note" | "person" | "place" | "list">("");
Change to:
const activeType = ref<"" | "note" | "person" | "place" | "list" | "task">("");
- Step 2: Add "Tasks" to the type filter sidebar
Find the type filter v-for in the template:
<button
v-for="[val, label, key] in ([['note','Notes','note'],['person','People','person'],['place','Places','place'],['list','Lists','list']] as [string,string,string][])"
Replace with:
<button
v-for="[val, label, key] in ([['note','Notes','note'],['task','Tasks','task'],['person','People','person'],['place','Places','place'],['list','Lists','list']] as [string,string,string][])"
Update the type cast on the click handler. Find:
@click="activeType = (val as '' | 'note' | 'person' | 'place' | 'list')"
Replace with:
@click="activeType = (val as '' | 'note' | 'person' | 'place' | 'list' | 'task')"
- Step 3: Add task card content in the template
In the card grid, find the note snippet section:
<!-- Note snippet -->
<p v-else-if="item.snippet" class="k-card-snippet">{{ item.snippet }}</p>
Add a task-specific section above it:
<!-- Task specifics -->
<div v-else-if="item.note_type === 'task'" class="k-card-task">
<div class="task-badges">
<span class="status-badge" :class="`status--${item.status}`">
{{ item.status === 'in_progress' ? 'in progress' : item.status }}
</span>
<span
v-if="item.priority && item.priority !== 'none'"
class="priority-badge"
:class="`priority--${item.priority}`"
>{{ item.priority }}</span>
</div>
<span
v-if="item.due_date"
class="task-due"
:class="{ 'task-overdue': isOverdue(item) }"
>{{ formatDate(item.due_date) }}</span>
<p v-if="item.snippet" class="k-card-snippet">{{ item.snippet }}</p>
</div>
<!-- Note snippet -->
<p v-else-if="item.snippet" class="k-card-snippet">{{ item.snippet }}</p>
- Step 4: Add
isOverduehelper and updateopenItemfor tasks
In the <script setup>, add after the formatDate function:
function isOverdue(item: KnowledgeItem): boolean {
if (!item.due_date || item.status === 'done' || item.status === 'cancelled') return false;
return new Date(item.due_date) < new Date(new Date().toDateString());
}
Update openItem to route tasks to their editor:
function openItem(item: KnowledgeItem) {
if (item.note_type === 'task') {
router.push(`/tasks/${item.id}`);
} else {
router.push(`/notes/${item.id}`);
}
}
- Step 5: Update the "New note" button to toggle interaction
Find the current new-note button markup:
<div class="new-note-wrap">
<button class="btn-new-note" @click="createNew('note')">+ New note</button>
<button class="btn-new-chevron" @click="newNoteMenuOpen = !newNoteMenuOpen" :class="{ open: newNoteMenuOpen }" title="Create specific type">▾</button>
<div v-if="newNoteMenuOpen" class="new-note-menu">
<button @click="createNew('note')">Note</button>
<button @click="createNew('person')">Person</button>
<button @click="createNew('place')">Place</button>
<button @click="createNew('list')">List</button>
</div>
</div>
Replace with:
<div class="new-note-wrap">
<button class="btn-new-note" @click="newNoteMenuOpen ? createNew('note') : (newNoteMenuOpen = true)">+ New note</button>
<div v-if="newNoteMenuOpen" class="new-note-menu">
<button @click="createNew('task')">Task</button>
<button @click="createNew('person')">Person</button>
<button @click="createNew('place')">Place</button>
<button @click="createNew('list')">List</button>
</div>
</div>
Add a click-outside handler. In the <script setup>, add after the createNew function:
function onClickOutsideNewNote(e: MouseEvent) {
const wrap = document.querySelector('.new-note-wrap');
if (wrap && !wrap.contains(e.target as Node)) {
newNoteMenuOpen.value = false;
}
}
In onMounted, add:
document.addEventListener('click', onClickOutsideNewNote);
In onUnmounted, add:
document.removeEventListener('click', onClickOutsideNewNote);
- Step 6: Add task card CSS
Append to the <style scoped> block:
/* ── Task card ──────────────────────────────────────────── */
.k-card--task { border-left: 3px solid #a78bfa; }
.k-card-task {
display: flex;
flex-direction: column;
gap: 6px;
}
.task-badges {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.status-badge {
font-size: 0.7rem;
padding: 1px 7px;
border-radius: 8px;
font-weight: 600;
}
.status--todo { background: var(--color-status-todo-bg); color: var(--color-status-todo); }
.status--in_progress { background: var(--color-status-in-progress-bg); color: var(--color-status-in-progress); }
.status--done { background: var(--color-status-done-bg); color: var(--color-status-done); }
.status--cancelled { background: var(--color-status-todo-bg); color: var(--color-status-todo); text-decoration: line-through; }
.priority-badge {
font-size: 0.7rem;
padding: 1px 7px;
border-radius: 8px;
font-weight: 600;
}
.priority--low { background: var(--color-priority-low-bg); color: var(--color-priority-low); }
.priority--normal { background: var(--color-priority-medium-bg); color: var(--color-priority-medium); }
.priority--high { background: var(--color-priority-high-bg); color: var(--color-priority-high); }
.task-due {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.task-overdue {
color: var(--color-overdue);
font-weight: 500;
}
Also add the task type badge color. Find:
.badge--list { background: rgba(56,189,248,0.15); color: #7dd3fc; }
Add after it:
.badge--task { background: rgba(167,139,250,0.15); color: #a78bfa; }
- Step 7: Remove the chevron button CSS
Find and delete:
.btn-new-chevron {
padding: 7px 9px;
border-radius: 0 8px 8px 0;
border: 1px solid rgba(99, 102, 241, 0.4);
background: rgba(99, 102, 241, 0.12);
color: var(--color-primary, #818cf8);
cursor: pointer;
font-size: 0.78rem;
line-height: 1;
transition: background 0.15s, transform 0.15s;
}
.btn-new-chevron:hover { background: rgba(99, 102, 241, 0.2); }
.btn-new-chevron.open { transform: scaleY(-1); }
Update .btn-new-note to have full border-radius now that the chevron is gone:
.btn-new-note {
flex: 1;
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(99, 102, 241, 0.4);
background: rgba(99, 102, 241, 0.12);
color: var(--color-primary, #818cf8);
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
text-align: left;
transition: background 0.15s;
}
- Step 8: Verify TypeScript compiles
cd /path/to/fabledassistant/frontend
npx tsc --noEmit
Expected: no new errors (pre-existing TipTap errors are fine).
- Step 9: Commit
git add frontend/src/views/KnowledgeView.vue
git commit -m "feat(knowledge): add task cards with status/priority/due-date display"
Task 3: Route redirects, navigation cleanup, dead code removal
Files:
- Modify:
frontend/src/router/index.ts - Modify:
frontend/src/components/AppHeader.vue - Modify:
frontend/src/App.vue - Delete:
frontend/src/views/NotesListView.vue - Delete:
frontend/src/views/TasksListView.vue
Context: The router currently has /notes and /tasks pointing to list view components. These become redirects to /. The AppHeader has "Tasks" in both desktop and mobile nav. The g+t keyboard shortcut navigates to /tasks which should change to /. The stores (notes.ts, tasks.ts) are used by other views so they stay.
- Step 1: Replace list view routes with redirects
In frontend/src/router/index.ts, find:
{
path: "/notes",
name: "notes",
component: () => import("@/views/NotesListView.vue"),
},
Replace with:
{
path: "/notes",
redirect: "/",
},
Find:
{
path: "/tasks",
name: "tasks",
component: () => import("@/views/TasksListView.vue"),
},
Replace with:
{
path: "/tasks",
redirect: "/",
},
- Step 2: Remove "Tasks" from AppHeader navigation
In frontend/src/components/AppHeader.vue, find in the desktop nav-center:
<router-link to="/tasks" class="nav-link">Tasks</router-link>
Delete this line.
Find in the mobile dropdown menu:
<router-link to="/tasks" class="nav-link">Tasks</router-link>
Delete this line.
- Step 3: Update
g+tkeyboard shortcut in App.vue
In frontend/src/App.vue, find in the onGlobalKeydown function, inside the if (pendingPrefix === "g") block:
case "t": router.push("/tasks"); break;
Replace with:
case "t": router.push("/"); break;
- Step 4: Update shortcuts overlay text
In frontend/src/App.vue, find in the shortcuts overlay template:
<kbd class="shortcut-key">t</kbd>
<span class="shortcut-desc">Tasks</span>
Replace the description:
<kbd class="shortcut-key">t</kbd>
<span class="shortcut-desc">Knowledge (tasks)</span>
- Step 5: Delete the deprecated list view files
rm frontend/src/views/NotesListView.vue
rm frontend/src/views/TasksListView.vue
- Step 6: Verify build
cd /path/to/fabledassistant/frontend
npx tsc --noEmit
Expected: no new errors. The deleted files were only imported via lazy () => import(...) in the router, which we already replaced with redirects.
- Step 7: Commit
git add -A frontend/src/views/NotesListView.vue frontend/src/views/TasksListView.vue \
frontend/src/router/index.ts frontend/src/components/AppHeader.vue frontend/src/App.vue
git commit -m "feat: deprecate /notes and /tasks routes; redirect to Knowledge view"