← Back to task

Commit b9619e08

commit b9619e08968587b3614a4bbcf44bc5808f889e97
Author: Coder Agent <coder@agents.omni>
Date:   Thu Feb 19 13:49:49 2026

    Studio tasks: compare parity with task web and roadmap
    
    Task-Id: t-460

diff --git a/Omni/Agentd/Studio.hs b/Omni/Agentd/Studio.hs
index e25f7bf8..f5f9f13a 100755
--- a/Omni/Agentd/Studio.hs
+++ b/Omni/Agentd/Studio.hs
@@ -94,7 +94,14 @@ type API =
     :<|> "notes" :> QueryParam "q" Text :> QueryParam "topic" Text :> Get '[Lucid.HTML] (Lucid.Html ())
     :<|> "notes" :> Capture "noteId" Int :> Get '[Lucid.HTML] (Lucid.Html ())
     -- Tasks
-    :<|> "tasks" :> QueryParam "q" Text :> QueryParam "status" Text :> Get '[Lucid.HTML] (Lucid.Html ())
+    :<|> "tasks"
+      :> QueryParam "q" Text
+      :> QueryParam "status" Text
+      :> QueryParam "priority" Text
+      :> QueryParam "namespace" Text
+      :> QueryParam "type" Text
+      :> QueryParam "sort" Text
+      :> Get '[Lucid.HTML] (Lucid.Html ())
     :<|> "tasks" :> Capture "taskId" Text :> Get '[Lucid.HTML] (Lucid.Html ())
     -- Health
     :<|> "health" :> Get '[PlainText] Text
diff --git a/Omni/Agentd/Studio/TASKS_PARITY_REPORT.md b/Omni/Agentd/Studio/TASKS_PARITY_REPORT.md
new file mode 100644
index 00000000..29c4ec67
--- /dev/null
+++ b/Omni/Agentd/Studio/TASKS_PARITY_REPORT.md
@@ -0,0 +1,46 @@
+# Studio Tasks vs task web: Feature comparison and improvement plan
+
+## Feature comparison
+
+| Feature | task web | Studio (after this change) |
+|---|---|---|
+| Task list view | Yes | Yes |
+| Task detail view | Yes | Yes |
+| Create task | No UI | No UI |
+| Edit task | Yes (status/priority/complexity/description) | No |
+| Task comments | Yes (timeline + add comment) | Read-only |
+| Epic/subtask tree | Yes | Partial (parent + children) |
+| Status filtering | Yes | Yes |
+| Priority display | Yes | Yes |
+| Search | Partial (filter-driven) | Yes (ID/title/description/namespace/parent) |
+| Markdown rendering | Yes | Yes |
+| Mobile responsive | Yes | Partial |
+
+## Gaps observed
+
+1. Studio still lacks task mutation flows (create/edit/update/comment).
+2. Studio does not have dedicated ready/blocked/intervention task views.
+3. Epic navigation is improved but not a full tree explorer.
+4. Task styling and interaction patterns are still less polished than task web.
+
+## Prioritized improvements
+
+1. **P0 — Add create/edit flows in Studio tasks**
+   - Scope: create task form, update status/priority/complexity/description.
+   - Effort: medium (~1-2 days).
+
+2. **P0 — Add comment authoring in Studio detail view**
+   - Scope: add-comment form and persisted comment posting.
+   - Effort: medium (~1 day).
+
+3. **P1 — Add queue-focused pages**
+   - Scope: ready queue, blocked queue, needs-human-action queue.
+   - Effort: medium (~1 day).
+
+4. **P1 — Add epic/subtask tree explorer**
+   - Scope: expandable hierarchy + parent/child navigation aids.
+   - Effort: medium (~1 day).
+
+5. **P2 — Align Studio task components/style with task web**
+   - Scope: shared badges/forms/layout patterns and responsive polish.
+   - Effort: low-medium (~0.5-1 day).
diff --git a/Omni/Agentd/Studio/Tasks.hs b/Omni/Agentd/Studio/Tasks.hs
index aeb40bbc..55318ce6 100644
--- a/Omni/Agentd/Studio/Tasks.hs
+++ b/Omni/Agentd/Studio/Tasks.hs
@@ -1,7 +1,8 @@
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE NoImplicitPrelude #-}
 
--- | Studio Tasks Page - task dashboard
+-- | Studio Tasks Page - richer task dashboard with parity roadmap.
 --
 -- : out studio-tasks
 -- : dep lucid
@@ -14,166 +15,450 @@ module Omni.Agentd.Studio.Tasks
 where
 
 import Alpha
