@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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { PersonalityDraft } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import {
|
|
7
|
+
parseIdentityMd, serializeIdentityMd,
|
|
8
|
+
parseUserMd, serializeUserMd,
|
|
9
|
+
parseSoulMd, serializeSoulMd,
|
|
10
|
+
} from '@/lib/personality-parser'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
agentId: string
|
|
14
|
+
fileType: 'IDENTITY.md' | 'USER.md' | 'SOUL.md'
|
|
15
|
+
content: string
|
|
16
|
+
onSave: (content: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const inputClass = 'w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors'
|
|
20
|
+
const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1'
|
|
21
|
+
|
|
22
|
+
export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
|
|
23
|
+
const [draft, setDraft] = useState<Record<string, string>>({})
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (fileType === 'IDENTITY.md') {
|
|
27
|
+
const parsed = parseIdentityMd(content)
|
|
28
|
+
setDraft({ name: parsed.name || '', creature: parsed.creature || '', vibe: parsed.vibe || '', emoji: parsed.emoji || '' })
|
|
29
|
+
} else if (fileType === 'USER.md') {
|
|
30
|
+
const parsed = parseUserMd(content)
|
|
31
|
+
setDraft({ name: parsed.name || '', callThem: parsed.callThem || '', pronouns: parsed.pronouns || '', timezone: parsed.timezone || '', notes: parsed.notes || '', context: parsed.context || '' })
|
|
32
|
+
} else if (fileType === 'SOUL.md') {
|
|
33
|
+
const parsed = parseSoulMd(content)
|
|
34
|
+
setDraft({ coreTruths: parsed.coreTruths || '', boundaries: parsed.boundaries || '', vibe: parsed.vibe || '', continuity: parsed.continuity || '' })
|
|
35
|
+
}
|
|
36
|
+
}, [content, fileType])
|
|
37
|
+
|
|
38
|
+
const update = (key: string, value: string) => {
|
|
39
|
+
setDraft((prev) => ({ ...prev, [key]: value }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleSave = () => {
|
|
43
|
+
let serialized = ''
|
|
44
|
+
if (fileType === 'IDENTITY.md') {
|
|
45
|
+
serialized = serializeIdentityMd(draft as PersonalityDraft['identity'])
|
|
46
|
+
} else if (fileType === 'USER.md') {
|
|
47
|
+
serialized = serializeUserMd(draft as PersonalityDraft['user'])
|
|
48
|
+
} else if (fileType === 'SOUL.md') {
|
|
49
|
+
serialized = serializeSoulMd(draft as PersonalityDraft['soul'])
|
|
50
|
+
}
|
|
51
|
+
onSave(serialized)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fields = fileType === 'IDENTITY.md'
|
|
55
|
+
? [
|
|
56
|
+
{ key: 'name', label: 'Name', placeholder: 'Agent display name' },
|
|
57
|
+
{ key: 'creature', label: 'Creature / Type', placeholder: 'e.g. fox, robot, wizard' },
|
|
58
|
+
{ key: 'vibe', label: 'Vibe', placeholder: 'e.g. calm, energetic, mysterious' },
|
|
59
|
+
{ key: 'emoji', label: 'Emoji / Icon', placeholder: 'e.g. a single emoji' },
|
|
60
|
+
]
|
|
61
|
+
: fileType === 'USER.md'
|
|
62
|
+
? [
|
|
63
|
+
{ key: 'name', label: 'User Name', placeholder: 'Your name' },
|
|
64
|
+
{ key: 'callThem', label: 'Call Them', placeholder: 'Nickname / preferred name' },
|
|
65
|
+
{ key: 'pronouns', label: 'Pronouns', placeholder: 'e.g. they/them' },
|
|
66
|
+
{ key: 'timezone', label: 'Timezone', placeholder: 'e.g. America/New_York' },
|
|
67
|
+
{ key: 'notes', label: 'Notes', placeholder: 'Quick notes' },
|
|
68
|
+
{ key: 'context', label: 'Context', placeholder: 'Additional context...', multiline: true },
|
|
69
|
+
]
|
|
70
|
+
: [
|
|
71
|
+
{ key: 'coreTruths', label: 'Core Truths', placeholder: 'What the agent believes...', multiline: true },
|
|
72
|
+
{ key: 'boundaries', label: 'Boundaries', placeholder: 'What the agent won\'t do...', multiline: true },
|
|
73
|
+
{ key: 'vibe', label: 'Vibe', placeholder: 'Personality tone and style...', multiline: true },
|
|
74
|
+
{ key: 'continuity', label: 'Continuity', placeholder: 'What persists between sessions...', multiline: true },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex flex-col gap-3">
|
|
79
|
+
{fields.map((f) => (
|
|
80
|
+
<div key={f.key}>
|
|
81
|
+
<label className={labelClass}>{f.label}</label>
|
|
82
|
+
{'multiline' in f && f.multiline ? (
|
|
83
|
+
<textarea
|
|
84
|
+
value={draft[f.key] || ''}
|
|
85
|
+
onChange={(e) => update(f.key, e.target.value)}
|
|
86
|
+
placeholder={f.placeholder}
|
|
87
|
+
rows={3}
|
|
88
|
+
className={`${inputClass} resize-none`}
|
|
89
|
+
style={{ fontFamily: 'ui-monospace, monospace' }}
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
value={draft[f.key] || ''}
|
|
95
|
+
onChange={(e) => update(f.key, e.target.value)}
|
|
96
|
+
placeholder={f.placeholder}
|
|
97
|
+
className={inputClass}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
<button
|
|
103
|
+
onClick={handleSave}
|
|
104
|
+
className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110"
|
|
105
|
+
style={{ fontFamily: 'inherit' }}
|
|
106
|
+
>
|
|
107
|
+
Apply to Raw Editor
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
|
|
6
|
+
export function SandboxEnvPanel() {
|
|
7
|
+
const [available, setAvailable] = useState<string[]>([])
|
|
8
|
+
const [allowed, setAllowed] = useState<Set<string>>(new Set())
|
|
9
|
+
const [loading, setLoading] = useState(true)
|
|
10
|
+
const [saving, setSaving] = useState(false)
|
|
11
|
+
const [error, setError] = useState('')
|
|
12
|
+
|
|
13
|
+
const load = useCallback(async () => {
|
|
14
|
+
setLoading(true)
|
|
15
|
+
setError('')
|
|
16
|
+
try {
|
|
17
|
+
const res = await api<{ available: string[]; allowed: string[] }>('GET', '/openclaw/sandbox-env')
|
|
18
|
+
setAvailable(res.available)
|
|
19
|
+
setAllowed(new Set(res.allowed))
|
|
20
|
+
} catch (err: unknown) {
|
|
21
|
+
setError(err instanceof Error ? err.message : 'Failed to load')
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false)
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
useEffect(() => { load() }, [load])
|
|
28
|
+
|
|
29
|
+
const toggle = async (key: string) => {
|
|
30
|
+
const next = new Set(allowed)
|
|
31
|
+
if (next.has(key)) next.delete(key)
|
|
32
|
+
else next.add(key)
|
|
33
|
+
setAllowed(next)
|
|
34
|
+
|
|
35
|
+
setSaving(true)
|
|
36
|
+
setError('')
|
|
37
|
+
try {
|
|
38
|
+
await api('PUT', '/openclaw/sandbox-env', { allowed: Array.from(next) })
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
setError(err instanceof Error ? err.message : 'Save failed')
|
|
41
|
+
} finally {
|
|
42
|
+
setSaving(false)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (loading) return <div className="text-[12px] text-text-3/50 py-2">Loading env keys...</div>
|
|
47
|
+
|
|
48
|
+
if (!available.length) {
|
|
49
|
+
return <div className="text-[12px] text-text-3/50 py-2">No .env keys found on gateway.</div>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex flex-col gap-2">
|
|
54
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">Sandbox Env Allowlist</label>
|
|
55
|
+
<div className="flex flex-col gap-1">
|
|
56
|
+
{available.map((key) => (
|
|
57
|
+
<label key={key} className="flex items-center gap-2 py-1 px-2 rounded-[8px] hover:bg-white/[0.02] cursor-pointer transition-colors">
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
checked={allowed.has(key)}
|
|
61
|
+
onChange={() => toggle(key)}
|
|
62
|
+
disabled={saving}
|
|
63
|
+
className="accent-accent-bright"
|
|
64
|
+
/>
|
|
65
|
+
<span className="text-[12px] font-mono text-text">{key}</span>
|
|
66
|
+
</label>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { SkillInstallOption } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
open: boolean
|
|
10
|
+
onClose: () => void
|
|
11
|
+
skillName: string
|
|
12
|
+
installOptions?: SkillInstallOption[]
|
|
13
|
+
onInstalled: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SkillInstallDialog({ open, onClose, skillName, installOptions = [], onInstalled }: Props) {
|
|
17
|
+
const [selectedMethod, setSelectedMethod] = useState<string>(installOptions[0]?.kind || 'brew')
|
|
18
|
+
const [installing, setInstalling] = useState(false)
|
|
19
|
+
const [error, setError] = useState('')
|
|
20
|
+
const [progress, setProgress] = useState('')
|
|
21
|
+
|
|
22
|
+
const handleInstall = async () => {
|
|
23
|
+
setInstalling(true)
|
|
24
|
+
setError('')
|
|
25
|
+
setProgress('Installing...')
|
|
26
|
+
try {
|
|
27
|
+
await api('POST', '/openclaw/skills/install', {
|
|
28
|
+
name: skillName,
|
|
29
|
+
installId: selectedMethod,
|
|
30
|
+
timeoutMs: 120_000,
|
|
31
|
+
})
|
|
32
|
+
setProgress('Installed successfully!')
|
|
33
|
+
onInstalled()
|
|
34
|
+
setTimeout(onClose, 1000)
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
setError(err instanceof Error ? err.message : 'Installation failed')
|
|
37
|
+
setProgress('')
|
|
38
|
+
} finally {
|
|
39
|
+
setInstalling(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const methods = installOptions.length > 0
|
|
44
|
+
? installOptions
|
|
45
|
+
: [
|
|
46
|
+
{ kind: 'brew' as const, label: 'Homebrew' },
|
|
47
|
+
{ kind: 'node' as const, label: 'npm/Node' },
|
|
48
|
+
{ kind: 'go' as const, label: 'Go install' },
|
|
49
|
+
{ kind: 'uv' as const, label: 'UV (Python)' },
|
|
50
|
+
{ kind: 'download' as const, label: 'Direct download' },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
55
|
+
<DialogContent className="sm:max-w-[400px]">
|
|
56
|
+
<DialogHeader>
|
|
57
|
+
<DialogTitle>Install {skillName}</DialogTitle>
|
|
58
|
+
</DialogHeader>
|
|
59
|
+
<div className="py-3 flex flex-col gap-3">
|
|
60
|
+
<label className="text-[12px] font-600 text-text-3">Install Method</label>
|
|
61
|
+
<div className="flex flex-wrap gap-2">
|
|
62
|
+
{methods.map((m) => (
|
|
63
|
+
<button
|
|
64
|
+
key={m.kind}
|
|
65
|
+
onClick={() => setSelectedMethod(m.kind)}
|
|
66
|
+
disabled={installing}
|
|
67
|
+
className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
68
|
+
${selectedMethod === m.kind
|
|
69
|
+
? 'bg-accent-soft text-accent-bright border-accent-bright/30'
|
|
70
|
+
: 'bg-transparent text-text-3 border-white/[0.08] hover:border-white/[0.15]'
|
|
71
|
+
}`}
|
|
72
|
+
style={{ fontFamily: 'inherit' }}
|
|
73
|
+
>
|
|
74
|
+
{m.label}
|
|
75
|
+
</button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
{progress && <p className="text-[12px] text-emerald-400">{progress}</p>}
|
|
79
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
80
|
+
</div>
|
|
81
|
+
<DialogFooter>
|
|
82
|
+
<button
|
|
83
|
+
onClick={onClose}
|
|
84
|
+
disabled={installing}
|
|
85
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
86
|
+
style={{ fontFamily: 'inherit' }}
|
|
87
|
+
>
|
|
88
|
+
Cancel
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleInstall}
|
|
92
|
+
disabled={installing}
|
|
93
|
+
className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer disabled:opacity-40 transition-all hover:brightness-110"
|
|
94
|
+
style={{ fontFamily: 'inherit' }}
|
|
95
|
+
>
|
|
96
|
+
{installing ? 'Installing...' : 'Install'}
|
|
97
|
+
</button>
|
|
98
|
+
</DialogFooter>
|
|
99
|
+
</DialogContent>
|
|
100
|
+
</Dialog>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { Agent } from '@/types'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { api } from '@/lib/api-client'
|
|
7
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
8
|
+
|
|
9
|
+
export function TrashList() {
|
|
10
|
+
const trashedAgents = useAppStore((s) => s.trashedAgents)
|
|
11
|
+
const loadTrashedAgents = useAppStore((s) => s.loadTrashedAgents)
|
|
12
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
13
|
+
const [confirmPermanent, setConfirmPermanent] = useState<Agent | null>(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => { loadTrashedAgents() }, []) // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
16
|
+
|
|
17
|
+
const handleRestore = async (id: string) => {
|
|
18
|
+
await api('POST', '/agents/trash', { id })
|
|
19
|
+
await Promise.all([loadTrashedAgents(), loadAgents()])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const handlePermanentDelete = async (id: string) => {
|
|
23
|
+
await api('DELETE', '/agents/trash', { id })
|
|
24
|
+
await loadTrashedAgents()
|
|
25
|
+
setConfirmPermanent(null)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const agents = Object.values(trashedAgents).sort(
|
|
29
|
+
(a, b) => (b.trashedAt ?? 0) - (a.trashedAt ?? 0),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (!agents.length) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
|
|
35
|
+
<div className="w-12 h-12 rounded-[14px] bg-white/[0.03] flex items-center justify-center">
|
|
36
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
|
|
37
|
+
<polyline points="3 6 5 6 21 6" />
|
|
38
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
<p className="text-[13px] text-text-3/50">Trash is empty</p>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex-1 overflow-y-auto">
|
|
48
|
+
<div className="flex flex-col gap-1 px-2 pb-4 pt-2">
|
|
49
|
+
{agents.map((agent) => (
|
|
50
|
+
<div
|
|
51
|
+
key={agent.id}
|
|
52
|
+
className="py-3 px-4 rounded-[14px] border border-white/[0.04] bg-white/[0.02]"
|
|
53
|
+
>
|
|
54
|
+
<div className="flex items-center gap-2.5">
|
|
55
|
+
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em] text-text-2/70">
|
|
56
|
+
{agent.name}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="text-[12px] text-text-3/50 mt-1 truncate">{agent.description}</div>
|
|
60
|
+
{agent.trashedAt && (
|
|
61
|
+
<div className="text-[11px] text-text-3/40 mt-1">
|
|
62
|
+
Trashed {formatRelative(agent.trashedAt)}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
<div className="flex items-center gap-2 mt-2.5">
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => handleRestore(agent.id)}
|
|
68
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
69
|
+
text-accent-bright cursor-pointer hover:bg-accent-soft transition-all"
|
|
70
|
+
style={{ fontFamily: 'inherit' }}
|
|
71
|
+
>
|
|
72
|
+
Restore
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => setConfirmPermanent(agent)}
|
|
76
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
77
|
+
text-red-400 cursor-pointer hover:bg-red-400/10 transition-all"
|
|
78
|
+
style={{ fontFamily: 'inherit' }}
|
|
79
|
+
>
|
|
80
|
+
Delete Forever
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<ConfirmDialog
|
|
88
|
+
open={!!confirmPermanent}
|
|
89
|
+
title="Permanently Delete"
|
|
90
|
+
message={`Permanently delete "${confirmPermanent?.name}"? This cannot be undone.`}
|
|
91
|
+
confirmLabel="Delete Forever"
|
|
92
|
+
danger
|
|
93
|
+
onConfirm={() => confirmPermanent && handlePermanentDelete(confirmPermanent.id)}
|
|
94
|
+
onCancel={() => setConfirmPermanent(null)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatRelative(ts: number): string {
|
|
101
|
+
const diff = Date.now() - ts
|
|
102
|
+
const mins = Math.floor(diff / 60_000)
|
|
103
|
+
if (mins < 1) return 'just now'
|
|
104
|
+
if (mins < 60) return `${mins}m ago`
|
|
105
|
+
const hours = Math.floor(mins / 60)
|
|
106
|
+
if (hours < 24) return `${hours}h ago`
|
|
107
|
+
const days = Math.floor(hours / 24)
|
|
108
|
+
return `${days}d ago`
|
|
109
|
+
}
|
|
@@ -15,6 +15,8 @@ import { SessionDebugPanel } from './session-debug-panel'
|
|
|
15
15
|
import { VoiceOverlay } from './voice-overlay'
|
|
16
16
|
import { useVoiceConversation } from '@/hooks/use-voice-conversation'
|
|
17
17
|
import { ChatInput } from '@/components/input/chat-input'
|
|
18
|
+
import { ChatPreviewPanel } from './chat-preview-panel'
|
|
19
|
+
import { InspectorPanel } from '@/components/agents/inspector-panel'
|
|
18
20
|
import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
|
|
19
21
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
20
22
|
import { speak } from '@/lib/tts'
|
|
@@ -37,13 +39,15 @@ export function ChatArea() {
|
|
|
37
39
|
const removeSessionFromStore = useAppStore((s) => s.removeSession)
|
|
38
40
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
39
41
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
40
|
-
const { messages, setMessages, streaming, streamingSessionId, sendMessage, stopStreaming, devServer: devServerStatus, setDevServer, debugOpen, setDebugOpen, ttsEnabled } = useChatStore()
|
|
42
|
+
const { messages, setMessages, streaming, streamingSessionId, sendMessage, stopStreaming, devServer: devServerStatus, setDevServer, debugOpen, setDebugOpen, ttsEnabled, previewContent, setPreviewContent } = useChatStore()
|
|
41
43
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
42
44
|
|
|
43
45
|
const agents = useAppStore((s) => s.agents)
|
|
44
46
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
45
47
|
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
46
48
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
49
|
+
const inspectorOpen = useAppStore((s) => s.inspectorOpen)
|
|
50
|
+
const currentAgent = session?.agentId ? agents[session.agentId] ?? null : null
|
|
47
51
|
|
|
48
52
|
const voice = useVoiceConversation()
|
|
49
53
|
const handleVoiceToggle = useCallback(() => {
|
|
@@ -243,8 +247,9 @@ export function ChatArea() {
|
|
|
243
247
|
const isEmpty = !messages.length && !streamingForThisSession
|
|
244
248
|
|
|
245
249
|
return (
|
|
250
|
+
<div className="flex-1 flex h-full min-h-0 min-w-0">
|
|
246
251
|
<div
|
|
247
|
-
className="flex-1 flex flex-col h-full min-h-0 relative"
|
|
252
|
+
className="flex-1 flex flex-col h-full min-h-0 min-w-0 relative"
|
|
248
253
|
onDragOver={handleDragOver}
|
|
249
254
|
onDragEnter={handleDragEnter}
|
|
250
255
|
onDragLeave={handleDragLeave}
|
|
@@ -423,6 +428,13 @@ export function ChatArea() {
|
|
|
423
428
|
</div>
|
|
424
429
|
)}
|
|
425
430
|
</div>
|
|
431
|
+
{isDesktop && previewContent && (
|
|
432
|
+
<ChatPreviewPanel content={previewContent} onClose={() => setPreviewContent(null)} />
|
|
433
|
+
)}
|
|
434
|
+
{isDesktop && inspectorOpen && currentAgent && (
|
|
435
|
+
<InspectorPanel agent={currentAgent} />
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
426
438
|
)
|
|
427
439
|
}
|
|
428
440
|
|