File size: 5,768 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import { useCLITaskStore } from '../../stores/cliTaskStore'
import { useTranslation } from '../../i18n'
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,
completedAndDismissed,
resetCompletedTasks,
} = useCLITaskStore()
const t = useTranslation()
if (tasks.length === 0) return null
// Don't show sticky bar if tasks were completed and the user already continued chatting
const allCompleted = tasks.every((tk) => tk.status === 'completed')
if (allCompleted && completedAndDismissed) return null
const completedCount = tasks.filter((tk) => tk.status === 'completed').length
const totalCount = tasks.length
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
return (
<div className="shrink-0 px-8">
<div className="mx-auto max-w-[860px] rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] overflow-hidden mb-2">
{/* Header — always visible, clickable to toggle */}
<div className="flex items-center gap-2 bg-[var(--color-surface-container)] px-2 py-1.5">
<button
type="button"
onClick={toggleExpanded}
className="flex min-w-0 flex-1 items-center gap-3 rounded-[var(--radius-md)] px-2 py-1 hover:bg-[var(--color-surface-container-low)] transition-colors"
>
<div className="flex items-center justify-center w-6 h-6 rounded-[var(--radius-md)] bg-[var(--color-secondary)]/10">
<span
className="material-symbols-outlined text-[14px] text-[var(--color-secondary)]"
>
checklist
</span>
</div>
<span className="text-xs font-semibold text-[var(--color-text-primary)]">
{t('tasks.title')}
</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>
{allCompleted && (
<button
type="button"
aria-label={t('tasks.dismissCompleted')}
onClick={() => { void resetCompletedTasks() }}
className="flex shrink-0 items-center justify-center rounded-[var(--radius-md)] p-1.5 text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] transition-colors"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
)}
</div>
{/* Expanded task list */}
{expanded && (
<div className="px-4 pb-2 pt-1 flex flex-col gap-0.5 max-h-[240px] overflow-y-auto border-t border-[var(--color-outline-variant)]/20">
{tasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</div>
)}
</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>
)
}
|