@swarmclawai/swarmclaw 0.5.2 → 0.6.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.
Files changed (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -0,0 +1,90 @@
1
+ 'use client'
2
+
3
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
4
+ import { CheckIcon } from '@/components/shared/check-icon'
5
+ import type { Agent } from '@/types'
6
+
7
+ interface Props {
8
+ agents: Agent[]
9
+ /** Currently selected agent ID(s). String for single-select, string[] for multi-select. */
10
+ selected: string | string[]
11
+ /** Called when an agent is clicked. In multi mode, caller should toggle; in single mode, set. */
12
+ onSelect: (agentId: string) => void
13
+ /** Show a "None" option at the top for optional single-select */
14
+ noneOption?: { label: string; onSelect: () => void }
15
+ /** Show orchestrator badge */
16
+ showOrchBadge?: boolean
17
+ /** Max height of the scrollable list */
18
+ maxHeight?: number
19
+ }
20
+
21
+ export function AgentPickerList({
22
+ agents,
23
+ selected,
24
+ onSelect,
25
+ noneOption,
26
+ showOrchBadge,
27
+ maxHeight = 220,
28
+ }: Props) {
29
+ const isSelected = (id: string) =>
30
+ Array.isArray(selected) ? selected.includes(id) : selected === id
31
+ const noneSelected = Array.isArray(selected) ? selected.length === 0 : !selected
32
+
33
+ if (agents.length === 0 && !noneOption) {
34
+ return <p className="text-[13px] text-text-3">No agents configured.</p>
35
+ }
36
+
37
+ return (
38
+ <div
39
+ className="flex flex-col gap-1 rounded-[14px] border border-white/[0.06] bg-surface p-1.5 overflow-y-auto"
40
+ style={{ maxHeight }}
41
+ >
42
+ {noneOption && (
43
+ <button
44
+ onClick={noneOption.onSelect}
45
+ className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
46
+ ${noneSelected ? 'bg-accent-soft' : 'bg-transparent hover:bg-white/[0.03]'}`}
47
+ style={{ fontFamily: 'inherit' }}
48
+ >
49
+ {noneSelected && (
50
+ <div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
51
+ )}
52
+ <div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
53
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={noneSelected ? 'text-accent-bright' : 'text-text-3'}>
54
+ <circle cx="12" cy="12" r="10" /><line x1="8" y1="12" x2="16" y2="12" />
55
+ </svg>
56
+ </div>
57
+ <span className={`text-[13px] font-600 flex-1 ${noneSelected ? 'text-accent-bright' : 'text-text-2'}`}>
58
+ {noneOption.label}
59
+ </span>
60
+ </button>
61
+ )}
62
+ {agents.map((a) => {
63
+ const active = isSelected(a.id)
64
+ return (
65
+ <button
66
+ key={a.id}
67
+ onClick={() => onSelect(a.id)}
68
+ className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
69
+ ${active ? 'bg-accent-soft' : 'bg-transparent hover:bg-white/[0.03]'}`}
70
+ style={{ fontFamily: 'inherit' }}
71
+ >
72
+ {active && (
73
+ <div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
74
+ )}
75
+ <AgentAvatar seed={a.avatarSeed || null} name={a.name} size={28} />
76
+ <span className={`text-[13px] font-600 flex-1 truncate ${active ? 'text-accent-bright' : 'text-text-2'}`}>
77
+ {a.name}
78
+ </span>
79
+ {showOrchBadge && a.isOrchestrator && (
80
+ <span className="text-[10px] text-text-3/60 flex items-center gap-0.5">
81
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>
82
+ </span>
83
+ )}
84
+ {active && <CheckIcon className="text-accent-bright shrink-0" />}
85
+ </button>
86
+ )
87
+ })}
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,156 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
4
+ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
7
+
8
+ export function AgentSwitchDialog() {
9
+ const [open, setOpen] = useState(false)
10
+ const [query, setQuery] = useState('')
11
+ const [selectedIdx, setSelectedIdx] = useState(0)
12
+ const inputRef = useRef<HTMLInputElement>(null)
13
+ const listRef = useRef<HTMLDivElement>(null)
14
+
15
+ const agents = useAppStore((s) => s.agents)
16
+ const currentAgentId = useAppStore((s) => s.currentAgentId)
17
+ const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
18
+
19
+ // Global Cmd+Shift+A / Ctrl+Shift+A listener
20
+ useEffect(() => {
21
+ const handler = (e: KeyboardEvent) => {
22
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'a') {
23
+ e.preventDefault()
24
+ setOpen((v) => !v)
25
+ }
26
+ }
27
+ window.addEventListener('keydown', handler)
28
+ return () => window.removeEventListener('keydown', handler)
29
+ }, [])
30
+
31
+ // Reset on open
32
+ useEffect(() => {
33
+ if (open) {
34
+ setQuery('')
35
+ setSelectedIdx(0)
36
+ setTimeout(() => inputRef.current?.focus(), 50)
37
+ }
38
+ }, [open])
39
+
40
+ const filtered = useMemo(() => {
41
+ const all = Object.values(agents).filter((a) => !a.trashedAt)
42
+ if (!query.trim()) return all
43
+ const q = query.toLowerCase()
44
+ return all.filter(
45
+ (a) => a.name.toLowerCase().includes(q) || (a.description || '').toLowerCase().includes(q),
46
+ )
47
+ }, [agents, query])
48
+
49
+ const handleSelect = useCallback((agentId: string) => {
50
+ setOpen(false)
51
+ void setCurrentAgent(agentId)
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [])
54
+
55
+ const handleKeyDown = (e: React.KeyboardEvent) => {
56
+ if (e.key === 'ArrowDown') {
57
+ e.preventDefault()
58
+ setSelectedIdx((i) => Math.min(i + 1, filtered.length - 1))
59
+ } else if (e.key === 'ArrowUp') {
60
+ e.preventDefault()
61
+ setSelectedIdx((i) => Math.max(i - 1, 0))
62
+ } else if (e.key === 'Enter' && filtered[selectedIdx]) {
63
+ e.preventDefault()
64
+ handleSelect(filtered[selectedIdx].id)
65
+ }
66
+ }
67
+
68
+ // Scroll selected into view
69
+ useEffect(() => {
70
+ if (!listRef.current) return
71
+ const el = listRef.current.children[selectedIdx] as HTMLElement | undefined
72
+ el?.scrollIntoView({ block: 'nearest' })
73
+ }, [selectedIdx])
74
+
75
+ return (
76
+ <Dialog open={open} onOpenChange={setOpen}>
77
+ <DialogContent
78
+ showCloseButton={false}
79
+ className="sm:max-w-[440px] p-0 bg-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
80
+ onKeyDown={handleKeyDown}
81
+ >
82
+ <DialogTitle className="sr-only">Switch Agent</DialogTitle>
83
+ {/* Search input */}
84
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06]">
85
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
86
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
87
+ <circle cx="12" cy="7" r="4" />
88
+ </svg>
89
+ <input
90
+ ref={inputRef}
91
+ value={query}
92
+ onChange={(e) => { setQuery(e.target.value); setSelectedIdx(0) }}
93
+ placeholder="Switch agent..."
94
+ className="flex-1 bg-transparent border-none outline-none text-[14px] text-text placeholder:text-text-3/60 font-[inherit]"
95
+ autoFocus
96
+ />
97
+ <kbd className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] border border-white/[0.08] text-[10px] font-mono text-text-3 shrink-0">
98
+ ESC
99
+ </kbd>
100
+ </div>
101
+
102
+ {/* Agent list */}
103
+ <div ref={listRef} className="max-h-[360px] overflow-y-auto py-1">
104
+ {filtered.length === 0 && (
105
+ <div className="px-4 py-8 text-center text-[13px] text-text-3/60">
106
+ No agents found
107
+ </div>
108
+ )}
109
+ {filtered.map((agent, idx) => (
110
+ <button
111
+ key={agent.id}
112
+ onClick={() => handleSelect(agent.id)}
113
+ onMouseEnter={() => setSelectedIdx(idx)}
114
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent
115
+ ${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
116
+ style={{ fontFamily: 'inherit' }}
117
+ >
118
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
119
+ <div className="flex-1 min-w-0">
120
+ <div className="flex items-center gap-2">
121
+ <span className="text-[13px] font-500 text-text truncate">{agent.name}</span>
122
+ {agent.id === currentAgentId && (
123
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-accent-bright/15 text-[10px] font-500 text-accent-bright shrink-0">
124
+ current
125
+ </span>
126
+ )}
127
+ </div>
128
+ {agent.description && (
129
+ <p className="text-[11px] text-text-3 truncate mt-0.5 m-0">{agent.description}</p>
130
+ )}
131
+ </div>
132
+ </button>
133
+ ))}
134
+ </div>
135
+
136
+ {/* Footer hint */}
137
+ {filtered.length > 0 && (
138
+ <div className="flex items-center gap-3 px-4 py-2 border-t border-white/[0.06] text-[11px] text-text-3/50">
139
+ <span className="flex items-center gap-1">
140
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">↑↓</kbd>
141
+ navigate
142
+ </span>
143
+ <span className="flex items-center gap-1">
144
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">↵</kbd>
145
+ select
146
+ </span>
147
+ <span className="flex items-center gap-1">
148
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">esc</kbd>
149
+ close
150
+ </span>
151
+ </div>
152
+ )}
153
+ </DialogContent>
154
+ </Dialog>
155
+ )
156
+ }
@@ -0,0 +1,165 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { CodeBlock } from '@/components/chat/code-block'
5
+
6
+ export const IMAGE_ATTACH_RE = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i
7
+ export const PREVIEWABLE_ATTACH_RE = /\.(html?|svg)$/i
8
+ export 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
9
+ export const PDF_ATTACH_RE = /\.pdf$/i
10
+ export const FILE_TYPE_COLORS: Record<string, string> = {
11
+ html: 'text-orange-400', htm: 'text-orange-400', svg: 'text-emerald-400',
12
+ js: 'text-yellow-400', jsx: 'text-yellow-400', ts: 'text-blue-400', tsx: 'text-blue-400',
13
+ py: 'text-green-400', json: 'text-amber-300', css: 'text-purple-400', scss: 'text-pink-400',
14
+ md: 'text-text-2', txt: 'text-text-3', pdf: 'text-red-400',
15
+ }
16
+
17
+ export function parseAttachmentUrl(filePath?: string, fileUrl?: string) {
18
+ const url = fileUrl || (filePath ? `/api/uploads/${filePath.split('/').pop()}` : '')
19
+ const rawName = filePath?.split('/').pop() || fileUrl?.split('/').pop() || 'file'
20
+ const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
21
+ return { url, filename }
22
+ }
23
+
24
+ export function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: string; isUserMsg?: boolean }) {
25
+ const isImage = IMAGE_ATTACH_RE.test(filename)
26
+ const isCode = CODE_ATTACH_RE.test(filename)
27
+ const isPdf = PDF_ATTACH_RE.test(filename)
28
+ const [lightbox, setLightbox] = useState(false)
29
+ const [codePreview, setCodePreview] = useState<string | null>(null)
30
+ const [codeExpanded, setCodeExpanded] = useState(false)
31
+
32
+ if (isImage) {
33
+ return (
34
+ <>
35
+ <img
36
+ src={url} alt="Attached"
37
+ loading="lazy"
38
+ className="max-w-[240px] rounded-[12px] mb-2 border border-white/10 cursor-pointer hover:border-white/25 transition-colors"
39
+ onClick={() => setLightbox(true)}
40
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
41
+ />
42
+ {lightbox && (
43
+ <div
44
+ className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer"
45
+ onClick={() => setLightbox(false)}
46
+ >
47
+ <img src={url} alt="Preview" className="max-w-[90vw] max-h-[90vh] rounded-[12px] shadow-2xl" />
48
+ </div>
49
+ )}
50
+ </>
51
+ )
52
+ }
53
+
54
+ if (isPdf) {
55
+ return (
56
+ <div className="mb-2 rounded-[12px] border border-white/[0.08] bg-[rgba(255,255,255,0.02)] overflow-hidden" style={{ maxWidth: 480 }}>
57
+ <div className="flex items-center gap-3 px-4 py-2.5">
58
+ <div className="flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 bg-red-500/10 text-red-400">
59
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
60
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
61
+ <polyline points="14 2 14 8 20 8" />
62
+ </svg>
63
+ </div>
64
+ <span className="text-[13px] font-500 truncate flex-1">{filename}</span>
65
+ <a href={url} download={filename} className="text-[11px] font-600 text-text-3 hover:text-text-2 no-underline">Download</a>
66
+ </div>
67
+ <iframe src={url} loading="lazy" className="w-full h-[300px] border-t border-white/[0.06]" title={filename} />
68
+ </div>
69
+ )
70
+ }
71
+
72
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
73
+ const colorClass = FILE_TYPE_COLORS[ext] || 'text-text-3'
74
+ const isPreviewable = PREVIEWABLE_ATTACH_RE.test(filename)
75
+
76
+ const chipBg = isUserMsg
77
+ ? 'bg-[rgba(0,0,0,0.25)] border-white/[0.12]'
78
+ : 'bg-[rgba(255,255,255,0.04)] border-white/[0.08]'
79
+ const iconBg = isUserMsg ? 'bg-white/[0.12]' : 'bg-white/[0.05]'
80
+ const btnBg = isUserMsg
81
+ ? 'bg-white/[0.12] hover:bg-white/[0.18] text-white/80'
82
+ : 'bg-white/[0.06] hover:bg-white/[0.10] text-text-3'
83
+
84
+ const handleCodePreview = async () => {
85
+ if (codePreview !== null) { setCodeExpanded(!codeExpanded); return }
86
+ try {
87
+ const serveUrl = `/api/files/serve?path=${encodeURIComponent(url.replace('/api/uploads/', ''))}`
88
+ const res = await fetch(url.startsWith('/api/files/') ? url : serveUrl)
89
+ if (!res.ok) return
90
+ const text = await res.text()
91
+ setCodePreview(text)
92
+ setCodeExpanded(true)
93
+ } catch {
94
+ // ignore
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className="mb-2">
100
+ <div className={`flex items-center gap-3 px-4 py-2.5 rounded-[12px] border ${chipBg}`}>
101
+ <div className={`flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 ${iconBg} ${colorClass}`}>
102
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
103
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
104
+ <polyline points="14 2 14 8 20 8" />
105
+ </svg>
106
+ </div>
107
+ <div className="flex flex-col flex-1 min-w-0">
108
+ <span className={`text-[13px] font-500 truncate ${isUserMsg ? 'text-white' : 'text-text'}`}>{filename}</span>
109
+ <span className={`text-[11px] uppercase tracking-wide ${isUserMsg ? 'text-white/50' : 'text-text-3/70'}`}>{ext || 'file'}</span>
110
+ </div>
111
+ {isCode && (
112
+ <button
113
+ onClick={handleCodePreview}
114
+ 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 ${
115
+ isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
116
+ }`}
117
+ >
118
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
119
+ <polyline points="16 18 22 12 16 6" />
120
+ <polyline points="8 6 2 12 8 18" />
121
+ </svg>
122
+ {codeExpanded ? 'Hide' : 'Preview'}
123
+ </button>
124
+ )}
125
+ {isPreviewable && (
126
+ <a href={url} target="_blank" rel="noopener noreferrer"
127
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${
128
+ isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
129
+ }`}
130
+ title="Preview in new tab">
131
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
132
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
133
+ <circle cx="12" cy="12" r="3" />
134
+ </svg>
135
+ Preview
136
+ </a>
137
+ )}
138
+ <a href={url} download={filename}
139
+ 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}`}>
140
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
141
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
142
+ <polyline points="7 10 12 15 17 10" />
143
+ <line x1="12" y1="15" x2="12" y2="3" />
144
+ </svg>
145
+ Download
146
+ </a>
147
+ </div>
148
+ {isCode && codeExpanded && codePreview !== null && (
149
+ <div className="mt-1 rounded-[10px] border border-white/[0.06] overflow-hidden" style={{ animation: 'fade-in 0.2s ease' }}>
150
+ <CodeBlock className={`language-${ext}`}>
151
+ {codePreview.split('\n').slice(0, codeExpanded ? undefined : 10).join('\n')}
152
+ </CodeBlock>
153
+ {codePreview.split('\n').length > 10 && (
154
+ <button
155
+ onClick={() => setCodeExpanded((v) => !v)}
156
+ 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"
157
+ >
158
+ {codePreview.split('\n').length > 10 ? `Show all ${codePreview.split('\n').length} lines` : 'Show less'}
159
+ </button>
160
+ )}
161
+ </div>
162
+ )}
163
+ </div>
164
+ )
165
+ }
@@ -1,8 +1,11 @@
1
1
  'use client'
2
2
 
3
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
4
+
3
5
  interface Props {
4
6
  user: string
5
7
  size?: 'xs' | 'sm' | 'md' | 'lg'
8
+ avatarSeed?: string
6
9
  }
7
10
 
8
11
  const sizes = {
@@ -22,7 +25,13 @@ function userGradient(name: string): string {
22
25
  return `linear-gradient(135deg, hsl(${hue}, 70%, 35%), hsl(${(hue + 30) % 360}, 75%, 50%))`
23
26
  }
24
27
 
25
- export function Avatar({ user, size = 'md' }: Props) {
28
+ const pixelSizes: Record<string, number> = { xs: 24, sm: 28, md: 36, lg: 72 }
29
+
30
+ export function Avatar({ user, size = 'md', avatarSeed }: Props) {
31
+ if (avatarSeed) {
32
+ return <AgentAvatar seed={avatarSeed} name={user} size={pixelSizes[size] || 36} />
33
+ }
34
+
26
35
  const initial = (user || '?')[0].toUpperCase()
27
36
  return (
28
37
  <div
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ size?: number
3
+ className?: string
4
+ }
5
+
6
+ export function CheckIcon({ size = 14, className }: Props) {
7
+ return (
8
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
9
+ <polyline points="20 6 9 17 4 12" />
10
+ </svg>
11
+ )
12
+ }
@@ -35,7 +35,7 @@ export function ConfirmDialog({ open, title, message, confirmLabel = 'Confirm',
35
35
  className={`flex-1 py-2.5 rounded-[12px] border-none text-[13px] font-600 cursor-pointer active:scale-[0.97] transition-all duration-200
36
36
  ${danger
37
37
  ? 'bg-danger text-white shadow-[0_4px_20px_rgba(244,63,94,0.2)]'
38
- : 'bg-[#6366F1] text-white shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}`}
38
+ : 'bg-accent-bright text-white shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}`}
39
39
  style={{ fontFamily: 'inherit' }}
40
40
  >
41
41
  {confirmLabel}
@@ -0,0 +1,32 @@
1
+ interface Props {
2
+ icon: React.ReactNode
3
+ title: string
4
+ subtitle?: string
5
+ action?: {
6
+ label: string
7
+ onClick: () => void
8
+ }
9
+ }
10
+
11
+ export function EmptyState({ icon, title, subtitle, action }: Props) {
12
+ return (
13
+ <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
14
+ <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
15
+ {icon}
16
+ </div>
17
+ <p className="font-display text-[15px] font-600 text-text-2">{title}</p>
18
+ {subtitle && <p className="text-[13px] text-text-3/50">{subtitle}</p>}
19
+ {action && (
20
+ <button
21
+ onClick={action.onClick}
22
+ className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
23
+ text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
24
+ shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
25
+ style={{ fontFamily: 'inherit' }}
26
+ >
27
+ {action.label}
28
+ </button>
29
+ )}
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import type { PendingFile } from '@/stores/use-chat-store'
4
+
5
+ export function FilePreview({ file, onRemove }: { file: PendingFile; onRemove: () => void }) {
6
+ const isImage = file.file.type.startsWith('image/')
7
+ return (
8
+ <div className="relative">
9
+ {isImage ? (
10
+ <img
11
+ src={URL.createObjectURL(file.file)}
12
+ alt="Preview"
13
+ className="h-16 rounded-[10px] object-cover border border-white/[0.06]"
14
+ />
15
+ ) : (
16
+ <div className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] border border-white/[0.06] bg-white/[0.03]">
17
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
18
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
19
+ <polyline points="14 2 14 8 20 8" />
20
+ </svg>
21
+ <span className="text-[13px] text-text-2 font-500 truncate max-w-[180px]">{file.file.name}</span>
22
+ </div>
23
+ )}
24
+ <button
25
+ onClick={onRemove}
26
+ className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full border border-white/10 bg-raised
27
+ text-text-2 text-[10px] cursor-pointer flex items-center justify-center
28
+ hover:bg-danger-soft hover:text-danger hover:border-danger/20 transition-colors"
29
+ >
30
+ &times;
31
+ </button>
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,2 @@
1
+ /** Standard input class for sheet/form inputs */
2
+ export const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
5
+
6
+ interface Shortcut {
7
+ keys: string[]
8
+ description: string
9
+ }
10
+
11
+ interface ShortcutGroup {
12
+ title: string
13
+ shortcuts: Shortcut[]
14
+ }
15
+
16
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent)
17
+ const MOD = isMac ? '\u2318' : 'Ctrl'
18
+
19
+ const GROUPS: ShortcutGroup[] = [
20
+ {
21
+ title: 'Navigation',
22
+ shortcuts: [
23
+ { keys: [MOD, 'K'], description: 'Open search' },
24
+ { keys: [MOD, 'Shift', 'A'], description: 'Switch agent' },
25
+ { keys: [MOD, 'N'], description: 'New chat' },
26
+ { keys: [MOD, 'Shift', 'T'], description: 'Jump to tasks' },
27
+ ],
28
+ },
29
+ {
30
+ title: 'Chat',
31
+ shortcuts: [
32
+ { keys: ['Enter'], description: 'Send message' },
33
+ { keys: ['Shift', 'Enter'], description: 'New line' },
34
+ { keys: ['Esc'], description: 'Cancel reply / close' },
35
+ ],
36
+ },
37
+ {
38
+ title: 'General',
39
+ shortcuts: [
40
+ { keys: ['?'], description: 'Show keyboard shortcuts' },
41
+ ],
42
+ },
43
+ ]
44
+
45
+ function Kbd({ children }: { children: string }) {
46
+ return (
47
+ <kbd className="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-[5px] bg-white/[0.08] border border-white/[0.1] text-[11px] font-mono text-text-2 leading-none">
48
+ {children}
49
+ </kbd>
50
+ )
51
+ }
52
+
53
+ export function KeyboardShortcutsDialog() {
54
+ const [open, setOpen] = useState(false)
55
+
56
+ useEffect(() => {
57
+ const handler = (e: KeyboardEvent) => {
58
+ // Ctrl+/ or Cmd+/
59
+ if ((e.metaKey || e.ctrlKey) && e.key === '/') {
60
+ e.preventDefault()
61
+ setOpen((v) => !v)
62
+ return
63
+ }
64
+ const tag = (e.target as HTMLElement)?.tagName
65
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
66
+ if ((e.target as HTMLElement)?.isContentEditable) return
67
+ if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
68
+ e.preventDefault()
69
+ setOpen((v) => !v)
70
+ }
71
+ }
72
+ window.addEventListener('keydown', handler)
73
+ return () => window.removeEventListener('keydown', handler)
74
+ }, [])
75
+
76
+ return (
77
+ <Dialog open={open} onOpenChange={setOpen}>
78
+ <DialogContent
79
+ showCloseButton={false}
80
+ className="sm:max-w-[420px] p-0 bg-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
81
+ >
82
+ <DialogTitle className="sr-only">Keyboard shortcuts</DialogTitle>
83
+ <div className="flex items-center justify-between px-5 py-3.5 border-b border-white/[0.06]">
84
+ <span className="text-[14px] font-600 text-text">Keyboard Shortcuts</span>
85
+ <kbd className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] border border-white/[0.08] text-[10px] font-mono text-text-3">
86
+ ESC
87
+ </kbd>
88
+ </div>
89
+ <div className="py-2 max-h-[400px] overflow-y-auto">
90
+ {GROUPS.map((group) => (
91
+ <div key={group.title} className="px-5 py-2">
92
+ <h3 className="text-[11px] font-700 uppercase tracking-wider text-text-3/60 mb-2">
93
+ {group.title}
94
+ </h3>
95
+ <div className="flex flex-col gap-1.5">
96
+ {group.shortcuts.map((shortcut) => (
97
+ <div
98
+ key={shortcut.description}
99
+ className="flex items-center justify-between py-1"
100
+ >
101
+ <span className="text-[13px] text-text-2">{shortcut.description}</span>
102
+ <div className="flex items-center gap-1">
103
+ {shortcut.keys.map((key, i) => (
104
+ <Kbd key={i}>{key}</Kbd>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ </DialogContent>
114
+ </Dialog>
115
+ )
116
+ }