+import qualified Data.List as List
 import qualified Data.Text as Text
+import Data.Time (UTCTime)
+import qualified Data.Time.Format as Time
 import qualified Lucid
 import qualified Omni.Agentd.Studio.Layout as Layout
 import qualified Omni.Task.Core as Task
+import Omni.Task.Web.Components (renderMarkdown)
 import Servant
 
--- | Tasks page
-tasksPage :: Maybe Text -> Maybe Text -> Servant.Handler (Lucid.Html ())
-tasksPage maybeQuery maybeStatus = do
+-- | Tasks page.
+tasksPage ::
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Servant.Handler (Lucid.Html ())
+tasksPage maybeQuery maybeStatus maybePriority maybeNamespace maybeType maybeSort = do
   allTasks <- liftIO Task.loadTasks
-  let filtered = filterTasks allTasks maybeQuery maybeStatus
+  let filtered = applyFilters allTasks maybeQuery maybeStatus maybePriority maybeNamespace maybeType
+      sorted = sortTasks maybeSort filtered
       statusCounts = countByStatus allTasks
-  pure <| Layout.pageHtml Layout.Page
-    { Layout.pageTitle = "Tasks"
-    , Layout.pageTab = Layout.NavTasks
-    , Layout.pageBody = do
-        Lucid.h1_ [Lucid.class_ "mb-4"] "Task Dashboard"
-        statsBar statusCounts
-        searchForm maybeQuery maybeStatus
-        if null filtered
-          then Lucid.div_ [Lucid.class_ "empty"] "No tasks found"
-          else do
-            Lucid.p_ [Lucid.class_ "text-muted text-small mb-4"] <|
-              Lucid.toHtml ("Showing " <> tshow (length filtered) <> " tasks")
-            tasksTable filtered
-    }
-
--- | Stats bar showing counts by status
-statsBar :: [(Text, Int)] -> Lucid.Html ()
-statsBar counts = Lucid.div_ [Lucid.class_ "search-bar", Lucid.style_ "margin-bottom: 16px"] <| do
-  forM_ counts <| \(status, count) -> do
-    let badgeClass = case status of
-          "Open" -> "badge badge-running"
-          "InProgress" -> "badge badge-running"
-          "Done" -> "badge badge-done"
-          "NeedsHelp" -> "badge badge-error"
-          _ -> "badge"
-    Lucid.a_ [Lucid.href_ ("/tasks?status=" <> status), Lucid.style_ "margin-right: 12px; text-decoration: none"] <| do
-      Lucid.span_ [Lucid.class_ badgeClass] <| Lucid.toHtml (status <> ": " <> tshow count)
-
--- | Search form
-searchForm :: Maybe Text -> Maybe Text -> Lucid.Html ()
-searchForm query status = 
+  pure
+    <| Layout.pageHtml
+      Layout.Page
+        { Layout.pageTitle = "Tasks",
+          Layout.pageTab = Layout.NavTasks,
+          Layout.pageBody = do
+            Lucid.h1_ [Lucid.class_ "mb-4"] "Task Dashboard"
+            Lucid.p_ [Lucid.class_ "text-muted mb-4"] "Studio now includes a richer task index and detail view plus a parity roadmap versus task web."
+
+            parityCard
+            improvementPlanCard
+            statsBar statusCounts
+            searchForm maybeQuery maybeStatus maybePriority maybeNamespace maybeType maybeSort
+
+            if null sorted
+              then Lucid.div_ [Lucid.class_ "empty"] "No tasks found"
+              else do
+                Lucid.p_ [Lucid.class_ "text-muted text-small mb-4"] <|
+                  Lucid.toHtml ("Showing " <> tshow (length sorted) <> " tasks")
+                tasksTable sorted
+        }
+
+parityCard :: Lucid.Html ()
+parityCard =
+  Lucid.div_ [Lucid.class_ "card"] <| do
+    Lucid.div_ [Lucid.class_ "card-title"] "Feature parity snapshot: task web vs Studio"
+    Lucid.table_ [Lucid.class_ "table"] <| do
+      Lucid.thead_ <| Lucid.tr_ <| do
+        Lucid.th_ "Feature"
+        Lucid.th_ "task web"
+        Lucid.th_ "Studio"
+      Lucid.tbody_ <| do
+        row "Task list view" "Yes" "Yes"
+        row "Task detail view" "Yes" "Yes"
+        row "Create task" "No" "No"
+        row "Edit task" "Yes (status/priority/complexity/description)" "No"
+        row "Task comments" "Yes (timeline + add comment)" "Read-only"
+        row "Epic/subtask tree" "Yes" "Partial (parent + children)"
+        row "Status filtering" "Yes" "Yes"
+        row "Priority display" "Yes" "Yes"
+        row "Search" "Partial" "Yes"
+        row "Markdown rendering" "Yes" "Yes"
+        row "Mobile responsive" "Yes" "Partial"
+  where
+    row feature web studio = Lucid.tr_ <| do
+      Lucid.td_ (Lucid.toHtml feature)
+      Lucid.td_ (Lucid.toHtml web)
+      Lucid.td_ (Lucid.toHtml studio)
+
+improvementPlanCard :: Lucid.Html ()
+improvementPlanCard =
+  Lucid.div_ [Lucid.class_ "card"] <| do
+    Lucid.div_ [Lucid.class_ "card-title"] "Prioritized improvement plan"
+    Lucid.ol_ [Lucid.style_ "padding-left: 20px; margin: 0;"] <| do
+      item "P0" "Add in-Studio task creation/edit flows" "High impact, medium effort (~1-2 days)"
+      item "P0" "Support comment authoring and status/priority edits in detail" "High impact, medium effort (~1 day)"
+      item "P1" "Add dedicated views for ready/blocked/intervention queues" "Medium impact, medium effort (~1 day)"
+      item "P1" "Add reusable task tree component for epic/subtask navigation" "Medium impact, medium effort (~1 day)"
+      item "P2" "Align Studio task styling/components with task web for consistency" "Medium impact, low-medium effort (~0.5-1 day)"
+  where
+    item pri label effort =
+      Lucid.li_ [Lucid.style_ "margin-bottom: 10px;"] <| do
+        Lucid.span_ [Lucid.class_ "badge", Lucid.style_ "margin-right: 8px;"] (Lucid.toHtml pri)
+        Lucid.span_ (Lucid.toHtml label)
+        Lucid.div_ [Lucid.class_ "text-small", Lucid.style_ "margin-top: 4px;"] (Lucid.toHtml effort)
+
+-- | Stats bar showing counts by status.
+statsBar :: [(Task.Status, Int)] -> Lucid.Html ()
+statsBar counts =
+  Lucid.div_ [Lucid.class_ "search-bar", Lucid.style_ "margin-bottom: 16px"] <| do
+    forM_ counts <| \(statusVal, count) -> do
+      let statusText = tshow statusVal
+      Lucid.a_
+        [ Lucid.href_ ("/tasks?status=" <> statusText),
+          Lucid.style_ "margin-right: 10px; text-decoration: none"
+        ]
+        <| Lucid.span_ [Lucid.class_ (statusBadgeClass statusVal)] <|
+          Lucid.toHtml (statusLabel statusVal <> ": " <> tshow count)
+
+-- | Search/filter form.
+searchForm ::
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Lucid.Html ()
+searchForm maybeQuery maybeStatus maybePriority maybeNamespace maybeType maybeSort =
   Lucid.form_ [Lucid.method_ "get", Lucid.action_ "/tasks", Lucid.class_ "search-bar"] <| do
     Lucid.input_
