# 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_TYPES` in the route file**

In `src/fabledassistant/routes/knowledge.py`, change:

```python
_VALID_TYPES = {"note", "person", "place", "list"}
```

to:

```python
_VALID_TYPES = {"note", "person", "place", "list", "task"}
```

- [ ] **Step 2: Update `_note_to_item` to 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:

```python
    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:

```python
    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_knowledge` to include tasks**

In the `query_knowledge` function, the "all types" filter currently excludes tasks. Change the base query and the `else` branch.

Find:

```python
        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:

```python
        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_search` to include tasks**

Find:

```python
        candidates = await semantic_search_notes(
            user_id=user_id,
            query=q,
            limit=min(200, limit * 8),
            threshold=0.3,
            is_task=False,
        )
```

Replace with:

```python
        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:

```python
    for _score, note in candidates:
        if note_type and note.entity_type != note_type:
            continue
```

Replace with:

```python
    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_ids` to include tasks**

Find:

```python
        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:

```python
        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_tags` to include task tags**

Find:

```python
        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:

```python
        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_counts` to include tasks**

Find:

```python
    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:

```python
    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**

```bash
cd /path/to/fabledassistant
make typecheck
make test
```

Expected: no errors.

- [ ] **Step 9: Commit**

```bash
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:

```ts
interface KnowledgeItem {
  id: number;
  note_type: "note" | "person" | "place" | "list";
  // ... existing fields
```

Change to:

```ts
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 `}`):

```ts
  // Task-specific
  status?: string;
  priority?: string;
  due_date?: string;
```

Update the `activeType` ref type:

```ts
const activeType = ref<"" | "note" | "person" | "place" | "list">(""); 
```

Change to:

```ts
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:

```html
          <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:

```html
          <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:

```html
            @click="activeType = (val as '' | 'note' | 'person' | 'place' | 'list')"
```

Replace with:

```html
            @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:

```html
              <!-- Note snippet -->
              <p v-else-if="item.snippet" class="k-card-snippet">{{ item.snippet }}</p>
```

Add a task-specific section above it:

```html
              <!-- 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 `isOverdue` helper and update `openItem` for tasks**

In the `<script setup>`, add after the `formatDate` function:

```ts
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:

```ts
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:

```html
        <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:

```html
        <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:

```ts
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:

```ts
document.addEventListener('click', onClickOutsideNewNote);
```

In `onUnmounted`, add:

```ts
document.removeEventListener('click', onClickOutsideNewNote);
```

- [ ] **Step 6: Add task card CSS**

Append to the `<style scoped>` block:

```css
/* ── 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:

```css
.badge--list   { background: rgba(56,189,248,0.15); color: #7dd3fc; }
```

Add after it:

```css
.badge--task   { background: rgba(167,139,250,0.15); color: #a78bfa; }
```

- [ ] **Step 7: Remove the chevron button CSS**

Find and delete:

```css
.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:

```css
.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**

```bash
cd /path/to/fabledassistant/frontend
npx tsc --noEmit
```

Expected: no new errors (pre-existing TipTap errors are fine).

- [ ] **Step 9: Commit**

```bash
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:

```ts
    {
      path: "/notes",
      name: "notes",
      component: () => import("@/views/NotesListView.vue"),
    },
```

Replace with:

```ts
    {
      path: "/notes",
      redirect: "/",
    },
```

Find:

```ts
    {
      path: "/tasks",
      name: "tasks",
      component: () => import("@/views/TasksListView.vue"),
    },
```

Replace with:

```ts
    {
      path: "/tasks",
      redirect: "/",
    },
```

- [ ] **Step 2: Remove "Tasks" from AppHeader navigation**

In `frontend/src/components/AppHeader.vue`, find in the desktop nav-center:

```html
        <router-link to="/tasks" class="nav-link">Tasks</router-link>
```

Delete this line.

Find in the mobile dropdown menu:

```html
      <router-link to="/tasks" class="nav-link">Tasks</router-link>
```

Delete this line.

- [ ] **Step 3: Update `g+t` keyboard shortcut in App.vue**

In `frontend/src/App.vue`, find in the `onGlobalKeydown` function, inside the `if (pendingPrefix === "g")` block:

```ts
      case "t": router.push("/tasks"); break;
```

Replace with:

```ts
      case "t": router.push("/"); break;
```

- [ ] **Step 4: Update shortcuts overlay text**

In `frontend/src/App.vue`, find in the shortcuts overlay template:

```html
                <kbd class="shortcut-key">t</kbd>
                <span class="shortcut-desc">Tasks</span>
```

Replace the description:

```html
                <kbd class="shortcut-key">t</kbd>
                <span class="shortcut-desc">Knowledge (tasks)</span>
```

- [ ] **Step 5: Delete the deprecated list view files**

```bash
rm frontend/src/views/NotesListView.vue
rm frontend/src/views/TasksListView.vue
```

- [ ] **Step 6: Verify build**

```bash
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**

```bash
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"
```
