@swarmclawai/swarmclaw 1.2.0 → 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 +19 -0
- package/package.json +5 -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]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +17 -20
- package/src/app/api/chats/[id]/messages/route.ts +15 -11
- 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/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +15 -12
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- 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 +20 -9
- package/src/components/chat/message-list.tsx +62 -42
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- 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/chat.ts +18 -2
- 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/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- 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 +23 -4
- 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 +14 -6
- 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/autonomy/supervisor-reflection.ts +14 -1
- 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 -1914
- 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/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
- package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- 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 -2263
- package/src/lib/server/openclaw/gateway.ts +8 -5
- 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/project-utils.ts +13 -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-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +8 -7
- 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 -1331
- 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 +78 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +6 -3
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- 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 -2058
- 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 +13 -8
- 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 -1374
- 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/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- 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/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- 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-eligibility.ts +6 -0
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +32 -32
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +78 -37
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +26 -6
- package/src/types/index.ts +6 -0
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getDaemonHealthSummary } from '@/lib/server/runtime/daemon-state'
|
|
3
|
+
import packageJson from '../../../../../package.json'
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const summary = getDaemonHealthSummary()
|
|
7
|
+
return NextResponse.json({
|
|
8
|
+
...summary,
|
|
9
|
+
version: packageJson.version,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -3,6 +3,9 @@ import fs from 'fs'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { genId } from '@/lib/id'
|
|
5
5
|
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
7
|
+
|
|
8
|
+
const TAG = 'api-upload'
|
|
6
9
|
|
|
7
10
|
export async function POST(req: Request) {
|
|
8
11
|
const filename = req.headers.get('x-filename') || 'image.png'
|
|
@@ -12,7 +15,7 @@ export async function POST(req: Request) {
|
|
|
12
15
|
|
|
13
16
|
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
14
17
|
fs.writeFileSync(filePath, buf)
|
|
15
|
-
|
|
18
|
+
log.info(TAG, `saved ${buf.length} bytes to ${filePath}`)
|
|
16
19
|
|
|
17
20
|
return NextResponse.json({ path: filePath, size: buf.length, url: `/api/uploads/${name}` })
|
|
18
21
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -730,6 +730,13 @@ const COMMAND_GROUPS = [
|
|
|
730
730
|
cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
|
|
731
731
|
],
|
|
732
732
|
},
|
|
733
|
+
{
|
|
734
|
+
name: 'system-status',
|
|
735
|
+
description: 'Lightweight system health summary',
|
|
736
|
+
commands: [
|
|
737
|
+
cmd('get', 'GET', '/system/status', 'Get system health summary (safe for external monitors)'),
|
|
738
|
+
],
|
|
739
|
+
},
|
|
733
740
|
{
|
|
734
741
|
name: 'usage',
|
|
735
742
|
description: 'Usage and cost summary',
|
package/src/cli/spec.js
CHANGED
|
@@ -496,6 +496,7 @@ const COMMAND_GROUPS = {
|
|
|
496
496
|
description: 'System and version endpoints',
|
|
497
497
|
commands: {
|
|
498
498
|
ip: { description: 'Get local bind IP/port', method: 'GET', path: '/ip' },
|
|
499
|
+
status: { description: 'Get lightweight system health summary (safe for external monitors)', method: 'GET', path: '/system/status' },
|
|
499
500
|
usage: { description: 'Get usage summary', method: 'GET', path: '/usage' },
|
|
500
501
|
version: { description: 'Get local/remote git version info', method: 'GET', path: '/version' },
|
|
501
502
|
update: { description: 'Update to latest stable release tag (fallback: main)', method: 'POST', path: '/version/update' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/app/api-client'
|
|
5
5
|
import { errorMessage } from '@/lib/shared-utils'
|
|
6
6
|
import { PersonalityBuilder } from './personality-builder'
|
|
@@ -8,6 +8,14 @@ import { PersonalityBuilder } from './personality-builder'
|
|
|
8
8
|
const FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
|
|
9
9
|
const GUIDED_FILES = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md'])
|
|
10
10
|
|
|
11
|
+
function makeInitialFiles(): Record<string, FileState> {
|
|
12
|
+
const initial: Record<string, FileState> = {}
|
|
13
|
+
for (const f of FILES) {
|
|
14
|
+
initial[f] = { content: '', original: '', loading: true, saving: false }
|
|
15
|
+
}
|
|
16
|
+
return initial
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
interface FileState {
|
|
12
20
|
content: string
|
|
13
21
|
original: string
|
|
@@ -22,45 +30,49 @@ interface Props {
|
|
|
22
30
|
|
|
23
31
|
export function AgentFilesEditor({ agentId }: Props) {
|
|
24
32
|
const [activeTab, setActiveTab] = useState<string>(FILES[0])
|
|
25
|
-
const [files, setFiles] = useState<Record<string, FileState>>(
|
|
33
|
+
const [files, setFiles] = useState<Record<string, FileState>>(makeInitialFiles)
|
|
26
34
|
const [guidedMode, setGuidedMode] = useState(false)
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Reset to loading state when agentId changes
|
|
37
|
+
const [prevAgentId, setPrevAgentId] = useState(agentId)
|
|
38
|
+
if (agentId !== prevAgentId) {
|
|
39
|
+
setPrevAgentId(agentId)
|
|
40
|
+
setFiles(makeInitialFiles())
|
|
41
|
+
}
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let cancelled = false
|
|
45
|
+
api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
|
|
46
|
+
.then((result) => {
|
|
47
|
+
if (cancelled) return
|
|
48
|
+
setFiles((prev) => {
|
|
49
|
+
const next = { ...prev }
|
|
50
|
+
for (const [name, data] of Object.entries(result)) {
|
|
51
|
+
next[name] = {
|
|
52
|
+
content: data.content,
|
|
53
|
+
original: data.content,
|
|
54
|
+
loading: false,
|
|
55
|
+
saving: false,
|
|
56
|
+
error: data.error,
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
return next
|
|
60
|
+
})
|
|
49
61
|
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
.catch((err: unknown) => {
|
|
63
|
+
if (cancelled) return
|
|
64
|
+
const message = errorMessage(err)
|
|
65
|
+
setFiles((prev) => {
|
|
66
|
+
const next = { ...prev }
|
|
67
|
+
for (const f of FILES) {
|
|
68
|
+
next[f] = { ...next[f], loading: false, error: message }
|
|
69
|
+
}
|
|
70
|
+
return next
|
|
71
|
+
})
|
|
58
72
|
})
|
|
59
|
-
}
|
|
73
|
+
return () => { cancelled = true }
|
|
60
74
|
}, [agentId])
|
|
61
75
|
|
|
62
|
-
useEffect(() => { loadFiles() }, [loadFiles])
|
|
63
|
-
|
|
64
76
|
const handleContentChange = (filename: string, content: string) => {
|
|
65
77
|
setFiles((prev) => ({
|
|
66
78
|
...prev,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
4
|
import type { PersonalityDraft } from '@/types'
|
|
5
|
-
import { api } from '@/lib/app/api-client'
|
|
6
5
|
import {
|
|
7
6
|
parseIdentityMd, serializeIdentityMd,
|
|
8
7
|
parseUserMd, serializeUserMd,
|
|
@@ -19,12 +18,12 @@ interface Props {
|
|
|
19
18
|
const inputClass = 'w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors'
|
|
20
19
|
const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1'
|
|
21
20
|
|
|
22
|
-
export function PersonalityBuilder({
|
|
21
|
+
export function PersonalityBuilder({ fileType, content, onSave }: Props) {
|
|
23
22
|
const [draft, setDraft] = useState<Record<string, string>>({})
|
|
24
23
|
const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
|
|
25
24
|
const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
const parsedContent = useMemo(() => {
|
|
28
27
|
let parsed: Record<string, string> = {}
|
|
29
28
|
if (fileType === 'IDENTITY.md') {
|
|
30
29
|
const p = parseIdentityMd(content)
|
|
@@ -36,11 +35,18 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
|
|
|
36
35
|
const p = parseSoulMd(content)
|
|
37
36
|
parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
setInitialDraft(parsed)
|
|
41
|
-
setSaveState('idle')
|
|
38
|
+
return parsed
|
|
42
39
|
}, [content, fileType])
|
|
43
40
|
|
|
41
|
+
// Reset form when content/fileType changes (render-time state adjustment)
|
|
42
|
+
const [syncKey, setSyncKey] = useState({ content, fileType })
|
|
43
|
+
if (content !== syncKey.content || fileType !== syncKey.fileType) {
|
|
44
|
+
setSyncKey({ content, fileType })
|
|
45
|
+
setDraft(parsedContent)
|
|
46
|
+
setInitialDraft(parsedContent)
|
|
47
|
+
setSaveState('idle')
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
const isDirty = useMemo(() => {
|
|
45
51
|
return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
|
|
46
52
|
}, [draft, initialDraft])
|
|
@@ -12,7 +12,7 @@ export function TrashList() {
|
|
|
12
12
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
13
13
|
const [confirmPermanent, setConfirmPermanent] = useState<Agent | null>(null)
|
|
14
14
|
|
|
15
|
-
useEffect(() => { loadTrashedAgents() }, [])
|
|
15
|
+
useEffect(() => { loadTrashedAgents() }, [loadTrashedAgents])
|
|
16
16
|
|
|
17
17
|
const handleRestore = async (id: string) => {
|
|
18
18
|
await api('POST', '/agents/trash', { id })
|
|
@@ -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 = (
|
|
@@ -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,
|
|
@@ -418,11 +428,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
418
428
|
const effectiveThinking = !isUser
|
|
419
429
|
? (liveStreamActive ? (liveStream?.thinking?.trim() ? liveStream.thinking : undefined) : message.thinking)
|
|
420
430
|
: undefined
|
|
421
|
-
|
|
431
|
+
|
|
432
|
+
const sourceText = liveStreamActive ? (liveStream?.text || message.text || '') : message.text
|
|
422
433
|
const connectorDeliveryTranscript = !isUser && message.kind === 'connector-delivery'
|
|
423
434
|
? (message.source?.deliveryTranscript?.trim() || '')
|
|
424
435
|
: ''
|
|
425
|
-
const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || '') : message.text)
|
|
436
|
+
const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || message.text || '') : message.text)
|
|
426
437
|
|
|
427
438
|
// Extract ALL media from ALL tool events for inline display after the message text.
|
|
428
439
|
// Covers send_file, browser screenshots, file tool outputs — everything.
|
|
@@ -100,13 +100,6 @@ const LiveStreamBubble = memo(function LiveStreamBubble(props: LiveStreamBubbleP
|
|
|
100
100
|
return <MessageBubble {...props} liveStream={liveStream} />
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
interface LastMessageMomentOverlayProps {
|
|
104
|
-
messages: Message[]
|
|
105
|
-
sessionId: string | null
|
|
106
|
-
agentId?: string | null
|
|
107
|
-
streaming: boolean
|
|
108
|
-
children: (momentOverlay: React.ReactNode, currentMoment: { kind: string } | null) => React.ReactNode
|
|
109
|
-
}
|
|
110
103
|
|
|
111
104
|
function useLastMessageMoment(messages: Message[], sessionId: string | null, agentId: string | null | undefined, streaming: boolean) {
|
|
112
105
|
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; key: string; name: string; input: string }
|
|
@@ -117,31 +110,24 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
|
|
|
117
110
|
setCurrentMoment({ kind: 'heartbeat' })
|
|
118
111
|
})
|
|
119
112
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
seededMomentSessionRef.current = sessionId
|
|
133
|
-
prevToolKeyRef.current = getLatestAssistantToolMoment(messages)?.key || null
|
|
134
|
-
setCurrentMoment(null)
|
|
135
|
-
}, [messages, sessionId])
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
if (!sessionId || seededMomentSessionRef.current !== sessionId) return
|
|
113
|
+
const [trackedSessionId, setTrackedSessionId] = useState<string | null>(null)
|
|
114
|
+
const [trackedToolKey, setTrackedToolKey] = useState<string | null>(null)
|
|
115
|
+
|
|
116
|
+
// Render-time: respond to sessionId/messages changes (avoids set-state-in-effect)
|
|
117
|
+
if (sessionId !== trackedSessionId) {
|
|
118
|
+
setTrackedSessionId(sessionId)
|
|
119
|
+
const initialKey = sessionId
|
|
120
|
+
? getLatestAssistantToolMoment(messages)?.key || null
|
|
121
|
+
: null
|
|
122
|
+
setTrackedToolKey(initialKey)
|
|
123
|
+
if (currentMoment !== null) setCurrentMoment(null)
|
|
124
|
+
} else if (sessionId) {
|
|
139
125
|
const moment = getLatestAssistantToolMoment(messages)
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
126
|
+
if (moment && moment.key !== trackedToolKey) {
|
|
127
|
+
setTrackedToolKey(moment.key)
|
|
128
|
+
setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
|
|
129
|
+
}
|
|
130
|
+
}
|
|
145
131
|
|
|
146
132
|
const momentOverlay = useMemo(() => {
|
|
147
133
|
if (streaming || !currentMoment) return null
|
|
@@ -161,13 +147,6 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
|
|
|
161
147
|
return { currentMoment, momentOverlay }
|
|
162
148
|
}
|
|
163
149
|
|
|
164
|
-
interface Props {
|
|
165
|
-
messages: Message[]
|
|
166
|
-
streaming: boolean
|
|
167
|
-
connectorFilter?: string | null
|
|
168
|
-
loading?: boolean
|
|
169
|
-
}
|
|
170
|
-
|
|
171
150
|
interface LiveThinkingLaneProps {
|
|
172
151
|
show: boolean
|
|
173
152
|
assistantName?: string
|
|
@@ -195,6 +174,13 @@ const LiveThinkingLane = memo(function LiveThinkingLane({
|
|
|
195
174
|
)
|
|
196
175
|
})
|
|
197
176
|
|
|
177
|
+
interface Props {
|
|
178
|
+
messages: Message[]
|
|
179
|
+
streaming: boolean
|
|
180
|
+
connectorFilter?: string | null
|
|
181
|
+
loading?: boolean
|
|
182
|
+
}
|
|
183
|
+
|
|
198
184
|
export function MessageList({ messages, streaming, connectorFilter = null, loading = false }: Props) {
|
|
199
185
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
200
186
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
@@ -202,6 +188,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
202
188
|
const snapUntilRef = useRef(0)
|
|
203
189
|
const prevSessionIdRef = useRef<string | null>(null)
|
|
204
190
|
const assistantRenderId = useChatStore((s) => s.assistantRenderId)
|
|
191
|
+
const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
|
|
205
192
|
const hasLiveArtifacts = useChatStore(selectHasLiveArtifacts)
|
|
206
193
|
const setMessages = useChatStore((s) => s.setMessages)
|
|
207
194
|
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
@@ -232,7 +219,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
232
219
|
const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
|
|
233
220
|
|
|
234
221
|
// Moment overlay for last assistant message (heartbeat or tool events)
|
|
235
|
-
const {
|
|
222
|
+
const { momentOverlay: lastMomentOverlay } = useLastMessageMoment(messages, sessionId, agent?.id, streaming)
|
|
236
223
|
|
|
237
224
|
// Unread count tracking
|
|
238
225
|
const unreadRef = useRef(0)
|
|
@@ -311,13 +298,46 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
311
298
|
return dedupeMessagesForDisplay(displayedMessages)
|
|
312
299
|
}, [messages, showAlerts, showOk])
|
|
313
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
|
+
|
|
314
332
|
const streamingAwareMessages = useMemo(() => (
|
|
315
333
|
buildStreamingAwareMessageList(baseDisplayedMessages, {
|
|
316
334
|
localStreaming: streaming,
|
|
317
335
|
hasLiveArtifacts,
|
|
318
336
|
assistantRenderId,
|
|
337
|
+
showLiveRow: showLiveStreamRow,
|
|
338
|
+
syntheticAssistant: latestPersistedStreamingMessage,
|
|
319
339
|
})
|
|
320
|
-
), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, streaming])
|
|
340
|
+
), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, latestPersistedStreamingMessage, showLiveStreamRow, streaming])
|
|
321
341
|
|
|
322
342
|
const filteredMessages = useMemo(() => {
|
|
323
343
|
let nextMessages = bookmarkFilter
|
|
@@ -637,7 +657,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
637
657
|
}, [searchOpen])
|
|
638
658
|
|
|
639
659
|
return (
|
|
640
|
-
<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">
|
|
641
661
|
<div className="shrink-0 px-4 md:px-12 lg:px-16 pt-3">
|
|
642
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">
|
|
643
663
|
<button
|
|
@@ -867,7 +887,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
867
887
|
{transcriptNodes}
|
|
868
888
|
<ApprovalCards agentId={agent?.id} />
|
|
869
889
|
<LiveThinkingLane
|
|
870
|
-
show={streaming && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
|
|
890
|
+
show={streaming && !showLiveStreamRow && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
|
|
871
891
|
assistantName={assistantName}
|
|
872
892
|
agentAvatarSeed={agent?.avatarSeed}
|
|
873
893
|
agentAvatarUrl={agent?.avatarUrl}
|