-      [ Lucid.class_ "search-input"
-      , Lucid.name_ "q"
-      , Lucid.placeholder_ "Search tasks..."
-      , Lucid.value_ (fromMaybe "" query)
+      [ Lucid.class_ "search-input",
+        Lucid.name_ "q",
+        Lucid.placeholder_ "Search by ID, title, description...",
+        Lucid.value_ (fromMaybe "" maybeQuery)
       ]
-    Lucid.select_ [Lucid.class_ "search-input", Lucid.name_ "status", Lucid.style_ "max-width: 150px"] <| do
+
+    Lucid.select_ [Lucid.class_ "search-input", Lucid.name_ "status", Lucid.style_ "max-width: 190px"] <| do
       Lucid.option_ [Lucid.value_ ""] "All statuses"
-      forM_ ["Open", "InProgress", "Done", "NeedsHelp"] <| \s ->
-        if Just s == status
-          then Lucid.option_ [Lucid.value_ s, Lucid.selected_ "selected"] (Lucid.toHtml s)
-          else Lucid.option_ [Lucid.value_ s] (Lucid.toHtml s)
-    Lucid.button_ [Lucid.class_ "btn", Lucid.type_ "submit"] "Search"
+      forM_ allStatuses <| \s ->
+        Lucid.option_
+          ([Lucid.value_ (tshow s)] <> selectedWhen (Just (tshow s) == maybeStatus))
+          (Lucid.toHtml (statusLabel s))
+
+    Lucid.select_ [Lucid.class_ "search-input", Lucid.name_ "priority", Lucid.style_ "max-width: 140px"] <| do
+      Lucid.option_ [Lucid.value_ ""] "All priorities"
+      forM_ allPriorities <| \p ->
+        Lucid.option_
+          ([Lucid.value_ (tshow p)] <> selectedWhen (Just (tshow p) == maybePriority))
+          (Lucid.toHtml (tshow p))
+
+    Lucid.select_ [Lucid.class_ "search-input", Lucid.name_ "type", Lucid.style_ "max-width: 130px"] <| do
+      Lucid.option_ [Lucid.value_ ""] "All types"
+      Lucid.option_ ([Lucid.value_ "task"] <> selectedWhen (maybeType == Just "task")) "Task"
+      Lucid.option_ ([Lucid.value_ "epic"] <> selectedWhen (maybeType == Just "epic")) "Epic"
+
+    Lucid.input_
+      [ Lucid.class_ "search-input",
+        Lucid.name_ "namespace",
+        Lucid.placeholder_ "Namespace",
+        Lucid.value_ (fromMaybe "" maybeNamespace),
+        Lucid.style_ "max-width: 220px"
+      ]
 
--- | Tasks table
+    Lucid.select_ [Lucid.class_ "search-input", Lucid.name_ "sort", Lucid.style_ "max-width: 220px"] <| do
+      optionSort "updated" "Recently updated"
+      optionSort "created-desc" "Newest created"
+      optionSort "created-asc" "Oldest created"
+      optionSort "priority-high" "Priority high → low"
+      optionSort "priority-low" "Priority low → high"
+      optionSort "status" "Status"
+
+    Lucid.button_ [Lucid.class_ "btn", Lucid.type_ "submit"] "Filter"
+    Lucid.a_ [Lucid.class_ "btn btn-secondary", Lucid.href_ "/tasks"] "Clear"
+  where
+    selectedWhen cond = [Lucid.selected_ "selected" | cond]
+    optionSort val label =
+      Lucid.option_ ([Lucid.value_ val] <> selectedWhen (fromMaybe "updated" maybeSort == val)) (Lucid.toHtml label)
+
+-- | Tasks table.
 tasksTable :: [Task.Task] -> Lucid.Html ()
-tasksTable tasks = Lucid.table_ [Lucid.class_ "table"] <| do
-  Lucid.thead_ <| Lucid.tr_ <| do
-    Lucid.th_ "ID"
-    Lucid.th_ "Title"
-    Lucid.th_ "Status"
-    Lucid.th_ "Priority"
-    Lucid.th_ "Namespace"
-  Lucid.tbody_ <| forM_ tasks renderTaskRow
-
--- | Render task row
+tasksTable tasks =
+  Lucid.table_ [Lucid.class_ "table"] <| do
+    Lucid.thead_ <| Lucid.tr_ <| do
+      Lucid.th_ "ID"
+      Lucid.th_ "Title"
+      Lucid.th_ "Type"
+      Lucid.th_ "Status"
+      Lucid.th_ "Priority"
+      Lucid.th_ "Namespace"
+      Lucid.th_ "Updated"
+    Lucid.tbody_ <| forM_ tasks renderTaskRow
+
+-- | Render task row.
 renderTaskRow :: Task.Task -> Lucid.Html ()
-renderTaskRow t = Lucid.tr_ <| do
-  Lucid.td_ <| Lucid.a_ [Lucid.href_ ("/tasks/" <> Task.taskId t)] 
-    (Lucid.toHtml <| Task.taskId t)
-  Lucid.td_ <| Lucid.toHtml (Task.taskTitle t)
-  Lucid.td_ <| statusBadge (Task.taskStatus t)
-  Lucid.td_ <| priorityBadge (Task.taskPriority t)
-  Lucid.td_ [Lucid.class_ "text-muted text-small"] <| 
-    Lucid.toHtml (fromMaybe "-" <| Task.taskNamespace t)
-
--- | Status badge
+renderTaskRow t =
+  Lucid.tr_ <| do
+    Lucid.td_ <| Lucid.a_ [Lucid.href_ ("/tasks/" <> Task.taskId t)] (Lucid.toHtml (Task.taskId t))
+    Lucid.td_ (Lucid.toHtml (Task.taskTitle t))
+    Lucid.td_ [Lucid.class_ "text-muted text-small"] (Lucid.toHtml (taskTypeLabel (Task.taskType t)))
+    Lucid.td_ (statusBadge (Task.taskStatus t))
+    Lucid.td_ (priorityBadge (Task.taskPriority t))
+    Lucid.td_ [Lucid.class_ "text-muted text-small"] (Lucid.toHtml (fromMaybe "-" (Task.taskNamespace t)))
+    Lucid.td_ [Lucid.class_ "text-muted text-small"] (Lucid.toHtml (formatTimestamp (Task.taskUpdatedAt t)))
+
+-- | Status badge.
 statusBadge :: Task.Status -> Lucid.Html ()
-statusBadge status = case status of
-  Task.Open -> Lucid.span_ [Lucid.class_ "badge badge-running"] "Open"
-  Task.InProgress -> Lucid.span_ [Lucid.class_ "badge badge-running"] "In Progress"
-  Task.Done -> Lucid.span_ [Lucid.class_ "badge badge-done"] "Done"
-  Task.NeedsHelp -> Lucid.span_ [Lucid.class_ "badge badge-error"] "Needs Help"
-  Task.Draft -> Lucid.span_ [Lucid.class_ "badge"] "Draft"
-  Task.Review -> Lucid.span_ [Lucid.class_ "badge badge-running"] "Review"
-  Task.Approved -> Lucid.span_ [Lucid.class_ "badge badge-done"] "Approved"
-
--- | Priority badge
+statusBadge statusVal = Lucid.span_ [Lucid.class_ (statusBadgeClass statusVal)] (Lucid.toHtml (statusLabel statusVal))
+
+statusBadgeClass :: Task.Status -> Text
+statusBadgeClass = \case
+  Task.Open -> "badge badge-open"
+  Task.Draft -> "badge"
+  Task.InProgress -> "badge badge-running"
+  Task.Review -> "badge badge-running"
+  Task.ReviewInProgress -> "badge badge-running"
+  Task.Approved -> "badge badge-done"
+  Task.Integrating -> "badge badge-running"
+  Task.Verified -> "badge badge-done"
+  Task.Done -> "badge badge-done"
+  Task.NeedsHelp -> "badge badge-error"
+
+statusLabel :: Task.Status -> Text
+statusLabel = \case
+  Task.InProgress -> "In Progress"
+  Task.ReviewInProgress -> "Review In Progress"
+  Task.NeedsHelp -> "Needs Help"
+  Task.Open -> "Open"
+  Task.Draft -> "Draft"
+  Task.Review -> "Review"
+  Task.Approved -> "Approved"
+  Task.Integrating -> "Integrating"
+  Task.Verified -> "Verified"
+  Task.Done -> "Done"
+
+-- | Priority badge.
 priorityBadge :: Task.Priority -> Lucid.Html ()
-priorityBadge pri = case pri of
-  Task.P0 -> Lucid.span_ [Lucid.class_ "badge badge-error"] "P0"
-  Task.P1 -> Lucid.span_ [Lucid.class_ "badge badge-error", Lucid.style_ "background: #da3633"] "P1"
-  Task.P2 -> Lucid.span_ [Lucid.class_ "badge badge-running"] "P2"
-  Task.P3 -> Lucid.span_ [Lucid.class_ "badge"] "P3"
-  Task.P4 -> Lucid.span_ [Lucid.class_ "badge"] "P4"
-
--- | Task detail page
+priorityBadge pri =
+  case pri of
+    Task.P0 -> Lucid.span_ [Lucid.class_ "badge badge-error"] "P0"
+    Task.P1 -> Lucid.span_ [Lucid.class_ "badge badge-error", Lucid.style_ "background: #da3633"] "P1"
+    Task.P2 -> Lucid.span_ [Lucid.class_ "badge badge-running"] "P2"
+    Task.P3 -> Lucid.span_ [Lucid.class_ "badge"] "P3"
+    Task.P4 -> Lucid.span_ [Lucid.class_ "badge"] "P4"
+
+-- | Task detail page.
 taskDetailPage :: Text -> Servant.Handler (Lucid.Html ())
 taskDetailPage taskIdVal = do
   allTasks <- liftIO Task.loadTasks
   let maybeTask = Task.findTask taskIdVal allTasks
-  pure <| Layout.pageHtml Layout.Page
-    { Layout.pageTitle = "Task Detail"
-    , Layout.pageTab = Layout.NavTasks
-    , Layout.pageBody = case maybeTask of
-        Nothing -> do
-          Lucid.h1_ "Task Not Found"
-          Lucid.p_ [Lucid.class_ "text-muted"] <| Lucid.toHtml ("ID: " <> taskIdVal)
-          Lucid.a_ [Lucid.href_ "/tasks"] "← Back to tasks"
-        Just t -> do
-          Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "text-muted"] "← Back to tasks"
-          Lucid.h1_ [Lucid.class_ "mt-4 mb-4"] <| do
-            statusBadge (Task.taskStatus t)
-            Lucid.span_ [Lucid.style_ "margin-left: 12px"] (Lucid.toHtml <| Task.taskTitle t)
-          Lucid.div_ [Lucid.class_ "card"] <| do
-            Lucid.div_ [Lucid.class_ "card-title"] "Description"
-            case Task.taskDescription t of
-              "" -> Lucid.p_ [Lucid.class_ "text-muted"] "(No description)"
-              desc -> Lucid.pre_ [Lucid.style_ "white-space: pre-wrap"] (Lucid.toHtml desc)
-          Lucid.div_ [Lucid.class_ "card mt-4"] <| do
-            Lucid.div_ [Lucid.class_ "card-title"] "Details"
-            Lucid.table_ [Lucid.class_ "table"] <| do
-              metaRow "ID" (Task.taskId t)
-              metaRow "Status" (tshow <| Task.taskStatus t)
-              metaRow "Priority" (tshow <| Task.taskPriority t)
-              metaRow "Type" (tshow <| Task.taskType t)
-              metaRow "Namespace" (fromMaybe "-" <| Task.taskNamespace t)
-              metaRow "Parent" (fromMaybe "-" <| Task.taskParent t)
-              metaRow "Created" (tshow <| Task.taskCreatedAt t)
-    }
+      childrenFor tid = filter (maybe False (Task.matchesId tid) <. Task.taskParent) allTasks
+  pure
+    <| Layout.pageHtml
+      Layout.Page
+        { Layout.pageTitle = "Task Detail",
+          Layout.pageTab = Layout.NavTasks,
+          Layout.pageBody = case maybeTask of
+            Nothing -> do
+              Lucid.h1_ "Task Not Found"
+              Lucid.p_ [Lucid.class_ "text-muted"] <| Lucid.toHtml ("ID: " <> taskIdVal)
+              Lucid.a_ [Lucid.href_ "/tasks"] "← Back to tasks"
+            Just t -> do
+              let children = childrenFor (Task.taskId t)
+
+              Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "text-muted"] "← Back to tasks"
+              Lucid.h1_ [Lucid.class_ "mt-4 mb-4"] <| do
+                statusBadge (Task.taskStatus t)
+                Lucid.span_ [Lucid.style_ "margin-left: 12px"] (Lucid.toHtml (Task.taskTitle t))
+
+              Lucid.div_ [Lucid.class_ "card"] <| do
+                Lucid.div_ [Lucid.class_ "card-title"] "Description"
+                if Text.null (Text.strip (Task.taskDescription t))
+                  then Lucid.p_ [Lucid.class_ "text-muted"] "(No description)"
+                  else Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown (Task.taskDescription t))
+
+              Lucid.div_ [Lucid.class_ "card mt-4"] <| do
+                Lucid.div_ [Lucid.class_ "card-title"] "Details"
+                Lucid.table_ [Lucid.class_ "table"] <| do
+                  metaRow "ID" (Task.taskId t)
+                  metaRow "Status" (statusLabel (Task.taskStatus t))
+                  metaRow "Priority" (tshow (Task.taskPriority t))
+                  metaRow "Type" (taskTypeLabel (Task.taskType t))
+                  metaRow "Namespace" (fromMaybe "-" (Task.taskNamespace t))
+                  metaRow "Parent" (fromMaybe "-" (Task.taskParent t))
+                  metaRow "Created" (formatTimestamp (Task.taskCreatedAt t))
+                  metaRow "Updated" (formatTimestamp (Task.taskUpdatedAt t))
+
+              unless (null (Task.taskDependencies t)) <| do
+                Lucid.div_ [Lucid.class_ "card mt-4"] <| do
+                  Lucid.div_ [Lucid.class_ "card-title"] "Dependencies"
+                  Lucid.ul_ <| do
+                    forM_ (Task.taskDependencies t) <| \dep ->
+                      Lucid.li_ <| do
+                        Lucid.a_ [Lucid.href_ ("/tasks/" <> Task.depId dep)] (Lucid.toHtml (Task.depId dep))
+                        Lucid.span_ [Lucid.class_ "text-muted", Lucid.style_ "margin-left: 8px;"]
+                          (Lucid.toHtml ("[" <> tshow (Task.depType dep) <> "]"))
+
+              unless (null children) <| do
+                Lucid.div_ [Lucid.class_ "card mt-4"] <| do
+                  Lucid.div_ [Lucid.class_ "card-title"] "Child tasks"
+                  Lucid.ul_ <| do
+                    forM_ children <| \child ->
+                      Lucid.li_ <| do
+                        Lucid.a_ [Lucid.href_ ("/tasks/" <> Task.taskId child)] (Lucid.toHtml (Task.taskId child))
+                        Lucid.span_ [Lucid.style_ "margin-left: 8px;"] (Lucid.toHtml (Task.taskTitle child))
+
+              Lucid.div_ [Lucid.class_ "card mt-4"] <| do
+                Lucid.div_ [Lucid.class_ "card-title"] "Comments"
+                if null (Task.taskComments t)
+                  then Lucid.p_ [Lucid.class_ "text-muted"] "No comments yet"
+                  else
+                    traverse_ renderComment (reverse (Task.taskComments t))
+        }
+
+renderComment :: Task.Comment -> Lucid.Html ()
+renderComment c =
+  Lucid.div_
+    [ Lucid.style_
+        "background: #0d1117; border: 1px solid #21262d; border-radius: 8px; padding: 12px; margin-bottom: 10px;"
+    ]
+    <| do
+      Lucid.div_ [Lucid.class_ "text-small", Lucid.style_ "margin-bottom: 8px;"] <| do
+        Lucid.span_ [Lucid.style_ "font-weight: 600; margin-right: 8px;"] (Lucid.toHtml (authorLabel (Task.commentAuthor c)))
+        Lucid.span_ [Lucid.class_ "text-muted"] (Lucid.toHtml (formatTimestamp (Task.commentCreatedAt c)))
+      Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown (Task.commentText c))
+
+authorLabel :: Task.CommentAuthor -> Text
+authorLabel = \case
+  Task.Human -> "Human"
+  Task.System -> "System"
+  Task.Agent role -> "Agent(" <> tshow role <> ")"
 
 metaRow :: Text -> Text -> Lucid.Html ()
