feat: add URL-based task deep links and agent task links
This commit is contained in:
parent
d1d75b3b15
commit
e8229cd290
|
|
@ -1,6 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
interface Agent {
|
interface Agent {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -665,7 +666,10 @@ export function TasksTab({ agent }: { agent: Agent }) {
|
||||||
<div key={task.id} className="bg-surface-1/50 rounded-lg p-4">
|
<div key={task.id} className="bg-surface-1/50 rounded-lg p-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h5 className="font-medium text-foreground">{task.title}</h5>
|
<Link href={`/tasks?taskId=${task.id}`} className="font-medium text-foreground hover:text-primary transition-colors">
|
||||||
|
{task.title}
|
||||||
|
</Link>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">Task #{task.id}</div>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-foreground/80 text-sm mt-1">{task.description}</p>
|
<p className="text-foreground/80 text-sm mt-1">{task.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { useMissionControl } from '@/store'
|
import { useMissionControl } from '@/store'
|
||||||
import { useSmartPoll } from '@/lib/use-smart-poll'
|
import { useSmartPoll } from '@/lib/use-smart-poll'
|
||||||
import { useFocusTrap } from '@/lib/use-focus-trap'
|
import { useFocusTrap } from '@/lib/use-focus-trap'
|
||||||
|
|
@ -67,6 +68,9 @@ const priorityColors: Record<string, string> = {
|
||||||
|
|
||||||
export function TaskBoardPanel() {
|
export function TaskBoardPanel() {
|
||||||
const { tasks: storeTasks, setTasks: storeSetTasks, selectedTask, setSelectedTask } = useMissionControl()
|
const { tasks: storeTasks, setTasks: storeSetTasks, selectedTask, setSelectedTask } = useMissionControl()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const [agents, setAgents] = useState<Agent[]>([])
|
const [agents, setAgents] = useState<Agent[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
@ -75,6 +79,23 @@ export function TaskBoardPanel() {
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
const [editingTask, setEditingTask] = useState<Task | null>(null)
|
const [editingTask, setEditingTask] = useState<Task | null>(null)
|
||||||
const dragCounter = useRef(0)
|
const dragCounter = useRef(0)
|
||||||
|
const selectedTaskIdFromUrl = Number.parseInt(searchParams.get('taskId') || '', 10)
|
||||||
|
|
||||||
|
const updateTaskUrl = useCallback((taskId: number | null, mode: 'push' | 'replace' = 'push') => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString())
|
||||||
|
if (typeof taskId === 'number' && Number.isFinite(taskId)) {
|
||||||
|
params.set('taskId', String(taskId))
|
||||||
|
} else {
|
||||||
|
params.delete('taskId')
|
||||||
|
}
|
||||||
|
const query = params.toString()
|
||||||
|
const href = query ? `${pathname}?${query}` : pathname
|
||||||
|
if (mode === 'replace') {
|
||||||
|
router.replace(href)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(href)
|
||||||
|
}, [pathname, router, searchParams])
|
||||||
|
|
||||||
// Augment store tasks with aegisApproved flag (computed, not stored)
|
// Augment store tasks with aegisApproved flag (computed, not stored)
|
||||||
const tasks: Task[] = storeTasks.map(t => ({
|
const tasks: Task[] = storeTasks.map(t => ({
|
||||||
|
|
@ -136,6 +157,26 @@ export function TaskBoardPanel() {
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [fetchData])
|
}, [fetchData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Number.isFinite(selectedTaskIdFromUrl)) {
|
||||||
|
if (selectedTask) setSelectedTask(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = tasks.find((task) => task.id === selectedTaskIdFromUrl)
|
||||||
|
if (match) {
|
||||||
|
if (selectedTask?.id !== match.id) {
|
||||||
|
setSelectedTask(match)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading) {
|
||||||
|
setError(`Task #${selectedTaskIdFromUrl} not found in current workspace`)
|
||||||
|
setSelectedTask(null)
|
||||||
|
}
|
||||||
|
}, [loading, selectedTask, selectedTaskIdFromUrl, setSelectedTask, tasks])
|
||||||
|
|
||||||
// Poll as SSE fallback — pauses when SSE is delivering events
|
// Poll as SSE fallback — pauses when SSE is delivering events
|
||||||
useSmartPoll(fetchData, 30000, { pauseWhenSseConnected: true })
|
useSmartPoll(fetchData, 30000, { pauseWhenSseConnected: true })
|
||||||
|
|
||||||
|
|
@ -342,8 +383,17 @@ export function TaskBoardPanel() {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={`${task.title}, ${task.priority} priority, ${task.status}`}
|
aria-label={`${task.title}, ${task.priority} priority, ${task.status}`}
|
||||||
onDragStart={(e) => handleDragStart(e, task)}
|
onDragStart={(e) => handleDragStart(e, task)}
|
||||||
onClick={() => setSelectedTask(task)}
|
onClick={() => {
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedTask(task) } }}
|
setSelectedTask(task)
|
||||||
|
updateTaskUrl(task.id)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedTask(task)
|
||||||
|
updateTaskUrl(task.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`bg-surface-1 rounded-lg p-3 cursor-pointer hover:bg-surface-2 transition-smooth border-l-4 ${priorityColors[task.priority]} ${
|
className={`bg-surface-1 rounded-lg p-3 cursor-pointer hover:bg-surface-2 transition-smooth border-l-4 ${priorityColors[task.priority]} ${
|
||||||
draggedTask?.id === task.id ? 'opacity-50' : ''
|
draggedTask?.id === task.id ? 'opacity-50' : ''
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -440,11 +490,15 @@ export function TaskBoardPanel() {
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={selectedTask}
|
task={selectedTask}
|
||||||
agents={agents}
|
agents={agents}
|
||||||
onClose={() => setSelectedTask(null)}
|
onClose={() => {
|
||||||
|
setSelectedTask(null)
|
||||||
|
updateTaskUrl(null)
|
||||||
|
}}
|
||||||
onUpdate={fetchData}
|
onUpdate={fetchData}
|
||||||
onEdit={(taskToEdit) => {
|
onEdit={(taskToEdit) => {
|
||||||
setEditingTask(taskToEdit)
|
setEditingTask(taskToEdit)
|
||||||
setSelectedTask(null)
|
setSelectedTask(null)
|
||||||
|
updateTaskUrl(null, 'replace')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue