diff --git a/desktop/src/components/chat/SessionTaskBar.tsx b/desktop/src/components/chat/SessionTaskBar.tsx
new file mode 100644
index 00000000..c798586f
--- /dev/null
+++ b/desktop/src/components/chat/SessionTaskBar.tsx
@@ -0,0 +1,130 @@
+import { useCLITaskStore } from '../../stores/cliTaskStore'
+import type { CLITask } from '../../types/cliTask'
+
+const statusConfig = {
+ pending: {
+ icon: 'radio_button_unchecked',
+ color: 'var(--color-text-tertiary)',
+ label: 'pending',
+ },
+ in_progress: {
+ icon: 'pending',
+ color: 'var(--color-warning)',
+ label: 'active',
+ },
+ completed: {
+ icon: 'check_circle',
+ color: 'var(--color-success)',
+ label: 'done',
+ },
+} as const
+
+export function SessionTaskBar() {
+ const { tasks, expanded, toggleExpanded } = useCLITaskStore()
+
+ if (tasks.length === 0) return null
+
+ const completedCount = tasks.filter((t) => t.status === 'completed').length
+ const totalCount = tasks.length
+ const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
+
+ return (
+
+ {/* Header — always visible, clickable to toggle */}
+
+
+ {/* Expanded task list */}
+ {expanded && (
+
+ {tasks.map((task) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+function TaskItem({ task }: { task: CLITask }) {
+ const config = statusConfig[task.status]
+
+ return (
+
+
+ {config.icon}
+
+
+
+
+
+ #{task.id}
+
+
+ {task.subject}
+
+
+
+ {task.status === 'in_progress' && task.activeForm && (
+
+
+
+ {task.activeForm}
+
+
+ )}
+
+ {task.owner && (
+
+ person
+ {task.owner}
+
+ )}
+
+
+ )
+}
diff --git a/desktop/src/components/layout/ContentRouter.tsx b/desktop/src/components/layout/ContentRouter.tsx
index 7e68c8a3..3282d476 100644
--- a/desktop/src/components/layout/ContentRouter.tsx
+++ b/desktop/src/components/layout/ContentRouter.tsx
@@ -4,7 +4,6 @@ import { useTeamStore } from '../../stores/teamStore'
import { EmptySession } from '../../pages/EmptySession'
import { ActiveSession } from '../../pages/ActiveSession'
import { ScheduledTasks } from '../../pages/ScheduledTasks'
-import { Tasks } from '../../pages/Tasks'
import { Settings } from '../../pages/Settings'
import { AgentTranscript } from '../../pages/AgentTranscript'
@@ -21,10 +20,6 @@ export function ContentRouter() {
return
}
- if (activeView === 'tasks') {
- return
- }
-
if (activeView === 'terminal') {
return
}
diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx
index a402370b..0c84db6d 100644
--- a/desktop/src/components/layout/Sidebar.tsx
+++ b/desktop/src/components/layout/Sidebar.tsx
@@ -86,13 +86,6 @@ export function Sidebar() {
>
Scheduled
- setActiveView('tasks')}
- icon={}
- >
- Tasks
-
{/* Project filter */}
@@ -288,11 +281,3 @@ function ClockIcon() {
)
}
-function TaskIcon() {
- return (
-
- )
-}
diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx
index 64db4b76..f8a30c1e 100644
--- a/desktop/src/pages/ActiveSession.tsx
+++ b/desktop/src/pages/ActiveSession.tsx
@@ -4,6 +4,7 @@ import { useChatStore } from '../stores/chatStore'
import { MessageList } from '../components/chat/MessageList'
import { ChatInput } from '../components/chat/ChatInput'
import { TeamStatusBar } from '../components/teams/TeamStatusBar'
+import { SessionTaskBar } from '../components/chat/SessionTaskBar'
export function ActiveSession() {
const activeSessionId = useSessionStore((s) => s.activeSessionId)
@@ -79,6 +80,9 @@ export function ActiveSession() {
{/* Message stream */}
+ {/* Session task bar — sticky at bottom */}
+
+
{/* Team status bar */}
diff --git a/desktop/src/pages/Tasks.tsx b/desktop/src/pages/Tasks.tsx
deleted file mode 100644
index 9dd19cc8..00000000
--- a/desktop/src/pages/Tasks.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import { useEffect } from 'react'
-import { useCLITaskStore } from '../stores/cliTaskStore'
-import type { CLITask, TaskListSummary } from '../types/cliTask'
-
-export function Tasks() {
- const { taskLists, selectedListId, tasks, isLoading, fetchTaskLists, selectTaskList, clearSelection } = useCLITaskStore()
-
- useEffect(() => { fetchTaskLists() }, [fetchTaskLists])
-
- return (
-
- {/* Sidebar: task lists */}
-
-
-
Task Lists
-
CLI task tracking sessions
-
-
- {isLoading && taskLists.length === 0 ? (
-
- ) : taskLists.length === 0 ? (
-
-
task_alt
-
No task lists found
-
- ) : (
- taskLists.map((list) => (
-
selectTaskList(list.id)}
- />
- ))
- )}
-
-
-
- {/* Main: task details */}
-
- {selectedListId ? (
-
- ) : (
-
-
checklist
-
Select a task list to view tasks
-
{taskLists.length} task list{taskLists.length !== 1 ? 's' : ''} available
-
- )}
-
-
- )
-}
-
-// ─── Task List Item ─────────────────────────────────────────
-
-function TaskListItem({ list, selected, onClick }: { list: TaskListSummary; selected: boolean; onClick: () => void }) {
- const isTeamOrNamed = !list.id.match(/^[0-9a-f]{8}-/)
- const displayName = isTeamOrNamed ? list.id : list.id.slice(0, 8) + '...'
-
- const progressPercent = list.taskCount > 0
- ? Math.round((list.completedCount / list.taskCount) * 100)
- : 0
-
- return (
-
- )
-}
-
-// ─── Task List Detail ──────────────────────────────────────
-
-function TaskListDetail({ listId, tasks, isLoading, onBack }: {
- listId: string
- tasks: CLITask[]
- isLoading: boolean
- onBack: () => void
-}) {
- const completedCount = tasks.filter((t) => t.status === 'completed').length
- const inProgressCount = tasks.filter((t) => t.status === 'in_progress').length
- const pendingCount = tasks.filter((t) => t.status === 'pending').length
-
- return (
-
- {/* Header */}
-
-
-
-
{listId}
-
- {tasks.length} tasks
- {completedCount > 0 && }
- {inProgressCount > 0 && }
- {pendingCount > 0 && }
-
-
-
-
- {/* Task list */}
- {isLoading ? (
-
- ) : tasks.length === 0 ? (
-
-
No tasks in this list
-
- ) : (
-
- {tasks.map((task) => (
-
- ))}
-
- )}
-
- )
-}
-
-// ─── Task Card ──────────────────────────────────────────────
-
-function TaskCard({ task, allTasks }: { task: CLITask; allTasks: CLITask[] }) {
- const statusConfig = {
- pending: { icon: 'radio_button_unchecked', color: 'text-[var(--color-text-tertiary)]', bg: '' },
- in_progress: { icon: 'pending', color: 'text-[var(--color-warning)]', bg: 'bg-amber-50/50' },
- completed: { icon: 'check_circle', color: 'text-[var(--color-success)]', bg: '' },
- }
-
- const config = statusConfig[task.status]
-
- // Resolve blocker names
- const blockerNames = task.blockedBy
- .map((id) => allTasks.find((t) => t.id === id))
- .filter(Boolean)
- .map((t) => `#${t!.id} ${t!.subject}`)
-
- const blockingNames = task.blocks
- .map((id) => allTasks.find((t) => t.id === id))
- .filter(Boolean)
- .map((t) => `#${t!.id}`)
-
- return (
-
- {/* Status icon */}
-
- {config.icon}
-
-
- {/* Content */}
-
-
- #{task.id}
- {task.subject}
-
-
- {task.description && (
-
{task.description}
- )}
-
- {/* Meta row */}
-
- {task.owner && (
-
- person
- {task.owner}
-
- )}
- {task.blockedBy.length > 0 && (
-
- block
- blocked by {task.blockedBy.map((id) => `#${id}`).join(', ')}
-
- )}
- {task.blocks.length > 0 && (
-
- arrow_forward
- blocks {blockingNames.join(', ')}
-
- )}
-
-
-
- )
-}
-
-// ─── Status Badge ───────────────────────────────────────────
-
-function StatusBadge({ status, count }: { status: string; count: number }) {
- const styles: Record = {
- completed: 'text-[var(--color-success)]',
- in_progress: 'text-[var(--color-warning)]',
- pending: 'text-[var(--color-text-tertiary)]',
- }
-
- const labels: Record = {
- completed: 'done',
- in_progress: 'active',
- pending: 'pending',
- }
-
- return (
-
- {count} {labels[status] || status}
-
- )
-}
diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts
index 03f12b2c..4b3cbbb1 100644
--- a/desktop/src/stores/chatStore.ts
+++ b/desktop/src/stores/chatStore.ts
@@ -2,6 +2,7 @@ import { create } from 'zustand'
import { wsManager } from '../api/websocket'
import { sessionsApi } from '../api/sessions'
import { useTeamStore } from './teamStore'
+import { useCLITaskStore } from './cliTaskStore'
import type { MessageEntry } from '../types/session'
import type { PermissionMode } from '../types/settings'
import type { AttachmentRef, ChatState, UIAttachment, UIMessage, ServerMessage, TokenUsage } from '../types/chat'
@@ -41,6 +42,11 @@ type ChatStore = {
handleServerMessage: (msg: ServerMessage) => void
}
+const TASK_TOOL_NAMES = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'])
+
+/** Track tool_use IDs for task-related tools, so we can refresh on tool_result */
+const pendingTaskToolUseIds = new Set()
+
let msgCounter = 0
const nextId = () => `msg-${++msgCounter}-${Date.now()}`
let elapsedTimer: ReturnType | null = null
@@ -94,8 +100,9 @@ export const useChatStore = create((set, get) => ({
get().handleServerMessage(msg)
})
- // Load history
+ // Load history and tasks
get().loadHistory(sessionId)
+ useCLITaskStore.getState().fetchSessionTasks(sessionId)
sessionsApi.getSlashCommands(sessionId)
.then(({ commands }) => {
if (get().connectedSessionId === sessionId) {
@@ -112,6 +119,7 @@ export const useChatStore = create((set, get) => ({
disconnectSession: () => {
wsManager.disconnect()
if (elapsedTimer) clearInterval(elapsedTimer)
+ useCLITaskStore.getState().clearTasks()
set({ connectedSessionId: null, chatState: 'idle' })
},
@@ -262,12 +270,13 @@ export const useChatStore = create((set, get) => ({
})
break
- case 'tool_use_complete':
+ case 'tool_use_complete': {
+ const toolName = msg.toolName || get().activeToolName || 'unknown'
set((s) => ({
messages: [...s.messages, {
id: nextId(),
type: 'tool_use',
- toolName: msg.toolName || s.activeToolName || 'unknown',
+ toolName,
toolUseId: msg.toolUseId || s.activeToolUseId || '',
input: msg.input,
timestamp: Date.now(),
@@ -277,7 +286,14 @@ export const useChatStore = create((set, get) => ({
activeThinkingId: null,
streamingToolInput: '',
}))
+ // Track task-related tool calls — refresh will happen on tool_result
+ // when the tool has actually finished executing and written to disk
+ if (TASK_TOOL_NAMES.has(toolName)) {
+ const useId = msg.toolUseId || get().activeToolUseId
+ if (useId) pendingTaskToolUseIds.add(useId)
+ }
break
+ }
case 'tool_result':
set((s) => ({
@@ -292,6 +308,11 @@ export const useChatStore = create((set, get) => ({
chatState: 'thinking',
activeThinkingId: null,
}))
+ // Refresh tasks after a task tool has finished executing
+ if (pendingTaskToolUseIds.has(msg.toolUseId)) {
+ pendingTaskToolUseIds.delete(msg.toolUseId)
+ useCLITaskStore.getState().refreshTasks()
+ }
break
case 'permission_request':
diff --git a/desktop/src/stores/cliTaskStore.ts b/desktop/src/stores/cliTaskStore.ts
index 1b67fb4b..a2cb41d5 100644
--- a/desktop/src/stores/cliTaskStore.ts
+++ b/desktop/src/stores/cliTaskStore.ts
@@ -1,45 +1,64 @@
import { create } from 'zustand'
import { cliTasksApi } from '../api/cliTasks'
-import type { CLITask, TaskListSummary } from '../types/cliTask'
+import type { CLITask } from '../types/cliTask'
type CLITaskStore = {
- taskLists: TaskListSummary[]
- selectedListId: string | null
+ /** Current session ID being tracked */
+ sessionId: string | null
+ /** Tasks for the current session */
tasks: CLITask[]
- isLoading: boolean
+ /** Whether the task bar is expanded */
+ expanded: boolean
- fetchTaskLists: () => Promise
- selectTaskList: (id: string) => Promise
- clearSelection: () => void
+ /** Fetch tasks for a given session (uses sessionId as taskListId) */
+ fetchSessionTasks: (sessionId: string) => Promise
+ /** Refresh tasks for the currently tracked session */
+ refreshTasks: () => Promise
+ /** Clear task tracking state */
+ clearTasks: () => void
+ /** Toggle expanded state */
+ toggleExpanded: () => void
}
-export const useCLITaskStore = create((set) => ({
- taskLists: [],
- selectedListId: null,
+export const useCLITaskStore = create((set, get) => ({
+ sessionId: null,
tasks: [],
- isLoading: false,
+ expanded: true,
- fetchTaskLists: async () => {
- set({ isLoading: true })
+ fetchSessionTasks: async (sessionId) => {
+ set({ sessionId })
try {
- const { lists } = await cliTasksApi.listTaskLists()
- set({ taskLists: lists, isLoading: false })
+ const { tasks } = await cliTasksApi.getTasksForList(sessionId)
+ // Only update if still tracking the same session
+ if (get().sessionId === sessionId) {
+ set({ tasks })
+ }
} catch {
- set({ isLoading: false })
+ // No tasks for this session — that's fine
+ if (get().sessionId === sessionId) {
+ set({ tasks: [] })
+ }
}
},
- selectTaskList: async (id) => {
- set({ selectedListId: id, isLoading: true })
+ refreshTasks: async () => {
+ const { sessionId } = get()
+ if (!sessionId) return
try {
- const { tasks } = await cliTasksApi.getTasksForList(id)
- set({ tasks, isLoading: false })
+ const { tasks } = await cliTasksApi.getTasksForList(sessionId)
+ if (get().sessionId === sessionId) {
+ set({ tasks })
+ }
} catch {
- set({ tasks: [], isLoading: false })
+ // ignore
}
},
- clearSelection: () => {
- set({ selectedListId: null, tasks: [] })
+ clearTasks: () => {
+ set({ sessionId: null, tasks: [] })
+ },
+
+ toggleExpanded: () => {
+ set((s) => ({ expanded: !s.expanded }))
},
}))
diff --git a/desktop/src/stores/uiStore.ts b/desktop/src/stores/uiStore.ts
index a25820ee..bfd43982 100644
--- a/desktop/src/stores/uiStore.ts
+++ b/desktop/src/stores/uiStore.ts
@@ -7,7 +7,7 @@ export type Toast = {
duration?: number
}
-type ActiveView = 'code' | 'scheduled' | 'tasks' | 'terminal' | 'history' | 'settings'
+type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'
type UIStore = {
theme: 'light' | 'dark'