Spaces:
Runtime error
Runtime error
nyk commited on
fix: migrate clawdbot CLI calls to gateway RPC and improve session labels (#407)
Browse filesReplace all `runClawdbot(['-c', ...])` invocations with
`callOpenClawGateway()` RPC calls in session control, spawn, and delete
routes. This eliminates the deprecated CLI dependency and uses the
structured gateway protocol instead.
Also add `formatSessionLabel()` helper and `useAgentSessions()` hook to
task-board-panel for better session dropdown display labels.
Closes #401, closes #406
src/app/api/sessions/[id]/control/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
-
import {
|
| 4 |
import { db_helpers } from '@/lib/db'
|
| 5 |
import { mutationLimiter } from '@/lib/rate-limit'
|
| 6 |
import { logger } from '@/lib/logger'
|
|
@@ -36,20 +36,14 @@ export async function POST(
|
|
| 36 |
)
|
| 37 |
}
|
| 38 |
|
| 39 |
-
let result
|
| 40 |
if (action === 'terminate') {
|
| 41 |
-
result = await
|
| 42 |
-
['-c', `sessions_kill("${id}")`],
|
| 43 |
-
{ timeoutMs: 10000 }
|
| 44 |
-
)
|
| 45 |
} else {
|
| 46 |
const message = action === 'monitor'
|
| 47 |
-
?
|
| 48 |
-
:
|
| 49 |
-
result = await
|
| 50 |
-
['-c', `sessions_send("${id}", ${JSON.stringify(message)})`],
|
| 51 |
-
{ timeoutMs: 10000 }
|
| 52 |
-
)
|
| 53 |
}
|
| 54 |
|
| 55 |
db_helpers.logActivity(
|
|
@@ -65,7 +59,7 @@ export async function POST(
|
|
| 65 |
success: true,
|
| 66 |
action,
|
| 67 |
session: id,
|
| 68 |
-
|
| 69 |
})
|
| 70 |
} catch (error: any) {
|
| 71 |
logger.error({ err: error }, 'Session control error')
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
+
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
| 4 |
import { db_helpers } from '@/lib/db'
|
| 5 |
import { mutationLimiter } from '@/lib/rate-limit'
|
| 6 |
import { logger } from '@/lib/logger'
|
|
|
|
| 36 |
)
|
| 37 |
}
|
| 38 |
|
| 39 |
+
let result: unknown
|
| 40 |
if (action === 'terminate') {
|
| 41 |
+
result = await callOpenClawGateway('sessions_kill', { sessionKey: id }, 10_000)
|
|
|
|
|
|
|
|
|
|
| 42 |
} else {
|
| 43 |
const message = action === 'monitor'
|
| 44 |
+
? { type: 'control', action: 'monitor' }
|
| 45 |
+
: { type: 'control', action: 'pause' }
|
| 46 |
+
result = await callOpenClawGateway('sessions_send', { sessionKey: id, message }, 10_000)
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
db_helpers.logActivity(
|
|
|
|
| 59 |
success: true,
|
| 60 |
action,
|
| 61 |
session: id,
|
| 62 |
+
result,
|
| 63 |
})
|
| 64 |
} catch (error: any) {
|
| 65 |
logger.error({ err: error }, 'Session control error')
|
src/app/api/sessions/route.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { scanCodexSessions } from '@/lib/codex-sessions'
|
|
| 5 |
import { scanHermesSessions } from '@/lib/hermes-sessions'
|
| 6 |
import { getDatabase, db_helpers } from '@/lib/db'
|
| 7 |
import { requireRole } from '@/lib/auth'
|
| 8 |
-
import {
|
| 9 |
import { mutationLimiter } from '@/lib/rate-limit'
|
| 10 |
import { logger } from '@/lib/logger'
|
| 11 |
|
|
@@ -60,7 +60,8 @@ export async function POST(request: NextRequest) {
|
|
| 60 |
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
| 61 |
}
|
| 62 |
|
| 63 |
-
let
|
|
|
|
| 64 |
let logDetail: string
|
| 65 |
|
| 66 |
switch (action) {
|
|
@@ -69,7 +70,8 @@ export async function POST(request: NextRequest) {
|
|
| 69 |
if (!VALID_THINKING_LEVELS.includes(level)) {
|
| 70 |
return NextResponse.json({ error: `Invalid thinking level. Must be: ${VALID_THINKING_LEVELS.join(', ')}` }, { status: 400 })
|
| 71 |
}
|
| 72 |
-
|
|
|
|
| 73 |
logDetail = `Set thinking=${level} on ${sessionKey}`
|
| 74 |
break
|
| 75 |
}
|
|
@@ -78,7 +80,8 @@ export async function POST(request: NextRequest) {
|
|
| 78 |
if (!VALID_VERBOSE_LEVELS.includes(level)) {
|
| 79 |
return NextResponse.json({ error: `Invalid verbose level. Must be: ${VALID_VERBOSE_LEVELS.join(', ')}` }, { status: 400 })
|
| 80 |
}
|
| 81 |
-
|
|
|
|
| 82 |
logDetail = `Set verbose=${level} on ${sessionKey}`
|
| 83 |
break
|
| 84 |
}
|
|
@@ -87,7 +90,8 @@ export async function POST(request: NextRequest) {
|
|
| 87 |
if (!VALID_REASONING_LEVELS.includes(level)) {
|
| 88 |
return NextResponse.json({ error: `Invalid reasoning level. Must be: ${VALID_REASONING_LEVELS.join(', ')}` }, { status: 400 })
|
| 89 |
}
|
| 90 |
-
|
|
|
|
| 91 |
logDetail = `Set reasoning=${level} on ${sessionKey}`
|
| 92 |
break
|
| 93 |
}
|
|
@@ -96,7 +100,8 @@ export async function POST(request: NextRequest) {
|
|
| 96 |
if (typeof label !== 'string' || label.length > 100) {
|
| 97 |
return NextResponse.json({ error: 'Label must be a string up to 100 characters' }, { status: 400 })
|
| 98 |
}
|
| 99 |
-
|
|
|
|
| 100 |
logDetail = `Set label="${label}" on ${sessionKey}`
|
| 101 |
break
|
| 102 |
}
|
|
@@ -104,7 +109,7 @@ export async function POST(request: NextRequest) {
|
|
| 104 |
return NextResponse.json({ error: 'Invalid action. Must be: set-thinking, set-verbose, set-reasoning, set-label' }, { status: 400 })
|
| 105 |
}
|
| 106 |
|
| 107 |
-
const result = await
|
| 108 |
|
| 109 |
db_helpers.logActivity(
|
| 110 |
'session_control',
|
|
@@ -115,7 +120,7 @@ export async function POST(request: NextRequest) {
|
|
| 115 |
{ session_key: sessionKey, action }
|
| 116 |
)
|
| 117 |
|
| 118 |
-
return NextResponse.json({ success: true, action, sessionKey,
|
| 119 |
} catch (error: any) {
|
| 120 |
logger.error({ err: error }, 'Session POST error')
|
| 121 |
return NextResponse.json({ error: error.message || 'Session action failed' }, { status: 500 })
|
|
@@ -137,10 +142,7 @@ export async function DELETE(request: NextRequest) {
|
|
| 137 |
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
| 138 |
}
|
| 139 |
|
| 140 |
-
const result = await
|
| 141 |
-
['-c', `session_delete("${sessionKey}")`],
|
| 142 |
-
{ timeoutMs: 10000 }
|
| 143 |
-
)
|
| 144 |
|
| 145 |
db_helpers.logActivity(
|
| 146 |
'session_control',
|
|
@@ -151,7 +153,7 @@ export async function DELETE(request: NextRequest) {
|
|
| 151 |
{ session_key: sessionKey, action: 'delete' }
|
| 152 |
)
|
| 153 |
|
| 154 |
-
return NextResponse.json({ success: true, sessionKey,
|
| 155 |
} catch (error: any) {
|
| 156 |
logger.error({ err: error }, 'Session DELETE error')
|
| 157 |
return NextResponse.json({ error: error.message || 'Session deletion failed' }, { status: 500 })
|
|
|
|
| 5 |
import { scanHermesSessions } from '@/lib/hermes-sessions'
|
| 6 |
import { getDatabase, db_helpers } from '@/lib/db'
|
| 7 |
import { requireRole } from '@/lib/auth'
|
| 8 |
+
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
| 9 |
import { mutationLimiter } from '@/lib/rate-limit'
|
| 10 |
import { logger } from '@/lib/logger'
|
| 11 |
|
|
|
|
| 60 |
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
| 61 |
}
|
| 62 |
|
| 63 |
+
let rpcMethod: string
|
| 64 |
+
let rpcParams: Record<string, unknown>
|
| 65 |
let logDetail: string
|
| 66 |
|
| 67 |
switch (action) {
|
|
|
|
| 70 |
if (!VALID_THINKING_LEVELS.includes(level)) {
|
| 71 |
return NextResponse.json({ error: `Invalid thinking level. Must be: ${VALID_THINKING_LEVELS.join(', ')}` }, { status: 400 })
|
| 72 |
}
|
| 73 |
+
rpcMethod = 'session_setThinking'
|
| 74 |
+
rpcParams = { sessionKey, level }
|
| 75 |
logDetail = `Set thinking=${level} on ${sessionKey}`
|
| 76 |
break
|
| 77 |
}
|
|
|
|
| 80 |
if (!VALID_VERBOSE_LEVELS.includes(level)) {
|
| 81 |
return NextResponse.json({ error: `Invalid verbose level. Must be: ${VALID_VERBOSE_LEVELS.join(', ')}` }, { status: 400 })
|
| 82 |
}
|
| 83 |
+
rpcMethod = 'session_setVerbose'
|
| 84 |
+
rpcParams = { sessionKey, level }
|
| 85 |
logDetail = `Set verbose=${level} on ${sessionKey}`
|
| 86 |
break
|
| 87 |
}
|
|
|
|
| 90 |
if (!VALID_REASONING_LEVELS.includes(level)) {
|
| 91 |
return NextResponse.json({ error: `Invalid reasoning level. Must be: ${VALID_REASONING_LEVELS.join(', ')}` }, { status: 400 })
|
| 92 |
}
|
| 93 |
+
rpcMethod = 'session_setReasoning'
|
| 94 |
+
rpcParams = { sessionKey, level }
|
| 95 |
logDetail = `Set reasoning=${level} on ${sessionKey}`
|
| 96 |
break
|
| 97 |
}
|
|
|
|
| 100 |
if (typeof label !== 'string' || label.length > 100) {
|
| 101 |
return NextResponse.json({ error: 'Label must be a string up to 100 characters' }, { status: 400 })
|
| 102 |
}
|
| 103 |
+
rpcMethod = 'session_setLabel'
|
| 104 |
+
rpcParams = { sessionKey, label }
|
| 105 |
logDetail = `Set label="${label}" on ${sessionKey}`
|
| 106 |
break
|
| 107 |
}
|
|
|
|
| 109 |
return NextResponse.json({ error: 'Invalid action. Must be: set-thinking, set-verbose, set-reasoning, set-label' }, { status: 400 })
|
| 110 |
}
|
| 111 |
|
| 112 |
+
const result = await callOpenClawGateway(rpcMethod, rpcParams, 10_000)
|
| 113 |
|
| 114 |
db_helpers.logActivity(
|
| 115 |
'session_control',
|
|
|
|
| 120 |
{ session_key: sessionKey, action }
|
| 121 |
)
|
| 122 |
|
| 123 |
+
return NextResponse.json({ success: true, action, sessionKey, result })
|
| 124 |
} catch (error: any) {
|
| 125 |
logger.error({ err: error }, 'Session POST error')
|
| 126 |
return NextResponse.json({ error: error.message || 'Session action failed' }, { status: 500 })
|
|
|
|
| 142 |
return NextResponse.json({ error: 'Invalid session key' }, { status: 400 })
|
| 143 |
}
|
| 144 |
|
| 145 |
+
const result = await callOpenClawGateway('session_delete', { sessionKey }, 10_000)
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
db_helpers.logActivity(
|
| 148 |
'session_control',
|
|
|
|
| 153 |
{ session_key: sessionKey, action: 'delete' }
|
| 154 |
)
|
| 155 |
|
| 156 |
+
return NextResponse.json({ success: true, sessionKey, result })
|
| 157 |
} catch (error: any) {
|
| 158 |
logger.error({ err: error }, 'Session DELETE error')
|
| 159 |
return NextResponse.json({ error: error.message || 'Session deletion failed' }, { status: 500 })
|
src/app/api/spawn/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
-
import { runClawdbot } from '@/lib/command'
|
| 3 |
import { requireRole } from '@/lib/auth'
|
|
|
|
| 4 |
import { config } from '@/lib/config'
|
| 5 |
import { readdir, readFile, stat } from 'fs/promises'
|
| 6 |
import { join } from 'path'
|
|
@@ -14,11 +14,6 @@ function getPreferredToolsProfile(): string {
|
|
| 14 |
return String(process.env.OPENCLAW_TOOLS_PROFILE || 'coding').trim() || 'coding'
|
| 15 |
}
|
| 16 |
|
| 17 |
-
async function runSpawnWithCompatibility(spawnPayload: Record<string, unknown>) {
|
| 18 |
-
const commandArg = `sessions_spawn(${JSON.stringify(spawnPayload)})`
|
| 19 |
-
return runClawdbot(['-c', commandArg], { timeoutMs: 10000 })
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
export async function POST(request: NextRequest) {
|
| 23 |
const auth = requireRole(request, 'operator')
|
| 24 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
|
@@ -68,19 +63,14 @@ export async function POST(request: NextRequest) {
|
|
| 68 |
}
|
| 69 |
|
| 70 |
try {
|
| 71 |
-
//
|
| 72 |
-
|
| 73 |
-
let
|
| 74 |
let compatibilityFallbackUsed = false
|
| 75 |
try {
|
| 76 |
-
|
| 77 |
-
stdout = result.stdout
|
| 78 |
-
stderr = result.stderr
|
| 79 |
} catch (firstError: any) {
|
| 80 |
-
const rawErr = String(firstError?.
|
| 81 |
-
// Only retry without tools.profile when the error specifically indicates the
|
| 82 |
-
// gateway doesn't recognize the tools/profile fields. Other errors (auth,
|
| 83 |
-
// network, model not found, etc.) should propagate immediately.
|
| 84 |
const isToolsSchemaError =
|
| 85 |
(rawErr.includes('unknown field') || rawErr.includes('unknown key') || rawErr.includes('invalid argument')) &&
|
| 86 |
(rawErr.includes('tools') || rawErr.includes('profile'))
|
|
@@ -88,23 +78,11 @@ export async function POST(request: NextRequest) {
|
|
| 88 |
|
| 89 |
const fallbackPayload = { ...spawnPayload }
|
| 90 |
delete (fallbackPayload as any).tools
|
| 91 |
-
|
| 92 |
-
stdout = fallback.stdout
|
| 93 |
-
stderr = fallback.stderr
|
| 94 |
compatibilityFallbackUsed = true
|
| 95 |
}
|
| 96 |
|
| 97 |
-
|
| 98 |
-
let sessionInfo = null
|
| 99 |
-
try {
|
| 100 |
-
// Look for session information in stdout
|
| 101 |
-
const sessionMatch = stdout.match(/Session created: (.+)/)
|
| 102 |
-
if (sessionMatch) {
|
| 103 |
-
sessionInfo = sessionMatch[1]
|
| 104 |
-
}
|
| 105 |
-
} catch (parseError) {
|
| 106 |
-
logger.error({ err: parseError }, 'Failed to parse session info')
|
| 107 |
-
}
|
| 108 |
|
| 109 |
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
| 110 |
logAuditEvent({
|
|
@@ -131,8 +109,7 @@ export async function POST(request: NextRequest) {
|
|
| 131 |
label,
|
| 132 |
timeoutSeconds: timeout,
|
| 133 |
createdAt: Date.now(),
|
| 134 |
-
|
| 135 |
-
stderr: stderr.trim(),
|
| 136 |
compatibility: {
|
| 137 |
toolsProfile: getPreferredToolsProfile(),
|
| 138 |
fallbackUsed: compatibilityFallbackUsed,
|
|
@@ -141,7 +118,7 @@ export async function POST(request: NextRequest) {
|
|
| 141 |
|
| 142 |
} catch (execError: any) {
|
| 143 |
logger.error({ err: execError }, 'Spawn execution error')
|
| 144 |
-
|
| 145 |
return NextResponse.json({
|
| 146 |
success: false,
|
| 147 |
spawnId,
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
+
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
|
| 4 |
import { config } from '@/lib/config'
|
| 5 |
import { readdir, readFile, stat } from 'fs/promises'
|
| 6 |
import { join } from 'path'
|
|
|
|
| 14 |
return String(process.env.OPENCLAW_TOOLS_PROFILE || 'coding').trim() || 'coding'
|
| 15 |
}
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
export async function POST(request: NextRequest) {
|
| 18 |
const auth = requireRole(request, 'operator')
|
| 19 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
try {
|
| 66 |
+
// Call gateway sessions_spawn directly. Try with tools.profile first,
|
| 67 |
+
// fall back without it for older gateways that don't support the field.
|
| 68 |
+
let result: any
|
| 69 |
let compatibilityFallbackUsed = false
|
| 70 |
try {
|
| 71 |
+
result = await callOpenClawGateway('sessions_spawn', spawnPayload, 15_000)
|
|
|
|
|
|
|
| 72 |
} catch (firstError: any) {
|
| 73 |
+
const rawErr = String(firstError?.message || '').toLowerCase()
|
|
|
|
|
|
|
|
|
|
| 74 |
const isToolsSchemaError =
|
| 75 |
(rawErr.includes('unknown field') || rawErr.includes('unknown key') || rawErr.includes('invalid argument')) &&
|
| 76 |
(rawErr.includes('tools') || rawErr.includes('profile'))
|
|
|
|
| 78 |
|
| 79 |
const fallbackPayload = { ...spawnPayload }
|
| 80 |
delete (fallbackPayload as any).tools
|
| 81 |
+
result = await callOpenClawGateway('sessions_spawn', fallbackPayload, 15_000)
|
|
|
|
|
|
|
| 82 |
compatibilityFallbackUsed = true
|
| 83 |
}
|
| 84 |
|
| 85 |
+
const sessionInfo = result?.sessionId || result?.session_id || null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
| 88 |
logAuditEvent({
|
|
|
|
| 109 |
label,
|
| 110 |
timeoutSeconds: timeout,
|
| 111 |
createdAt: Date.now(),
|
| 112 |
+
result,
|
|
|
|
| 113 |
compatibility: {
|
| 114 |
toolsProfile: getPreferredToolsProfile(),
|
| 115 |
fallbackUsed: compatibilityFallbackUsed,
|
|
|
|
| 118 |
|
| 119 |
} catch (execError: any) {
|
| 120 |
logger.error({ err: execError }, 'Spawn execution error')
|
| 121 |
+
|
| 122 |
return NextResponse.json({
|
| 123 |
success: false,
|
| 124 |
spawnId,
|
src/components/panels/task-board-panel.tsx
CHANGED
|
@@ -95,22 +95,43 @@ const STATUS_COLUMN_KEYS = [
|
|
| 95 |
{ key: 'done', titleKey: 'colDone', color: 'bg-green-500/20 text-green-400' },
|
| 96 |
]
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
/** Fetch active gateway sessions for a given agent name. */
|
| 99 |
function useAgentSessions(agentName: string | undefined) {
|
| 100 |
-
const [sessions, setSessions] = useState<Array<{ key: string; id: string; channel?: string; label?: string }>>([])
|
| 101 |
useEffect(() => {
|
| 102 |
if (!agentName) { setSessions([]); return }
|
| 103 |
let cancelled = false
|
| 104 |
-
fetch('/api/sessions
|
| 105 |
.then(r => r.json())
|
| 106 |
.then(data => {
|
| 107 |
if (cancelled) return
|
| 108 |
-
const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; label?: string; active?: boolean }>
|
| 109 |
const filtered = all.filter(s =>
|
| 110 |
s.agent?.toLowerCase() === agentName.toLowerCase() ||
|
| 111 |
s.key?.toLowerCase().includes(agentName.toLowerCase())
|
| 112 |
)
|
| 113 |
-
setSessions(filtered.map(s => ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
})
|
| 115 |
.catch(() => { if (!cancelled) setSessions([]) })
|
| 116 |
return () => { cancelled = true }
|
|
@@ -2052,7 +2073,7 @@ function CreateTaskModal({
|
|
| 2052 |
<option value="">New session (default)</option>
|
| 2053 |
{agentSessions.map(s => (
|
| 2054 |
<option key={s.key} value={s.key}>
|
| 2055 |
-
{s.
|
| 2056 |
</option>
|
| 2057 |
))}
|
| 2058 |
</select>
|
|
@@ -2305,7 +2326,7 @@ function EditTaskModal({
|
|
| 2305 |
<option value="">New session (default)</option>
|
| 2306 |
{agentSessions.map(s => (
|
| 2307 |
<option key={s.key} value={s.key}>
|
| 2308 |
-
{s.
|
| 2309 |
</option>
|
| 2310 |
))}
|
| 2311 |
</select>
|
|
|
|
| 95 |
{ key: 'done', titleKey: 'colDone', color: 'bg-green-500/20 text-green-400' },
|
| 96 |
]
|
| 97 |
|
| 98 |
+
/** Build a human-readable label for a session key like "agent:nefes:telegram-group-123" */
|
| 99 |
+
function formatSessionLabel(s: { key: string; channel?: string; kind?: string; label?: string }): string {
|
| 100 |
+
if (s.label) return s.label
|
| 101 |
+
// Extract the identifier part after the last colon: "agent:name:main" → "main"
|
| 102 |
+
const parts = (s.key || '').split(':')
|
| 103 |
+
const identifier = parts.length > 2 ? parts.slice(2).join(':') : s.key
|
| 104 |
+
const channel = s.channel || ''
|
| 105 |
+
if (channel && identifier !== 'main') {
|
| 106 |
+
return `${channel} (${identifier})`
|
| 107 |
+
}
|
| 108 |
+
if (channel) return `${channel} (${s.kind || 'default'})`
|
| 109 |
+
return identifier || s.key
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
/** Fetch active gateway sessions for a given agent name. */
|
| 113 |
function useAgentSessions(agentName: string | undefined) {
|
| 114 |
+
const [sessions, setSessions] = useState<Array<{ key: string; id: string; channel?: string; kind?: string; label?: string; displayLabel: string }>>([])
|
| 115 |
useEffect(() => {
|
| 116 |
if (!agentName) { setSessions([]); return }
|
| 117 |
let cancelled = false
|
| 118 |
+
fetch('/api/sessions')
|
| 119 |
.then(r => r.json())
|
| 120 |
.then(data => {
|
| 121 |
if (cancelled) return
|
| 122 |
+
const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; kind?: string; label?: string; active?: boolean }>
|
| 123 |
const filtered = all.filter(s =>
|
| 124 |
s.agent?.toLowerCase() === agentName.toLowerCase() ||
|
| 125 |
s.key?.toLowerCase().includes(agentName.toLowerCase())
|
| 126 |
)
|
| 127 |
+
setSessions(filtered.map(s => ({
|
| 128 |
+
key: s.key,
|
| 129 |
+
id: s.id,
|
| 130 |
+
channel: s.channel,
|
| 131 |
+
kind: s.kind,
|
| 132 |
+
label: s.label,
|
| 133 |
+
displayLabel: formatSessionLabel(s),
|
| 134 |
+
})))
|
| 135 |
})
|
| 136 |
.catch(() => { if (!cancelled) setSessions([]) })
|
| 137 |
return () => { cancelled = true }
|
|
|
|
| 2073 |
<option value="">New session (default)</option>
|
| 2074 |
{agentSessions.map(s => (
|
| 2075 |
<option key={s.key} value={s.key}>
|
| 2076 |
+
{s.displayLabel}
|
| 2077 |
</option>
|
| 2078 |
))}
|
| 2079 |
</select>
|
|
|
|
| 2326 |
<option value="">New session (default)</option>
|
| 2327 |
{agentSessions.map(s => (
|
| 2328 |
<option key={s.key} value={s.key}>
|
| 2329 |
+
{s.displayLabel}
|
| 2330 |
</option>
|
| 2331 |
))}
|
| 2332 |
</select>
|