@swarmclawai/swarmclaw 0.5.3 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -25,7 +25,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
25
25
|
return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const { title, content, tags } = body as Record<string, unknown>
|
|
28
|
+
const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
|
|
29
29
|
|
|
30
30
|
const updates: Record<string, unknown> = {}
|
|
31
31
|
if (typeof title === 'string' && title.trim()) {
|
|
@@ -36,13 +36,24 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const existingMeta = (existing.metadata || {}) as Record<string, unknown>
|
|
39
|
+
const metaUpdates: Record<string, unknown> = { ...existingMeta }
|
|
40
|
+
|
|
39
41
|
if (Array.isArray(tags)) {
|
|
40
42
|
const normalizedTags = (tags as unknown[]).filter(
|
|
41
43
|
(t): t is string => typeof t === 'string' && t.trim().length > 0,
|
|
42
44
|
)
|
|
43
|
-
|
|
45
|
+
metaUpdates.tags = normalizedTags
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (scope === 'global' || scope === 'agent') {
|
|
49
|
+
metaUpdates.scope = scope
|
|
50
|
+
metaUpdates.agentIds = scope === 'agent' && Array.isArray(agentIds)
|
|
51
|
+
? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
|
|
52
|
+
: []
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
updates.metadata = metaUpdates
|
|
56
|
+
|
|
46
57
|
const updated = db.update(id, updates)
|
|
47
58
|
if (!updated) {
|
|
48
59
|
return notFound()
|
|
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
|
|
|
25
25
|
return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const { title, content, tags } = body as Record<string, unknown>
|
|
28
|
+
const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
|
|
29
29
|
|
|
30
30
|
if (typeof title !== 'string' || !title.trim()) {
|
|
31
31
|
return NextResponse.json({ error: 'title is required.' }, { status: 400 })
|
|
@@ -38,10 +38,17 @@ export async function POST(req: Request) {
|
|
|
38
38
|
? (tags as unknown[]).filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
39
39
|
: undefined
|
|
40
40
|
|
|
41
|
+
const normalizedScope = scope === 'agent' ? 'agent' as const : 'global' as const
|
|
42
|
+
const normalizedAgentIds = Array.isArray(agentIds)
|
|
43
|
+
? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
|
|
44
|
+
: []
|
|
45
|
+
|
|
41
46
|
const entry = addKnowledge({
|
|
42
47
|
title: title.trim(),
|
|
43
48
|
content,
|
|
44
49
|
tags: normalizedTags,
|
|
50
|
+
scope: normalizedScope,
|
|
51
|
+
agentIds: normalizedAgentIds,
|
|
45
52
|
})
|
|
46
53
|
|
|
47
54
|
return NextResponse.json(entry)
|
|
@@ -26,7 +26,13 @@ export async function GET(req: Request) {
|
|
|
26
26
|
const requestedLimit = parseOptionalInt(searchParams.get('limit'))
|
|
27
27
|
const requestedLinkedLimit = parseOptionalInt(searchParams.get('linkedLimit'))
|
|
28
28
|
|
|
29
|
+
const counts = searchParams.get('counts') === 'true'
|
|
29
30
|
const db = getMemoryDb()
|
|
31
|
+
|
|
32
|
+
if (counts) {
|
|
33
|
+
return NextResponse.json(db.countsByAgent())
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
const defaults = getMemoryLookupLimits()
|
|
31
37
|
const limits = resolveLookupRequest(defaults, {
|
|
32
38
|
depth: requestedDepth,
|
|
@@ -106,6 +112,8 @@ export async function POST(req: Request) {
|
|
|
106
112
|
image: image as MemoryImage | null | undefined,
|
|
107
113
|
imagePath: image && typeof image === 'object' && 'path' in image ? String((image as { path: string }).path) : null,
|
|
108
114
|
linkedMemoryIds: body.linkedMemoryIds as string[] | undefined,
|
|
115
|
+
pinned: body.pinned === true,
|
|
116
|
+
sharedWith: Array.isArray(body.sharedWith) ? body.sharedWith as string[] : undefined,
|
|
109
117
|
})
|
|
110
118
|
return NextResponse.json(entry)
|
|
111
119
|
}
|
|
@@ -25,12 +25,16 @@ export async function GET(req: Request) {
|
|
|
25
25
|
|
|
26
26
|
export async function POST(req: Request) {
|
|
27
27
|
const body = (await req.json()) as Record<string, unknown>
|
|
28
|
+
const actionLabel = typeof body.actionLabel === 'string' ? body.actionLabel : undefined
|
|
29
|
+
const actionUrl = typeof body.actionUrl === 'string' ? body.actionUrl : undefined
|
|
28
30
|
const id = genId()
|
|
29
31
|
const notification: AppNotification = {
|
|
30
32
|
id,
|
|
31
33
|
type: (['info', 'success', 'warning', 'error'].includes(body.type as string) ? body.type : 'info') as AppNotification['type'],
|
|
32
34
|
title: typeof body.title === 'string' ? body.title : 'Notification',
|
|
33
35
|
message: typeof body.message === 'string' ? body.message : undefined,
|
|
36
|
+
actionLabel,
|
|
37
|
+
actionUrl,
|
|
34
38
|
entityType: typeof body.entityType === 'string' ? body.entityType : undefined,
|
|
35
39
|
entityId: typeof body.entityId === 'string' ? body.entityId : undefined,
|
|
36
40
|
read: false,
|
|
@@ -4,7 +4,7 @@ import { loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
|
|
|
4
4
|
import { enqueueTask } from '@/lib/server/queue'
|
|
5
5
|
|
|
6
6
|
export async function POST(req: Request) {
|
|
7
|
-
const { agentId, task } = await req.json()
|
|
7
|
+
const { agentId, task } = await req.json().catch(() => ({}))
|
|
8
8
|
if (!agentId || !task) {
|
|
9
9
|
return NextResponse.json({ error: 'agentId and task are required' }, { status: 400 })
|
|
10
10
|
}
|
|
@@ -49,9 +49,9 @@ export async function POST(req: Request) {
|
|
|
49
49
|
fs.writeFileSync(dest, code, 'utf8')
|
|
50
50
|
|
|
51
51
|
return NextResponse.json({ ok: true, filename: sanitized })
|
|
52
|
-
} catch (err:
|
|
52
|
+
} catch (err: unknown) {
|
|
53
53
|
return NextResponse.json(
|
|
54
|
-
{ error: 'Failed to install plugin', message: err.message },
|
|
54
|
+
{ error: 'Failed to install plugin', message: err instanceof Error ? err.message : String(err) },
|
|
55
55
|
{ status: 500 },
|
|
56
56
|
)
|
|
57
57
|
}
|
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
} from '@/lib/server/storage'
|
|
10
10
|
|
|
11
11
|
interface SearchResult {
|
|
12
|
-
type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill'
|
|
12
|
+
type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill' | 'message'
|
|
13
13
|
id: string
|
|
14
14
|
title: string
|
|
15
15
|
description?: string
|
|
16
16
|
status?: string
|
|
17
|
+
messageIndex?: number
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const MAX_RESULTS = 20
|
|
@@ -49,6 +50,56 @@ function searchCollection(
|
|
|
49
50
|
return results
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function searchMessages(
|
|
54
|
+
sessions: Record<string, Record<string, unknown>>,
|
|
55
|
+
agents: Record<string, Record<string, unknown>>,
|
|
56
|
+
needle: string,
|
|
57
|
+
): SearchResult[] {
|
|
58
|
+
const results: SearchResult[] = []
|
|
59
|
+
const MAX_MSG_RESULTS = 10
|
|
60
|
+
for (const [sessionId, session] of Object.entries(sessions)) {
|
|
61
|
+
if (results.length >= MAX_MSG_RESULTS) break
|
|
62
|
+
if (!Array.isArray(session.messages) || !session.messages.length) continue
|
|
63
|
+
const messages = session.messages as Array<Record<string, unknown>>
|
|
64
|
+
const agentId = session.agentId as string | undefined
|
|
65
|
+
const agentName = agentId && agents[agentId] ? (agents[agentId].name as string) : undefined
|
|
66
|
+
const sessionName = (session.name as string) || 'Untitled'
|
|
67
|
+
for (let i = 0; i < messages.length; i++) {
|
|
68
|
+
if (results.length >= MAX_MSG_RESULTS) break
|
|
69
|
+
const msg = messages[i]
|
|
70
|
+
const text = typeof msg?.text === 'string' ? msg.text : ''
|
|
71
|
+
if (!text) continue
|
|
72
|
+
const idx = text.toLowerCase().indexOf(needle)
|
|
73
|
+
if (idx === -1) continue
|
|
74
|
+
// Build snippet with context around match
|
|
75
|
+
const start = Math.max(0, idx - 30)
|
|
76
|
+
const end = Math.min(text.length, idx + needle.length + 50)
|
|
77
|
+
const snippet = (start > 0 ? '...' : '') + text.slice(start, end).replace(/\n/g, ' ') + (end < text.length ? '...' : '')
|
|
78
|
+
const msgTime = typeof msg.time === 'number' ? msg.time : 0
|
|
79
|
+
const timeAgo = msgTime ? formatTimeAgo(msgTime) : ''
|
|
80
|
+
results.push({
|
|
81
|
+
type: 'message',
|
|
82
|
+
id: sessionId,
|
|
83
|
+
title: snippet,
|
|
84
|
+
description: [agentName || sessionName, timeAgo].filter(Boolean).join(' · '),
|
|
85
|
+
messageIndex: i,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return results
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatTimeAgo(ts: number): string {
|
|
93
|
+
const diff = Date.now() - ts
|
|
94
|
+
const mins = Math.floor(diff / 60000)
|
|
95
|
+
if (mins < 1) return 'just now'
|
|
96
|
+
if (mins < 60) return `${mins}m ago`
|
|
97
|
+
const hours = Math.floor(mins / 60)
|
|
98
|
+
if (hours < 24) return `${hours}h ago`
|
|
99
|
+
const days = Math.floor(hours / 24)
|
|
100
|
+
return `${days}d ago`
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
export async function GET(req: Request) {
|
|
53
104
|
const { searchParams } = new URL(req.url)
|
|
54
105
|
const q = (searchParams.get('q') || '').trim().toLowerCase()
|
|
@@ -71,6 +122,7 @@ export async function GET(req: Request) {
|
|
|
71
122
|
searchCollection(schedules, 'schedule', q, 'name', 'taskPrompt', 'status'),
|
|
72
123
|
searchCollection(webhooks, 'webhook', q, 'name', 'source'),
|
|
73
124
|
searchCollection(skills, 'skill', q, 'name', 'description'),
|
|
125
|
+
searchMessages(sessions, agents, q),
|
|
74
126
|
]
|
|
75
127
|
|
|
76
128
|
// Proportional allocation across types
|
|
@@ -17,6 +17,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
17
17
|
const attachedFiles = Array.isArray(body.attachedFiles) ? body.attachedFiles.filter((f: unknown) => typeof f === 'string') as string[] : undefined
|
|
18
18
|
const internal = body.internal === true
|
|
19
19
|
const queueMode = normalizeQueueMode(body.queueMode, internal)
|
|
20
|
+
const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
|
|
20
21
|
|
|
21
22
|
const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
|
|
22
23
|
if (!message.trim() && !hasFiles) {
|
|
@@ -46,6 +47,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
46
47
|
source: internal ? 'heartbeat' : 'chat',
|
|
47
48
|
mode: queueMode,
|
|
48
49
|
onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
|
|
50
|
+
replyToId,
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
log.info('chat', `Enqueued session run ${run.runId}`, {
|
|
@@ -4,7 +4,7 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
4
4
|
|
|
5
5
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
6
|
const { id } = await params
|
|
7
|
-
const body = await req.json() as { messageIndex: number; newText: string }
|
|
7
|
+
const body = await req.json().catch(() => ({})) as { messageIndex: number; newText: string }
|
|
8
8
|
const sessions = loadSessions()
|
|
9
9
|
const session = sessions[id]
|
|
10
10
|
if (!session) return notFound()
|
|
@@ -5,7 +5,7 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
5
5
|
|
|
6
6
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
7
|
const { id } = await params
|
|
8
|
-
const body = await req.json() as { messageIndex: number }
|
|
8
|
+
const body = await req.json().catch(() => ({})) as { messageIndex: number }
|
|
9
9
|
const sessions = loadSessions()
|
|
10
10
|
const source = sessions[id]
|
|
11
11
|
if (!source) return notFound()
|
|
@@ -2,11 +2,57 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadSessions, saveSessions } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
|
|
5
|
-
export async function GET(
|
|
5
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
6
|
const { id } = await params
|
|
7
7
|
const sessions = loadSessions()
|
|
8
8
|
if (!sessions[id]) return notFound()
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
const url = new URL(req.url)
|
|
11
|
+
const limitParam = url.searchParams.get('limit')
|
|
12
|
+
const beforeParam = url.searchParams.get('before')
|
|
13
|
+
|
|
14
|
+
const allMessages = sessions[id].messages
|
|
15
|
+
const total = allMessages.length
|
|
16
|
+
|
|
17
|
+
// If no limit param, return all messages (backward compatible)
|
|
18
|
+
if (!limitParam) {
|
|
19
|
+
return NextResponse.json(allMessages)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const limit = Math.max(1, Math.min(500, parseInt(limitParam) || 100))
|
|
23
|
+
const before = beforeParam !== null ? parseInt(beforeParam) : total
|
|
24
|
+
|
|
25
|
+
// Return `limit` messages ending just before `before` index
|
|
26
|
+
const start = Math.max(0, before - limit)
|
|
27
|
+
const end = Math.max(0, before)
|
|
28
|
+
const messages = allMessages.slice(start, end)
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
messages,
|
|
32
|
+
total,
|
|
33
|
+
hasMore: start > 0,
|
|
34
|
+
startIndex: start,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
39
|
+
const { id } = await params
|
|
40
|
+
const body = await req.json() as { kind?: string }
|
|
41
|
+
if (body.kind !== 'context-clear') {
|
|
42
|
+
return NextResponse.json({ error: 'Only context-clear kind is supported' }, { status: 400 })
|
|
43
|
+
}
|
|
44
|
+
const sessions = loadSessions()
|
|
45
|
+
const session = sessions[id]
|
|
46
|
+
if (!session) return notFound()
|
|
47
|
+
|
|
48
|
+
session.messages.push({
|
|
49
|
+
role: 'user',
|
|
50
|
+
text: '',
|
|
51
|
+
kind: 'context-clear',
|
|
52
|
+
time: Date.now(),
|
|
53
|
+
})
|
|
54
|
+
saveSessions(sessions)
|
|
55
|
+
return NextResponse.json({ ok: true })
|
|
10
56
|
}
|
|
11
57
|
|
|
12
58
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -25,3 +71,25 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
25
71
|
saveSessions(sessions)
|
|
26
72
|
return NextResponse.json(session.messages[messageIndex])
|
|
27
73
|
}
|
|
74
|
+
|
|
75
|
+
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
76
|
+
const { id } = await params
|
|
77
|
+
const body = await req.json() as { messageIndex: number }
|
|
78
|
+
const sessions = loadSessions()
|
|
79
|
+
const session = sessions[id]
|
|
80
|
+
if (!session) return notFound()
|
|
81
|
+
|
|
82
|
+
const { messageIndex } = body
|
|
83
|
+
if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
|
|
84
|
+
return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only allow deleting context-clear markers (safety guard)
|
|
88
|
+
if (session.messages[messageIndex].kind !== 'context-clear') {
|
|
89
|
+
return NextResponse.json({ error: 'Only context-clear markers can be removed' }, { status: 400 })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
session.messages.splice(messageIndex, 1)
|
|
93
|
+
saveSessions(sessions)
|
|
94
|
+
return NextResponse.json({ ok: true })
|
|
95
|
+
}
|
|
@@ -69,6 +69,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
69
69
|
if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
|
|
70
70
|
if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
|
|
71
71
|
if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
|
|
72
|
+
if (updates.claudeSessionId !== undefined) sessions[id].claudeSessionId = updates.claudeSessionId
|
|
73
|
+
if (updates.codexThreadId !== undefined) sessions[id].codexThreadId = updates.codexThreadId
|
|
74
|
+
if (updates.opencodeSessionId !== undefined) sessions[id].opencodeSessionId = updates.opencodeSessionId
|
|
75
|
+
if (updates.delegateResumeIds !== undefined) sessions[id].delegateResumeIds = updates.delegateResumeIds
|
|
72
76
|
if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
|
|
73
77
|
ensureMainSessionFlag(sessions[id])
|
|
74
78
|
|
|
@@ -23,7 +23,7 @@ export async function GET(_req: Request) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function DELETE(req: Request) {
|
|
26
|
-
const { ids } = await req.json() as { ids: string[] }
|
|
26
|
+
const { ids } = await req.json().catch(() => ({ ids: [] })) as { ids: string[] }
|
|
27
27
|
if (!Array.isArray(ids) || !ids.length) {
|
|
28
28
|
return new NextResponse('Missing ids', { status: 400 })
|
|
29
29
|
}
|
|
@@ -43,7 +43,7 @@ export async function DELETE(req: Request) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function POST(req: Request) {
|
|
46
|
-
const body = await req.json()
|
|
46
|
+
const body = await req.json().catch(() => ({}))
|
|
47
47
|
let cwd = (body.cwd || '').trim()
|
|
48
48
|
if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
|
|
49
49
|
else if (cwd === '~') cwd = os.homedir()
|
|
@@ -64,7 +64,7 @@ export async function POST(req: Request) {
|
|
|
64
64
|
|
|
65
65
|
sessions[id] = {
|
|
66
66
|
id, name: sessionName, cwd,
|
|
67
|
-
user: body.user || '
|
|
67
|
+
user: body.user || 'user',
|
|
68
68
|
provider: body.provider || agent?.provider || 'claude-cli',
|
|
69
69
|
model: body.model || agent?.model || '',
|
|
70
70
|
credentialId: body.credentialId || agent?.credentialId || null,
|
|
@@ -56,5 +56,14 @@ export async function PUT(req: Request) {
|
|
|
56
56
|
settings.maxLinkedMemoriesExpanded = nextLinked
|
|
57
57
|
|
|
58
58
|
saveSettings(settings)
|
|
59
|
+
|
|
60
|
+
// Restart heartbeat service when heartbeat-related settings change
|
|
61
|
+
const heartbeatKeys = ['heartbeatIntervalSec', 'heartbeatInterval', 'heartbeatPrompt', 'heartbeatEnabled', 'heartbeatActiveStart', 'heartbeatActiveEnd']
|
|
62
|
+
if (heartbeatKeys.some((k) => k in body)) {
|
|
63
|
+
import('@/lib/server/heartbeat-service').then(({ restartHeartbeatService }) => {
|
|
64
|
+
restartHeartbeatService()
|
|
65
|
+
}).catch(() => { /* heartbeat service may not be initialized yet */ })
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
return NextResponse.json(settings)
|
|
60
69
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
3
3
|
import { getDeviceId, wsConnect } from '@/lib/providers/openclaw'
|
|
4
|
+
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
4
5
|
|
|
5
6
|
type SetupProvider =
|
|
6
7
|
| 'openai'
|
|
@@ -15,20 +16,6 @@ type SetupProvider =
|
|
|
15
16
|
| 'ollama'
|
|
16
17
|
| 'openclaw'
|
|
17
18
|
|
|
18
|
-
const OPENAI_COMPATIBLE_PROVIDER_INFO: Record<
|
|
19
|
-
'openai' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks',
|
|
20
|
-
{ name: string; defaultEndpoint: string }
|
|
21
|
-
> = {
|
|
22
|
-
openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
|
|
23
|
-
google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
24
|
-
deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
|
|
25
|
-
groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
|
|
26
|
-
together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
|
|
27
|
-
mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
|
|
28
|
-
xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
|
|
29
|
-
fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
|
|
30
|
-
}
|
|
31
|
-
|
|
32
19
|
interface SetupCheckBody {
|
|
33
20
|
provider?: string
|
|
34
21
|
apiKey?: string
|
|
@@ -196,7 +183,7 @@ export async function POST(req: Request) {
|
|
|
196
183
|
switch (provider) {
|
|
197
184
|
case 'openai': {
|
|
198
185
|
if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
|
|
199
|
-
const info =
|
|
186
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.openai
|
|
200
187
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
|
|
201
188
|
return NextResponse.json(result)
|
|
202
189
|
}
|
|
@@ -212,7 +199,7 @@ export async function POST(req: Request) {
|
|
|
212
199
|
case 'mistral':
|
|
213
200
|
case 'xai':
|
|
214
201
|
case 'fireworks': {
|
|
215
|
-
const info =
|
|
202
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS[provider]
|
|
216
203
|
if (!apiKey) return NextResponse.json({ ok: false, message: `${info.name} API key is required.` })
|
|
217
204
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
|
|
218
205
|
return NextResponse.json(result)
|
|
@@ -18,6 +18,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
18
18
|
const body = await req.json()
|
|
19
19
|
const result = mutateItem(ops, id, (skill) => {
|
|
20
20
|
const normalized = normalizeSkillPayload({ ...skill, ...body })
|
|
21
|
+
const updatedScope = body.scope === 'agent' ? 'agent' as const : body.scope === 'global' ? 'global' as const : skill.scope
|
|
22
|
+
const updatedAgentIds = updatedScope === 'agent' && Array.isArray(body.agentIds)
|
|
23
|
+
? (body.agentIds as unknown[]).filter((aid): aid is string => typeof aid === 'string')
|
|
24
|
+
: updatedScope === 'agent' ? (skill.agentIds || []) : []
|
|
21
25
|
return {
|
|
22
26
|
...skill,
|
|
23
27
|
...body,
|
|
@@ -27,6 +31,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
27
31
|
content: normalized.content,
|
|
28
32
|
sourceUrl: normalized.sourceUrl,
|
|
29
33
|
sourceFormat: normalized.sourceFormat,
|
|
34
|
+
scope: updatedScope,
|
|
35
|
+
agentIds: updatedAgentIds,
|
|
30
36
|
id,
|
|
31
37
|
updatedAt: Date.now(),
|
|
32
38
|
}
|
|
@@ -14,6 +14,10 @@ export async function POST(req: Request) {
|
|
|
14
14
|
const skills = loadSkills()
|
|
15
15
|
const id = genId()
|
|
16
16
|
const normalized = normalizeSkillPayload(body)
|
|
17
|
+
const scope = body.scope === 'agent' ? 'agent' as const : 'global' as const
|
|
18
|
+
const agentIds = scope === 'agent' && Array.isArray(body.agentIds)
|
|
19
|
+
? (body.agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
|
|
20
|
+
: []
|
|
17
21
|
skills[id] = {
|
|
18
22
|
id,
|
|
19
23
|
name: normalized.name,
|
|
@@ -22,6 +26,8 @@ export async function POST(req: Request) {
|
|
|
22
26
|
description: normalized.description || '',
|
|
23
27
|
sourceUrl: normalized.sourceUrl,
|
|
24
28
|
sourceFormat: normalized.sourceFormat,
|
|
29
|
+
scope,
|
|
30
|
+
agentIds,
|
|
25
31
|
createdAt: Date.now(),
|
|
26
32
|
updatedAt: Date.now(),
|
|
27
33
|
}
|
|
@@ -8,6 +8,8 @@ import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/ta
|
|
|
8
8
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
9
9
|
import { notify } from '@/lib/server/ws-hub'
|
|
10
10
|
import { createNotification } from '@/lib/server/create-notification'
|
|
11
|
+
import { enqueueSystemEvent } from '@/lib/server/system-events'
|
|
12
|
+
import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
|
|
11
13
|
|
|
12
14
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
13
15
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -34,6 +36,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
34
36
|
tasks[id].updatedAt = Date.now()
|
|
35
37
|
} else {
|
|
36
38
|
Object.assign(tasks[id], body, { updatedAt: Date.now() })
|
|
39
|
+
// Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
|
|
40
|
+
if (body.projectId === null) delete tasks[id].projectId
|
|
37
41
|
}
|
|
38
42
|
tasks[id].id = id // prevent id overwrite
|
|
39
43
|
|
|
@@ -84,6 +88,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
84
88
|
entityType: 'task',
|
|
85
89
|
entityId: id,
|
|
86
90
|
})
|
|
91
|
+
|
|
92
|
+
// Enqueue system event + heartbeat wake
|
|
93
|
+
if (tasks[id].sessionId) {
|
|
94
|
+
enqueueSystemEvent(tasks[id].sessionId, `Task ${tasks[id].status}: ${tasks[id].title}`)
|
|
95
|
+
}
|
|
96
|
+
if (tasks[id].agentId) {
|
|
97
|
+
requestHeartbeatNow({ agentId: tasks[id].agentId, reason: 'task-completed' })
|
|
98
|
+
}
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
// Dependency check: cannot queue a task if any blocker is incomplete
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
|
|
4
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import { createNotification } from '@/lib/server/create-notification'
|
|
7
|
+
import type { BoardTaskStatus } from '@/types'
|
|
8
|
+
|
|
9
|
+
const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bulk update tasks — batch status changes, agent/project reassignment, or archive/delete.
|
|
13
|
+
*
|
|
14
|
+
* POST body:
|
|
15
|
+
* ids: string[] — required, task IDs to update
|
|
16
|
+
* status?: BoardTaskStatus — move all to this status
|
|
17
|
+
* agentId?: string | null — reassign agent (null to clear)
|
|
18
|
+
* projectId?: string | null — reassign project (null to clear)
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(req: Request) {
|
|
21
|
+
const body = await req.json()
|
|
22
|
+
const ids: unknown = body.ids
|
|
23
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
24
|
+
return NextResponse.json({ error: 'ids must be a non-empty array' }, { status: 400 })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const taskIds = ids.filter((id): id is string => typeof id === 'string')
|
|
28
|
+
if (taskIds.length === 0) {
|
|
29
|
+
return NextResponse.json({ error: 'No valid task IDs provided' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tasks = loadTasks()
|
|
33
|
+
let updated = 0
|
|
34
|
+
const results: string[] = []
|
|
35
|
+
|
|
36
|
+
for (const id of taskIds) {
|
|
37
|
+
if (!tasks[id]) continue
|
|
38
|
+
const prevStatus = tasks[id].status
|
|
39
|
+
|
|
40
|
+
if (typeof body.status === 'string' && VALID_STATUSES.includes(body.status as BoardTaskStatus)) {
|
|
41
|
+
tasks[id].status = body.status as BoardTaskStatus
|
|
42
|
+
if (body.status === 'archived' && prevStatus !== 'archived') {
|
|
43
|
+
tasks[id].archivedAt = Date.now()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ('agentId' in body) {
|
|
48
|
+
tasks[id].agentId = body.agentId === null ? '' : String(body.agentId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ('projectId' in body) {
|
|
52
|
+
if (body.projectId === null) {
|
|
53
|
+
delete tasks[id].projectId
|
|
54
|
+
} else {
|
|
55
|
+
tasks[id].projectId = String(body.projectId)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
tasks[id].updatedAt = Date.now()
|
|
60
|
+
updated++
|
|
61
|
+
results.push(id)
|
|
62
|
+
|
|
63
|
+
// Side-effects for status transitions
|
|
64
|
+
if (prevStatus !== tasks[id].status) {
|
|
65
|
+
logActivity({
|
|
66
|
+
entityType: 'task',
|
|
67
|
+
entityId: id,
|
|
68
|
+
action: 'updated',
|
|
69
|
+
actor: 'user',
|
|
70
|
+
summary: `Bulk update: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})`,
|
|
71
|
+
})
|
|
72
|
+
pushMainLoopEventToMainSessions({
|
|
73
|
+
type: 'task_status_changed',
|
|
74
|
+
text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
|
|
75
|
+
})
|
|
76
|
+
if (tasks[id].status === 'completed' || tasks[id].status === 'failed') {
|
|
77
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
78
|
+
}
|
|
79
|
+
if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
|
|
80
|
+
enqueueTask(id)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
saveTasks(tasks)
|
|
86
|
+
|
|
87
|
+
if (updated > 0) {
|
|
88
|
+
const action = body.status
|
|
89
|
+
? `moved ${updated} task(s) to ${body.status}`
|
|
90
|
+
: `updated ${updated} task(s)`
|
|
91
|
+
createNotification({
|
|
92
|
+
type: 'success',
|
|
93
|
+
title: `Bulk update: ${action}`,
|
|
94
|
+
entityType: 'task',
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
notify('tasks')
|
|
99
|
+
return NextResponse.json({ updated, ids: results })
|
|
100
|
+
}
|