Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/app/api/agents/[id]/hide/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
import { getDatabase } from '@/lib/db'
import { requireRole } from '@/lib/auth'
import { logger } from '@/lib/logger'

/**
* POST /api/agents/[id]/hide - Hide an agent from the UI
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })

try {
const db = getDatabase()
const { id } = await params
const workspaceId = auth.user.workspace_id ?? 1

const idNum = Number(id)
const agent = isNaN(idNum)
? db.prepare('SELECT id, name FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any
: db.prepare('SELECT id, name FROM agents WHERE id = ? AND workspace_id = ?').get(idNum, workspaceId) as any

if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}

db.prepare('UPDATE agents SET hidden = 1, updated_at = unixepoch() WHERE id = ?').run(agent.id)

return NextResponse.json({ success: true, agent_id: agent.id, hidden: true })
} catch (error) {
logger.error({ err: error }, 'POST /api/agents/[id]/hide error')
return NextResponse.json({ error: 'Failed to hide agent' }, { status: 500 })
}
}

/**
* DELETE /api/agents/[id]/hide - Unhide an agent
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })

try {
const db = getDatabase()
const { id } = await params
const workspaceId = auth.user.workspace_id ?? 1

const idNum = Number(id)
const agent = isNaN(idNum)
? db.prepare('SELECT id, name FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any
: db.prepare('SELECT id, name FROM agents WHERE id = ? AND workspace_id = ?').get(idNum, workspaceId) as any

if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}

db.prepare('UPDATE agents SET hidden = 0, updated_at = unixepoch() WHERE id = ?').run(agent.id)

return NextResponse.json({ success: true, agent_id: agent.id, hidden: false })
} catch (error) {
logger.error({ err: error }, 'DELETE /api/agents/[id]/hide error')
return NextResponse.json({ error: 'Failed to unhide agent' }, { status: 500 })
}
}
10 changes: 9 additions & 1 deletion src/app/api/agents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ export async function GET(request: NextRequest) {
// Parse query parameters
const status = searchParams.get('status');
const role = searchParams.get('role');
const showHidden = searchParams.get('show_hidden') === 'true';
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200);
const offset = parseInt(searchParams.get('offset') || '0');

// Build dynamic query
let query = 'SELECT * FROM agents WHERE workspace_id = ?';
const params: any[] = [workspaceId];

if (!showHidden) {
query += ' AND hidden = 0';
}

if (status) {
query += ' AND status = ?';
Expand Down Expand Up @@ -116,6 +121,9 @@ export async function GET(request: NextRequest) {
// Get total count for pagination
let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE workspace_id = ?';
const countParams: any[] = [workspaceId];
if (!showHidden) {
countQuery += ' AND hidden = 0';
}
if (status) {
countQuery += ' AND status = ?';
countParams.push(status);
Expand Down
44 changes: 42 additions & 2 deletions src/components/panels/agent-squad-panel-phase3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export function AgentSquadPanelPhase3() {
const [autoRefresh, setAutoRefresh] = useState(true)
const [syncing, setSyncing] = useState(false)
const [syncToast, setSyncToast] = useState<string | null>(null)
const [showHidden, setShowHidden] = useState(false)

// Sync agents from gateway config or local disk
const syncFromConfig = async (source?: 'local') => {
Expand Down Expand Up @@ -142,7 +143,8 @@ export function AgentSquadPanelPhase3() {
setError(null)
if (agents.length === 0) setLoading(true)

const response = await fetch('/api/agents')
const url = showHidden ? '/api/agents?show_hidden=true' : '/api/agents'
const response = await fetch(url)
if (response.status === 401) {
window.location.assign('/login?next=%2Fagents')
return
Expand All @@ -162,7 +164,7 @@ export function AgentSquadPanelPhase3() {
} finally {
setLoading(false)
}
}, [agents.length, setAgents])
}, [agents.length, setAgents, showHidden])

// Smart polling with visibility pause
useSmartPoll(fetchAgents, 30000, { enabled: autoRefresh, pauseWhenSseConnected: true })
Expand Down Expand Up @@ -223,6 +225,25 @@ export function AgentSquadPanelPhase3() {
}
}

// Re-fetch when showHidden changes
useEffect(() => {
fetchAgents()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showHidden])

const toggleAgentHidden = async (agentId: number, hide: boolean) => {
try {
const response = await fetch(`/api/agents/${agentId}/hide`, {
method: hide ? 'POST' : 'DELETE',
})
if (!response.ok) throw new Error('Failed to update visibility')
fetchAgents()
} catch (error) {
log.error('Failed to toggle agent visibility:', error)
setError('Failed to update agent visibility')
}
}

const deleteAgent = async (agentId: number, removeWorkspace: boolean) => {
const previousAgents = agents
setAgents(agents.filter((agent) => agent.id !== agentId))
Expand Down Expand Up @@ -333,6 +354,13 @@ export function AgentSquadPanelPhase3() {
>
{t('syncLocal')}
</Button>
<Button
onClick={() => setShowHidden(!showHidden)}
variant={showHidden ? 'success' : 'secondary'}
size="sm"
>
{showHidden ? 'Showing hidden' : 'Show hidden'}
</Button>
<Button
onClick={() => setShowCreateModal(true)}
size="sm"
Expand Down Expand Up @@ -399,6 +427,7 @@ export function AgentSquadPanelPhase3() {
onClick={() => setSelectedAgent(agent)}
>
<div className={`pointer-events-none absolute inset-y-0 left-0 w-1 bg-gradient-to-b ${(statusCardStyles[agent.status] || defaultCardStyle).edge}`} />
{agent.hidden ? <div className="absolute top-2 right-2 text-2xs text-slate-500">hidden</div> : null}

{/* Header: avatar + name + status */}
<div className="flex items-start justify-between mb-2">
Expand Down Expand Up @@ -494,6 +523,17 @@ export function AgentSquadPanelPhase3() {
>
{t('spawn')}
</Button>
<Button
onClick={(e) => {
e.stopPropagation()
toggleAgentHidden(agent.id, !agent.hidden)
}}
size="xs"
variant="ghost"
className="h-6 px-2 text-xs text-slate-400 hover:bg-slate-500/15 hover:text-slate-300"
>
{agent.hidden ? 'Unhide' : 'Hide'}
</Button>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/panels/task-board-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -773,13 +773,13 @@ export function TaskBoardPanel() {
)}

