@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
|
@@ -367,12 +367,14 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
367
367
|
const toolEvents = message.toolEvents ?? emptyToolEvents
|
|
368
368
|
const toolEventsForMedia = useMemo(
|
|
369
369
|
() => (liveStreamActive
|
|
370
|
-
? liveToolEvents.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
? (liveToolEvents.length > 0
|
|
371
|
+
? liveToolEvents.map((event) => ({
|
|
372
|
+
name: event.name,
|
|
373
|
+
input: event.input,
|
|
374
|
+
output: event.output,
|
|
375
|
+
error: event.status === 'error' || undefined,
|
|
376
|
+
}))
|
|
377
|
+
: toolEvents)
|
|
376
378
|
: toolEvents),
|
|
377
379
|
[liveStreamActive, liveToolEvents, toolEvents],
|
|
378
380
|
)
|
|
@@ -383,7 +385,15 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
383
385
|
)
|
|
384
386
|
const displayToolEvents = useMemo(
|
|
385
387
|
() => (liveStreamActive
|
|
386
|
-
? liveToolEvents.
|
|
388
|
+
? (liveToolEvents.length > 0
|
|
389
|
+
? liveToolEvents.filter((ev) => ev.name !== 'send_file' || ev.status === 'error')
|
|
390
|
+
: persistedToolEvents.map((ev, i) => ({
|
|
391
|
+
id: ev.toolCallId || `${message.time}-${ev.name}-${i}`,
|
|
392
|
+
name: ev.name,
|
|
393
|
+
input: ev.input,
|
|
394
|
+
output: ev.output,
|
|
395
|
+
status: ev.error ? 'error' as const : 'done' as const,
|
|
396
|
+
})))
|
|
387
397
|
: persistedToolEvents.map((ev, i) => ({
|
|
388
398
|
id: ev.toolCallId || `${message.time}-${ev.name}-${i}`,
|
|
389
399
|
name: ev.name,
|
|
@@ -419,11 +429,11 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
419
429
|
? (liveStreamActive ? (liveStream?.thinking?.trim() ? liveStream.thinking : undefined) : message.thinking)
|
|
420
430
|
: undefined
|
|
421
431
|
|
|
422
|
-
const sourceText = liveStreamActive ? (liveStream?.text || '') : message.text
|
|
432
|
+
const sourceText = liveStreamActive ? (liveStream?.text || message.text || '') : message.text
|
|
423
433
|
const connectorDeliveryTranscript = !isUser && message.kind === 'connector-delivery'
|
|
424
434
|
? (message.source?.deliveryTranscript?.trim() || '')
|
|
425
435
|
: ''
|
|
426
|
-
const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || '') : message.text)
|
|
436
|
+
const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || message.text || '') : message.text)
|
|
427
437
|
|
|
428
438
|
// Extract ALL media from ALL tool events for inline display after the message text.
|
|
429
439
|
// Covers send_file, browser screenshots, file tool outputs — everything.
|
|
@@ -188,6 +188,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
188
188
|
const snapUntilRef = useRef(0)
|
|
189
189
|
const prevSessionIdRef = useRef<string | null>(null)
|
|
190
190
|
const assistantRenderId = useChatStore((s) => s.assistantRenderId)
|
|
191
|
+
const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
|
|
191
192
|
const hasLiveArtifacts = useChatStore(selectHasLiveArtifacts)
|
|
192
193
|
const setMessages = useChatStore((s) => s.setMessages)
|
|
193
194
|
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
@@ -297,13 +298,46 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
297
298
|
return dedupeMessagesForDisplay(displayedMessages)
|
|
298
299
|
}, [messages, showAlerts, showOk])
|
|
299
300
|
|
|
301
|
+
const latestPersistedStreamingMessage = useMemo(() => {
|
|
302
|
+
for (let i = baseDisplayedMessages.length - 1; i >= 0; i -= 1) {
|
|
303
|
+
const candidate = baseDisplayedMessages[i]
|
|
304
|
+
if (candidate.role === 'assistant' && candidate.streaming === true) {
|
|
305
|
+
return candidate
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null
|
|
309
|
+
}, [baseDisplayedMessages])
|
|
310
|
+
|
|
311
|
+
const currentRunHasCompletedAssistant = useMemo(
|
|
312
|
+
() => (
|
|
313
|
+
streaming
|
|
314
|
+
&& thinkingStartTime > 0
|
|
315
|
+
&& baseDisplayedMessages.some((message) => (
|
|
316
|
+
message.role === 'assistant'
|
|
317
|
+
&& message.streaming !== true
|
|
318
|
+
&& message.kind !== 'system'
|
|
319
|
+
&& message.kind !== 'heartbeat'
|
|
320
|
+
&& typeof message.time === 'number'
|
|
321
|
+
&& message.time >= thinkingStartTime
|
|
322
|
+
))
|
|
323
|
+
),
|
|
324
|
+
[baseDisplayedMessages, streaming, thinkingStartTime],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const showLiveStreamRow = streaming
|
|
328
|
+
&& !!assistantRenderId
|
|
329
|
+
&& !currentRunHasCompletedAssistant
|
|
330
|
+
&& (hasLiveArtifacts || !!latestPersistedStreamingMessage)
|
|
331
|
+
|
|
300
332
|
const streamingAwareMessages = useMemo(() => (
|
|
301
333
|
buildStreamingAwareMessageList(baseDisplayedMessages, {
|
|
302
334
|
localStreaming: streaming,
|
|
303
335
|
hasLiveArtifacts,
|
|
304
336
|
assistantRenderId,
|
|
337
|
+
showLiveRow: showLiveStreamRow,
|
|
338
|
+
syntheticAssistant: latestPersistedStreamingMessage,
|
|
305
339
|
})
|
|
306
|
-
), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, streaming])
|
|
340
|
+
), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, latestPersistedStreamingMessage, showLiveStreamRow, streaming])
|
|
307
341
|
|
|
308
342
|
const filteredMessages = useMemo(() => {
|
|
309
343
|
let nextMessages = bookmarkFilter
|
|
@@ -623,7 +657,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
623
657
|
}, [searchOpen])
|
|
624
658
|
|
|
625
659
|
return (
|
|
626
|
-
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden" data-testid="message-list">
|
|
660
|
+
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden isolate" data-testid="message-list">
|
|
627
661
|
<div className="shrink-0 px-4 md:px-12 lg:px-16 pt-3">
|
|
628
662
|
<div className="flex flex-wrap items-center gap-2 rounded-[14px] border border-white/[0.06] bg-surface/55 px-3 py-2 backdrop-blur-sm">
|
|
629
663
|
<button
|
|
@@ -853,7 +887,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
853
887
|
{transcriptNodes}
|
|
854
888
|
<ApprovalCards agentId={agent?.id} />
|
|
855
889
|
<LiveThinkingLane
|
|
856
|
-
show={streaming && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
|
|
890
|
+
show={streaming && !showLiveStreamRow && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
|
|
857
891
|
assistantName={assistantName}
|
|
858
892
|
agentAvatarSeed={agent?.avatarSeed}
|
|
859
893
|
agentAvatarUrl={agent?.avatarUrl}
|
|
@@ -50,6 +50,10 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
50
50
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
51
51
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
52
52
|
const visibleQueuedMessages = listQueuedMessagesForSession(queuedMessages, sessionId)
|
|
53
|
+
const sendingQueuedMessages = visibleQueuedMessages.filter((item) => item.sending)
|
|
54
|
+
const pendingQueuedMessages = visibleQueuedMessages.filter((item) => !item.sending)
|
|
55
|
+
const displayedQueuedMessages = [...sendingQueuedMessages, ...pendingQueuedMessages]
|
|
56
|
+
const nextPendingRunId = pendingQueuedMessages[0]?.runId ?? null
|
|
53
57
|
const shouldQueue = !!sessionId && (busy || visibleQueuedMessages.length > 0)
|
|
54
58
|
|
|
55
59
|
useEffect(() => {
|
|
@@ -178,6 +182,8 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
178
182
|
const hasContent = value.trim().length > 0 || pendingFiles.length > 0
|
|
179
183
|
const queueStatusLabel = !busy
|
|
180
184
|
? 'Queue ready'
|
|
185
|
+
: sendingQueuedMessages.length > 0 && pendingQueuedMessages.length === 0
|
|
186
|
+
? 'Sending now'
|
|
181
187
|
: streamPhase === 'queued'
|
|
182
188
|
? 'Queued'
|
|
183
189
|
: streamPhase === 'tool' && streamToolName
|
|
@@ -191,6 +197,8 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
191
197
|
: 'Working'
|
|
192
198
|
const queueStatusDetail = !busy
|
|
193
199
|
? 'Queued messages are ready and will dispatch automatically.'
|
|
200
|
+
: sendingQueuedMessages.length > 0 && pendingQueuedMessages.length === 0
|
|
201
|
+
? 'The queued message has been accepted and should appear in the transcript shortly.'
|
|
194
202
|
: 'Queued messages will send automatically when the current turn finishes.'
|
|
195
203
|
|
|
196
204
|
return (
|
|
@@ -229,9 +237,16 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
229
237
|
<span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${busy ? 'bg-amber-300' : 'bg-white/[0.45]'}`} />
|
|
230
238
|
</span>
|
|
231
239
|
<span className="label-mono text-amber-300/80">Message queue</span>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
{pendingQueuedMessages.length > 0 && (
|
|
241
|
+
<span className="rounded-pill border border-amber-400/15 bg-amber-400/10 px-2 py-0.5 text-[10px] font-600 text-amber-200">
|
|
242
|
+
{pendingQueuedMessages.length}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
{sendingQueuedMessages.length > 0 && (
|
|
246
|
+
<span className="rounded-pill border border-sky-300/15 bg-sky-300/10 px-2 py-0.5 text-[10px] font-600 text-sky-200">
|
|
247
|
+
{sendingQueuedMessages.length} sending
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
235
250
|
<span className={`rounded-pill border px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] ${
|
|
236
251
|
busy
|
|
237
252
|
? 'border-amber-300/20 bg-amber-300/10 text-amber-100'
|
|
@@ -256,7 +271,7 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
256
271
|
Stop
|
|
257
272
|
</button>
|
|
258
273
|
)}
|
|
259
|
-
{sessionId &&
|
|
274
|
+
{sessionId && pendingQueuedMessages.length > 0 && (
|
|
260
275
|
<button
|
|
261
276
|
type="button"
|
|
262
277
|
onClick={() => { void clearQueuedMessagesForSession(sessionId) }}
|
|
@@ -268,21 +283,26 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
268
283
|
</div>
|
|
269
284
|
</div>
|
|
270
285
|
<div className="max-h-[184px] space-y-1.5 overflow-y-auto px-2.5 py-2.5">
|
|
271
|
-
{
|
|
286
|
+
{displayedQueuedMessages.map((item, index) => (
|
|
272
287
|
<div
|
|
273
288
|
key={item.runId}
|
|
274
289
|
className={`group flex items-start gap-3 rounded-[12px] border px-3 py-2.5 transition-all ${
|
|
275
|
-
|
|
276
|
-
? 'border-
|
|
277
|
-
:
|
|
290
|
+
item.sending
|
|
291
|
+
? 'border-sky-300/15 bg-sky-300/[0.06]'
|
|
292
|
+
: item.runId === nextPendingRunId
|
|
293
|
+
? 'border-amber-300/20 bg-amber-300/[0.07]'
|
|
294
|
+
: 'border-white/[0.05] bg-white/[0.02]'
|
|
278
295
|
}`}
|
|
279
296
|
>
|
|
280
297
|
<div className={`mt-0.5 flex h-6 min-w-6 items-center justify-center rounded-[8px] px-2 text-[10px] font-700 ${
|
|
281
|
-
|
|
282
|
-
? 'bg-
|
|
283
|
-
:
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
item.sending
|
|
299
|
+
? 'bg-sky-300/15 text-sky-100'
|
|
300
|
+
: item.runId === nextPendingRunId
|
|
301
|
+
? 'bg-amber-300/15 text-amber-100'
|
|
302
|
+
: 'border-white/[0.05] bg-white/[0.02]'
|
|
303
|
+
}`}
|
|
304
|
+
>
|
|
305
|
+
{item.sending ? '>' : pendingQueuedMessages.findIndex((candidate) => candidate.runId === item.runId) + 1}
|
|
286
306
|
</div>
|
|
287
307
|
<div className="min-w-0 flex-1">
|
|
288
308
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -290,7 +310,7 @@ export function ChatInput({ streaming, busy, onSend, onStop, extensionChatAction
|
|
|
290
310
|
<span className="rounded-pill border border-sky-300/15 bg-sky-300/10 px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-sky-200 animate-pulse">
|
|
291
311
|
Sending
|
|
292
312
|
</span>
|
|
293
|
-
) :
|
|
313
|
+
) : item.runId === nextPendingRunId && (
|
|
294
314
|
<span className="rounded-pill border border-amber-300/15 bg-amber-300/10 px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-amber-100">
|
|
295
315
|
Next
|
|
296
316
|
</span>
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
2
|
-
import { log } from '@/lib/server/logger'
|
|
3
2
|
|
|
4
3
|
const TAG = 'instrumentation'
|
|
5
4
|
|
|
6
5
|
export async function register() {
|
|
7
6
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
7
|
+
const { log } = await import('@/lib/server/logger')
|
|
8
8
|
const isWorkerOnly = process.env.SWARMCLAW_WORKER_ONLY === '1'
|
|
9
9
|
const { initWsServer, closeWsServer } = await import('./lib/server/ws-hub')
|
|
10
10
|
const { ensureDaemonStarted } = await import('@/lib/server/runtime/daemon-state')
|
|
@@ -43,16 +43,17 @@ describe('chat-streaming-state', () => {
|
|
|
43
43
|
localStreaming: true,
|
|
44
44
|
hasLiveArtifacts: true,
|
|
45
45
|
assistantRenderId: 'render-1',
|
|
46
|
+
showLiveRow: true,
|
|
47
|
+
syntheticAssistant: messages[1],
|
|
46
48
|
} as StreamingAwareMessageListOptions),
|
|
47
49
|
[
|
|
48
50
|
{ role: 'user', text: 'hello', time: 1 },
|
|
49
51
|
{
|
|
50
52
|
role: 'assistant',
|
|
51
|
-
text: '
|
|
52
|
-
time:
|
|
53
|
+
text: 'partial',
|
|
54
|
+
time: 2,
|
|
53
55
|
kind: 'chat',
|
|
54
56
|
streaming: true,
|
|
55
|
-
thinking: 'working...',
|
|
56
57
|
clientRenderId: 'render-1',
|
|
57
58
|
},
|
|
58
59
|
],
|
|
@@ -69,6 +70,7 @@ describe('chat-streaming-state', () => {
|
|
|
69
70
|
localStreaming: true,
|
|
70
71
|
hasLiveArtifacts: true,
|
|
71
72
|
assistantRenderId: 'render-2',
|
|
73
|
+
showLiveRow: true,
|
|
72
74
|
} as StreamingAwareMessageListOptions),
|
|
73
75
|
[
|
|
74
76
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -84,6 +86,43 @@ describe('chat-streaming-state', () => {
|
|
|
84
86
|
)
|
|
85
87
|
})
|
|
86
88
|
|
|
89
|
+
it('can show a synthetic live row for server-driven runs using the latest persisted streaming artifact', () => {
|
|
90
|
+
const messages: Message[] = [
|
|
91
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
92
|
+
{
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
text: 'Drafting the handoff now.',
|
|
95
|
+
time: 2,
|
|
96
|
+
streaming: true,
|
|
97
|
+
thinking: 'Collecting the completion details',
|
|
98
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
buildStreamingAwareMessageList(messages, {
|
|
104
|
+
localStreaming: true,
|
|
105
|
+
hasLiveArtifacts: false,
|
|
106
|
+
assistantRenderId: 'render-server',
|
|
107
|
+
showLiveRow: true,
|
|
108
|
+
syntheticAssistant: messages[1],
|
|
109
|
+
}),
|
|
110
|
+
[
|
|
111
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
112
|
+
{
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
text: 'Drafting the handoff now.',
|
|
115
|
+
time: 2,
|
|
116
|
+
kind: 'chat',
|
|
117
|
+
streaming: true,
|
|
118
|
+
thinking: 'Collecting the completion details',
|
|
119
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
120
|
+
clientRenderId: 'render-server',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
87
126
|
it('replaces trailing streaming assistant messages with the completed assistant message', () => {
|
|
88
127
|
const messages: Message[] = [
|
|
89
128
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -10,6 +10,8 @@ export interface StreamingAwareMessageListOptions {
|
|
|
10
10
|
localStreaming: boolean
|
|
11
11
|
hasLiveArtifacts: boolean
|
|
12
12
|
assistantRenderId?: string | null
|
|
13
|
+
showLiveRow?: boolean
|
|
14
|
+
syntheticAssistant?: Partial<Message> | null
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function isStreamingAssistantMessage(
|
|
@@ -27,13 +29,13 @@ function isStreamingAssistantMessage(
|
|
|
27
29
|
|
|
28
30
|
export function shouldHidePersistedStreamingAssistantMessage(
|
|
29
31
|
message: Message,
|
|
30
|
-
opts: { localStreaming: boolean; hasLiveArtifacts: boolean },
|
|
32
|
+
opts: { localStreaming: boolean; hasLiveArtifacts: boolean; showLiveRow?: boolean },
|
|
31
33
|
): boolean {
|
|
34
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
32
35
|
return (
|
|
33
|
-
|
|
36
|
+
showLiveRow
|
|
34
37
|
&& message.role === 'assistant'
|
|
35
38
|
&& message.streaming === true
|
|
36
|
-
&& opts.hasLiveArtifacts
|
|
37
39
|
)
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -41,22 +43,32 @@ export function buildStreamingAwareMessageList(
|
|
|
41
43
|
messages: Message[],
|
|
42
44
|
opts: StreamingAwareMessageListOptions,
|
|
43
45
|
): Message[] {
|
|
44
|
-
const
|
|
46
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
47
|
+
const nextMessages = showLiveRow
|
|
45
48
|
? messages.filter((message) => !shouldHidePersistedStreamingAssistantMessage(message, opts))
|
|
46
49
|
: messages
|
|
47
50
|
|
|
48
|
-
if (!
|
|
51
|
+
if (!showLiveRow || !opts.assistantRenderId) {
|
|
49
52
|
return nextMessages
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const syntheticAssistantMessage: Message = {
|
|
53
56
|
role: 'assistant',
|
|
54
|
-
text: '',
|
|
55
|
-
time: 0,
|
|
56
|
-
kind: 'chat',
|
|
57
|
+
text: typeof opts.syntheticAssistant?.text === 'string' ? opts.syntheticAssistant.text : '',
|
|
58
|
+
time: typeof opts.syntheticAssistant?.time === 'number' ? opts.syntheticAssistant.time : 0,
|
|
59
|
+
kind: opts.syntheticAssistant?.kind || 'chat',
|
|
57
60
|
streaming: true,
|
|
58
61
|
clientRenderId: opts.assistantRenderId,
|
|
59
62
|
}
|
|
63
|
+
if (typeof opts.syntheticAssistant?.thinking === 'string') {
|
|
64
|
+
syntheticAssistantMessage.thinking = opts.syntheticAssistant.thinking
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(opts.syntheticAssistant?.toolEvents)) {
|
|
67
|
+
syntheticAssistantMessage.toolEvents = opts.syntheticAssistant.toolEvents
|
|
68
|
+
}
|
|
69
|
+
if (opts.syntheticAssistant?.source) {
|
|
70
|
+
syntheticAssistantMessage.source = opts.syntheticAssistant.source
|
|
71
|
+
}
|
|
60
72
|
|
|
61
73
|
return [
|
|
62
74
|
...nextMessages,
|
|
@@ -27,7 +27,7 @@ describe('queued-message-queue', () => {
|
|
|
27
27
|
it('replaces queued items only for the requested session', () => {
|
|
28
28
|
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
29
29
|
{ runId: 'q4', sessionId: 'session-a', text: 'replacement', queuedAt: 4, position: 1 },
|
|
30
|
-
])
|
|
30
|
+
], { activeRunId: null })
|
|
31
31
|
assert.deepEqual(
|
|
32
32
|
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
33
33
|
['q4'],
|
|
@@ -38,6 +38,28 @@ describe('queued-message-queue', () => {
|
|
|
38
38
|
)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
it('keeps only the newly active run as a sending placeholder when it disappears from the queue snapshot', () => {
|
|
42
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
43
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
44
|
+
], { activeRunId: 'q1' })
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => [item.runId, item.sending === true]),
|
|
48
|
+
[['q1', true], ['q3', false]],
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('drops missing stale queue rows that are not the active run', () => {
|
|
53
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
54
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
55
|
+
], { activeRunId: 'run-other' })
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
59
|
+
['q3'],
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
41
63
|
it('removes queued items by stable id', () => {
|
|
42
64
|
assert.deepEqual(removeQueuedMessageById(queue, 'q2').map((item) => item.runId), ['q1', 'q3'])
|
|
43
65
|
})
|
|
@@ -41,18 +41,27 @@ export function snapshotToQueuedMessages(snapshot: SessionQueueSnapshot): Queued
|
|
|
41
41
|
return snapshot.items.map((item) => ({ ...item }))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ReplaceQueuedMessagesOptions {
|
|
45
|
+
activeRunId?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
export function replaceQueuedMessagesForSession(
|
|
45
49
|
queue: QueuedSessionMessage[],
|
|
46
50
|
sessionId: string,
|
|
47
51
|
nextItems: QueuedSessionMessage[],
|
|
52
|
+
options: ReplaceQueuedMessagesOptions = {},
|
|
48
53
|
): QueuedSessionMessage[] {
|
|
49
54
|
const otherSessions = queue.filter((item) => item.sessionId !== sessionId)
|
|
50
55
|
const previousForSession = queue.filter((item) => item.sessionId === sessionId && !item.sending)
|
|
51
56
|
// Detect consumed messages: items in local state but not in server snapshot.
|
|
52
|
-
// Keep
|
|
57
|
+
// Keep only the run that actually became active visible as "sending" so it
|
|
58
|
+
// doesn't vanish from the UI before the transcript refresh catches up.
|
|
53
59
|
const nextRunIds = new Set(nextItems.map((item) => item.runId))
|
|
60
|
+
const activeRunId = typeof options.activeRunId === 'string' && options.activeRunId.trim()
|
|
61
|
+
? options.activeRunId
|
|
62
|
+
: null
|
|
54
63
|
const consumed = previousForSession
|
|
55
|
-
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId))
|
|
64
|
+
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId) && activeRunId === item.runId)
|
|
56
65
|
.map((item) => ({ ...item, sending: true }))
|
|
57
66
|
return [
|
|
58
67
|
...otherSessions,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { isStderrNoise, buildCliEnv, isCliProvider, CLI_PROVIDER_CAPABILITIES } from './cli-utils'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// isStderrNoise
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
describe('isStderrNoise', () => {
|
|
11
|
+
it('returns true for MallocStackLogging lines', () => {
|
|
12
|
+
assert.equal(isStderrNoise('MallocStackLogging: could not tag MSL'), true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns true for blank/whitespace lines', () => {
|
|
16
|
+
assert.equal(isStderrNoise(''), true)
|
|
17
|
+
assert.equal(isStderrNoise(' '), true)
|
|
18
|
+
assert.equal(isStderrNoise('\t'), true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns false for real error text', () => {
|
|
22
|
+
assert.equal(isStderrNoise('Error: connection refused'), false)
|
|
23
|
+
assert.equal(isStderrNoise('FATAL: segfault'), false)
|
|
24
|
+
assert.equal(isStderrNoise('Permission denied'), false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// buildCliEnv
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
describe('buildCliEnv', () => {
|
|
33
|
+
it('strips SWARMCLAW_ prefixed vars from env', () => {
|
|
34
|
+
const orig = process.env.SWARMCLAW_TEST_VAR
|
|
35
|
+
process.env.SWARMCLAW_TEST_VAR = 'should_be_stripped'
|
|
36
|
+
try {
|
|
37
|
+
const env = buildCliEnv()
|
|
38
|
+
assert.equal(env.SWARMCLAW_TEST_VAR, undefined)
|
|
39
|
+
} finally {
|
|
40
|
+
if (orig === undefined) delete process.env.SWARMCLAW_TEST_VAR
|
|
41
|
+
else process.env.SWARMCLAW_TEST_VAR = orig
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('deletes MallocStackLogging', () => {
|
|
46
|
+
const orig = process.env.MallocStackLogging
|
|
47
|
+
process.env.MallocStackLogging = '1'
|
|
48
|
+
try {
|
|
49
|
+
const env = buildCliEnv()
|
|
50
|
+
assert.equal(env.MallocStackLogging, undefined)
|
|
51
|
+
} finally {
|
|
52
|
+
if (orig === undefined) delete process.env.MallocStackLogging
|
|
53
|
+
else process.env.MallocStackLogging = orig
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('injects provided key-value pairs', () => {
|
|
58
|
+
const env = buildCliEnv({ inject: { MY_CUSTOM_KEY: 'hello' } })
|
|
59
|
+
assert.equal(env.MY_CUSTOM_KEY, 'hello')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('preserves unrelated user env vars', () => {
|
|
63
|
+
const env = buildCliEnv()
|
|
64
|
+
assert.equal(env.PATH, process.env.PATH)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('supports custom stripPrefixes', () => {
|
|
68
|
+
const orig = process.env.CUSTOM_PREFIX_VAR
|
|
69
|
+
process.env.CUSTOM_PREFIX_VAR = 'should_be_stripped'
|
|
70
|
+
try {
|
|
71
|
+
const env = buildCliEnv({ stripPrefixes: ['CUSTOM_PREFIX_'] })
|
|
72
|
+
assert.equal(env.CUSTOM_PREFIX_VAR, undefined)
|
|
73
|
+
} finally {
|
|
74
|
+
if (orig === undefined) delete process.env.CUSTOM_PREFIX_VAR
|
|
75
|
+
else process.env.CUSTOM_PREFIX_VAR = orig
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('sets TERM=dumb and NO_COLOR=1', () => {
|
|
80
|
+
const env = buildCliEnv()
|
|
81
|
+
assert.equal(env.TERM, 'dumb')
|
|
82
|
+
assert.equal(env.NO_COLOR, '1')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// isCliProvider
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe('isCliProvider', () => {
|
|
91
|
+
it('returns true for known CLI providers', () => {
|
|
92
|
+
assert.equal(isCliProvider('claude-cli'), true)
|
|
93
|
+
assert.equal(isCliProvider('codex-cli'), true)
|
|
94
|
+
assert.equal(isCliProvider('opencode-cli'), true)
|
|
95
|
+
assert.equal(isCliProvider('gemini-cli'), true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns false for non-CLI providers', () => {
|
|
99
|
+
assert.equal(isCliProvider('openai'), false)
|
|
100
|
+
assert.equal(isCliProvider('anthropic'), false)
|
|
101
|
+
assert.equal(isCliProvider(''), false)
|
|
102
|
+
assert.equal(isCliProvider('google'), false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// CLI_PROVIDER_CAPABILITIES
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('CLI_PROVIDER_CAPABILITIES', () => {
|
|
111
|
+
it('has entries for all 4 CLI providers', () => {
|
|
112
|
+
assert.ok('claude-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
113
|
+
assert.ok('codex-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
114
|
+
assert.ok('opencode-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
115
|
+
assert.ok('gemini-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('each entry is a non-empty string', () => {
|
|
119
|
+
for (const [key, value] of Object.entries(CLI_PROVIDER_CAPABILITIES)) {
|
|
120
|
+
assert.equal(typeof value, 'string', `${key} should be a string`)
|
|
121
|
+
assert.ok(value.length > 0, `${key} should be non-empty`)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadActivity as loadStoredActivity, logActivity as writeActivityLog } from '@/lib/server/storage'
|
|
2
|
+
import { perf } from '@/lib/server/runtime/perf'
|
|
3
|
+
|
|
4
|
+
export function loadActivity() {
|
|
5
|
+
return perf.measureSync('repository', 'activity.list', () => loadStoredActivity())
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function logActivity(entry: {
|
|
9
|
+
entityType: string
|
|
10
|
+
entityId: string
|
|
11
|
+
action: string
|
|
12
|
+
actor: string
|
|
13
|
+
actorId?: string
|
|
14
|
+
summary: string
|
|
15
|
+
detail?: Record<string, unknown>
|
|
16
|
+
}) {
|
|
17
|
+
perf.measureSync('repository', 'activity.log', () => writeActivityLog(entry), {
|
|
18
|
+
entityType: entry.entityType,
|
|
19
|
+
action: entry.action,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it } from 'node:test'
|
|
3
|
+
import type { Agent, ProviderType } from '@/types'
|
|
3
4
|
import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from './agent-availability'
|
|
4
5
|
|
|
5
6
|
describe('isWorkerOnlyAgent', () => {
|
|
6
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw']
|
|
7
|
-
const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', '
|
|
7
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'] satisfies ProviderType[]
|
|
8
|
+
const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', 'together'] satisfies ProviderType[]
|
|
9
|
+
|
|
10
|
+
function withProvider(provider: unknown): Pick<Agent, 'provider'> {
|
|
11
|
+
return { provider } as Pick<Agent, 'provider'>
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
for (const provider of CLI_PROVIDERS) {
|
|
10
15
|
it(`returns true for ${provider}`, () => {
|
|
11
|
-
assert.equal(isWorkerOnlyAgent(
|
|
16
|
+
assert.equal(isWorkerOnlyAgent(withProvider(provider)), true)
|
|
12
17
|
})
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
for (const provider of NON_CLI_PROVIDERS) {
|
|
16
21
|
it(`returns false for ${provider}`, () => {
|
|
17
|
-
assert.equal(isWorkerOnlyAgent(
|
|
22
|
+
assert.equal(isWorkerOnlyAgent(withProvider(provider)), false)
|
|
18
23
|
})
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -27,7 +32,7 @@ describe('isWorkerOnlyAgent', () => {
|
|
|
27
32
|
})
|
|
28
33
|
|
|
29
34
|
it('returns false for empty provider string', () => {
|
|
30
|
-
assert.equal(isWorkerOnlyAgent(
|
|
35
|
+
assert.equal(isWorkerOnlyAgent(withProvider('')), false)
|
|
31
36
|
})
|
|
32
37
|
})
|
|
33
38
|
|