Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue'
|
|
import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next'
|
|
import {
|
|
scheduleCenterApi,
|
|
type ScheduleCenterCommanderSummary,
|
|
type ScheduleCenterDateResponse,
|
|
type ScheduleCenterDaySummary,
|
|
} from '@/api/scheduleCenter'
|
|
import { taskApi, type Task, type TaskDispatchStatus, type TaskQuadrant } from '@/api/task'
|
|
import type { Conversation } from '@/api/conversation'
|
|
|
|
export interface SidebarFocusItem {
|
|
id: string
|
|
label: string
|
|
title: string
|
|
meta: string
|
|
tone: 'done' | 'doing' | 'pending'
|
|
}
|
|
|
|
export interface SidebarNewsItem {
|
|
id: string
|
|
title: string
|
|
meta: string
|
|
}
|
|
|
|
export interface TodayStatusQuadrantViewTask {
|
|
id: string
|
|
title: string
|
|
completed: boolean
|
|
status: string
|
|
dispatchStatus: string
|
|
assigneeLabel: string
|
|
}
|
|
|
|
export interface TodayStatusQuadrantView {
|
|
id: string
|
|
title: string
|
|
subtitle: string
|
|
color: string
|
|
glowColor: string
|
|
icon: string
|
|
tasks: TodayStatusQuadrantViewTask[]
|
|
}
|
|
|
|
export const sidebarCollapsedModules = [
|
|
{ id: 'calendar', label: 'Calendar', icon: Sun },
|
|
{ id: 'status', label: 'Status', icon: Database },
|
|
{ id: 'focus', label: 'Focus', icon: Sparkles },
|
|
{ id: 'kanban', label: 'Issues', icon: ListTodo },
|
|
{ id: 'review', label: 'Review', icon: CornerDownLeft },
|
|
]
|
|
|
|
const ISSUE_QUADRANT_META: Record<TaskQuadrant, Omit<TodayStatusQuadrantView, 'tasks'>> = {
|
|
'urgent-important': {
|
|
id: 'urgent-important',
|
|
title: 'Important & Urgent',
|
|
subtitle: 'CRITICAL',
|
|
color: '#ff4757',
|
|
glowColor: 'rgba(255, 71, 87, 0.4)',
|
|
icon: '!',
|
|
},
|
|
'not-urgent-important': {
|
|
id: 'not-urgent-important',
|
|
title: 'Important & Planned',
|
|
subtitle: 'PLANNED',
|
|
color: '#ffd93d',
|
|
glowColor: 'rgba(255, 217, 61, 0.4)',
|
|
icon: 'P',
|
|
},
|
|
'urgent-not-important': {
|
|
id: 'urgent-not-important',
|
|
title: 'Urgent & Delegated',
|
|
subtitle: 'DELEGATE',
|
|
color: '#00d4ff',
|
|
glowColor: 'rgba(0, 212, 255, 0.4)',
|
|
icon: 'D',
|
|
},
|
|
'not-urgent-not-important': {
|
|
id: 'not-urgent-not-important',
|
|
title: 'Backlog / Low Priority',
|
|
subtitle: 'BACKLOG',
|
|
color: '#6bcf7f',
|
|
glowColor: 'rgba(107, 207, 127, 0.4)',
|
|
icon: 'B',
|
|
},
|
|
}
|
|
|
|
export function formatDateKey(date: Date) {
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
function formatMonthKey(date: Date) {
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
return `${year}-${month}`
|
|
}
|
|
|
|
function toneFromStatus(status: string) {
|
|
if (status === 'done' || status === 'completed') return 'done' as const
|
|
if (status === 'in_progress' || status === 'running' || status === 'queued') return 'doing' as const
|
|
return 'pending' as const
|
|
}
|
|
|
|
function focusLabelByQuadrant(quadrant?: string | null) {
|
|
if (!quadrant) return 'Task'
|
|
if (quadrant === 'urgent-important') return 'Critical'
|
|
if (quadrant === 'not-urgent-important') return 'Planned'
|
|
if (quadrant === 'urgent-not-important') return 'Delegated'
|
|
return 'Backlog'
|
|
}
|
|
|
|
function focusMeta(status: string, dispatchStatus: string, assigneeType?: string | null, assigneeId?: string | null) {
|
|
const statusMap: Record<string, string> = {
|
|
todo: 'Pending',
|
|
in_progress: 'In progress',
|
|
done: 'Done',
|
|
cancelled: 'Cancelled',
|
|
}
|
|
const dispatchMap: Record<string, string> = {
|
|
idle: 'Not dispatched',
|
|
queued: 'Queued',
|
|
running: 'Running',
|
|
completed: 'Completed',
|
|
failed: 'Failed',
|
|
}
|
|
const statusLabel = dispatchStatus !== 'idle' ? dispatchMap[dispatchStatus] : (statusMap[status] || 'Pending')
|
|
if (assigneeType && assigneeId) return `${statusLabel} / ${assigneeType}:${assigneeId}`
|
|
if (assigneeType) return `${statusLabel} / ${assigneeType}`
|
|
return statusLabel
|
|
}
|
|
|
|
function deriveIssueQuadrant(task: Task): TaskQuadrant {
|
|
if (task.quadrant) return task.quadrant
|
|
if (task.priority === 'high' || task.priority === 'urgent') return 'urgent-important'
|
|
if (task.status === 'in_progress') return 'not-urgent-important'
|
|
if (task.priority === 'medium') return 'urgent-not-important'
|
|
return 'not-urgent-not-important'
|
|
}
|
|
|
|
function buildIssueCommanderSummary(tasks: Task[]): ScheduleCenterCommanderSummary {
|
|
const summary: ScheduleCenterCommanderSummary = {
|
|
total: 0,
|
|
queued: 0,
|
|
running: 0,
|
|
completed: 0,
|
|
failed: 0,
|
|
overall_status: 'idle',
|
|
}
|
|
|
|
tasks.forEach((task) => {
|
|
const state = task.dispatch_status as TaskDispatchStatus
|
|
if (state === 'idle') return
|
|
|
|
summary.total += 1
|
|
if (state === 'queued') summary.queued += 1
|
|
if (state === 'running') summary.running += 1
|
|
if (state === 'completed') summary.completed += 1
|
|
if (state === 'failed') summary.failed += 1
|
|
})
|
|
|
|
if (summary.running > 0) {
|
|
summary.overall_status = 'running'
|
|
} else if (summary.queued > 0) {
|
|
summary.overall_status = 'queued'
|
|
} else if (summary.failed > 0 && summary.completed === 0) {
|
|
summary.overall_status = 'failed'
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
export function useSidebarPlan(
|
|
clientTimeRef: { value: Date },
|
|
loadDailyDigestFn: () => void,
|
|
conversationsRef: Ref<Conversation[]> | Conversation[] = [],
|
|
) {
|
|
const todayPlanDetail = ref<ScheduleCenterDateResponse | null>(null)
|
|
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
|
|
const issueTasks = ref<Task[]>([])
|
|
const selectedDate = ref<string | null>(null)
|
|
|
|
const conversationDateMap = computed(() => {
|
|
const map = new Map<string, boolean>()
|
|
const conversations = Array.isArray(conversationsRef) ? conversationsRef : (conversationsRef.value ?? [])
|
|
conversations.forEach((conv) => {
|
|
map.set(formatDateKey(new Date(conv.updated_at)), true)
|
|
})
|
|
return map
|
|
})
|
|
|
|
const todayDateKey = computed(() => formatDateKey(clientTimeRef.value))
|
|
const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item])))
|
|
|
|
const issueStatusQuadrants = computed<TodayStatusQuadrantView[]>(() => (
|
|
Object.values(ISSUE_QUADRANT_META).map((meta) => ({
|
|
...meta,
|
|
tasks: issueTasks.value
|
|
.filter((task) => deriveIssueQuadrant(task) === meta.id)
|
|
.map((task) => ({
|
|
id: task.id,
|
|
title: task.title,
|
|
completed: task.status === 'done',
|
|
status: task.status,
|
|
dispatchStatus: task.dispatch_status,
|
|
assigneeLabel: task.assignee_id ? `${task.assignee_type ?? 'owner'}:${task.assignee_id}` : (task.assignee_type ?? 'unassigned'),
|
|
})),
|
|
}))
|
|
))
|
|
|
|
const issueCommanderSummary = computed<ScheduleCenterCommanderSummary>(() => buildIssueCommanderSummary(issueTasks.value))
|
|
|
|
const calendarCells = computed(() => {
|
|
const year = clientTimeRef.value.getFullYear()
|
|
const month = clientTimeRef.value.getMonth()
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
|
const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7
|
|
const todayKey = todayDateKey.value
|
|
const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = []
|
|
|
|
for (let index = 0; index < firstDayOffset; index += 1) {
|
|
cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
|
}
|
|
|
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
|
const monthDate = new Date(year, month, day)
|
|
const dateKey = formatDateKey(monthDate)
|
|
const summary = monthPlanSummaryMap.value.get(dateKey)
|
|
const busy = Boolean(summary && (summary.todo_total + summary.task_due_total + summary.goal_total + summary.reminder_total) > 0)
|
|
const hasConv = conversationDateMap.value.get(dateKey) || false
|
|
cells.push({ key: dateKey, value: day, active: dateKey === todayKey, busy, selected: dateKey === selectedDate.value, hasConversation: hasConv })
|
|
}
|
|
|
|
while (cells.length % 7 !== 0) {
|
|
cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
|
}
|
|
|
|
return cells
|
|
})
|
|
|
|
const calendarYear = computed(() => clientTimeRef.value.getFullYear())
|
|
const calendarMonth = computed(() => clientTimeRef.value.getMonth() + 1)
|
|
|
|
const issueStatusCounters = computed(() => {
|
|
const done = issueTasks.value.filter((item) => item.status === 'done').length
|
|
const doing = issueTasks.value.filter((item) => item.status === 'in_progress').length
|
|
const pending = issueTasks.value.filter((item) => item.status === 'todo').length
|
|
const total = done + doing + pending
|
|
return { done, doing, pending, total, completion: total > 0 ? Math.round((done / total) * 100) : 0 }
|
|
})
|
|
|
|
const monthReviewStats = computed(() => monthPlanDays.value.reduce(
|
|
(acc, item) => {
|
|
acc.todoTotal += item.todo_total
|
|
acc.todoCompleted += item.todo_completed
|
|
acc.taskTotal += item.task_due_total
|
|
acc.reminderTotal += item.reminder_total
|
|
acc.goalTotal += item.goal_total
|
|
acc.highPriorityTotal += item.high_priority_total
|
|
if (item.todo_total + item.task_due_total + item.reminder_total + item.goal_total > 0) acc.activeDays += 1
|
|
return acc
|
|
},
|
|
{ todoTotal: 0, todoCompleted: 0, taskTotal: 0, reminderTotal: 0, goalTotal: 0, highPriorityTotal: 0, activeDays: 0 },
|
|
))
|
|
|
|
const sidebarWeekLabels = [
|
|
{ label: 'M', isWeekend: false },
|
|
{ label: 'T', isWeekend: false },
|
|
{ label: 'W', isWeekend: false },
|
|
{ label: 'T', isWeekend: false },
|
|
{ label: 'F', isWeekend: false },
|
|
{ label: 'S', isWeekend: true },
|
|
{ label: 'S', isWeekend: true },
|
|
]
|
|
|
|
const sidebarStatusHeadline = computed(() => '')
|
|
|
|
const sidebarStatusBreakdown = computed(() => [
|
|
{ key: 'done', label: 'Completed', value: issueStatusCounters.value.done, tone: 'done' },
|
|
{ key: 'doing', label: 'In Progress', value: issueStatusCounters.value.doing, tone: 'doing' },
|
|
{ key: 'pending', label: 'Pending', value: issueStatusCounters.value.pending, tone: 'pending' },
|
|
{ key: 'total', label: 'Total', value: issueStatusCounters.value.total, tone: 'total' },
|
|
])
|
|
|
|
const sidebarFocusItems = computed<SidebarFocusItem[]>(() => (
|
|
(todayPlanDetail.value?.focus_tasks ?? []).map((task) => ({
|
|
id: task.id,
|
|
label: focusLabelByQuadrant(task.quadrant),
|
|
title: task.title,
|
|
meta: focusMeta(task.status, task.dispatch_status, task.assignee_type, task.assignee_id),
|
|
tone: toneFromStatus(task.dispatch_status !== 'idle' ? task.dispatch_status : task.status),
|
|
}))
|
|
))
|
|
|
|
const sidebarReviewAchievements = computed(() => {
|
|
const stats = monthReviewStats.value
|
|
const items = [
|
|
stats.todoCompleted > 0 ? `Completed ${stats.todoCompleted} todos this month.` : '',
|
|
stats.activeDays > 0 ? `${stats.activeDays} active planning days recorded this month.` : '',
|
|
stats.highPriorityTotal > 0 ? `${stats.highPriorityTotal} high-priority items were tracked.` : '',
|
|
].filter(Boolean)
|
|
|
|
if (items.length > 0) return items.slice(0, 3)
|
|
return ['Monthly review data is still accumulating.']
|
|
})
|
|
|
|
const sidebarReviewReflections = computed(() => {
|
|
const stats = monthReviewStats.value
|
|
const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0)
|
|
const items = [
|
|
pendingTodoCount > 0 ? `${pendingTodoCount} todos are still open and may need to be broken down further.` : '',
|
|
stats.highPriorityTotal >= 8 ? 'High-priority load is dense. Consider narrowing the active mainline.' : '',
|
|
stats.reminderTotal >= Math.max(6, stats.activeDays) ? 'Reminder volume is high. A fixed review block may help reduce interruption cost.' : '',
|
|
].filter(Boolean)
|
|
|
|
if (items.length > 0) return items.slice(0, 3)
|
|
return ['Execution rhythm looks stable.']
|
|
})
|
|
|
|
const sidebarFeedItems = computed<SidebarNewsItem[]>(() => [
|
|
{ id: 'fallback-1', title: 'Task orchestration is replacing simple one-shot assistants in modern AI workflows.', meta: 'Industry' },
|
|
{ id: 'fallback-2', title: 'Shared planning state across chat and execution surfaces is becoming a core product boundary.', meta: 'Product' },
|
|
{ id: 'fallback-3', title: 'This feed is still placeholder content and can be replaced by a real RSS source later.', meta: 'System' },
|
|
])
|
|
|
|
const topbarFeedItems = computed(() => (
|
|
sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : []
|
|
))
|
|
|
|
async function loadIssueStatusSnapshot() {
|
|
try {
|
|
const response = await taskApi.list()
|
|
issueTasks.value = response.data.filter((task) => task.status !== 'cancelled')
|
|
} catch (err) {
|
|
console.warn('Failed to load issue status snapshot:', err)
|
|
issueTasks.value = []
|
|
}
|
|
}
|
|
|
|
async function loadDailyPlanSnapshot(date = new Date()) {
|
|
const dateKey = formatDateKey(date)
|
|
const monthKey = formatMonthKey(date)
|
|
try {
|
|
const [todayResponse, monthResponse] = await Promise.all([
|
|
scheduleCenterApi.date(dateKey),
|
|
scheduleCenterApi.month(monthKey),
|
|
])
|
|
todayPlanDetail.value = todayResponse.data
|
|
monthPlanDays.value = monthResponse.data.days
|
|
} catch (err) {
|
|
console.warn('Failed to load sidebar plan snapshot:', err)
|
|
todayPlanDetail.value = null
|
|
monthPlanDays.value = []
|
|
}
|
|
}
|
|
|
|
async function loadSidebarPlanSnapshot(date = new Date()) {
|
|
await Promise.all([
|
|
loadDailyPlanSnapshot(date),
|
|
loadIssueStatusSnapshot(),
|
|
])
|
|
}
|
|
|
|
watch(todayDateKey, (next, previous) => {
|
|
if (next === previous) return
|
|
void loadDailyDigestFn()
|
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
|
})
|
|
|
|
function selectCalendarDate(dateKey: string) {
|
|
selectedDate.value = dateKey
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadDailyDigestFn()
|
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('jarvis:today-status-refresh', handleExternalRefresh)
|
|
}
|
|
})
|
|
|
|
function handleExternalRefresh() {
|
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
if (typeof window !== 'undefined') {
|
|
window.removeEventListener('jarvis:today-status-refresh', handleExternalRefresh)
|
|
}
|
|
})
|
|
|
|
return {
|
|
todayPlanDetail,
|
|
monthPlanDays,
|
|
todayDateKey,
|
|
monthPlanSummaryMap,
|
|
calendarCells,
|
|
calendarYear,
|
|
calendarMonth,
|
|
issueStatusCounters,
|
|
monthReviewStats,
|
|
sidebarWeekLabels,
|
|
sidebarStatusHeadline,
|
|
sidebarStatusBreakdown,
|
|
sidebarFocusItems,
|
|
issueStatusQuadrants,
|
|
issueCommanderSummary,
|
|
sidebarReviewAchievements,
|
|
sidebarReviewReflections,
|
|
sidebarFeedItems,
|
|
topbarFeedItems,
|
|
loadSidebarPlanSnapshot,
|
|
sidebarCollapsedModules,
|
|
selectedDate,
|
|
selectCalendarDate,
|
|
conversationDateMap,
|
|
}
|
|
}
|