@swarmclawai/swarmclaw 0.4.0 → 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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- 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 +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- 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 +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -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 +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -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 +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- 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 +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- 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 +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -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 +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- 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 +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
|
|
4
|
+
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
approval: PendingExecApproval
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ExecApprovalCard({ approval }: Props) {
|
|
11
|
+
const resolveApproval = useApprovalStore((s) => s.resolveApproval)
|
|
12
|
+
|
|
13
|
+
const handleResolve = (decision: ExecApprovalDecision) => {
|
|
14
|
+
resolveApproval(approval.id, decision)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const expired = approval.expiresAtMs < Date.now()
|
|
18
|
+
const disabled = !!approval.resolving || expired
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="my-2 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] p-3.5">
|
|
22
|
+
<div className="flex items-center gap-2 mb-2">
|
|
23
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400 shrink-0">
|
|
24
|
+
<path d="M12 9v2m0 4h.01" />
|
|
25
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
26
|
+
</svg>
|
|
27
|
+
<span className="text-[12px] font-600 text-amber-400">Execution Approval Required</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{approval.ask && (
|
|
31
|
+
<p className="text-[13px] text-text-2 mb-2">{approval.ask}</p>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<div className="rounded-[8px] bg-black/20 px-3 py-2 mb-2 overflow-x-auto">
|
|
35
|
+
<code className="text-[12px] text-text font-mono whitespace-pre-wrap break-all">
|
|
36
|
+
{approval.command}
|
|
37
|
+
</code>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-text-3/60 mb-3">
|
|
41
|
+
{approval.cwd && <span>cwd: {approval.cwd}</span>}
|
|
42
|
+
{approval.host && <span>host: {approval.host}</span>}
|
|
43
|
+
{approval.security && (
|
|
44
|
+
<span className={approval.security === 'high' ? 'text-red-400' : ''}>
|
|
45
|
+
security: {approval.security}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{approval.error && (
|
|
51
|
+
<p className="text-[12px] text-red-400 mb-2">{approval.error}</p>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{expired ? (
|
|
55
|
+
<p className="text-[12px] text-text-3/50 italic">Approval expired</p>
|
|
56
|
+
) : (
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => handleResolve('allow-once')}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-emerald-500/10 text-[12px] font-600
|
|
62
|
+
text-emerald-400 cursor-pointer hover:bg-emerald-500/20 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
63
|
+
style={{ fontFamily: 'inherit' }}
|
|
64
|
+
>
|
|
65
|
+
{approval.resolving ? '...' : 'Allow Once'}
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => handleResolve('allow-always')}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
71
|
+
text-text-3 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
72
|
+
style={{ fontFamily: 'inherit' }}
|
|
73
|
+
>
|
|
74
|
+
Always Allow
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => handleResolve('deny')}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
80
|
+
text-red-400 cursor-pointer hover:bg-red-400/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
81
|
+
style={{ fontFamily: 'inherit' }}
|
|
82
|
+
>
|
|
83
|
+
Deny
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -7,6 +7,7 @@ import rehypeHighlight from 'rehype-highlight'
|
|
|
7
7
|
import type { Message } from '@/types'
|
|
8
8
|
import { useAppStore } from '@/stores/use-app-store'
|
|
9
9
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
10
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
10
11
|
import { CodeBlock } from './code-block'
|
|
11
12
|
import { ToolCallBubble } from './tool-call-bubble'
|
|
12
13
|
import { ToolRequestBanner } from './tool-request-banner'
|
|
@@ -170,6 +171,8 @@ function heartbeatSummary(text: string): string {
|
|
|
170
171
|
|
|
171
172
|
const IMAGE_ATTACH_RE = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i
|
|
172
173
|
const PREVIEWABLE_ATTACH_RE = /\.(html?|svg)$/i
|
|
174
|
+
const CODE_ATTACH_RE = /\.(js|jsx|ts|tsx|css|json|md|txt|py|sh|rb|go|rs|c|cpp|h|java|yaml|yml|toml|xml|sql|graphql)$/i
|
|
175
|
+
const PDF_ATTACH_RE = /\.pdf$/i
|
|
173
176
|
const FILE_TYPE_COLORS: Record<string, string> = {
|
|
174
177
|
html: 'text-orange-400', htm: 'text-orange-400', svg: 'text-emerald-400',
|
|
175
178
|
js: 'text-yellow-400', jsx: 'text-yellow-400', ts: 'text-blue-400', tsx: 'text-blue-400',
|
|
@@ -186,10 +189,49 @@ function parseAttachmentUrl(filePath?: string, fileUrl?: string) {
|
|
|
186
189
|
|
|
187
190
|
function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: string; isUserMsg?: boolean }) {
|
|
188
191
|
const isImage = IMAGE_ATTACH_RE.test(filename)
|
|
192
|
+
const isCode = CODE_ATTACH_RE.test(filename)
|
|
193
|
+
const isPdf = PDF_ATTACH_RE.test(filename)
|
|
194
|
+
const [lightbox, setLightbox] = useState(false)
|
|
195
|
+
const [codePreview, setCodePreview] = useState<string | null>(null)
|
|
196
|
+
const [codeExpanded, setCodeExpanded] = useState(false)
|
|
197
|
+
|
|
189
198
|
if (isImage) {
|
|
190
199
|
return (
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
<>
|
|
201
|
+
<img
|
|
202
|
+
src={url} alt="Attached"
|
|
203
|
+
loading="lazy"
|
|
204
|
+
className="max-w-[240px] rounded-[12px] mb-2 border border-white/10 cursor-pointer hover:border-white/25 transition-colors"
|
|
205
|
+
onClick={() => setLightbox(true)}
|
|
206
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
207
|
+
/>
|
|
208
|
+
{lightbox && (
|
|
209
|
+
<div
|
|
210
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer"
|
|
211
|
+
onClick={() => setLightbox(false)}
|
|
212
|
+
>
|
|
213
|
+
<img src={url} alt="Preview" className="max-w-[90vw] max-h-[90vh] rounded-[12px] shadow-2xl" />
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isPdf) {
|
|
221
|
+
return (
|
|
222
|
+
<div className="mb-2 rounded-[12px] border border-white/[0.08] bg-[rgba(255,255,255,0.02)] overflow-hidden" style={{ maxWidth: 480 }}>
|
|
223
|
+
<div className="flex items-center gap-3 px-4 py-2.5">
|
|
224
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 bg-red-500/10 text-red-400">
|
|
225
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
226
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
227
|
+
<polyline points="14 2 14 8 20 8" />
|
|
228
|
+
</svg>
|
|
229
|
+
</div>
|
|
230
|
+
<span className="text-[13px] font-500 truncate flex-1">{filename}</span>
|
|
231
|
+
<a href={url} download={filename} className="text-[11px] font-600 text-text-3 hover:text-text-2 no-underline">Download</a>
|
|
232
|
+
</div>
|
|
233
|
+
<iframe src={url} loading="lazy" className="w-full h-[300px] border-t border-white/[0.06]" title={filename} />
|
|
234
|
+
</div>
|
|
193
235
|
)
|
|
194
236
|
}
|
|
195
237
|
|
|
@@ -197,7 +239,6 @@ function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: s
|
|
|
197
239
|
const colorClass = FILE_TYPE_COLORS[ext] || 'text-text-3'
|
|
198
240
|
const isPreviewable = PREVIEWABLE_ATTACH_RE.test(filename)
|
|
199
241
|
|
|
200
|
-
// Solid bg so chip is readable on both user (purple) and assistant bubbles
|
|
201
242
|
const chipBg = isUserMsg
|
|
202
243
|
? 'bg-[rgba(0,0,0,0.25)] border-white/[0.12]'
|
|
203
244
|
: 'bg-[rgba(255,255,255,0.04)] border-white/[0.08]'
|
|
@@ -206,40 +247,85 @@ function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: s
|
|
|
206
247
|
? 'bg-white/[0.12] hover:bg-white/[0.18] text-white/80'
|
|
207
248
|
: 'bg-white/[0.06] hover:bg-white/[0.10] text-text-3'
|
|
208
249
|
|
|
250
|
+
const handleCodePreview = async () => {
|
|
251
|
+
if (codePreview !== null) { setCodeExpanded(!codeExpanded); return }
|
|
252
|
+
try {
|
|
253
|
+
const serveUrl = `/api/files/serve?path=${encodeURIComponent(url.replace('/api/uploads/', ''))}`
|
|
254
|
+
const res = await fetch(url.startsWith('/api/files/') ? url : serveUrl)
|
|
255
|
+
if (!res.ok) return
|
|
256
|
+
const text = await res.text()
|
|
257
|
+
setCodePreview(text)
|
|
258
|
+
setCodeExpanded(true)
|
|
259
|
+
} catch {
|
|
260
|
+
// ignore
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
209
264
|
return (
|
|
210
|
-
<div className=
|
|
211
|
-
<div className={`flex items-center
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
265
|
+
<div className="mb-2">
|
|
266
|
+
<div className={`flex items-center gap-3 px-4 py-2.5 rounded-[12px] border ${chipBg}`}>
|
|
267
|
+
<div className={`flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 ${iconBg} ${colorClass}`}>
|
|
268
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
269
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
270
|
+
<polyline points="14 2 14 8 20 8" />
|
|
271
|
+
</svg>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
274
|
+
<span className={`text-[13px] font-500 truncate ${isUserMsg ? 'text-white' : 'text-text'}`}>{filename}</span>
|
|
275
|
+
<span className={`text-[11px] uppercase tracking-wide ${isUserMsg ? 'text-white/50' : 'text-text-3/70'}`}>{ext || 'file'}</span>
|
|
276
|
+
</div>
|
|
277
|
+
{isCode && (
|
|
278
|
+
<button
|
|
279
|
+
onClick={handleCodePreview}
|
|
280
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 border-none cursor-pointer ${
|
|
281
|
+
isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
|
|
282
|
+
}`}
|
|
283
|
+
>
|
|
284
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
285
|
+
<polyline points="16 18 22 12 16 6" />
|
|
286
|
+
<polyline points="8 6 2 12 8 18" />
|
|
287
|
+
</svg>
|
|
288
|
+
{codeExpanded ? 'Hide' : 'Preview'}
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
291
|
+
{isPreviewable && (
|
|
292
|
+
<a href={url} target="_blank" rel="noopener noreferrer"
|
|
293
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${
|
|
294
|
+
isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
|
|
295
|
+
}`}
|
|
296
|
+
title="Preview in new tab">
|
|
297
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
298
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
299
|
+
<circle cx="12" cy="12" r="3" />
|
|
300
|
+
</svg>
|
|
301
|
+
Preview
|
|
302
|
+
</a>
|
|
303
|
+
)}
|
|
304
|
+
<a href={url} download={filename}
|
|
305
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${btnBg}`}>
|
|
227
306
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
228
|
-
<path d="
|
|
229
|
-
<
|
|
307
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
308
|
+
<polyline points="7 10 12 15 17 10" />
|
|
309
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
230
310
|
</svg>
|
|
231
|
-
|
|
311
|
+
Download
|
|
232
312
|
</a>
|
|
313
|
+
</div>
|
|
314
|
+
{isCode && codeExpanded && codePreview !== null && (
|
|
315
|
+
<div className="mt-1 rounded-[10px] border border-white/[0.06] overflow-hidden" style={{ animation: 'fade-in 0.2s ease' }}>
|
|
316
|
+
<CodeBlock className={`language-${ext}`}>
|
|
317
|
+
{codePreview.split('\n').slice(0, codeExpanded ? undefined : 10).join('\n')}
|
|
318
|
+
</CodeBlock>
|
|
319
|
+
{codePreview.split('\n').length > 10 && (
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => setCodeExpanded((v) => !v)}
|
|
322
|
+
className="w-full px-3 py-1.5 text-[10px] text-text-3 hover:text-text-2 bg-white/[0.02] hover:bg-white/[0.04] border-none border-t border-white/[0.06] cursor-pointer transition-colors"
|
|
323
|
+
>
|
|
324
|
+
{codePreview.split('\n').length > 10 ? `Show all ${codePreview.split('\n').length} lines` : 'Show less'}
|
|
325
|
+
</button>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
233
328
|
)}
|
|
234
|
-
<a href={url} download={filename}
|
|
235
|
-
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${btnBg}`}>
|
|
236
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
237
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
238
|
-
<polyline points="7 10 12 15 17 10" />
|
|
239
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
240
|
-
</svg>
|
|
241
|
-
Download
|
|
242
|
-
</a>
|
|
243
329
|
</div>
|
|
244
330
|
)
|
|
245
331
|
}
|
|
@@ -280,17 +366,25 @@ function renderAttachments(message: Message) {
|
|
|
280
366
|
interface Props {
|
|
281
367
|
message: Message
|
|
282
368
|
assistantName?: string
|
|
369
|
+
agentAvatarSeed?: string
|
|
370
|
+
agentName?: string
|
|
283
371
|
isLast?: boolean
|
|
284
372
|
onRetry?: () => void
|
|
373
|
+
messageIndex?: number
|
|
374
|
+
onToggleBookmark?: (index: number) => void
|
|
375
|
+
onEditResend?: (index: number, newText: string) => void
|
|
376
|
+
onFork?: (index: number) => void
|
|
285
377
|
}
|
|
286
378
|
|
|
287
|
-
export const MessageBubble = memo(function MessageBubble({ message, assistantName, isLast, onRetry }: Props) {
|
|
379
|
+
export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork }: Props) {
|
|
288
380
|
const isUser = message.role === 'user'
|
|
289
381
|
const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
|
|
290
382
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
291
383
|
const [copied, setCopied] = useState(false)
|
|
292
384
|
const [heartbeatExpanded, setHeartbeatExpanded] = useState(false)
|
|
293
385
|
const [toolEventsExpanded, setToolEventsExpanded] = useState(false)
|
|
386
|
+
const [editing, setEditing] = useState(false)
|
|
387
|
+
const [editText, setEditText] = useState('')
|
|
294
388
|
const toolEvents = message.toolEvents || []
|
|
295
389
|
const hasToolEvents = !isUser && toolEvents.length > 0
|
|
296
390
|
const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
|
|
@@ -309,7 +403,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
309
403
|
>
|
|
310
404
|
{/* Sender label + timestamp */}
|
|
311
405
|
<div className={`flex items-center gap-2.5 mb-2 px-1 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
312
|
-
{!isUser && <AiAvatar size="sm" />}
|
|
406
|
+
{!isUser && (agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" />)}
|
|
313
407
|
<span className={`text-[12px] font-600 ${isUser ? 'text-accent-bright/70' : 'text-text-3'}`}>
|
|
314
408
|
{isUser ? (currentUser ? currentUser.charAt(0).toUpperCase() + currentUser.slice(1) : 'You') : (assistantName || 'Claude')}
|
|
315
409
|
</span>
|
|
@@ -414,12 +508,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
414
508
|
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(src)
|
|
415
509
|
if (isVideo) {
|
|
416
510
|
return (
|
|
417
|
-
<video src={src} controls className="max-w-full rounded-[10px] border border-white/10 my-2" />
|
|
511
|
+
<video src={src} controls preload="none" className="max-w-full rounded-[10px] border border-white/10 my-2" />
|
|
418
512
|
)
|
|
419
513
|
}
|
|
420
514
|
return (
|
|
421
515
|
<a href={src} download target="_blank" rel="noopener noreferrer" className="block my-2">
|
|
422
|
-
<img src={src} alt={alt || 'File'} className="max-w-full rounded-[10px] border border-white/10 hover:border-white/25 transition-colors cursor-pointer" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
516
|
+
<img src={src} alt={alt || 'File'} loading="lazy" className="max-w-full rounded-[10px] border border-white/10 hover:border-white/25 transition-colors cursor-pointer" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
423
517
|
</a>
|
|
424
518
|
)
|
|
425
519
|
},
|
|
@@ -518,6 +612,16 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
518
612
|
toolOutputs={toolEvents.map((e) => e.output || '').filter(Boolean)}
|
|
519
613
|
/>}
|
|
520
614
|
|
|
615
|
+
{/* Bookmark indicator */}
|
|
616
|
+
{message.bookmarked && (
|
|
617
|
+
<div className={`flex items-center gap-1 mt-1 px-1 ${isUser ? 'justify-end' : ''}`}>
|
|
618
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="#F59E0B" stroke="#F59E0B" strokeWidth="2" className="shrink-0">
|
|
619
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
620
|
+
</svg>
|
|
621
|
+
<span className="text-[10px] text-[#F59E0B]/70 font-600">Bookmarked</span>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
|
|
521
625
|
{/* Action buttons */}
|
|
522
626
|
<div className={`flex items-center gap-1 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
|
|
523
627
|
<button
|
|
@@ -533,6 +637,53 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
533
637
|
</svg>
|
|
534
638
|
{copied ? 'Copied' : 'Copy'}
|
|
535
639
|
</button>
|
|
640
|
+
{typeof messageIndex === 'number' && onToggleBookmark && (
|
|
641
|
+
<button
|
|
642
|
+
onClick={() => onToggleBookmark(messageIndex)}
|
|
643
|
+
aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
|
|
644
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
645
|
+
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all"
|
|
646
|
+
style={{ fontFamily: 'inherit', color: message.bookmarked ? '#F59E0B' : undefined }}
|
|
647
|
+
>
|
|
648
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
649
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
650
|
+
</svg>
|
|
651
|
+
{message.bookmarked ? 'Unbookmark' : 'Bookmark'}
|
|
652
|
+
</button>
|
|
653
|
+
)}
|
|
654
|
+
{isUser && typeof messageIndex === 'number' && onEditResend && (
|
|
655
|
+
<button
|
|
656
|
+
onClick={() => { setEditText(message.text); setEditing(true) }}
|
|
657
|
+
aria-label="Edit and resend"
|
|
658
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
659
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
660
|
+
style={{ fontFamily: 'inherit' }}
|
|
661
|
+
>
|
|
662
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
663
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
664
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
665
|
+
</svg>
|
|
666
|
+
Edit
|
|
667
|
+
</button>
|
|
668
|
+
)}
|
|
669
|
+
{typeof messageIndex === 'number' && onFork && (
|
|
670
|
+
<button
|
|
671
|
+
onClick={() => onFork(messageIndex)}
|
|
672
|
+
aria-label="Fork conversation from here"
|
|
673
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
674
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
675
|
+
style={{ fontFamily: 'inherit' }}
|
|
676
|
+
>
|
|
677
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
678
|
+
<circle cx="12" cy="18" r="3" />
|
|
679
|
+
<circle cx="6" cy="6" r="3" />
|
|
680
|
+
<circle cx="18" cy="6" r="3" />
|
|
681
|
+
<path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9" />
|
|
682
|
+
<path d="M12 12v3" />
|
|
683
|
+
</svg>
|
|
684
|
+
Fork
|
|
685
|
+
</button>
|
|
686
|
+
)}
|
|
536
687
|
{!isUser && isLast && onRetry && (
|
|
537
688
|
<button
|
|
538
689
|
onClick={onRetry}
|
|
@@ -549,6 +700,37 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
549
700
|
</button>
|
|
550
701
|
)}
|
|
551
702
|
</div>
|
|
703
|
+
|
|
704
|
+
{/* Inline edit mode */}
|
|
705
|
+
{editing && (
|
|
706
|
+
<div className={`max-w-[85%] md:max-w-[72%] mt-2 ${isUser ? 'self-end' : ''}`} style={{ animation: 'fade-in 0.2s ease' }}>
|
|
707
|
+
<textarea
|
|
708
|
+
value={editText}
|
|
709
|
+
onChange={(e) => setEditText(e.target.value)}
|
|
710
|
+
className="w-full min-h-[80px] p-3 rounded-[12px] bg-surface border border-white/[0.08] text-text text-[14px] resize-y outline-none focus:border-accent-bright/30"
|
|
711
|
+
style={{ fontFamily: 'inherit' }}
|
|
712
|
+
/>
|
|
713
|
+
<div className="flex gap-2 mt-2 justify-end">
|
|
714
|
+
<button
|
|
715
|
+
onClick={() => setEditing(false)}
|
|
716
|
+
className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 text-text-3 bg-white/[0.04] hover:bg-white/[0.07] border-none cursor-pointer transition-colors"
|
|
717
|
+
>
|
|
718
|
+
Cancel
|
|
719
|
+
</button>
|
|
720
|
+
<button
|
|
721
|
+
onClick={() => {
|
|
722
|
+
if (editText.trim() && typeof messageIndex === 'number' && onEditResend) {
|
|
723
|
+
onEditResend(messageIndex, editText.trim())
|
|
724
|
+
setEditing(false)
|
|
725
|
+
}
|
|
726
|
+
}}
|
|
727
|
+
className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 text-white bg-accent-bright hover:bg-accent-bright/80 border-none cursor-pointer transition-colors"
|
|
728
|
+
>
|
|
729
|
+
Save & Resend
|
|
730
|
+
</button>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
)}
|
|
552
734
|
</div>
|
|
553
735
|
)
|
|
554
736
|
})
|