-metaRow label value = Lucid.tr_ <| do
-  Lucid.td_ [Lucid.style_ "width: 150px; color: #8b949e;"] (Lucid.toHtml label)
-  Lucid.td_ (Lucid.toHtml value)
-
--- | Filter tasks by query and status
-filterTasks :: [Task.Task] -> Maybe Text -> Maybe Text -> [Task.Task]
-filterTasks tasks maybeQuery maybeStatus =
-  let byStatus = case maybeStatus of
-        Just "Open" -> filter (\t -> Task.taskStatus t == Task.Open) tasks
-        Just "InProgress" -> filter (\t -> Task.taskStatus t == Task.InProgress) tasks
-        Just "Done" -> filter (\t -> Task.taskStatus t == Task.Done) tasks
-        Just "NeedsHelp" -> filter (\t -> Task.taskStatus t == Task.NeedsHelp) tasks
-        _ -> tasks
-      byQuery = case maybeQuery of
-        Just q | not (Text.null q) -> 
-          filter (\t -> Text.isInfixOf (Text.toLower q) (Text.toLower <| Task.taskTitle t)) byStatus
-        _ -> byStatus
-  in take 100 byQuery
-
--- | Count tasks by status
-countByStatus :: [Task.Task] -> [(Text, Int)]
+metaRow label value =
+  Lucid.tr_ <| do
+    Lucid.td_ [Lucid.style_ "width: 170px; color: #8b949e;"] (Lucid.toHtml label)
+    Lucid.td_ (Lucid.toHtml value)
+
+-- | Filter tasks by query, status, priority, namespace, and type.
+applyFilters ::
+  [Task.Task] ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  Maybe Text ->
+  [Task.Task]
+applyFilters tasks maybeQuery maybeStatus maybePriority maybeNamespace maybeType =
+  filter matches tasks
+  where
+    queryNeedle = Text.toCaseFold </ nonEmpty maybeQuery
+    statusFilter = parseStatus =<< nonEmpty maybeStatus
+    priorityFilter = parsePriority =<< nonEmpty maybePriority
+    typeFilter = parseTaskType =<< nonEmpty maybeType
+    namespaceNeedle = Text.toCaseFold </ nonEmpty maybeNamespace
+
+    matches t =
+      matchesQuery t
+        && matchesStatus t
+        && matchesPriority t
+        && matchesNamespace t
+        && matchesType t
+
+    matchesQuery t = case queryNeedle of
+      Nothing -> True
+      Just needle ->
+        let haystack =
+              Text.toCaseFold
+                <| Text.intercalate
+                  " "
+                  [ Task.taskId t,
+                    Task.taskTitle t,
+                    Task.taskDescription t,
+                    fromMaybe "" (Task.taskNamespace t),
+                    fromMaybe "" (Task.taskParent t)
+                  ]
+         in needle `Text.isInfixOf` haystack
+
+    matchesStatus t = maybe True (== Task.taskStatus t) statusFilter
+
+    matchesPriority t = maybe True (== Task.taskPriority t) priorityFilter
+
+    matchesNamespace t = case namespaceNeedle of
+      Nothing -> True
+      Just needle ->
+        maybe False (Text.isInfixOf needle <. Text.toCaseFold) (Task.taskNamespace t)
+
+    matchesType t = maybe True (== Task.taskType t) typeFilter
+
+-- | Sort task list according to requested sort key.
+sortTasks :: Maybe Text -> [Task.Task] -> [Task.Task]
+sortTasks maybeSort =
+  case fromMaybe "updated" maybeSort of
+    "created-asc" -> List.sortBy (comparing Task.taskCreatedAt)
+    "created-desc" -> List.sortBy (comparing (Down <. Task.taskCreatedAt))
+    "priority-high" -> List.sortBy (comparing Task.taskPriority <> comparing (Down <. Task.taskUpdatedAt))
+    "priority-low" -> List.sortBy (comparing (Down <. Task.taskPriority) <> comparing (Down <. Task.taskUpdatedAt))
+    "status" -> List.sortBy (comparing (statusOrder <. Task.taskStatus) <> comparing (Down <. Task.taskUpdatedAt))
+    _ -> List.sortBy (comparing (Down <. Task.taskUpdatedAt))
+
+countByStatus :: [Task.Task] -> [(Task.Status, Int)]
 countByStatus tasks =