{/* Kanban Board */}
<div className="flex-1 flex gap-4 p-4 overflow-x-auto" role="region" aria-label={t('taskBoard')}>
<div className="flex-1 min-h-0 flex gap-4 p-4 overflow-x-auto" role="region" aria-label={t('taskBoard')}>
{statusColumns.map(column => (
<div
key={column.key}
role="region"
aria-label={t('columnAriaLabel', { title: column.title, count: tasksByStatus[column.key]?.length || 0 })}
className="flex-1 min-w-80 bg-surface-0 border border-border/60 rounded-xl flex flex-col transition-colors duration-200 [&.drag-over]:border-primary/40 [&.drag-over]:bg-primary/[0.02]"
className="flex-1 min-w-80 min-h-0 bg-surface-0 border border-border/60 rounded-xl flex flex-col transition-colors duration-200 [&.drag-over]:border-primary/40 [&.drag-over]:bg-primary/[0.02]"
onDragEnter={(e) => handleDragEnter(e, column.key)}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,12 @@ const migrations: Migration[] = [
db.exec(`CREATE INDEX IF NOT EXISTS idx_gateway_health_logs_gateway_id ON gateway_health_logs(gateway_id)`)
db.exec(`CREATE INDEX IF NOT EXISTS idx_gateway_health_logs_probed_at ON gateway_health_logs(probed_at)`)
}
},
{
id: '042_agent_hidden',
up(db: Database.Database) {
db.exec(`ALTER TABLE agents ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0`)
}
}
]

Expand Down
1 change: 1 addition & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface Agent {
last_activity?: string
created_at: number
updated_at: number
hidden?: number
config?: JsonValue
taskStats?: {
total: number
Expand Down