@swarmclawai/swarmclaw 0.6.2 → 0.6.4
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 +45 -44
- package/package.json +1 -1
- package/src/app/api/tts/route.ts +16 -36
- package/src/app/api/tts/stream/route.ts +14 -43
- package/src/app/page.tsx +7 -3
- package/src/components/auth/access-key-gate.tsx +22 -11
- package/src/components/chat/chat-area.tsx +30 -2
- package/src/components/chat/chat-header.tsx +70 -3
- package/src/components/chat/message-bubble.tsx +11 -1
- package/src/components/chat/message-list.tsx +3 -71
- package/src/components/chat/tool-call-bubble.test.ts +28 -0
- package/src/components/chat/tool-call-bubble.tsx +13 -1
- package/src/components/chatrooms/chatroom-input.tsx +6 -5
- package/src/components/connectors/connector-sheet.tsx +16 -1
- package/src/components/input/chat-input.tsx +5 -4
- package/src/components/layout/app-layout.tsx +5 -6
- package/src/components/logs/log-list.tsx +7 -7
- package/src/components/sessions/new-session-sheet.tsx +4 -3
- package/src/hooks/use-media-query.ts +30 -4
- package/src/lib/api-client.ts +6 -18
- package/src/lib/fetch-timeout.ts +17 -0
- package/src/lib/notification-sounds.ts +4 -4
- package/src/lib/safe-storage.ts +42 -0
- package/src/lib/server/chat-execution.ts +74 -3
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +398 -31
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- package/src/lib/server/queue.ts +130 -1
- package/src/lib/server/session-tools/connector.ts +540 -94
- package/src/lib/server/session-tools/file.ts +26 -7
- package/src/lib/server/session-tools/web-output.test.ts +29 -0
- package/src/lib/server/session-tools/web-output.ts +16 -0
- package/src/lib/server/session-tools/web.ts +8 -5
- package/src/lib/server/stream-agent-chat.ts +7 -0
- package/src/lib/view-routes.ts +5 -1
- package/src/stores/use-app-store.ts +9 -11
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# SwarmClaw
|
|
2
2
|
|
|
3
3
|
[](https://github.com/swarmclawai/swarmclaw/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/swarmclawai/swarmclaw/releases)
|
|
5
|
+
[](https://www.npmjs.com/package/@swarmclawai/swarmclaw)
|
|
5
6
|
|
|
6
7
|
<p align="center">
|
|
7
8
|
<img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
|
|
@@ -22,48 +23,6 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
22
23
|
- Review agent system prompts before giving them shell or browser tools
|
|
23
24
|
- Repeated failed access key attempts are rate-limited to slow brute-force attacks
|
|
24
25
|
|
|
25
|
-
## Features
|
|
26
|
-
|
|
27
|
-
- **15 Built-in Providers** — Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, plus custom OpenAI-compatible endpoints
|
|
28
|
-
- **OpenClaw Gateway** — Per-agent toggle to connect any agent to a local or remote OpenClaw gateway. Each agent gets its own gateway URL and token — run a swarm of OpenClaws from one dashboard. The `openclaw` CLI ships as a bundled dependency (no separate install needed)
|
|
29
|
-
- **OpenClaw Control Plane** — Built-in gateway connection controls, reload mode switching (hot/hybrid/full), config issue detection/repair, remote history sync, and live execution approval handling
|
|
30
|
-
- **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
|
|
31
|
-
- **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
|
|
32
|
-
- **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
|
|
33
|
-
- **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
|
|
34
|
-
- **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
|
|
35
|
-
- **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
|
|
36
|
-
- **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
|
|
37
|
-
- **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
|
|
38
|
-
- **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
|
|
39
|
-
- **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
|
|
40
|
-
- **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
|
|
41
|
-
- **Scheduling** — Cron-based agent scheduling with human-friendly presets
|
|
42
|
-
- **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
|
|
43
|
-
- **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
|
|
44
|
-
- **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
|
|
45
|
-
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, and file/image-aware chat context
|
|
46
|
-
- **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
|
|
47
|
-
- **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
|
|
48
|
-
- **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
|
|
49
|
-
- **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
|
|
50
|
-
- **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
|
|
51
|
-
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
|
|
52
|
-
- **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
|
|
53
|
-
- **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
|
|
54
|
-
- **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
|
|
55
|
-
- **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
|
|
56
|
-
- **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
|
|
57
|
-
- **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
|
|
58
|
-
- **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
|
|
59
|
-
- **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
|
|
60
|
-
- **Secrets Vault** — Encrypted storage for API keys and service tokens
|
|
61
|
-
- **Custom Providers** — Add any OpenAI-compatible API as a provider
|
|
62
|
-
- **MCP Servers** — Connect agents to any Model Context Protocol server. Per-agent server selection with tool discovery and per-tool disable toggles
|
|
63
|
-
- **Sandboxed Code Execution** — Agents can write and run JS/TS (Deno) or Python scripts in an isolated sandbox with network access, scoped filesystem, and artifact output
|
|
64
|
-
- **Real-Time Sync** — WebSocket push notifications for instant UI updates across tabs and devices (fallback to polling when WS is unavailable)
|
|
65
|
-
- **Mobile-First UI** — Responsive glass-themed dark interface, works on phone and desktop
|
|
66
|
-
|
|
67
26
|
## Requirements
|
|
68
27
|
|
|
69
28
|
- **Node.js** 22.6+
|
|
@@ -88,7 +47,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
88
47
|
```
|
|
89
48
|
|
|
90
49
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
91
|
-
To pin a version: `SWARMCLAW_VERSION=v0.6.
|
|
50
|
+
To pin a version: `SWARMCLAW_VERSION=v0.6.2 curl ... | bash`
|
|
92
51
|
|
|
93
52
|
Or run locally from the repo (friendly for non-technical users):
|
|
94
53
|
|
|
@@ -146,6 +105,48 @@ Notes:
|
|
|
146
105
|
- OpenClaw is configured per-agent via the **OpenClaw Gateway** toggle (not in the setup wizard).
|
|
147
106
|
- You can skip setup and configure everything later in the sidebar.
|
|
148
107
|
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
- **15 Built-in Providers** — Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, plus custom OpenAI-compatible endpoints
|
|
111
|
+
- **OpenClaw Gateway** — Per-agent toggle to connect any agent to a local or remote OpenClaw gateway. Each agent gets its own gateway URL and token — run a swarm of OpenClaws from one dashboard. The `openclaw` CLI ships as a bundled dependency (no separate install needed)
|
|
112
|
+
- **OpenClaw Control Plane** — Built-in gateway connection controls, reload mode switching (hot/hybrid/full), config issue detection/repair, remote history sync, and live execution approval handling
|
|
113
|
+
- **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
|
|
114
|
+
- **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
|
|
115
|
+
- **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
|
|
116
|
+
- **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
|
|
117
|
+
- **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
|
|
118
|
+
- **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
|
|
119
|
+
- **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
|
|
120
|
+
- **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
|
|
121
|
+
- **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
|
|
122
|
+
- **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
|
|
123
|
+
- **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
|
|
124
|
+
- **Scheduling** — Cron-based agent scheduling with human-friendly presets
|
|
125
|
+
- **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
|
|
126
|
+
- **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
|
|
127
|
+
- **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
|
|
128
|
+
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, and file/image-aware chat context
|
|
129
|
+
- **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
|
|
130
|
+
- **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
|
|
131
|
+
- **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
|
|
132
|
+
- **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
|
|
133
|
+
- **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
|
|
134
|
+
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
|
|
135
|
+
- **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
|
|
136
|
+
- **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
|
|
137
|
+
- **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
|
|
138
|
+
- **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
|
|
139
|
+
- **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
|
|
140
|
+
- **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
|
|
141
|
+
- **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
|
|
142
|
+
- **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
|
|
143
|
+
- **Secrets Vault** — Encrypted storage for API keys and service tokens
|
|
144
|
+
- **Custom Providers** — Add any OpenAI-compatible API as a provider
|
|
145
|
+
- **MCP Servers** — Connect agents to any Model Context Protocol server. Per-agent server selection with tool discovery and per-tool disable toggles
|
|
146
|
+
- **Sandboxed Code Execution** — Agents can write and run JS/TS (Deno) or Python scripts in an isolated sandbox with network access, scoped filesystem, and artifact output
|
|
147
|
+
- **Real-Time Sync** — WebSocket push notifications for instant UI updates across tabs and devices (fallback to polling when WS is unavailable)
|
|
148
|
+
- **Mobile-First UI** — Responsive glass-themed dark interface, works on phone and desktop
|
|
149
|
+
|
|
149
150
|
## Configuration
|
|
150
151
|
|
|
151
152
|
All config lives in `.env.local` (auto-generated):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/app/api/tts/route.ts
CHANGED
|
@@ -1,41 +1,21 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { explainElevenLabsError, resolveElevenLabsConfig, synthesizeElevenLabsMp3 } from '@/lib/server/elevenlabs'
|
|
3
3
|
|
|
4
4
|
export async function POST(req: Request) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
try {
|
|
6
|
+
const { text, voiceId } = await req.json()
|
|
7
|
+
if (!String(text || '').trim()) {
|
|
8
|
+
return new NextResponse('No text provided', { status: 400 })
|
|
9
|
+
}
|
|
10
|
+
resolveElevenLabsConfig(voiceId)
|
|
11
|
+
const audioBuffer = await synthesizeElevenLabsMp3({ text: String(text || ''), voiceId })
|
|
12
|
+
return new NextResponse(new Uint8Array(audioBuffer), {
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'audio/mpeg',
|
|
15
|
+
'Cache-Control': 'no-cache',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
return new NextResponse(explainElevenLabsError(err), { status: 500 })
|
|
11
20
|
}
|
|
12
|
-
|
|
13
|
-
const { text, voiceId } = await req.json()
|
|
14
|
-
const voice = voiceId || ELEVENLABS_VOICE
|
|
15
|
-
const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: {
|
|
18
|
-
'xi-api-key': ELEVENLABS_KEY,
|
|
19
|
-
'Content-Type': 'application/json',
|
|
20
|
-
'Accept': 'audio/mpeg',
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
text,
|
|
24
|
-
model_id: 'eleven_multilingual_v2',
|
|
25
|
-
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
26
|
-
}),
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
if (!apiRes.ok) {
|
|
30
|
-
const err = await apiRes.text()
|
|
31
|
-
return new NextResponse(err, { status: apiRes.status })
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const audioBuffer = await apiRes.arrayBuffer()
|
|
35
|
-
return new NextResponse(audioBuffer, {
|
|
36
|
-
headers: {
|
|
37
|
-
'Content-Type': 'audio/mpeg',
|
|
38
|
-
'Cache-Control': 'no-cache',
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
21
|
}
|
|
@@ -1,49 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { explainElevenLabsError, requestElevenLabsMp3Stream } from '@/lib/server/elevenlabs'
|
|
2
2
|
|
|
3
3
|
export async function POST(req: Request) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const { text, voiceId } = await req.json()
|
|
13
|
-
if (!text?.trim()) {
|
|
14
|
-
return new Response('No text provided', { status: 400 })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const voice = voiceId || ELEVENLABS_VOICE
|
|
18
|
-
const apiRes = await fetch(
|
|
19
|
-
`https://api.elevenlabs.io/v1/text-to-speech/${voice}/stream`,
|
|
20
|
-
{
|
|
21
|
-
method: 'POST',
|
|
4
|
+
try {
|
|
5
|
+
const { text, voiceId } = await req.json()
|
|
6
|
+
if (!String(text || '').trim()) {
|
|
7
|
+
return new Response('No text provided', { status: 400 })
|
|
8
|
+
}
|
|
9
|
+
const apiRes = await requestElevenLabsMp3Stream({ text: String(text || ''), voiceId })
|
|
10
|
+
return new Response(apiRes.body, {
|
|
22
11
|
headers: {
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
12
|
+
'Content-Type': 'audio/mpeg',
|
|
13
|
+
'Transfer-Encoding': 'chunked',
|
|
14
|
+
'Cache-Control': 'no-cache',
|
|
26
15
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
31
|
-
output_format: 'mp3_22050_32',
|
|
32
|
-
}),
|
|
33
|
-
},
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
if (!apiRes.ok) {
|
|
37
|
-
const err = await apiRes.text()
|
|
38
|
-
return new Response(err, { status: apiRes.status })
|
|
16
|
+
})
|
|
17
|
+
} catch (err: unknown) {
|
|
18
|
+
return new Response(explainElevenLabsError(err), { status: 500 })
|
|
39
19
|
}
|
|
40
|
-
|
|
41
|
-
// Pipe the streaming response directly
|
|
42
|
-
return new Response(apiRes.body, {
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'audio/mpeg',
|
|
45
|
-
'Transfer-Encoding': 'chunked',
|
|
46
|
-
'Cache-Control': 'no-cache',
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
20
|
}
|
package/src/app/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { initAudioContext } from '@/lib/tts'
|
|
6
6
|
import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
|
|
7
7
|
import { connectWs, disconnectWs } from '@/lib/ws-client'
|
|
8
|
+
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
8
9
|
import { useWs } from '@/hooks/use-ws'
|
|
9
10
|
import { AccessKeyGate } from '@/components/auth/access-key-gate'
|
|
10
11
|
import { UserPicker } from '@/components/auth/user-picker'
|
|
@@ -12,6 +13,8 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
|
|
|
12
13
|
import { AppLayout } from '@/components/layout/app-layout'
|
|
13
14
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
14
15
|
|
|
16
|
+
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
17
|
+
|
|
15
18
|
function FullScreenLoader() {
|
|
16
19
|
return (
|
|
17
20
|
<div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
|
|
@@ -158,11 +161,11 @@ export default function Home() {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
try {
|
|
161
|
-
const res = await
|
|
164
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
162
165
|
method: 'POST',
|
|
163
166
|
headers: { 'Content-Type': 'application/json' },
|
|
164
167
|
body: JSON.stringify({ key }),
|
|
165
|
-
})
|
|
168
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
166
169
|
if (res.ok) {
|
|
167
170
|
setAuthenticated(true)
|
|
168
171
|
} else {
|
|
@@ -171,8 +174,9 @@ export default function Home() {
|
|
|
171
174
|
}
|
|
172
175
|
} catch {
|
|
173
176
|
setAuthenticated(true)
|
|
177
|
+
} finally {
|
|
178
|
+
setAuthChecked(true)
|
|
174
179
|
}
|
|
175
|
-
setAuthChecked(true)
|
|
176
180
|
}, [])
|
|
177
181
|
|
|
178
182
|
// After auth, try to restore username from server settings
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
4
|
import { setStoredAccessKey } from '@/lib/api-client'
|
|
5
|
+
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
5
6
|
|
|
6
7
|
interface AccessKeyGateProps {
|
|
7
8
|
onAuthenticated: () => void
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
12
|
+
|
|
10
13
|
export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
11
14
|
const [key, setKey] = useState('')
|
|
12
15
|
const [error, setError] = useState('')
|
|
@@ -19,16 +22,22 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
19
22
|
const [copied, setCopied] = useState(false)
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let cancelled = false
|
|
26
|
+
;(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetchWithTimeout('/api/auth', {}, AUTH_CHECK_TIMEOUT_MS)
|
|
29
|
+
const data = await res.json().catch(() => ({}))
|
|
30
|
+
if (!cancelled && data.firstTime && data.key) {
|
|
26
31
|
setFirstTime(true)
|
|
27
32
|
setGeneratedKey(data.key)
|
|
28
33
|
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Auth check failed:', err)
|
|
36
|
+
} finally {
|
|
37
|
+
if (!cancelled) setChecking(false)
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
return () => { cancelled = true }
|
|
32
41
|
}, [])
|
|
33
42
|
|
|
34
43
|
const handleCopyKey = async () => {
|
|
@@ -44,14 +53,16 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
44
53
|
const handleClaimKey = async () => {
|
|
45
54
|
setLoading(true)
|
|
46
55
|
try {
|
|
47
|
-
const res = await
|
|
56
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
48
57
|
method: 'POST',
|
|
49
58
|
headers: { 'Content-Type': 'application/json' },
|
|
50
59
|
body: JSON.stringify({ key: generatedKey }),
|
|
51
|
-
})
|
|
60
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
52
61
|
if (res.ok) {
|
|
53
62
|
setStoredAccessKey(generatedKey)
|
|
54
63
|
onAuthenticated()
|
|
64
|
+
} else {
|
|
65
|
+
setError('Invalid access key')
|
|
55
66
|
}
|
|
56
67
|
} catch {
|
|
57
68
|
setError('Connection failed')
|
|
@@ -69,11 +80,11 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
69
80
|
setError('')
|
|
70
81
|
|
|
71
82
|
try {
|
|
72
|
-
const res = await
|
|
83
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
73
84
|
method: 'POST',
|
|
74
85
|
headers: { 'Content-Type': 'application/json' },
|
|
75
86
|
body: JSON.stringify({ key: trimmed }),
|
|
76
|
-
})
|
|
87
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
77
88
|
if (res.ok) {
|
|
78
89
|
setStoredAccessKey(trimmed)
|
|
79
90
|
onAuthenticated()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useCallback, useState, useRef } from 'react'
|
|
3
|
+
import { useEffect, useCallback, useState, useRef, useMemo } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { useWs } from '@/hooks/use-ws'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -65,6 +65,26 @@ export function ChatArea() {
|
|
|
65
65
|
const [browserActive, setBrowserActive] = useState(false)
|
|
66
66
|
const [heartbeatHistoryOpen, setHeartbeatHistoryOpen] = useState(false)
|
|
67
67
|
const [messagesLoading, setMessagesLoading] = useState(true)
|
|
68
|
+
const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
|
|
69
|
+
|
|
70
|
+
// Collect unique connector sources from messages for filter UI
|
|
71
|
+
const { connectorSources, hasDirectMessages } = useMemo(() => {
|
|
72
|
+
const sources = new Map<string, { platform: string; connectorName: string }>()
|
|
73
|
+
let hasDirect = false
|
|
74
|
+
for (const msg of messages) {
|
|
75
|
+
if (msg.source?.connectorId && !sources.has(msg.source.connectorId)) {
|
|
76
|
+
sources.set(msg.source.connectorId, {
|
|
77
|
+
platform: msg.source.platform,
|
|
78
|
+
connectorName: msg.source.connectorName,
|
|
79
|
+
})
|
|
80
|
+
} else if (!msg.source?.connectorId && msg.role === 'user') {
|
|
81
|
+
hasDirect = true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { connectorSources: sources, hasDirectMessages: hasDirect }
|
|
85
|
+
}, [messages])
|
|
86
|
+
// Show source filter when there are genuinely multiple sources (2+ connectors, or connector + direct)
|
|
87
|
+
const hasMultipleSources = connectorSources.size > 1 || (connectorSources.size > 0 && hasDirectMessages)
|
|
68
88
|
const [isDragging, setIsDragging] = useState(false)
|
|
69
89
|
const dragCounter = useRef(0)
|
|
70
90
|
const setPendingImage = useChatStore((s) => s.setPendingImage)
|
|
@@ -277,6 +297,10 @@ export function ChatArea() {
|
|
|
277
297
|
onVoiceToggle={handleVoiceToggle}
|
|
278
298
|
heartbeatHistoryOpen={heartbeatHistoryOpen}
|
|
279
299
|
onToggleHeartbeatHistory={() => setHeartbeatHistoryOpen((v) => !v)}
|
|
300
|
+
connectorSources={connectorSources}
|
|
301
|
+
connectorFilter={connectorFilter}
|
|
302
|
+
onConnectorFilterChange={setConnectorFilter}
|
|
303
|
+
hasMultipleSources={hasMultipleSources}
|
|
280
304
|
/>
|
|
281
305
|
)}
|
|
282
306
|
{!isDesktop && (
|
|
@@ -291,6 +315,10 @@ export function ChatArea() {
|
|
|
291
315
|
voiceActive={voice.active}
|
|
292
316
|
voiceSupported={voice.supported}
|
|
293
317
|
onVoiceToggle={handleVoiceToggle}
|
|
318
|
+
connectorSources={connectorSources}
|
|
319
|
+
connectorFilter={connectorFilter}
|
|
320
|
+
onConnectorFilterChange={setConnectorFilter}
|
|
321
|
+
hasMultipleSources={hasMultipleSources}
|
|
294
322
|
/>
|
|
295
323
|
)}
|
|
296
324
|
<DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
|
|
@@ -356,7 +384,7 @@ export function ChatArea() {
|
|
|
356
384
|
</div>
|
|
357
385
|
</div>
|
|
358
386
|
) : (
|
|
359
|
-
<MessageList messages={messages} streaming={streamingForThisSession} />
|
|
387
|
+
<MessageList messages={messages} streaming={streamingForThisSession} connectorFilter={connectorFilter} />
|
|
360
388
|
)}
|
|
361
389
|
|
|
362
390
|
{voice.active && (
|
|
@@ -53,9 +53,13 @@ interface Props {
|
|
|
53
53
|
voiceSupported?: boolean
|
|
54
54
|
heartbeatHistoryOpen?: boolean
|
|
55
55
|
onToggleHeartbeatHistory?: () => void
|
|
56
|
+
connectorSources?: Map<string, { platform: string; connectorName: string }>
|
|
57
|
+
connectorFilter?: string | null
|
|
58
|
+
onConnectorFilterChange?: (filter: string | null) => void
|
|
59
|
+
hasMultipleSources?: boolean
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, heartbeatHistoryOpen, onToggleHeartbeatHistory }: Props) {
|
|
62
|
+
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, heartbeatHistoryOpen, onToggleHeartbeatHistory, connectorSources, connectorFilter, onConnectorFilterChange, hasMultipleSources }: Props) {
|
|
59
63
|
const ttsEnabled = useChatStore((s) => s.ttsEnabled)
|
|
60
64
|
const toggleTts = useChatStore((s) => s.toggleTts)
|
|
61
65
|
const soundEnabled = useChatStore((s) => s.soundEnabled)
|
|
@@ -89,6 +93,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
89
93
|
const [heartbeatSaving, setHeartbeatSaving] = useState(false)
|
|
90
94
|
const [hbDropdownOpen, setHbDropdownOpen] = useState(false)
|
|
91
95
|
const hbDropdownRef = useRef<HTMLDivElement>(null)
|
|
96
|
+
const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
|
|
97
|
+
const sourceDropdownRef = useRef<HTMLDivElement>(null)
|
|
92
98
|
const [mainLoopSaving, setMainLoopSaving] = useState(false)
|
|
93
99
|
const [mainLoopError, setMainLoopError] = useState('')
|
|
94
100
|
const [mainLoopNotice, setMainLoopNotice] = useState('')
|
|
@@ -399,6 +405,15 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
399
405
|
return () => document.removeEventListener('mousedown', handler)
|
|
400
406
|
}, [hbDropdownOpen])
|
|
401
407
|
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (!sourceDropdownOpen) return
|
|
410
|
+
const handler = (e: MouseEvent) => {
|
|
411
|
+
if (sourceDropdownRef.current && !sourceDropdownRef.current.contains(e.target as Node)) setSourceDropdownOpen(false)
|
|
412
|
+
}
|
|
413
|
+
document.addEventListener('mousedown', handler)
|
|
414
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
415
|
+
}, [sourceDropdownOpen])
|
|
416
|
+
|
|
402
417
|
useEffect(() => {
|
|
403
418
|
if (!modelSwitcherOpen) return
|
|
404
419
|
const handler = (e: MouseEvent) => {
|
|
@@ -439,10 +454,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
439
454
|
return () => clearTimeout(timer)
|
|
440
455
|
}, [mainLoopNotice])
|
|
441
456
|
|
|
442
|
-
// Context bar shows for tools, mission controls, memories, task links, resume handles, browser
|
|
457
|
+
// Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
|
|
443
458
|
const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
|
|
444
459
|
const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
|
|
445
|
-
const
|
|
460
|
+
const hasSourceFilter = !!hasMultipleSources
|
|
461
|
+
const hasContextBar = !!(hasToolToggles || isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
|
|
446
462
|
|
|
447
463
|
return (
|
|
448
464
|
<header
|
|
@@ -863,6 +879,57 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
863
879
|
Memories
|
|
864
880
|
</button>
|
|
865
881
|
)}
|
|
882
|
+
{hasSourceFilter && onConnectorFilterChange && connectorSources && (
|
|
883
|
+
<div className="relative shrink-0" ref={sourceDropdownRef}>
|
|
884
|
+
<button
|
|
885
|
+
onClick={() => setSourceDropdownOpen((o) => !o)}
|
|
886
|
+
className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600 shrink-0 ${
|
|
887
|
+
connectorFilter
|
|
888
|
+
? 'bg-accent-soft/60 text-accent-bright/80 hover:bg-accent-soft'
|
|
889
|
+
: 'bg-white/[0.03] text-text-3/50 hover:bg-white/[0.06] hover:text-text-3/70'
|
|
890
|
+
}`}
|
|
891
|
+
title="Filter by message source"
|
|
892
|
+
>
|
|
893
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
894
|
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
895
|
+
</svg>
|
|
896
|
+
{connectorFilter
|
|
897
|
+
? (connectorSources.get(connectorFilter)?.connectorName || 'Source')
|
|
898
|
+
: 'Source'}
|
|
899
|
+
<svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="opacity-40">
|
|
900
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
901
|
+
</svg>
|
|
902
|
+
</button>
|
|
903
|
+
{sourceDropdownOpen && (
|
|
904
|
+
<div className="absolute top-full left-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[140px]">
|
|
905
|
+
<button
|
|
906
|
+
onClick={() => { onConnectorFilterChange(null); setSourceDropdownOpen(false) }}
|
|
907
|
+
className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none flex items-center gap-2 ${
|
|
908
|
+
!connectorFilter ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
|
|
909
|
+
}`}
|
|
910
|
+
>
|
|
911
|
+
All Sources
|
|
912
|
+
</button>
|
|
913
|
+
{Array.from(connectorSources.entries()).map(([cid, info]) => {
|
|
914
|
+
const active = connectorFilter === cid
|
|
915
|
+
const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
|
|
916
|
+
return (
|
|
917
|
+
<button
|
|
918
|
+
key={cid}
|
|
919
|
+
onClick={() => { onConnectorFilterChange(active ? null : cid); setSourceDropdownOpen(false) }}
|
|
920
|
+
className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none flex items-center gap-2 ${
|
|
921
|
+
active ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
|
|
922
|
+
}`}
|
|
923
|
+
>
|
|
924
|
+
<ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
|
|
925
|
+
{info.connectorName || meta?.label || info.platform}
|
|
926
|
+
</button>
|
|
927
|
+
)
|
|
928
|
+
})}
|
|
929
|
+
</div>
|
|
930
|
+
)}
|
|
931
|
+
</div>
|
|
932
|
+
)}
|
|
866
933
|
{isOpenClawAgent && openclawSessionKey && (
|
|
867
934
|
<>
|
|
868
935
|
<button
|
|
@@ -90,6 +90,12 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
90
90
|
blocked: '#EF4444',
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function isGeneratedBrowserScreenshot(url: string): boolean {
|
|
94
|
+
const match = url.match(/\/api\/uploads\/([^/?#]+)/)
|
|
95
|
+
if (!match?.[1]) return false
|
|
96
|
+
return /^(browser|screenshot)-\d+\./i.test(match[1])
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
// AttachmentChip, parseAttachmentUrl, regex constants, and FILE_TYPE_COLORS
|
|
94
100
|
// are now imported from @/components/shared/attachment-chip
|
|
95
101
|
|
|
@@ -162,6 +168,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
162
168
|
// Collect URLs from the visible (last) tool event to avoid showing duplicates
|
|
163
169
|
const lastOutput = toolEvents[toolEvents.length - 1]?.output || ''
|
|
164
170
|
const visibleMedia = extractMedia(lastOutput)
|
|
171
|
+
const hasNamedVisibleImage = visibleMedia.images.some((url) => !isGeneratedBrowserScreenshot(url))
|
|
165
172
|
const seen = new Set<string>([
|
|
166
173
|
...visibleMedia.images,
|
|
167
174
|
...visibleMedia.videos,
|
|
@@ -175,7 +182,10 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
175
182
|
for (const ev of toolEvents.slice(0, -1)) {
|
|
176
183
|
if (!ev.output) continue
|
|
177
184
|
const m = extractMedia(ev.output)
|
|
178
|
-
for (const url of m.images) {
|
|
185
|
+
for (const url of m.images) {
|
|
186
|
+
if (hasNamedVisibleImage && isGeneratedBrowserScreenshot(url)) continue
|
|
187
|
+
if (!seen.has(url)) { seen.add(url); images.push(url) }
|
|
188
|
+
}
|
|
179
189
|
for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
|
|
180
190
|
for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
|
|
181
191
|
for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
|