@swarmclawai/swarmclaw 0.4.5 → 0.5.0
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 +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -1,22 +1,72 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMemo } from 'react'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
4
|
import ReactMarkdown from 'react-markdown'
|
|
5
5
|
import remarkGfm from 'remark-gfm'
|
|
6
6
|
import rehypeHighlight from 'rehype-highlight'
|
|
7
7
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
import { CodeBlock } from './code-block'
|
|
9
10
|
import { ToolCallBubble } from './tool-call-bubble'
|
|
10
|
-
import { useChatStore } from '@/stores/use-chat-store'
|
|
11
|
+
import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
|
|
12
|
+
|
|
13
|
+
function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
14
|
+
const [expanded, setExpanded] = useState(false)
|
|
15
|
+
const shouldCollapse = toolEvents.length > 2
|
|
16
|
+
const latestTool = toolEvents[toolEvents.length - 1]
|
|
17
|
+
|
|
18
|
+
if (shouldCollapse && !expanded) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => setExpanded(true)}
|
|
24
|
+
className="self-start flex items-center gap-2 px-3 py-1.5 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] cursor-pointer transition-colors"
|
|
25
|
+
>
|
|
26
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/60">
|
|
27
|
+
<polyline points="6 9 12 15 18 9" />
|
|
28
|
+
</svg>
|
|
29
|
+
<span className="text-[11px] text-text-3 font-mono">
|
|
30
|
+
{toolEvents.length} tool calls
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-[10px] text-text-3/50">
|
|
33
|
+
latest: {latestTool?.name || 'unknown'}
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
42
|
+
{shouldCollapse && (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={() => setExpanded(false)}
|
|
46
|
+
className="self-start px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-[11px] text-text-3 border border-white/[0.06] cursor-pointer transition-colors"
|
|
47
|
+
>
|
|
48
|
+
Collapse tool calls
|
|
49
|
+
</button>
|
|
50
|
+
)}
|
|
51
|
+
{toolEvents.map((event) => (
|
|
52
|
+
<ToolCallBubble key={event.id} event={event} />
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
11
57
|
|
|
12
58
|
interface Props {
|
|
13
59
|
text: string
|
|
14
60
|
assistantName?: string
|
|
61
|
+
agentAvatarSeed?: string
|
|
62
|
+
agentName?: string
|
|
15
63
|
}
|
|
16
64
|
|
|
17
|
-
export function StreamingBubble({ text, assistantName }: Props) {
|
|
65
|
+
export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
|
|
18
66
|
const rendered = useMemo(() => text, [text])
|
|
19
67
|
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
68
|
+
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
69
|
+
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
20
70
|
|
|
21
71
|
return (
|
|
22
72
|
<div
|
|
@@ -24,18 +74,17 @@ export function StreamingBubble({ text, assistantName }: Props) {
|
|
|
24
74
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
25
75
|
>
|
|
26
76
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
27
|
-
<AiAvatar size="sm" />
|
|
77
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
28
78
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
29
79
|
<span className="w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
80
|
+
{streamPhase === 'tool' && streamToolName && (
|
|
81
|
+
<span className="text-[10px] text-text-3/50 font-mono">Using {streamToolName}...</span>
|
|
82
|
+
)}
|
|
30
83
|
</div>
|
|
31
84
|
|
|
32
|
-
{/* Tool call events */}
|
|
85
|
+
{/* Tool call events (collapsible when > 2) */}
|
|
33
86
|
{toolEvents.length > 0 && (
|
|
34
|
-
<
|
|
35
|
-
{toolEvents.map((event) => (
|
|
36
|
-
<ToolCallBubble key={event.id} event={event} />
|
|
37
|
-
))}
|
|
38
|
-
</div>
|
|
87
|
+
<ToolEventsSection toolEvents={toolEvents} />
|
|
39
88
|
)}
|
|
40
89
|
|
|
41
90
|
{rendered && (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Message } from '@/types'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
lastMessage: Message | null
|
|
7
|
+
onSend: (text: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getSuggestions(msg: Message | null): string[] {
|
|
11
|
+
if (!msg?.text) return ['Continue', 'Tell me more']
|
|
12
|
+
const text = msg.text
|
|
13
|
+
|
|
14
|
+
// Error patterns
|
|
15
|
+
if (/error|exception|failed|traceback|panic|ECONNREFUSED|ETIMEDOUT/i.test(text)) {
|
|
16
|
+
return ['Can you fix this?', 'Try an alternative approach', 'Explain the error']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Code blocks present
|
|
20
|
+
if (/```[\s\S]*```/.test(text)) {
|
|
21
|
+
return ['Explain this code', 'Write tests for this', 'Any improvements?']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// File mentions
|
|
25
|
+
if (/`\/[\w./-]+\.\w+`/.test(text) || /\b(created|modified|updated|wrote|saved)\b.*\b(file|files)\b/i.test(text)) {
|
|
26
|
+
return ['Show me the file', 'Make changes to it', 'What else needs updating?']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Task completion signals
|
|
30
|
+
if (/\b(done|complete|finished|ready|all set|successfully)\b/i.test(text)) {
|
|
31
|
+
return ["What's next?", 'Summarize what was done', 'Any remaining issues?']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Question asked by assistant
|
|
35
|
+
if (/\?\s*$/.test(text.trim())) {
|
|
36
|
+
return ['Yes, go ahead', 'No, try a different approach', 'Tell me more about the options']
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// List/steps presented
|
|
40
|
+
if (/^\s*(\d+\.|[-*])\s/m.test(text)) {
|
|
41
|
+
return ['Start with the first step', 'Can you elaborate?', 'Any alternatives?']
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ['Continue', 'Tell me more', 'Can you explain further?']
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function SuggestionsBar({ lastMessage, onSend }: Props) {
|
|
48
|
+
const suggestions = lastMessage?.suggestions?.length === 3
|
|
49
|
+
? lastMessage.suggestions
|
|
50
|
+
: getSuggestions(lastMessage)
|
|
51
|
+
|
|
52
|
+
if (!suggestions.length) return null
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className="flex flex-wrap gap-2 px-1 pt-2"
|
|
57
|
+
style={{ animation: 'fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
58
|
+
>
|
|
59
|
+
{suggestions.map((text) => (
|
|
60
|
+
<button
|
|
61
|
+
key={text}
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => onSend(text)}
|
|
64
|
+
className="rounded-full px-3.5 py-1.5 text-[12px] font-500 border border-white/[0.06] bg-white/[0.03]
|
|
65
|
+
text-text-3 hover:text-text-2 hover:bg-white/[0.06] hover:border-white/[0.10]
|
|
66
|
+
cursor-pointer transition-all active:scale-[0.97]"
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
{text}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
4
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
assistantName?: string
|
|
9
|
+
agentAvatarSeed?: string
|
|
10
|
+
agentName?: string
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
export function ThinkingIndicator({ assistantName }: Props) {
|
|
13
|
+
export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }: Props) {
|
|
14
|
+
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
15
|
+
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
16
|
+
|
|
17
|
+
const statusText = streamPhase === 'tool' && streamToolName
|
|
18
|
+
? `Using ${streamToolName}...`
|
|
19
|
+
: 'Thinking...'
|
|
20
|
+
|
|
10
21
|
return (
|
|
11
22
|
<div className="flex flex-col items-start"
|
|
12
23
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
13
24
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
14
|
-
<AiAvatar size="sm" />
|
|
25
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
15
26
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
16
27
|
</div>
|
|
17
28
|
<div className="bubble-ai px-6 py-5">
|
|
18
|
-
<div className="flex gap-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
<div className="flex items-center gap-3">
|
|
30
|
+
<div className="flex gap-2">
|
|
31
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
32
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
33
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
34
|
+
</div>
|
|
35
|
+
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
22
36
|
</div>
|
|
23
37
|
</div>
|
|
24
38
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react'
|
|
4
4
|
import type { ToolEvent } from '@/stores/use-chat-store'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
|
|
7
8
|
const TOOL_COLORS: Record<string, string> = {
|
|
@@ -251,6 +252,61 @@ function extractMedia(output: string): { images: string[]; videos: string[]; pdf
|
|
|
251
252
|
return { images, videos, pdfs, files, cleanText }
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
import type { AppSettings } from '@/types'
|
|
256
|
+
|
|
257
|
+
/** Settings keys that can be quick-fixed from error output */
|
|
258
|
+
const TIMEOUT_SETTINGS: Array<{ pattern: RegExp; settingKey: keyof AppSettings; label: string; increment: number }> = [
|
|
259
|
+
{ pattern: /Claude Code CLI timed out/i, settingKey: 'claudeCodeTimeoutSec', label: 'Claude Code Timeout', increment: 600 },
|
|
260
|
+
{ pattern: /Codex CLI timed out|OpenCode CLI timed out/i, settingKey: 'cliProcessTimeoutSec', label: 'CLI Process Timeout', increment: 600 },
|
|
261
|
+
{ pattern: /command timed out|shell.*timed out/i, settingKey: 'shellCommandTimeoutSec', label: 'Shell Timeout', increment: 60 },
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
/** Inline quick-fix button for timeout errors */
|
|
265
|
+
function TimeoutQuickFix({ event }: { event: ToolEvent }) {
|
|
266
|
+
const [applied, setApplied] = useState(false)
|
|
267
|
+
if (event.status !== 'error' || !event.output) return null
|
|
268
|
+
|
|
269
|
+
const match = TIMEOUT_SETTINGS.find((s) => s.pattern.test(event.output || ''))
|
|
270
|
+
if (!match) return null
|
|
271
|
+
|
|
272
|
+
const handleIncrease = async (e: React.MouseEvent) => {
|
|
273
|
+
e.stopPropagation()
|
|
274
|
+
const store = useAppStore.getState()
|
|
275
|
+
const current = (store.appSettings[match.settingKey] as number) || 0
|
|
276
|
+
const newValue = current + match.increment
|
|
277
|
+
await store.updateSettings({ [match.settingKey]: newValue })
|
|
278
|
+
setApplied(true)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (applied) {
|
|
282
|
+
const store = useAppStore.getState()
|
|
283
|
+
const val = store.appSettings[match.settingKey] as number
|
|
284
|
+
return (
|
|
285
|
+
<div className="flex items-center gap-2 mt-2 text-[12px] text-emerald-400 font-500">
|
|
286
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
287
|
+
{match.label} increased to {val}s
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
onClick={handleIncrease}
|
|
296
|
+
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600
|
|
297
|
+
bg-amber-500/10 border border-amber-500/20 text-amber-400 hover:bg-amber-500/20
|
|
298
|
+
cursor-pointer transition-all"
|
|
299
|
+
style={{ fontFamily: 'inherit' }}
|
|
300
|
+
>
|
|
301
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
302
|
+
<circle cx="12" cy="12" r="10" />
|
|
303
|
+
<polyline points="12 6 12 12 16 14" />
|
|
304
|
+
</svg>
|
|
305
|
+
Increase {match.label}
|
|
306
|
+
</button>
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
254
310
|
export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
255
311
|
const [expanded, setExpanded] = useState(false)
|
|
256
312
|
const [imgExpanded, setImgExpanded] = useState(false)
|
|
@@ -338,7 +394,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
338
394
|
role="link"
|
|
339
395
|
tabIndex={0}
|
|
340
396
|
onClick={handleAgentClick}
|
|
341
|
-
onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as
|
|
397
|
+
onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as unknown as React.MouseEvent)}
|
|
342
398
|
className="text-accent-bright hover:underline cursor-pointer font-600"
|
|
343
399
|
>
|
|
344
400
|
{delegationInfo.agentName}
|
|
@@ -383,6 +439,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
383
439
|
{formattedCleanOutput}
|
|
384
440
|
</pre>
|
|
385
441
|
)}
|
|
442
|
+
<TimeoutQuickFix event={event} />
|
|
386
443
|
</>
|
|
387
444
|
)}
|
|
388
445
|
</div>
|
|
@@ -397,23 +454,39 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
397
454
|
<img
|
|
398
455
|
src={src}
|
|
399
456
|
alt={`Screenshot ${i + 1}`}
|
|
457
|
+
loading="lazy"
|
|
400
458
|
className={`rounded-[10px] border border-white/10 cursor-pointer transition-all duration-200 hover:border-white/25 ${imgExpanded ? 'max-w-full' : 'max-w-[400px]'}`}
|
|
401
459
|
onClick={(e) => { e.stopPropagation(); setImgExpanded(!imgExpanded) }}
|
|
402
460
|
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
403
461
|
/>
|
|
404
|
-
<
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
462
|
+
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity">
|
|
463
|
+
<button
|
|
464
|
+
onClick={(e) => {
|
|
465
|
+
e.stopPropagation()
|
|
466
|
+
useChatStore.getState().setPreviewContent({ type: 'image', url: src, title: `${label} — Screenshot` })
|
|
467
|
+
}}
|
|
468
|
+
className="bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80 border-none cursor-pointer"
|
|
469
|
+
title="Open in side panel"
|
|
470
|
+
>
|
|
471
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
|
472
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
473
|
+
<line x1="12" y1="3" x2="12" y2="21" />
|
|
474
|
+
</svg>
|
|
475
|
+
</button>
|
|
476
|
+
<a
|
|
477
|
+
href={src}
|
|
478
|
+
download
|
|
479
|
+
onClick={(e) => e.stopPropagation()}
|
|
480
|
+
className="bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80"
|
|
481
|
+
title="Download"
|
|
482
|
+
>
|
|
483
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
|
484
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
485
|
+
<polyline points="7 10 12 15 17 10" />
|
|
486
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
487
|
+
</svg>
|
|
488
|
+
</a>
|
|
489
|
+
</div>
|
|
417
490
|
</div>
|
|
418
491
|
))}
|
|
419
492
|
</div>
|
|
@@ -423,7 +496,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
423
496
|
{media.videos.length > 0 && (
|
|
424
497
|
<div className="mt-2 flex flex-col gap-2">
|
|
425
498
|
{media.videos.map((src, i) => (
|
|
426
|
-
<video key={i} src={src} controls playsInline className="max-w-full rounded-[10px] border border-white/10" />
|
|
499
|
+
<video key={i} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
427
500
|
))}
|
|
428
501
|
</div>
|
|
429
502
|
)}
|
|
@@ -433,7 +506,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
433
506
|
<div className="mt-2 flex flex-col gap-2">
|
|
434
507
|
{media.pdfs.map((file, i) => (
|
|
435
508
|
<div key={i} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
436
|
-
<iframe src={file.url} className="w-full h-[400px] bg-white" title={file.name} />
|
|
509
|
+
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
437
510
|
<a
|
|
438
511
|
href={file.url}
|
|
439
512
|
download
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
import { TOOL_LABELS } from '@/lib/tool-definitions'
|
|
7
8
|
|
|
@@ -15,6 +16,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
15
16
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
16
17
|
const sessions = useAppStore((s) => s.sessions)
|
|
17
18
|
const [granted, setGranted] = useState<Set<string>>(new Set())
|
|
19
|
+
const continueSentRef = useRef(false)
|
|
18
20
|
|
|
19
21
|
const toolRequests: { toolId: string; reason: string }[] = []
|
|
20
22
|
const seen = new Set<string>()
|
|
@@ -53,7 +55,23 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
53
55
|
const updated = [...currentTools, toolId]
|
|
54
56
|
await api('PUT', `/sessions/${sid}`, { tools: updated })
|
|
55
57
|
await loadSessions()
|
|
56
|
-
|
|
58
|
+
const newGranted = new Set(granted).add(toolId)
|
|
59
|
+
setGranted(newGranted)
|
|
60
|
+
|
|
61
|
+
// Auto-continue: once all requested tools are granted, send a follow-up message
|
|
62
|
+
const allGranted = toolRequests.every(
|
|
63
|
+
(r) => newGranted.has(r.toolId) || updated.includes(r.toolId),
|
|
64
|
+
)
|
|
65
|
+
if (allGranted && !continueSentRef.current) {
|
|
66
|
+
continueSentRef.current = true
|
|
67
|
+
// Small delay to let the session update propagate
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
const { streaming, sendMessage } = useChatStore.getState()
|
|
70
|
+
if (!streaming) {
|
|
71
|
+
sendMessage('Continue')
|
|
72
|
+
}
|
|
73
|
+
}, 300)
|
|
74
|
+
}
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
return (
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { ChatTraceBlock } from '@/types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
trace: ChatTraceBlock
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TraceBlock({ trace }: Props) {
|
|
11
|
+
const [collapsed, setCollapsed] = useState(trace.collapsed !== false)
|
|
12
|
+
|
|
13
|
+
const bgColor = trace.type === 'thinking'
|
|
14
|
+
? 'bg-purple-500/[0.04] border-purple-500/10'
|
|
15
|
+
: trace.type === 'tool-call'
|
|
16
|
+
? 'bg-sky-500/[0.04] border-sky-500/10'
|
|
17
|
+
: 'bg-emerald-500/[0.04] border-emerald-500/10'
|
|
18
|
+
|
|
19
|
+
const labelColor = trace.type === 'thinking'
|
|
20
|
+
? 'text-purple-400/70'
|
|
21
|
+
: trace.type === 'tool-call'
|
|
22
|
+
? 'text-sky-400/70'
|
|
23
|
+
: 'text-emerald-400/70'
|
|
24
|
+
|
|
25
|
+
const icon = trace.type === 'thinking'
|
|
26
|
+
? '...'
|
|
27
|
+
: trace.type === 'tool-call'
|
|
28
|
+
? '>'
|
|
29
|
+
: '<'
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={`my-1 rounded-[8px] border ${bgColor} overflow-hidden`}>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
35
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer border-none bg-transparent transition-colors hover:bg-white/[0.02] ${labelColor}`}
|
|
36
|
+
style={{ fontFamily: 'inherit' }}
|
|
37
|
+
>
|
|
38
|
+
<span className="font-mono text-[10px] w-4 shrink-0">{collapsed ? '+' : '-'}</span>
|
|
39
|
+
<span className="font-mono text-[10px] shrink-0">{icon}</span>
|
|
40
|
+
<span className="text-[11px] font-600 truncate">
|
|
41
|
+
{trace.label || trace.type.replace('-', ' ')}
|
|
42
|
+
</span>
|
|
43
|
+
</button>
|
|
44
|
+
{!collapsed && (
|
|
45
|
+
<div className="px-3 pb-2">
|
|
46
|
+
<pre className={`text-[11px] leading-relaxed whitespace-pre-wrap break-words m-0 ${
|
|
47
|
+
trace.type === 'thinking'
|
|
48
|
+
? 'text-text-3/60 italic'
|
|
49
|
+
: 'text-text-3/70 font-mono'
|
|
50
|
+
}`}>
|
|
51
|
+
{trace.content.length > 2000
|
|
52
|
+
? trace.content.slice(0, 2000) + '\n... (truncated)'
|
|
53
|
+
: trace.content}
|
|
54
|
+
</pre>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse message text with [[prefix]] markers into text and trace blocks */
|
|
62
|
+
export function parseTraceBlocks(text: string): Array<{ type: 'text'; content: string } | ChatTraceBlock> {
|
|
63
|
+
const blocks: Array<{ type: 'text'; content: string } | ChatTraceBlock> = []
|
|
64
|
+
const regex = /\[\[(thinking|tool|tool-result|trace|meta)\]\]([\s\S]*?)(?=\[\[(thinking|tool|tool-result|trace|meta)\]\]|$)/g
|
|
65
|
+
|
|
66
|
+
let lastEnd = 0
|
|
67
|
+
let match: RegExpExecArray | null
|
|
68
|
+
|
|
69
|
+
while ((match = regex.exec(text)) !== null) {
|
|
70
|
+
// Add any text before this match
|
|
71
|
+
if (match.index > lastEnd) {
|
|
72
|
+
const before = text.slice(lastEnd, match.index).trim()
|
|
73
|
+
if (before) blocks.push({ type: 'text', content: before })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const prefix = match[1]
|
|
77
|
+
const content = match[2].trim()
|
|
78
|
+
if (content) {
|
|
79
|
+
if (prefix === 'thinking' || prefix === 'trace') {
|
|
80
|
+
blocks.push({ type: 'thinking', content, collapsed: true })
|
|
81
|
+
} else if (prefix === 'tool') {
|
|
82
|
+
const firstLine = content.split('\n')[0] || ''
|
|
83
|
+
blocks.push({ type: 'tool-call', content, label: firstLine.slice(0, 60), collapsed: true })
|
|
84
|
+
} else if (prefix === 'tool-result') {
|
|
85
|
+
blocks.push({ type: 'tool-result', content, collapsed: true })
|
|
86
|
+
}
|
|
87
|
+
// meta is ignored
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lastEnd = match.index + match[0].length
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add remaining text
|
|
94
|
+
if (lastEnd === 0) {
|
|
95
|
+
// No trace markers found
|
|
96
|
+
if (text.trim()) blocks.push({ type: 'text', content: text })
|
|
97
|
+
} else if (lastEnd < text.length) {
|
|
98
|
+
const remaining = text.slice(lastEnd).trim()
|
|
99
|
+
if (remaining) blocks.push({ type: 'text', content: remaining })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return blocks
|
|
103
|
+
}
|
|
@@ -14,6 +14,7 @@ export function ProjectList() {
|
|
|
14
14
|
const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
|
|
15
15
|
const [search, setSearch] = useState('')
|
|
16
16
|
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
18
|
useEffect(() => { loadProjects() }, [])
|
|
18
19
|
|
|
19
20
|
const filtered = useMemo(() => {
|