mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
refactor: move task tracking from standalone page into session bottom bar
Remove the separate Tasks sidebar page (wrong concept — was showing scheduled tasks). Instead, display CLI task progress as a sticky bar at the bottom of the active session, matching the official desktop app behavior. Tasks are refreshed in real-time by detecting TaskCreate/ TaskUpdate tool_result messages over WebSocket. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56b4fe3585
commit
9b74e064c7
130
desktop/src/components/chat/SessionTaskBar.tsx
Normal file
130
desktop/src/components/chat/SessionTaskBar.tsx
Normal file
@ -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 (
|
||||
<div className="border-t border-[var(--color-border)] bg-[var(--color-surface)] shrink-0">
|
||||
{/* Header — always visible, clickable to toggle */}
|
||||
<button
|
||||
onClick={toggleExpanded}
|
||||
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||
>
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px]"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
checklist
|
||||
</span>
|
||||
|
||||
<span className="text-xs font-semibold text-[var(--color-text-primary)]">
|
||||
Tasks
|
||||
</span>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--color-border)] overflow-hidden max-w-[200px]">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${progressPercent}%`,
|
||||
backgroundColor: completedCount === totalCount
|
||||
? 'var(--color-success)'
|
||||
: 'var(--color-brand)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-[10px] text-[var(--color-text-tertiary)] tabular-nums">
|
||||
{completedCount}/{totalCount}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="material-symbols-outlined text-[14px] text-[var(--color-text-tertiary)] transition-transform duration-200"
|
||||
style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
expand_less
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded task list */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-2 flex flex-col gap-0.5 max-h-[240px] overflow-y-auto">
|
||||
{tasks.map((task) => (
|
||||
<TaskItem key={task.id} task={task} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskItem({ task }: { task: CLITask }) {
|
||||
const config = statusConfig[task.status]
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2 py-1.5 px-1 rounded-md">
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px] mt-px shrink-0"
|
||||
style={{ color: config.color, fontVariationSettings: "'FILL' 1" }}
|
||||
>
|
||||
{config.icon}
|
||||
</span>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] font-mono text-[var(--color-text-tertiary)]">
|
||||
#{task.id}
|
||||
</span>
|
||||
<span className={`text-xs ${
|
||||
task.status === 'completed'
|
||||
? 'text-[var(--color-text-tertiary)] line-through'
|
||||
: 'text-[var(--color-text-primary)]'
|
||||
}`}>
|
||||
{task.subject}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{task.status === 'in_progress' && task.activeForm && (
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-warning)] animate-pulse" />
|
||||
<span className="text-[10px] text-[var(--color-warning)]">
|
||||
{task.activeForm}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.owner && (
|
||||
<span className="text-[10px] text-[var(--color-text-tertiary)] mt-0.5 inline-flex items-center gap-0.5">
|
||||
<span className="material-symbols-outlined text-[10px]">person</span>
|
||||
{task.owner}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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 <ScheduledTasks />
|
||||
}
|
||||
|
||||
if (activeView === 'tasks') {
|
||||
return <Tasks />
|
||||
}
|
||||
|
||||
if (activeView === 'terminal') {
|
||||
return <TerminalPlaceholder />
|
||||
}
|
||||
|
||||
@ -86,13 +86,6 @@ export function Sidebar() {
|
||||
>
|
||||
Scheduled
|
||||
</NavItem>
|
||||
<NavItem
|
||||
active={activeView === 'tasks'}
|
||||
onClick={() => setActiveView('tasks')}
|
||||
icon={<TaskIcon />}
|
||||
>
|
||||
Tasks
|
||||
</NavItem>
|
||||
</div>
|
||||
|
||||
{/* Project filter */}
|
||||
@ -288,11 +281,3 @@ function ClockIcon() {
|
||||
)
|
||||
}
|
||||
|
||||
function TaskIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 11l3 3L22 4" />
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 */}
|
||||
<MessageList />
|
||||
|
||||
{/* Session task bar — sticky at bottom */}
|
||||
<SessionTaskBar />
|
||||
|
||||
{/* Team status bar */}
|
||||
<TeamStatusBar />
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Sidebar: task lists */}
|
||||
<div className="w-64 border-r border-[var(--color-border)] flex flex-col overflow-hidden bg-[var(--color-surface)]">
|
||||
<div className="px-4 py-4 border-b border-[var(--color-border)]">
|
||||
<h2 className="text-sm font-bold text-[var(--color-text-primary)]">Task Lists</h2>
|
||||
<p className="text-[10px] text-[var(--color-text-tertiary)] mt-0.5">CLI task tracking sessions</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto py-1">
|
||||
{isLoading && taskLists.length === 0 ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin w-4 h-4 border-2 border-[var(--color-brand)] border-t-transparent rounded-full" />
|
||||
</div>
|
||||
) : taskLists.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center">
|
||||
<span className="material-symbols-outlined text-[28px] text-[var(--color-text-tertiary)] block mb-2">task_alt</span>
|
||||
<p className="text-xs text-[var(--color-text-tertiary)]">No task lists found</p>
|
||||
</div>
|
||||
) : (
|
||||
taskLists.map((list) => (
|
||||
<TaskListItem
|
||||
key={list.id}
|
||||
list={list}
|
||||
selected={selectedListId === list.id}
|
||||
onClick={() => selectTaskList(list.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main: task details */}
|
||||
<div className="flex-1 overflow-y-auto bg-[var(--color-surface)]">
|
||||
{selectedListId ? (
|
||||
<TaskListDetail
|
||||
listId={selectedListId}
|
||||
tasks={tasks}
|
||||
isLoading={isLoading}
|
||||
onBack={clearSelection}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span className="material-symbols-outlined text-[48px] text-[var(--color-text-tertiary)] mb-3">checklist</span>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">Select a task list to view tasks</p>
|
||||
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">{taskLists.length} task list{taskLists.length !== 1 ? 's' : ''} available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full flex flex-col gap-1 px-4 py-2.5 text-left transition-colors ${
|
||||
selected
|
||||
? 'bg-[var(--color-surface-selected)]'
|
||||
: 'hover:bg-[var(--color-surface-hover)]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-text-tertiary)]">
|
||||
{isTeamOrNamed ? 'group' : 'terminal'}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-[var(--color-text-primary)] truncate">{displayName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-5">
|
||||
{/* Mini progress bar */}
|
||||
<div className="flex-1 h-1 rounded-full bg-[var(--color-border)] overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--color-success)] transition-all"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-text-tertiary)] flex-shrink-0">
|
||||
{list.completedCount}/{list.taskCount}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<div className="px-8 py-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<button onClick={onBack} className="p-1 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors text-[var(--color-text-secondary)] lg:hidden">
|
||||
<span className="material-symbols-outlined text-[18px]">arrow_back</span>
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-lg font-bold text-[var(--color-text-primary)] truncate">{listId}</h1>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-[var(--color-text-tertiary)]">
|
||||
<span>{tasks.length} tasks</span>
|
||||
{completedCount > 0 && <StatusBadge status="completed" count={completedCount} />}
|
||||
{inProgressCount > 0 && <StatusBadge status="in_progress" count={inProgressCount} />}
|
||||
{pendingCount > 0 && <StatusBadge status="pending" count={pendingCount} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-brand)] border-t-transparent rounded-full" />
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-sm text-[var(--color-text-tertiary)]">No tasks in this list</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{tasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} allTasks={tasks} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<div className={`flex gap-3 px-4 py-3 rounded-xl border border-[var(--color-border)] transition-colors hover:border-[var(--color-border-focus)] ${config.bg}`}>
|
||||
{/* Status icon */}
|
||||
<span className={`material-symbols-outlined text-[20px] mt-0.5 flex-shrink-0 ${config.color}`} style={{ fontVariationSettings: "'FILL' 1" }}>
|
||||
{config.icon}
|
||||
</span>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-mono text-[var(--color-text-tertiary)]">#{task.id}</span>
|
||||
<span className="text-sm font-medium text-[var(--color-text-primary)]">{task.subject}</span>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className="text-xs text-[var(--color-text-secondary)] mt-0.5 line-clamp-2">{task.description}</p>
|
||||
)}
|
||||
|
||||
{/* Meta row */}
|
||||
<div className="flex items-center gap-3 mt-1.5 flex-wrap">
|
||||
{task.owner && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-[var(--color-text-tertiary)]">
|
||||
<span className="material-symbols-outlined text-[12px]">person</span>
|
||||
{task.owner}
|
||||
</span>
|
||||
)}
|
||||
{task.blockedBy.length > 0 && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-[var(--color-error)]" title={blockerNames.join(', ')}>
|
||||
<span className="material-symbols-outlined text-[12px]">block</span>
|
||||
blocked by {task.blockedBy.map((id) => `#${id}`).join(', ')}
|
||||
</span>
|
||||
)}
|
||||
{task.blocks.length > 0 && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-[var(--color-text-tertiary)]" title={blockingNames.join(', ')}>
|
||||
<span className="material-symbols-outlined text-[12px]">arrow_forward</span>
|
||||
blocks {blockingNames.join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Status Badge ───────────────────────────────────────────
|
||||
|
||||
function StatusBadge({ status, count }: { status: string; count: number }) {
|
||||
const styles: Record<string, string> = {
|
||||
completed: 'text-[var(--color-success)]',
|
||||
in_progress: 'text-[var(--color-warning)]',
|
||||
pending: 'text-[var(--color-text-tertiary)]',
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
completed: 'done',
|
||||
in_progress: 'active',
|
||||
pending: 'pending',
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`${styles[status] || ''}`}>
|
||||
{count} {labels[status] || status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -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<string>()
|
||||
|
||||
let msgCounter = 0
|
||||
const nextId = () => `msg-${++msgCounter}-${Date.now()}`
|
||||
let elapsedTimer: ReturnType<typeof setInterval> | null = null
|
||||
@ -94,8 +100,9 @@ export const useChatStore = create<ChatStore>((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<ChatStore>((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<ChatStore>((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<ChatStore>((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<ChatStore>((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':
|
||||
|
||||
@ -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<void>
|
||||
selectTaskList: (id: string) => Promise<void>
|
||||
clearSelection: () => void
|
||||
/** Fetch tasks for a given session (uses sessionId as taskListId) */
|
||||
fetchSessionTasks: (sessionId: string) => Promise<void>
|
||||
/** Refresh tasks for the currently tracked session */
|
||||
refreshTasks: () => Promise<void>
|
||||
/** Clear task tracking state */
|
||||
clearTasks: () => void
|
||||
/** Toggle expanded state */
|
||||
toggleExpanded: () => void
|
||||
}
|
||||
|
||||
export const useCLITaskStore = create<CLITaskStore>((set) => ({
|
||||
taskLists: [],
|
||||
selectedListId: null,
|
||||
export const useCLITaskStore = create<CLITaskStore>((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 }))
|
||||
},
|
||||
}))
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user