feat: add task URL deep-linking and agent task links

This commit is contained in:
Nyk 2026-03-04 22:24:42 +07:00
parent d1d75b3b15
commit 4b3781c9cc
2 changed files with 62 additions and 4 deletions

View File

@ -1,6 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
interface Agent {
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 className="flex items-start justify-between">
<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 && (
<p className="text-foreground/80 text-sm mt-1">{task.description}</p>
)}

View File

@ -1,6 +1,7 @@
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll'
import { useFocusTrap } from '@/lib/use-focus-trap'
@ -67,6 +68,9 @@ const priorityColors: Record<string, string> = {
export function TaskBoardPanel() {
const { tasks: storeTasks, setTasks: storeSetTasks, selectedTask, setSelectedTask } = useMissionControl()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [agents, setAgents] = useState<Agent[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -75,6 +79,23 @@ export function TaskBoardPanel() {
const [showCreateModal, setShowCreateModal] = useState(false)
const [editingTask, setEditingTask] = useState<Task | null>(null)
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)
const tasks: Task[] = storeTasks.map(t => ({
@ -136,6 +157,26 @@ export function TaskBoardPanel() {
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
useSmartPoll(fetchData, 30000, { pauseWhenSseConnected: true })
@ -342,8 +383,17 @@ export function TaskBoardPanel() {
tabIndex={0}
aria-label={`${task.title}, ${task.priority} priority, ${task.status}`}
onDragStart={(e) => handleDragStart(e, task)}
onClick={() => setSelectedTask(task)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedTask(task) } }}
onClick={() => {
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]} ${
draggedTask?.id === task.id ? 'opacity-50' : ''
}`}
@ -440,11 +490,15 @@ export function TaskBoardPanel() {
<TaskDetailModal
task={selectedTask}
agents={agents}
onClose={() => setSelectedTask(null)}
onClose={() => {
setSelectedTask(null)
updateTaskUrl(null)
}}
onUpdate={fetchData}
onEdit={(taskToEdit) => {
setEditingTask(taskToEdit)
setSelectedTask(null)
updateTaskUrl(null, 'replace')
}}
/>
)}