@swarmclawai/swarmclaw 0.6.6 → 0.6.7
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 +57 -27
- package/package.json +6 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +17 -1
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +19 -1
- package/src/app/api/chatrooms/route.ts +12 -2
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +20 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/cli/index.js +5 -0
- package/src/cli/index.ts +223 -39
- package/src/components/agents/agent-card.tsx +37 -6
- package/src/components/agents/agent-chat-list.tsx +78 -2
- package/src/components/agents/agent-sheet.tsx +79 -0
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +22 -7
- package/src/components/chat/message-bubble.tsx +14 -14
- package/src/components/chat/message-list.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +164 -22
- package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +23 -2
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/layout/app-layout.tsx +17 -1
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +91 -16
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +41 -1
- package/src/lib/server/chatroom-helpers.ts +22 -1
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/manager.ts +159 -3
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +9 -0
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/queue.ts +12 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +22 -3
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/storage.ts +120 -6
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/stores/use-app-store.ts +7 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +38 -1
- package/tsconfig.json +2 -1
|
@@ -4,6 +4,7 @@ import { useState, useCallback } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { updateTask, archiveTask } from '@/lib/tasks'
|
|
7
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
8
|
import type { BoardTask } from '@/types'
|
|
8
9
|
|
|
9
10
|
function timeAgo(ts: number) {
|
|
@@ -30,7 +31,9 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
30
31
|
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
31
32
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
32
33
|
const [dragging, setDragging] = useState(false)
|
|
34
|
+
const [confirmArchive, setConfirmArchive] = useState(false)
|
|
33
35
|
|
|
36
|
+
const tasks = useAppStore((s) => s.tasks)
|
|
34
37
|
const agent = agents[task.agentId]
|
|
35
38
|
const project = task.projectId ? projects[task.projectId] : null
|
|
36
39
|
|
|
@@ -117,7 +120,7 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
117
120
|
)}
|
|
118
121
|
{isBlocked && (
|
|
119
122
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-rose-400 shrink-0 mt-0.5">
|
|
120
|
-
<title>{`Blocked by ${task.blockedBy?.
|
|
123
|
+
<title>{`Blocked by: ${(task.blockedBy || []).map((bid) => tasks[bid]?.title || bid).join(', ')}`}</title>
|
|
121
124
|
<rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
122
125
|
</svg>
|
|
123
126
|
)}
|
|
@@ -211,6 +214,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
211
214
|
{task.comments.length}
|
|
212
215
|
</span>
|
|
213
216
|
)}
|
|
217
|
+
{Array.isArray(task.blocks) && task.blocks.length > 0 && (
|
|
218
|
+
<span
|
|
219
|
+
className="px-1.5 py-0.5 rounded-[5px] bg-amber-500/10 text-amber-400 text-[10px] font-600"
|
|
220
|
+
title={`Blocks: ${task.blocks.map((bid) => tasks[bid]?.title || bid).join(', ')}`}
|
|
221
|
+
>
|
|
222
|
+
blocks {task.blocks.length}
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
214
225
|
|
|
215
226
|
{task.status === 'backlog' && (
|
|
216
227
|
<button
|
|
@@ -236,7 +247,8 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
236
247
|
|
|
237
248
|
{(task.status === 'completed' || task.status === 'failed') && !task.sessionId && (
|
|
238
249
|
<button
|
|
239
|
-
onClick={
|
|
250
|
+
onClick={(e) => { e.stopPropagation(); setConfirmArchive(true) }}
|
|
251
|
+
aria-label="Archive task"
|
|
240
252
|
className="ml-auto px-2.5 py-1 rounded-[8px] text-[11px] font-600 bg-white/[0.04] text-text-3 border-none cursor-pointer
|
|
241
253
|
opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white/[0.08]"
|
|
242
254
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -304,6 +316,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
304
316
|
))}
|
|
305
317
|
</div>
|
|
306
318
|
)}
|
|
319
|
+
<ConfirmDialog
|
|
320
|
+
open={confirmArchive}
|
|
321
|
+
title="Archive Task"
|
|
322
|
+
message={`Archive "${task.title}"? You can view archived tasks later.`}
|
|
323
|
+
confirmLabel="Archive"
|
|
324
|
+
onConfirm={() => { setConfirmArchive(false); handleArchive({ stopPropagation: () => {} } as React.MouseEvent) }}
|
|
325
|
+
onCancel={() => setConfirmArchive(false)}
|
|
326
|
+
/>
|
|
307
327
|
</div>
|
|
308
328
|
)
|
|
309
329
|
}
|
|
@@ -54,6 +54,8 @@ export function TaskSheet() {
|
|
|
54
54
|
const [tags, setTags] = useState<string[]>([])
|
|
55
55
|
const [tagInput, setTagInput] = useState('')
|
|
56
56
|
const [blockedBy, setBlockedBy] = useState<string[]>([])
|
|
57
|
+
const [depSearch, setDepSearch] = useState('')
|
|
58
|
+
const [depError, setDepError] = useState<string | null>(null)
|
|
57
59
|
const [dueAt, setDueAt] = useState<string>('')
|
|
58
60
|
const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
|
|
59
61
|
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
|
|
@@ -76,6 +78,8 @@ export function TaskSheet() {
|
|
|
76
78
|
setFile(editing.file || null)
|
|
77
79
|
setTags(editing.tags || [])
|
|
78
80
|
setBlockedBy(editing.blockedBy || [])
|
|
81
|
+
setDepSearch('')
|
|
82
|
+
setDepError(null)
|
|
79
83
|
setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
|
|
80
84
|
setCustomFields(editing.customFields || {})
|
|
81
85
|
setPriority(editing.priority || '')
|
|
@@ -89,6 +93,8 @@ export function TaskSheet() {
|
|
|
89
93
|
setFile(null)
|
|
90
94
|
setTags([])
|
|
91
95
|
setBlockedBy([])
|
|
96
|
+
setDepSearch('')
|
|
97
|
+
setDepError(null)
|
|
92
98
|
setDueAt('')
|
|
93
99
|
setCustomFields({})
|
|
94
100
|
setPriority('')
|
|
@@ -119,11 +125,25 @@ export function TaskSheet() {
|
|
|
119
125
|
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
|
|
120
126
|
priority: priority || undefined,
|
|
121
127
|
} as Partial<BoardTask> & { title: string; description: string; agentId: string }
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
try {
|
|
129
|
+
if (editing) {
|
|
130
|
+
const res = await updateTask(editing.id, payload)
|
|
131
|
+
if (res && typeof res === 'object' && 'error' in res) {
|
|
132
|
+
setDepError(String((res as unknown as Record<string, unknown>).error))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
const res = await createTask(payload)
|
|
137
|
+
if (res && typeof res === 'object' && 'error' in res) {
|
|
138
|
+
setDepError(String((res as unknown as Record<string, unknown>).error))
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err: unknown) {
|
|
143
|
+
setDepError(err instanceof Error ? err.message : String(err))
|
|
144
|
+
return
|
|
126
145
|
}
|
|
146
|
+
setDepError(null)
|
|
127
147
|
await loadTasks()
|
|
128
148
|
onClose()
|
|
129
149
|
}
|
|
@@ -683,18 +703,73 @@ export function TaskSheet() {
|
|
|
683
703
|
{/* Dependencies */}
|
|
684
704
|
<div className="mb-8">
|
|
685
705
|
<SectionLabel>Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span></SectionLabel>
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
706
|
+
{/* Selected blockers as removable chips */}
|
|
707
|
+
{blockedBy.length > 0 && (
|
|
708
|
+
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
709
|
+
{blockedBy.map((bid) => {
|
|
710
|
+
const bt = tasks[bid]
|
|
711
|
+
return (
|
|
712
|
+
<span key={bid} className="inline-flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-rose-500/10 text-rose-400 text-[12px] font-600">
|
|
713
|
+
{bt ? bt.title : bid}
|
|
714
|
+
<button
|
|
715
|
+
onClick={() => setBlockedBy((prev) => prev.filter((b) => b !== bid))}
|
|
716
|
+
className="text-rose-400/60 hover:text-rose-400 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none"
|
|
717
|
+
>
|
|
718
|
+
×
|
|
719
|
+
</button>
|
|
720
|
+
</span>
|
|
721
|
+
)
|
|
722
|
+
})}
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
{/* Searchable dropdown for adding dependencies */}
|
|
726
|
+
<div className="relative">
|
|
727
|
+
<input
|
|
728
|
+
type="text"
|
|
729
|
+
value={depSearch}
|
|
730
|
+
onChange={(e) => setDepSearch(e.target.value)}
|
|
731
|
+
placeholder="Search tasks to add as dependency..."
|
|
732
|
+
className={inputClass}
|
|
733
|
+
style={{ fontFamily: 'inherit' }}
|
|
734
|
+
/>
|
|
735
|
+
{depSearch.trim() && (
|
|
736
|
+
<div className="absolute z-20 top-full left-0 right-0 mt-1 max-h-[200px] overflow-y-auto rounded-[12px] border border-white/[0.08] bg-surface shadow-xl">
|
|
737
|
+
{Object.values(tasks)
|
|
738
|
+
.filter((t) =>
|
|
739
|
+
t.id !== editingId &&
|
|
740
|
+
t.status !== 'archived' &&
|
|
741
|
+
!blockedBy.includes(t.id) &&
|
|
742
|
+
t.title.toLowerCase().includes(depSearch.toLowerCase())
|
|
743
|
+
)
|
|
744
|
+
.slice(0, 10)
|
|
745
|
+
.map((t) => (
|
|
746
|
+
<button
|
|
747
|
+
key={t.id}
|
|
748
|
+
onClick={() => {
|
|
749
|
+
setBlockedBy((prev) => [...prev, t.id])
|
|
750
|
+
setDepSearch('')
|
|
751
|
+
}}
|
|
752
|
+
className="w-full text-left px-4 py-2.5 text-[13px] text-text-2 hover:bg-surface-2 cursor-pointer border-none bg-transparent transition-colors flex items-center gap-2"
|
|
753
|
+
style={{ fontFamily: 'inherit' }}
|
|
754
|
+
>
|
|
755
|
+
<span className="flex-1 truncate">{t.title}</span>
|
|
756
|
+
<span className="text-[10px] text-text-3 shrink-0">({t.status})</span>
|
|
757
|
+
</button>
|
|
758
|
+
))}
|
|
759
|
+
{Object.values(tasks).filter((t) =>
|
|
760
|
+
t.id !== editingId &&
|
|
761
|
+
t.status !== 'archived' &&
|
|
762
|
+
!blockedBy.includes(t.id) &&
|
|
763
|
+
t.title.toLowerCase().includes(depSearch.toLowerCase())
|
|
764
|
+
).length === 0 && (
|
|
765
|
+
<div className="px-4 py-3 text-[13px] text-text-3">No matching tasks</div>
|
|
766
|
+
)}
|
|
767
|
+
</div>
|
|
768
|
+
)}
|
|
769
|
+
</div>
|
|
770
|
+
{depError && (
|
|
771
|
+
<p className="mt-2 text-[12px] text-red-400 font-600">{depError}</p>
|
|
772
|
+
)}
|
|
698
773
|
{editing && Array.isArray(editing.blocks) && editing.blocks.length > 0 && (
|
|
699
774
|
<div className="mt-3">
|
|
700
775
|
<span className="text-[11px] font-600 text-text-3 uppercase tracking-[0.06em]">Blocks:</span>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import {
|
|
5
|
-
LineChart, Line, BarChart, Bar,
|
|
5
|
+
LineChart, Line, BarChart, Bar, Cell,
|
|
6
6
|
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
|
|
7
7
|
} from 'recharts'
|
|
8
8
|
import { useAppStore } from '@/stores/use-app-store'
|
|
@@ -32,7 +32,7 @@ interface UsageResponse {
|
|
|
32
32
|
records: unknown[]
|
|
33
33
|
totalTokens: number
|
|
34
34
|
totalCost: number
|
|
35
|
-
byAgent: Record<string, { tokens: number;
|
|
35
|
+
byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }>
|
|
36
36
|
byProvider: Record<string, { tokens: number; cost: number }>
|
|
37
37
|
timeSeries: TimePoint[]
|
|
38
38
|
providerHealth?: Record<string, ProviderHealthEntry>
|
|
@@ -171,8 +171,8 @@ export function MetricsDashboard() {
|
|
|
171
171
|
const agentData = Object.entries(data?.byAgent ?? {})
|
|
172
172
|
.sort((a, b) => b[1].cost - a[1].cost)
|
|
173
173
|
.slice(0, 10)
|
|
174
|
-
.map(([
|
|
175
|
-
name: name.length >
|
|
174
|
+
.map(([_id, v]) => ({
|
|
175
|
+
name: v.name.length > 16 ? v.name.slice(0, 16) + '…' : v.name,
|
|
176
176
|
cost: Math.round(v.cost * 10000) / 10000,
|
|
177
177
|
}))
|
|
178
178
|
|
|
@@ -270,32 +270,20 @@ export function MetricsDashboard() {
|
|
|
270
270
|
)}
|
|
271
271
|
</ChartCard>
|
|
272
272
|
|
|
273
|
-
<ChartCard title="
|
|
273
|
+
<ChartCard title="Agent Breakdown">
|
|
274
274
|
{agentData.length > 0 ? (
|
|
275
275
|
<ResponsiveContainer width="100%" height={280}>
|
|
276
|
-
<
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
outerRadius={100}
|
|
283
|
-
paddingAngle={2}
|
|
284
|
-
dataKey="cost"
|
|
285
|
-
nameKey="name"
|
|
286
|
-
>
|
|
276
|
+
<BarChart data={agentData} layout="vertical" margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
|
|
277
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
|
|
278
|
+
<XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `$${v}`} />
|
|
279
|
+
<YAxis type="category" dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={100} />
|
|
280
|
+
<Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatCost(value ?? 0), 'Cost']} />
|
|
281
|
+
<Bar dataKey="cost" radius={[0, 4, 4, 0]}>
|
|
287
282
|
{agentData.map((_entry, i) => (
|
|
288
283
|
<Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
|
|
289
284
|
))}
|
|
290
|
-
</
|
|
291
|
-
|
|
292
|
-
<Legend
|
|
293
|
-
verticalAlign="bottom"
|
|
294
|
-
iconType="circle"
|
|
295
|
-
iconSize={8}
|
|
296
|
-
formatter={(value: string) => <span style={{ color: '#a0a0b0', fontSize: 11 }}>{value}</span>}
|
|
297
|
-
/>
|
|
298
|
-
</PieChart>
|
|
285
|
+
</Bar>
|
|
286
|
+
</BarChart>
|
|
299
287
|
</ResponsiveContainer>
|
|
300
288
|
) : (
|
|
301
289
|
<EmptyChart />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseSwipeOptions {
|
|
4
|
+
onSwipe: (direction: 'left' | 'right') => void
|
|
5
|
+
/** Only trigger right-swipe from this many pixels from the left edge */
|
|
6
|
+
edgeWidth?: number
|
|
7
|
+
/** Minimum horizontal distance to count as a swipe */
|
|
8
|
+
threshold?: number
|
|
9
|
+
/** Whether left-swipe is currently allowed (e.g. sidebar is open) */
|
|
10
|
+
leftSwipeEnabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSwipe({
|
|
14
|
+
onSwipe,
|
|
15
|
+
edgeWidth = 40,
|
|
16
|
+
threshold = 50,
|
|
17
|
+
leftSwipeEnabled = false,
|
|
18
|
+
}: UseSwipeOptions) {
|
|
19
|
+
const startX = useRef(0)
|
|
20
|
+
const startY = useRef(0)
|
|
21
|
+
const isEdge = useRef(false)
|
|
22
|
+
|
|
23
|
+
const onTouchStart = useCallback((e: React.TouchEvent) => {
|
|
24
|
+
const touch = e.touches[0]
|
|
25
|
+
startX.current = touch.clientX
|
|
26
|
+
startY.current = touch.clientY
|
|
27
|
+
isEdge.current = touch.clientX <= edgeWidth
|
|
28
|
+
}, [edgeWidth])
|
|
29
|
+
|
|
30
|
+
// No-op — we only evaluate on touchend, but callers may wire this for consistency
|
|
31
|
+
const onTouchMove = useCallback(() => {}, [])
|
|
32
|
+
|
|
33
|
+
const onTouchEnd = useCallback((e: React.TouchEvent) => {
|
|
34
|
+
const touch = e.changedTouches[0]
|
|
35
|
+
const dx = touch.clientX - startX.current
|
|
36
|
+
const dy = touch.clientY - startY.current
|
|
37
|
+
// Ignore if vertical movement dominates
|
|
38
|
+
if (Math.abs(dy) > Math.abs(dx)) return
|
|
39
|
+
if (Math.abs(dx) < threshold) return
|
|
40
|
+
|
|
41
|
+
if (dx > 0 && isEdge.current) {
|
|
42
|
+
onSwipe('right')
|
|
43
|
+
} else if (dx < 0 && leftSwipeEnabled) {
|
|
44
|
+
onSwipe('left')
|
|
45
|
+
}
|
|
46
|
+
}, [threshold, leftSwipeEnabled, onSwipe])
|
|
47
|
+
|
|
48
|
+
return { onTouchStart, onTouchMove, onTouchEnd }
|
|
49
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import https from 'https'
|
|
3
3
|
import type { StreamChatOptions } from './index'
|
|
4
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
4
5
|
|
|
5
6
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
6
7
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
@@ -23,7 +24,7 @@ function fileToContentBlocks(filePath: string): any[] {
|
|
|
23
24
|
return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
27
|
+
export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
27
28
|
return new Promise((resolve) => {
|
|
28
29
|
const messages = buildMessages(session, message, imagePath, loadHistory, imageUrl)
|
|
29
30
|
const model = session.model || 'claude-sonnet-4-6'
|
|
@@ -43,9 +44,21 @@ export function streamAnthropicChat({ session, message, imagePath, imageUrl, api
|
|
|
43
44
|
const payload = JSON.stringify(body)
|
|
44
45
|
const abortController = { aborted: false }
|
|
45
46
|
let fullResponse = ''
|
|
47
|
+
let apiReqRef: ReturnType<typeof https.request> | null = null
|
|
48
|
+
|
|
49
|
+
if (signal) {
|
|
50
|
+
if (signal.aborted) {
|
|
51
|
+
abortController.aborted = true
|
|
52
|
+
} else {
|
|
53
|
+
signal.addEventListener('abort', () => {
|
|
54
|
+
abortController.aborted = true
|
|
55
|
+
apiReqRef?.destroy()
|
|
56
|
+
}, { once: true })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
46
59
|
|
|
47
60
|
const apiReq = https.request({
|
|
48
|
-
hostname:
|
|
61
|
+
hostname: PROVIDER_DEFAULTS.anthropic,
|
|
49
62
|
path: '/v1/messages',
|
|
50
63
|
method: 'POST',
|
|
51
64
|
headers: {
|
|
@@ -109,6 +122,7 @@ export function streamAnthropicChat({ session, message, imagePath, imageUrl, api
|
|
|
109
122
|
})
|
|
110
123
|
})
|
|
111
124
|
|
|
125
|
+
apiReqRef = apiReq
|
|
112
126
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
113
127
|
|
|
114
128
|
apiReq.on('error', (e) => {
|
|
@@ -24,7 +24,7 @@ function findClaude(): string {
|
|
|
24
24
|
|
|
25
25
|
const CLAUDE = findClaude()
|
|
26
26
|
|
|
27
|
-
export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active }: StreamChatOptions): Promise<string> {
|
|
27
|
+
export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
28
28
|
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
29
29
|
let prompt = message
|
|
30
30
|
if (imagePath) {
|
|
@@ -108,6 +108,12 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
|
|
|
108
108
|
proc.stdin!.end()
|
|
109
109
|
|
|
110
110
|
active.set(session.id, proc)
|
|
111
|
+
|
|
112
|
+
if (signal) {
|
|
113
|
+
if (signal.aborted) { proc.kill(); }
|
|
114
|
+
else signal.addEventListener('abort', () => { proc.kill() }, { once: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
let fullResponse = ''
|
|
112
118
|
let buf = ''
|
|
113
119
|
let eventCount = 0
|
|
@@ -29,6 +29,8 @@ export interface StreamChatOptions {
|
|
|
29
29
|
active: Map<string, any>
|
|
30
30
|
loadHistory: (sessionId: string) => any[]
|
|
31
31
|
onUsage?: (usage: StreamChatUsage) => void
|
|
32
|
+
/** Abort signal from the caller — providers should use this to cancel in-flight requests. */
|
|
33
|
+
signal?: AbortSignal
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface BuiltinProviderConfig extends ProviderInfo {
|
|
@@ -351,6 +353,11 @@ export async function streamChatWithFailover(
|
|
|
351
353
|
t: 'md',
|
|
352
354
|
text: JSON.stringify({ failover: { from: credId, reason: err.message?.slice(0, 100) } }),
|
|
353
355
|
})}\n\n`)
|
|
356
|
+
// Exponential backoff for rate-limit / server errors (skip for auth rotation)
|
|
357
|
+
if (statusCode !== 401) {
|
|
358
|
+
const delay = Math.min(500 * Math.pow(2, i), 8000)
|
|
359
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
360
|
+
}
|
|
354
361
|
continue
|
|
355
362
|
}
|
|
356
363
|
throw err
|
|
@@ -2,16 +2,17 @@ import fs from 'fs'
|
|
|
2
2
|
import http from 'http'
|
|
3
3
|
import https from 'https'
|
|
4
4
|
import type { StreamChatOptions } from './index'
|
|
5
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
5
6
|
|
|
6
7
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
7
8
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
8
9
|
|
|
9
|
-
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
10
|
+
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
10
11
|
return new Promise((resolve) => {
|
|
11
12
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
12
13
|
const model = session.model || 'llama3'
|
|
13
14
|
// Cloud: no endpoint but API key present → use Ollama cloud
|
|
14
|
-
const endpoint = session.apiEndpoint || (apiKey ?
|
|
15
|
+
const endpoint = session.apiEndpoint || (apiKey ? PROVIDER_DEFAULTS.ollamaCloud : PROVIDER_DEFAULTS.ollama)
|
|
15
16
|
|
|
16
17
|
const parsed = new URL(endpoint)
|
|
17
18
|
const isHttps = parsed.protocol === 'https:'
|
|
@@ -26,6 +27,18 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
26
27
|
|
|
27
28
|
const abortController = { aborted: false }
|
|
28
29
|
let fullResponse = ''
|
|
30
|
+
let apiReqRef: ReturnType<typeof http.request> | null = null
|
|
31
|
+
|
|
32
|
+
if (signal) {
|
|
33
|
+
if (signal.aborted) {
|
|
34
|
+
abortController.aborted = true
|
|
35
|
+
} else {
|
|
36
|
+
signal.addEventListener('abort', () => {
|
|
37
|
+
abortController.aborted = true
|
|
38
|
+
apiReqRef?.destroy()
|
|
39
|
+
}, { once: true })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
const headers: Record<string, string> = {
|
|
31
44
|
'Content-Type': 'application/json',
|
|
@@ -87,6 +100,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
87
100
|
})
|
|
88
101
|
})
|
|
89
102
|
|
|
103
|
+
apiReqRef = apiReq
|
|
90
104
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
91
105
|
|
|
92
106
|
apiReq.on('error', (e: NodeJS.ErrnoException) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
3
4
|
|
|
4
5
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
5
6
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
@@ -43,7 +44,7 @@ async function fileToContentParts(filePath: string): Promise<any[]> {
|
|
|
43
44
|
return [{ type: 'text', text: `[Attached file: ${name}]` }]
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
47
|
+
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
47
48
|
return new Promise(async (resolve) => {
|
|
48
49
|
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
|
|
49
50
|
const model = session.model || 'gpt-4o'
|
|
@@ -58,7 +59,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
58
59
|
let fullResponse = ''
|
|
59
60
|
|
|
60
61
|
// Support custom base URLs for custom providers
|
|
61
|
-
const baseUrl = session.apiEndpoint ||
|
|
62
|
+
const baseUrl = session.apiEndpoint || PROVIDER_DEFAULTS.openai
|
|
62
63
|
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
63
64
|
|
|
64
65
|
// OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
|
|
@@ -67,6 +68,10 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
67
68
|
const contentType = session.contentType || 'application/json'
|
|
68
69
|
|
|
69
70
|
const abortController = new AbortController()
|
|
71
|
+
if (signal) {
|
|
72
|
+
if (signal.aborted) abortController.abort()
|
|
73
|
+
else signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
|
74
|
+
}
|
|
70
75
|
active.set(session.id, { kill: () => abortController.abort() })
|
|
71
76
|
|
|
72
77
|
try {
|
|
@@ -279,7 +279,7 @@ async function connectToGateway(
|
|
|
279
279
|
|
|
280
280
|
// --- Provider ---
|
|
281
281
|
|
|
282
|
-
export function streamOpenClawChat({ session, message, imagePath, apiKey, write, active }: StreamChatOptions): Promise<string> {
|
|
282
|
+
export function streamOpenClawChat({ session, message, imagePath, apiKey, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
283
283
|
let prompt = message
|
|
284
284
|
if (imagePath) {
|
|
285
285
|
prompt = `[The user has shared an image at: ${imagePath}]\n\n${message}`
|
|
@@ -316,6 +316,11 @@ export function streamOpenClawChat({ session, message, imagePath, apiKey, write,
|
|
|
316
316
|
|
|
317
317
|
active.set(session.id, { kill: () => { ws.close(); clearTimeout(timeout); finish('Aborted.') } })
|
|
318
318
|
|
|
319
|
+
if (signal) {
|
|
320
|
+
if (signal.aborted) { ws.close(); clearTimeout(timeout); finish('Aborted.'); return }
|
|
321
|
+
signal.addEventListener('abort', () => { ws.close(); clearTimeout(timeout); finish('Aborted.') }, { once: true })
|
|
322
|
+
}
|
|
323
|
+
|
|
319
324
|
const agentReqId = randomUUID()
|
|
320
325
|
ws.send(JSON.stringify({
|
|
321
326
|
type: 'req',
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface ScheduleTemplate {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
description: string
|
|
5
|
+
icon: string
|
|
6
|
+
category: 'monitoring' | 'reporting' | 'maintenance' | 'content'
|
|
7
|
+
defaults: {
|
|
8
|
+
taskPrompt: string
|
|
9
|
+
scheduleType: 'cron' | 'interval'
|
|
10
|
+
cron?: string
|
|
11
|
+
intervalMs?: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SCHEDULE_TEMPLATES: ScheduleTemplate[] = [
|
|
16
|
+
{
|
|
17
|
+
id: 'daily-digest',
|
|
18
|
+
name: 'Daily Digest',
|
|
19
|
+
description: 'Summarize activity from the past 24 hours each morning',
|
|
20
|
+
icon: 'Newspaper',
|
|
21
|
+
category: 'reporting',
|
|
22
|
+
defaults: {
|
|
23
|
+
taskPrompt: 'Summarize all notable activity, events, and updates from the past 24 hours. Highlight key metrics, completed tasks, and anything that needs attention.',
|
|
24
|
+
scheduleType: 'cron',
|
|
25
|
+
cron: '0 9 * * *',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'weekly-report',
|
|
30
|
+
name: 'Weekly Report',
|
|
31
|
+
description: 'Generate a weekly metrics and progress report every Monday',
|
|
32
|
+
icon: 'BarChart3',
|
|
33
|
+
category: 'reporting',
|
|
34
|
+
defaults: {
|
|
35
|
+
taskPrompt: 'Generate a comprehensive weekly report covering key metrics, completed tasks, ongoing work, blockers, and recommendations for the coming week.',
|
|
36
|
+
scheduleType: 'cron',
|
|
37
|
+
cron: '0 10 * * 1',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'health-monitor',
|
|
42
|
+
name: 'Health Monitor',
|
|
43
|
+
description: 'Check system health and service status every 5 minutes',
|
|
44
|
+
icon: 'HeartPulse',
|
|
45
|
+
category: 'monitoring',
|
|
46
|
+
defaults: {
|
|
47
|
+
taskPrompt: 'Perform a system health check. Verify all services are running, check resource usage (CPU, memory, disk), and report any anomalies or degraded performance.',
|
|
48
|
+
scheduleType: 'interval',
|
|
49
|
+
intervalMs: 300000,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'content-generation',
|
|
54
|
+
name: 'Content Generation',
|
|
55
|
+
description: 'Generate daily content such as posts, summaries, or briefs',
|
|
56
|
+
icon: 'PenLine',
|
|
57
|
+
category: 'content',
|
|
58
|
+
defaults: {
|
|
59
|
+
taskPrompt: 'Generate fresh content based on current trends and recent activity. Create a well-structured draft ready for review and publishing.',
|
|
60
|
+
scheduleType: 'cron',
|
|
61
|
+
cron: '0 8 * * *',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'data-cleanup',
|
|
66
|
+
name: 'Data Cleanup',
|
|
67
|
+
description: 'Run weekly cleanup of stale data and temporary files',
|
|
68
|
+
icon: 'Trash2',
|
|
69
|
+
category: 'maintenance',
|
|
70
|
+
defaults: {
|
|
71
|
+
taskPrompt: 'Identify and clean up stale data, expired records, orphaned files, and temporary resources. Log what was removed and any issues encountered.',
|
|
72
|
+
scheduleType: 'cron',
|
|
73
|
+
cron: '0 2 * * 0',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'metric-snapshot',
|
|
78
|
+
name: 'Metric Snapshot',
|
|
79
|
+
description: 'Capture an hourly snapshot of key metrics and KPIs',
|
|
80
|
+
icon: 'Activity',
|
|
81
|
+
category: 'monitoring',
|
|
82
|
+
defaults: {
|
|
83
|
+
taskPrompt: 'Capture a snapshot of all key metrics and KPIs. Record current values, compare against previous snapshots, and flag any significant changes or threshold breaches.',
|
|
84
|
+
scheduleType: 'interval',
|
|
85
|
+
intervalMs: 3600000,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'security-audit',
|
|
90
|
+
name: 'Security Audit',
|
|
91
|
+
description: 'Run a daily security scan and vulnerability check',
|
|
92
|
+
icon: 'ShieldCheck',
|
|
93
|
+
category: 'monitoring',
|
|
94
|
+
defaults: {
|
|
95
|
+
taskPrompt: 'Perform a security audit. Check for unusual access patterns, review authentication logs, scan for known vulnerabilities, and report any security concerns.',
|
|
96
|
+
scheduleType: 'cron',
|
|
97
|
+
cron: '0 0 * * *',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'backup-check',
|
|
102
|
+
name: 'Backup Check',
|
|
103
|
+
description: 'Verify backup integrity and completeness daily',
|
|
104
|
+
icon: 'DatabaseBackup',
|
|
105
|
+
category: 'maintenance',
|
|
106
|
+
defaults: {
|
|
107
|
+
taskPrompt: 'Verify that all scheduled backups completed successfully. Check backup integrity, storage usage, and retention policy compliance. Alert on any failures.',
|
|
108
|
+
scheduleType: 'cron',
|
|
109
|
+
cron: '0 3 * * *',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
/** Subset of templates to feature in the empty state */
|
|
115
|
+
export const FEATURED_TEMPLATE_IDS = ['daily-digest', 'health-monitor', 'content-generation'] as const
|