@swarmclawai/swarmclaw 1.2.1 → 1.2.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 +9 -0
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
|
-
import { loadSessions, devServers, localIP } from '@/lib/server/storage'
|
|
4
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
4
|
import { resolveDevServerLaunchDir } from '@/lib/server/runtime/devserver-launch'
|
|
5
|
+
import { clearDevServer, getDevServer, hasDevServer, registerDevServer, stopDevServer, updateDevServerUrl } from '@/lib/server/runtime/runtime-state'
|
|
6
|
+
import { localIP } from '@/lib/server/runtime/network'
|
|
7
|
+
import { listSessions } from '@/lib/server/sessions/session-repository'
|
|
6
8
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
7
9
|
import { sleep } from '@/lib/shared-utils'
|
|
8
10
|
import net from 'net'
|
|
@@ -56,22 +58,21 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
|
|
|
56
58
|
if (match) {
|
|
57
59
|
const detectedPort = match[1]
|
|
58
60
|
detectedUrl = `http://${localIP()}:${detectedPort}`
|
|
59
|
-
|
|
60
|
-
if (ds) ds.url = detectedUrl
|
|
61
|
+
updateDevServerUrl(id, detectedUrl)
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
proc.stdout!.on('data', onData)
|
|
66
67
|
proc.stderr!.on('data', onData)
|
|
67
|
-
proc.on('close', () => {
|
|
68
|
-
proc.on('error', () =>
|
|
68
|
+
proc.on('close', () => { clearDevServer(id); log.info(TAG, `dev server stopped for ${id}`) })
|
|
69
|
+
proc.on('error', () => clearDevServer(id))
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
registerDevServer(id, { proc, url: `http://${localIP()}:${port}` })
|
|
71
72
|
log.info(TAG, `starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
|
|
72
73
|
|
|
73
74
|
await sleep(4000)
|
|
74
|
-
const ds =
|
|
75
|
+
const ds = getDevServer(id)
|
|
75
76
|
if (!ds) {
|
|
76
77
|
return {
|
|
77
78
|
status: 502,
|
|
@@ -99,7 +100,7 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
|
|
|
99
100
|
|
|
100
101
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
101
102
|
const { id } = await params
|
|
102
|
-
const sessions =
|
|
103
|
+
const sessions = listSessions()
|
|
103
104
|
const session = sessions[id]
|
|
104
105
|
if (!session) return notFound()
|
|
105
106
|
|
|
@@ -108,8 +109,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
108
109
|
const { action } = body
|
|
109
110
|
|
|
110
111
|
if (action === 'start') {
|
|
111
|
-
if (
|
|
112
|
-
const ds =
|
|
112
|
+
if (hasDevServer(id)) {
|
|
113
|
+
const ds = getDevServer(id)!
|
|
113
114
|
return NextResponse.json({ running: true, url: ds.url })
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -126,18 +127,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
126
127
|
return NextResponse.json(result.body, result.status ? { status: result.status } : undefined)
|
|
127
128
|
|
|
128
129
|
} else if (action === 'stop') {
|
|
129
|
-
|
|
130
|
-
const ds = devServers.get(id)!
|
|
131
|
-
try { ds.proc.kill('SIGTERM') } catch {}
|
|
132
|
-
if (typeof ds.proc.pid === 'number') {
|
|
133
|
-
try { process.kill(-ds.proc.pid, 'SIGTERM') } catch {}
|
|
134
|
-
}
|
|
135
|
-
devServers.delete(id)
|
|
136
|
-
}
|
|
130
|
+
stopDevServer(id)
|
|
137
131
|
return NextResponse.json({ running: false })
|
|
138
132
|
|
|
139
133
|
} else if (action === 'status') {
|
|
140
|
-
return NextResponse.json({ running:
|
|
134
|
+
return NextResponse.json({ running: hasDevServer(id), url: getDevServer(id)?.url })
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
return NextResponse.json({ running: false })
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadStoredItem, upsertStoredItem, active } from '@/lib/server/storage'
|
|
3
2
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
3
|
import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
|
|
5
4
|
import { appendSessionNote } from '@/lib/server/session-note'
|
|
6
5
|
import { getSessionRunState } from '@/lib/server/runtime/session-run-manager'
|
|
7
|
-
import
|
|
6
|
+
import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
|
|
7
|
+
import type { Message } from '@/types'
|
|
8
8
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
9
9
|
|
|
10
10
|
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
11
|
const { id } = await params
|
|
12
|
-
const session =
|
|
12
|
+
const session = getSession(id)
|
|
13
13
|
if (!session) return notFound()
|
|
14
14
|
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// those are only computed at runtime from the active map and run ledger.
|
|
16
|
+
// Use persisted fields plus the run ledger. Process-local execution state is
|
|
17
|
+
// intentionally excluded here so stale registry entries do not block cleanup.
|
|
19
18
|
const sessionClaimsActive = session.active === true
|
|
20
19
|
|| (typeof session.currentRunId === 'string' && session.currentRunId.trim().length > 0)
|
|
21
|
-
|| active.has(id)
|
|
22
20
|
|| !!getSessionRunState(id).runningRunId
|
|
23
21
|
if (!sessionClaimsActive && materializeStreamingAssistantArtifacts(session.messages)) {
|
|
24
|
-
|
|
22
|
+
saveSession(id, session)
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
const url = new URL(req.url)
|
|
@@ -63,7 +61,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
63
61
|
if (error) return error
|
|
64
62
|
|
|
65
63
|
if (body.kind === 'context-clear') {
|
|
66
|
-
const session =
|
|
64
|
+
const session = getSession(id)
|
|
67
65
|
if (!session) return notFound()
|
|
68
66
|
|
|
69
67
|
session.messages.push({
|
|
@@ -72,7 +70,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
72
70
|
kind: 'context-clear',
|
|
73
71
|
time: Date.now(),
|
|
74
72
|
})
|
|
75
|
-
|
|
73
|
+
saveSession(id, session)
|
|
76
74
|
return NextResponse.json({ ok: true })
|
|
77
75
|
}
|
|
78
76
|
|
|
@@ -84,7 +82,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
84
82
|
kind: body.messageKind || 'system',
|
|
85
83
|
})
|
|
86
84
|
if (!inserted) {
|
|
87
|
-
const session =
|
|
85
|
+
const session = getSession(id)
|
|
88
86
|
if (!session) return notFound()
|
|
89
87
|
return NextResponse.json({ error: 'Note text is required' }, { status: 400 })
|
|
90
88
|
}
|
|
@@ -98,7 +96,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
98
96
|
const { id } = await params
|
|
99
97
|
const { data: body, error } = await safeParseBody<{ messageIndex: number; bookmarked: boolean }>(req)
|
|
100
98
|
if (error) return error
|
|
101
|
-
const session =
|
|
99
|
+
const session = getSession(id)
|
|
102
100
|
if (!session) return notFound()
|
|
103
101
|
|
|
104
102
|
const { messageIndex, bookmarked } = body
|
|
@@ -107,7 +105,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
session.messages[messageIndex].bookmarked = bookmarked
|
|
110
|
-
|
|
108
|
+
saveSession(id, session)
|
|
111
109
|
return NextResponse.json(session.messages[messageIndex])
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -115,7 +113,7 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ id: s
|
|
|
115
113
|
const { id } = await params
|
|
116
114
|
const { data: body, error } = await safeParseBody<{ messageIndex: number }>(req)
|
|
117
115
|
if (error) return error
|
|
118
|
-
const session =
|
|
116
|
+
const session = getSession(id)
|
|
119
117
|
if (!session) return notFound()
|
|
120
118
|
|
|
121
119
|
const { messageIndex } = body
|
|
@@ -129,6 +127,6 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ id: s
|
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
session.messages.splice(messageIndex, 1)
|
|
132
|
-
|
|
130
|
+
saveSession(id, session)
|
|
133
131
|
return NextResponse.json({ ok: true })
|
|
134
132
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { loadAgents } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
5
5
|
import { resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
|
|
6
6
|
import { clearMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
|
|
7
7
|
import { cleanupSessionProcesses } from '@/lib/server/runtime/process-manager'
|
|
8
|
+
import { deleteSession, getSession, saveSession } from '@/lib/server/sessions/session-repository'
|
|
9
|
+
import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
8
10
|
import { getSessionQueueSnapshot, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
|
|
9
11
|
import { normalizeCapabilitySelection } from '@/lib/capability-selection'
|
|
10
12
|
import { enrichSessionWithMissionSummary } from '@/lib/server/missions/mission-service'
|
|
@@ -12,12 +14,12 @@ import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
|
12
14
|
|
|
13
15
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
16
|
const { id } = await params
|
|
15
|
-
const session =
|
|
17
|
+
const session = getSession(id)
|
|
16
18
|
if (!session) return notFound()
|
|
17
19
|
|
|
18
20
|
const run = getSessionRunState(id)
|
|
19
21
|
const queue = getSessionQueueSnapshot(id)
|
|
20
|
-
session.active =
|
|
22
|
+
session.active = !!run.runningRunId
|
|
21
23
|
session.queuedCount = queue.queueLength
|
|
22
24
|
session.currentRunId = run.runningRunId || null
|
|
23
25
|
|
|
@@ -28,7 +30,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
28
30
|
const { id } = await params
|
|
29
31
|
const { data: updates, error } = await safeParseBody(req)
|
|
30
32
|
if (error) return error
|
|
31
|
-
const session =
|
|
33
|
+
const session = getSession(id) as Record<string, unknown> | null
|
|
32
34
|
if (!session) return notFound()
|
|
33
35
|
|
|
34
36
|
if (updates.resetMainLoopState === true) {
|
|
@@ -142,17 +144,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
142
144
|
if (updates.delegateResumeIds !== undefined) session.delegateResumeIds = updates.delegateResumeIds
|
|
143
145
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
saveSession(id, session)
|
|
146
148
|
return NextResponse.json(enrichSessionWithMissionSummary(session as never))
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
150
152
|
const { id } = await params
|
|
151
|
-
if (!
|
|
152
|
-
|
|
153
|
-
try { active.get(id)?.kill() } catch {}
|
|
154
|
-
active.delete(id)
|
|
155
|
-
}
|
|
153
|
+
if (!getSession(id)) return notFound()
|
|
154
|
+
stopActiveSessionProcess(id)
|
|
156
155
|
cleanupSessionProcesses(id)
|
|
157
156
|
deleteSession(id)
|
|
158
157
|
return new NextResponse('OK')
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
|
|
3
|
-
import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
|
|
4
3
|
import { cancelSessionRuns } from '@/lib/server/runtime/session-run-manager'
|
|
4
|
+
import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
5
|
+
import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
|
|
5
6
|
import type { Session } from '@/types'
|
|
6
7
|
|
|
7
8
|
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
9
|
const { id } = await params
|
|
9
10
|
const cancel = cancelSessionRuns(id, 'Stopped by user')
|
|
10
|
-
const session =
|
|
11
|
+
const session = getSession(id) as Session | null
|
|
11
12
|
if (session && Array.isArray(session.messages) && materializeStreamingAssistantArtifacts(session.messages)) {
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
if (active.has(id)) {
|
|
15
|
-
try { active.get(id)?.kill() } catch {}
|
|
16
|
-
active.delete(id)
|
|
13
|
+
saveSession(id, session)
|
|
17
14
|
}
|
|
15
|
+
stopActiveSessionProcess(id)
|
|
18
16
|
return NextResponse.json({ ok: true, ...cancel })
|
|
19
17
|
}
|
|
@@ -10,11 +10,13 @@ test('chat messages route materializes stale streaming artifacts even if runtime
|
|
|
10
10
|
returnedText: string | null
|
|
11
11
|
persistedStreaming: boolean | null
|
|
12
12
|
persistedText: string | null
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
}>(`
|
|
14
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
15
|
+
const routeMod = await import('./src/app/api/chats/[id]/messages/route')
|
|
16
|
+
const runtimeStateMod = await import('./src/lib/server/runtime/runtime-state')
|
|
17
|
+
const storage = storageMod.default || storageMod
|
|
18
|
+
const route = routeMod.default || routeMod
|
|
19
|
+
const runtimeState = runtimeStateMod.default || runtimeStateMod
|
|
18
20
|
|
|
19
21
|
storage.upsertStoredItem('sessions', 'session-stale', {
|
|
20
22
|
id: 'session-stale',
|
|
@@ -37,7 +39,7 @@ test('chat messages route materializes stale streaming artifacts even if runtime
|
|
|
37
39
|
],
|
|
38
40
|
})
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
runtimeState.registerActiveSessionProcess('session-stale', { kill() {} })
|
|
41
43
|
|
|
42
44
|
const response = await route.GET(
|
|
43
45
|
new Request('http://local/api/chats/session-stale/messages'),
|
|
@@ -3,9 +3,11 @@ import { genId } from '@/lib/id'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { perf } from '@/lib/server/runtime/perf'
|
|
6
|
-
import {
|
|
6
|
+
import { loadAgents } from '@/lib/server/storage'
|
|
7
7
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
8
8
|
import { notify } from '@/lib/server/ws-hub'
|
|
9
|
+
import { deleteSession, listSessions, replaceSessions } from '@/lib/server/sessions/session-repository'
|
|
10
|
+
import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
9
11
|
import { getSessionQueueSnapshot, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
|
|
10
12
|
import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
11
13
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
|
|
@@ -25,11 +27,11 @@ export async function GET(req: Request) {
|
|
|
25
27
|
const endPerf = perf.start('api', 'GET /api/chats')
|
|
26
28
|
// Note: pruneThreadConnectorMirrors and materializeStreamingAssistantArtifacts
|
|
27
29
|
// are handled by the daemon periodic health check, not on every list fetch.
|
|
28
|
-
const sessions =
|
|
30
|
+
const sessions = listSessions()
|
|
29
31
|
for (const id of Object.keys(sessions)) {
|
|
30
32
|
const run = getSessionRunState(id)
|
|
31
33
|
const queue = getSessionQueueSnapshot(id)
|
|
32
|
-
sessions[id].active =
|
|
34
|
+
sessions[id].active = !!run.runningRunId
|
|
33
35
|
sessions[id].queuedCount = queue.queueLength
|
|
34
36
|
sessions[id].currentRunId = run.runningRunId || null
|
|
35
37
|
}
|
|
@@ -59,14 +61,11 @@ export async function DELETE(req: Request) {
|
|
|
59
61
|
if (!Array.isArray(ids) || !ids.length) {
|
|
60
62
|
return new NextResponse('Missing ids', { status: 400 })
|
|
61
63
|
}
|
|
62
|
-
const sessions =
|
|
64
|
+
const sessions = listSessions()
|
|
63
65
|
let deleted = 0
|
|
64
66
|
for (const id of ids) {
|
|
65
67
|
if (!sessions[id]) continue
|
|
66
|
-
|
|
67
|
-
try { active.get(id)?.kill() } catch {}
|
|
68
|
-
active.delete(id)
|
|
69
|
-
}
|
|
68
|
+
stopActiveSessionProcess(id)
|
|
70
69
|
deleteSession(id)
|
|
71
70
|
deleted += 1
|
|
72
71
|
}
|
|
@@ -83,7 +82,7 @@ export async function POST(req: Request) {
|
|
|
83
82
|
else if (!cwd) cwd = WORKSPACE_DIR
|
|
84
83
|
|
|
85
84
|
const id = body.id || genId()
|
|
86
|
-
const sessions =
|
|
85
|
+
const sessions = listSessions()
|
|
87
86
|
const agent = body.agentId ? loadAgents()[body.agentId] : null
|
|
88
87
|
if (isAgentDisabled(agent)) {
|
|
89
88
|
return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start chats') }, { status: 409 })
|
|
@@ -162,7 +161,7 @@ export async function POST(req: Request) {
|
|
|
162
161
|
sessions[id] = (body.provider || body.model || body.credentialId || body.apiEndpoint)
|
|
163
162
|
? nextSession
|
|
164
163
|
: applyResolvedRoute(nextSession, resolvedRoute)
|
|
165
|
-
|
|
164
|
+
replaceSessions(sessions)
|
|
166
165
|
notify('sessions')
|
|
167
166
|
return NextResponse.json(sessions[id])
|
|
168
167
|
}
|
package/src/app/api/ip/route.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { localIP } from '@/lib/server/
|
|
2
|
+
import { localIP } from '@/lib/server/runtime/network'
|
|
3
3
|
export const dynamic = 'force-dynamic'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
export async function GET(
|
|
6
|
+
export async function GET() {
|
|
7
7
|
return NextResponse.json({ ip: localIP(), port: parseInt(process.env.PORT || '3000') })
|
|
8
8
|
}
|
|
@@ -3,7 +3,7 @@ import { spawn, type ChildProcess } from 'child_process'
|
|
|
3
3
|
import http from 'http'
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import path from 'path'
|
|
6
|
-
import { localIP } from '@/lib/server/
|
|
6
|
+
import { localIP } from '@/lib/server/runtime/network'
|
|
7
7
|
import { resolveDevServerLaunchDir } from '@/lib/server/runtime/devserver-launch'
|
|
8
8
|
import { resolvePathWithinBaseDir } from '@/lib/server/path-utils'
|
|
9
9
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
@@ -1,65 +1,26 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
|
|
5
|
-
import { notify } from '@/lib/server/ws-hub'
|
|
2
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
|
+
import { deleteProjectAndDetachReferences, getProject, updateProject } from '@/lib/server/projects/project-service'
|
|
6
4
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
7
5
|
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
-
const ops: CollectionOps<any> = { load: loadProjects, save: saveProjects, deleteFn: deleteProject, topic: 'projects' }
|
|
10
|
-
|
|
11
6
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
12
7
|
const { id } = await params
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
15
|
-
return NextResponse.json(
|
|
8
|
+
const project = getProject(id)
|
|
9
|
+
if (!project) return notFound()
|
|
10
|
+
return NextResponse.json(project)
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
19
14
|
const { id } = await params
|
|
20
15
|
const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
21
16
|
if (error) return error
|
|
22
|
-
const result =
|
|
23
|
-
const patch = normalizeProjectPatchInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
|
|
24
|
-
Object.assign(project, patch, { updatedAt: Date.now() })
|
|
25
|
-
delete (project as Record<string, unknown>).id
|
|
26
|
-
project.id = id
|
|
27
|
-
return project
|
|
28
|
-
})
|
|
17
|
+
const result = updateProject(id, body && typeof body === 'object' ? body : {})
|
|
29
18
|
if (!result) return notFound()
|
|
30
|
-
ensureProjectWorkspace(id, result.name)
|
|
31
19
|
return NextResponse.json(result)
|
|
32
20
|
}
|
|
33
21
|
|
|
34
22
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
35
23
|
const { id } = await params
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
// Clear projectId from referencing entities
|
|
39
|
-
const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void, topic: string) => {
|
|
40
|
-
const items = load()
|
|
41
|
-
let changed = false
|
|
42
|
-
for (const item of Object.values(items)) {
|
|
43
|
-
if (item.projectId === id) {
|
|
44
|
-
item.projectId = undefined
|
|
45
|
-
changed = true
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (changed) {
|
|
49
|
-
save(items)
|
|
50
|
-
notify(topic)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
clearProjectId(
|
|
55
|
-
loadAgents as unknown as () => Record<string, Record<string, unknown>>,
|
|
56
|
-
saveAgents as unknown as (data: Record<string, Record<string, unknown>>) => void,
|
|
57
|
-
'agents',
|
|
58
|
-
)
|
|
59
|
-
clearProjectId(loadTasks as unknown as () => Record<string, Record<string, unknown>>, saveTasks as unknown as (data: Record<string, Record<string, unknown>>) => void, 'tasks')
|
|
60
|
-
clearProjectId(loadSchedules as unknown as () => Record<string, Record<string, unknown>>, saveSchedules as unknown as (data: Record<string, Record<string, unknown>>) => void, 'schedules')
|
|
61
|
-
clearProjectId(loadSkills as unknown as () => Record<string, Record<string, unknown>>, saveSkills as unknown as (data: Record<string, Record<string, unknown>>) => void, 'skills')
|
|
62
|
-
clearProjectId(loadSecrets as unknown as () => Record<string, Record<string, unknown>>, saveSecrets as unknown as (data: Record<string, Record<string, unknown>>) => void, 'secrets')
|
|
63
|
-
|
|
24
|
+
if (!deleteProjectAndDetachReferences(id)) return notFound()
|
|
64
25
|
return NextResponse.json({ ok: true })
|
|
65
26
|
}
|
|
@@ -27,6 +27,7 @@ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
|
27
27
|
import { speak } from '@/lib/tts'
|
|
28
28
|
import { api } from '@/lib/app/api-client'
|
|
29
29
|
import { messagesDiffer } from '@/lib/chat/chat-streaming-state'
|
|
30
|
+
import { createAssistantRenderId } from '@/lib/chat/assistant-render-id'
|
|
30
31
|
import { getSessionLastMessage } from '@/lib/chat/session-summary'
|
|
31
32
|
import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
32
33
|
|
|
@@ -71,6 +72,36 @@ export function ChatArea() {
|
|
|
71
72
|
const setPreviewContent = useChatStore((s) => s.setPreviewContent)
|
|
72
73
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
73
74
|
|
|
75
|
+
const markSessionLocallyIdle = useCallback((targetSessionId: string) => {
|
|
76
|
+
const appState = useAppStore.getState()
|
|
77
|
+
const existing = appState.sessions[targetSessionId]
|
|
78
|
+
if (!existing) return
|
|
79
|
+
appState.updateSessionInStore({
|
|
80
|
+
...existing,
|
|
81
|
+
active: false,
|
|
82
|
+
currentRunId: null,
|
|
83
|
+
})
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const startServerStreamingPlaceholder = useCallback((targetSessionId: string, phase: 'queued' | 'thinking' | 'connecting' = 'thinking') => {
|
|
87
|
+
useChatStore.setState((state) => {
|
|
88
|
+
const sameServerStream = state.streaming
|
|
89
|
+
&& state.streamSource === 'server'
|
|
90
|
+
&& state.streamingSessionId === targetSessionId
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
streaming: true,
|
|
94
|
+
streamingSessionId: targetSessionId,
|
|
95
|
+
streamSource: 'server',
|
|
96
|
+
streamPhase: sameServerStream ? state.streamPhase : phase,
|
|
97
|
+
streamText: '',
|
|
98
|
+
displayText: '',
|
|
99
|
+
assistantRenderId: sameServerStream && state.assistantRenderId ? state.assistantRenderId : createAssistantRenderId(),
|
|
100
|
+
thinkingStartTime: sameServerStream && state.thinkingStartTime > 0 ? state.thinkingStartTime : Date.now(),
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
74
105
|
const currentAgent = useAppStore((s) => {
|
|
75
106
|
const agentId = session?.agentId
|
|
76
107
|
return agentId ? s.agents[agentId] ?? null : null
|
|
@@ -178,14 +209,12 @@ export function ChatArea() {
|
|
|
178
209
|
})
|
|
179
210
|
|
|
180
211
|
const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
|
|
181
|
-
if (sessionAtLoad?.active)
|
|
182
|
-
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
|
|
183
|
-
}
|
|
212
|
+
if (sessionAtLoad?.active) startServerStreamingPlaceholder(requestedSessionId)
|
|
184
213
|
|
|
185
214
|
return () => {
|
|
186
215
|
cancelled = true
|
|
187
216
|
}
|
|
188
|
-
}, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages])
|
|
217
|
+
}, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages, startServerStreamingPlaceholder])
|
|
189
218
|
|
|
190
219
|
useEffect(() => {
|
|
191
220
|
if (!sessionId || messagesLoading) return
|
|
@@ -195,9 +224,7 @@ export function ChatArea() {
|
|
|
195
224
|
void refreshSession(requestedSessionId).then(() => {
|
|
196
225
|
if (cancelled || selectActiveSessionId(useAppStore.getState()) !== requestedSessionId) return
|
|
197
226
|
const refreshed = useAppStore.getState().sessions[requestedSessionId]
|
|
198
|
-
if (refreshed?.active)
|
|
199
|
-
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
|
|
200
|
-
}
|
|
227
|
+
if (refreshed?.active) startServerStreamingPlaceholder(requestedSessionId)
|
|
201
228
|
}).catch((err) => console.error('Failed to refresh session:', err))
|
|
202
229
|
|
|
203
230
|
void devServer(requestedSessionId, 'status').then((r) => {
|
|
@@ -213,7 +240,7 @@ export function ChatArea() {
|
|
|
213
240
|
cancelled = true
|
|
214
241
|
window.clearTimeout(timer)
|
|
215
242
|
}
|
|
216
|
-
}, [messagesLoading, refreshSession, sessionId, setDevServer])
|
|
243
|
+
}, [messagesLoading, refreshSession, sessionId, setDevServer, startServerStreamingPlaceholder])
|
|
217
244
|
|
|
218
245
|
useEffect(() => {
|
|
219
246
|
if (!sessionId || messagesLoading) return
|
|
@@ -292,19 +319,12 @@ export function ChatArea() {
|
|
|
292
319
|
&& !chatState.streaming
|
|
293
320
|
&& chatState.streamingSessionId !== sessionId
|
|
294
321
|
) {
|
|
295
|
-
|
|
296
|
-
streaming: true,
|
|
297
|
-
streamingSessionId: sessionId,
|
|
298
|
-
streamSource: 'server',
|
|
299
|
-
streamPhase: 'thinking',
|
|
300
|
-
streamText: '',
|
|
301
|
-
thinkingStartTime: Date.now(),
|
|
302
|
-
})
|
|
322
|
+
startServerStreamingPlaceholder(sessionId)
|
|
303
323
|
}
|
|
304
324
|
} catch (err) {
|
|
305
325
|
console.error('Failed to refresh queue:', err)
|
|
306
326
|
}
|
|
307
|
-
}, [loadQueuedMessages, sessionId])
|
|
327
|
+
}, [loadQueuedMessages, sessionId, startServerStreamingPlaceholder])
|
|
308
328
|
|
|
309
329
|
// Subscribe to WS messages for this session — always subscribe when session exists,
|
|
310
330
|
// only enable fallback polling when actively needed
|
|
@@ -316,7 +336,7 @@ export function ChatArea() {
|
|
|
316
336
|
useWs(
|
|
317
337
|
sessionId ? 'runs' : '',
|
|
318
338
|
refreshQueue,
|
|
319
|
-
sessionId && (isServerActive || queuedCount > 0) ?
|
|
339
|
+
sessionId && (isServerActive || queuedCount > 0) ? 2_500 : undefined,
|
|
320
340
|
)
|
|
321
341
|
|
|
322
342
|
// Listen for stream-end signal from the server — clears streaming state
|
|
@@ -325,11 +345,12 @@ export function ChatArea() {
|
|
|
325
345
|
if (!sessionId) return
|
|
326
346
|
const state = useChatStore.getState()
|
|
327
347
|
if (state.streamSource === 'server' && state.streamingSessionId === sessionId) {
|
|
328
|
-
|
|
348
|
+
markSessionLocallyIdle(sessionId)
|
|
349
|
+
useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
|
|
329
350
|
void refreshMessages()
|
|
330
351
|
void refreshSession(sessionId)
|
|
331
352
|
}
|
|
332
|
-
}, [sessionId, refreshMessages, refreshSession])
|
|
353
|
+
}, [markSessionLocallyIdle, sessionId, refreshMessages, refreshSession])
|
|
333
354
|
useWs(sessionId ? `stream-end:${sessionId}` : '', handleStreamEnd)
|
|
334
355
|
|
|
335
356
|
// Keep the local typing indicator aligned with the server's active state
|
|
@@ -338,7 +359,7 @@ export function ChatArea() {
|
|
|
338
359
|
const state = useChatStore.getState()
|
|
339
360
|
if (isServerActive) {
|
|
340
361
|
if (!state.streaming && !state.streamText) {
|
|
341
|
-
|
|
362
|
+
startServerStreamingPlaceholder(sessionId)
|
|
342
363
|
}
|
|
343
364
|
return
|
|
344
365
|
}
|
|
@@ -349,9 +370,10 @@ export function ChatArea() {
|
|
|
349
370
|
) {
|
|
350
371
|
// Server finished — clear all streaming state and fetch final messages
|
|
351
372
|
fetchMessages(sessionId).then(setMessages).catch(() => {})
|
|
352
|
-
|
|
373
|
+
markSessionLocallyIdle(sessionId)
|
|
374
|
+
useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
|
|
353
375
|
}
|
|
354
|
-
}, [isServerActive, sessionId, setMessages])
|
|
376
|
+
}, [isServerActive, markSessionLocallyIdle, sessionId, setMessages, startServerStreamingPlaceholder])
|
|
355
377
|
|
|
356
378
|
// Poll browser status while session has browser tools
|
|
357
379
|
const hasBrowserTool = getEnabledToolIds(session).includes('browser')
|
|
@@ -37,6 +37,41 @@ describe('MessageBubble', () => {
|
|
|
37
37
|
assert.doesNotMatch(html, /streaming-cursor/)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
it('falls back to persisted streaming content when the live stream payload is temporarily empty', async () => {
|
|
41
|
+
const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
|
|
42
|
+
const MessageBubble = (
|
|
43
|
+
messageBubbleModule.MessageBubble
|
|
44
|
+
|| (messageBubbleModule.default as { MessageBubble?: unknown } | undefined)?.MessageBubble
|
|
45
|
+
|| (messageBubbleModule['module.exports'] as { MessageBubble?: unknown } | undefined)?.MessageBubble
|
|
46
|
+
) as typeof import('./message-bubble').MessageBubble | undefined
|
|
47
|
+
assert.ok(MessageBubble)
|
|
48
|
+
|
|
49
|
+
const html = renderToStaticMarkup(
|
|
50
|
+
React.createElement(MessageBubble, {
|
|
51
|
+
message: {
|
|
52
|
+
role: 'assistant',
|
|
53
|
+
text: 'Recovered persisted partial text',
|
|
54
|
+
time: Date.now(),
|
|
55
|
+
kind: 'chat',
|
|
56
|
+
streaming: true,
|
|
57
|
+
},
|
|
58
|
+
assistantName: 'Hal2k-3',
|
|
59
|
+
agentName: 'Hal2k-3',
|
|
60
|
+
liveStream: {
|
|
61
|
+
active: true,
|
|
62
|
+
phase: 'responding',
|
|
63
|
+
toolName: '',
|
|
64
|
+
text: '',
|
|
65
|
+
thinking: '',
|
|
66
|
+
toolEvents: [],
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert.match(html, /Recovered persisted partial text/)
|
|
72
|
+
assert.match(html, /streaming-cursor/)
|
|
73
|
+
})
|
|
74
|
+
|
|
40
75
|
it('renders upload-linked screenshots inline without duplicating them at the bottom', async () => {
|
|
41
76
|
const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
|
|
42
77
|
const MessageBubble = (
|