-  [ ("Open", length <| filter (\t -> Task.taskStatus t == Task.Open) tasks)
-  , ("InProgress", length <| filter (\t -> Task.taskStatus t == Task.InProgress) tasks)
-  , ("Done", length <| filter (\t -> Task.taskStatus t == Task.Done) tasks)
-  , ("NeedsHelp", length <| filter (\t -> Task.taskStatus t == Task.NeedsHelp) tasks)
+  [ (statusVal, length <| filter ((== statusVal) <. Task.taskStatus) tasks)
+    | statusVal <- allStatuses
+  ]
+
+allStatuses :: [Task.Status]
+allStatuses =
+  [ Task.Draft,
+    Task.Open,
+    Task.InProgress,
+    Task.Review,
+    Task.ReviewInProgress,
+    Task.Approved,
+    Task.Integrating,
+    Task.Verified,
+    Task.Done,
+    Task.NeedsHelp
   ]
+
+allPriorities :: [Task.Priority]
+allPriorities = [Task.P0, Task.P1, Task.P2, Task.P3, Task.P4]
+
+statusOrder :: Task.Status -> Int
+statusOrder = \case
+  Task.NeedsHelp -> 0
+  Task.Review -> 1
+  Task.ReviewInProgress -> 2
+  Task.InProgress -> 3
+  Task.Open -> 4
+  Task.Draft -> 5
+  Task.Approved -> 6
+  Task.Integrating -> 7
+  Task.Verified -> 8
+  Task.Done -> 9
+
+parseStatus :: Text -> Maybe Task.Status
+parseStatus = readMaybe <. Text.unpack
+
+parsePriority :: Text -> Maybe Task.Priority
+parsePriority = readMaybe <. Text.unpack
+
+parseTaskType :: Text -> Maybe Task.TaskType
+parseTaskType raw =
+  case Text.toCaseFold raw of
+    "epic" -> Just Task.Epic
+    "task" -> Just Task.WorkTask
+    "worktask" -> Just Task.WorkTask
+    other -> readMaybe (Text.unpack other)
+
+nonEmpty :: Maybe Text -> Maybe Text
+nonEmpty = \case
+  Nothing -> Nothing
+  Just t
+    | Text.null (Text.strip t) -> Nothing
+    | otherwise -> Just (Text.strip t)
+
+taskTypeLabel :: Task.TaskType -> Text
+taskTypeLabel Task.Epic = "Epic"
+taskTypeLabel Task.WorkTask = "Task"
+
+formatTimestamp :: UTCTime -> Text
+formatTimestamp ts = Text.pack (Time.formatTime Time.defaultTimeLocale "%Y-%m-%d %H:%M UTC" ts)
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index b5a3ef9a..d5ac4993 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -5,6 +5,7 @@
 {-# LANGUAGE NoImplicitPrelude #-}
 
 -- : dep http-api-data
+-- : dep sqlite-simple
 module Omni.Task.Core where
 
 import Alpha