botschat 0.1.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/LICENSE +201 -0
- package/README.md +213 -0
- package/migrations/0001_initial.sql +88 -0
- package/migrations/0002_rename_projects_to_channels.sql +53 -0
- package/migrations/0003_messages.sql +14 -0
- package/migrations/0004_jobs.sql +15 -0
- package/migrations/0005_deleted_cron_jobs.sql +6 -0
- package/migrations/0006_tasks_add_model.sql +2 -0
- package/migrations/0007_sessions.sql +25 -0
- package/migrations/0008_remove_openclaw_fields.sql +8 -0
- package/package.json +53 -0
- package/packages/api/package.json +17 -0
- package/packages/api/src/do/connection-do.ts +929 -0
- package/packages/api/src/env.ts +8 -0
- package/packages/api/src/index.ts +297 -0
- package/packages/api/src/routes/agents.ts +68 -0
- package/packages/api/src/routes/auth.ts +105 -0
- package/packages/api/src/routes/channels.ts +185 -0
- package/packages/api/src/routes/jobs.ts +65 -0
- package/packages/api/src/routes/models.ts +22 -0
- package/packages/api/src/routes/pairing.ts +76 -0
- package/packages/api/src/routes/projects.ts +177 -0
- package/packages/api/src/routes/sessions.ts +171 -0
- package/packages/api/src/routes/tasks.ts +375 -0
- package/packages/api/src/routes/upload.ts +52 -0
- package/packages/api/src/utils/auth.ts +101 -0
- package/packages/api/src/utils/id.ts +19 -0
- package/packages/api/tsconfig.json +18 -0
- package/packages/plugin/dist/index.d.ts +19 -0
- package/packages/plugin/dist/index.d.ts.map +1 -0
- package/packages/plugin/dist/index.js +17 -0
- package/packages/plugin/dist/index.js.map +1 -0
- package/packages/plugin/dist/src/accounts.d.ts +12 -0
- package/packages/plugin/dist/src/accounts.d.ts.map +1 -0
- package/packages/plugin/dist/src/accounts.js +103 -0
- package/packages/plugin/dist/src/accounts.js.map +1 -0
- package/packages/plugin/dist/src/channel.d.ts +206 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -0
- package/packages/plugin/dist/src/channel.js +1248 -0
- package/packages/plugin/dist/src/channel.js.map +1 -0
- package/packages/plugin/dist/src/runtime.d.ts +3 -0
- package/packages/plugin/dist/src/runtime.d.ts.map +1 -0
- package/packages/plugin/dist/src/runtime.js +18 -0
- package/packages/plugin/dist/src/runtime.js.map +1 -0
- package/packages/plugin/dist/src/types.d.ts +179 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -0
- package/packages/plugin/dist/src/types.js +6 -0
- package/packages/plugin/dist/src/types.js.map +1 -0
- package/packages/plugin/dist/src/ws-client.d.ts +51 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -0
- package/packages/plugin/dist/src/ws-client.js +170 -0
- package/packages/plugin/dist/src/ws-client.js.map +1 -0
- package/packages/plugin/openclaw.plugin.json +11 -0
- package/packages/plugin/package.json +39 -0
- package/packages/plugin/tsconfig.json +20 -0
- package/packages/web/dist/assets/index-C-wI8eHy.css +1 -0
- package/packages/web/dist/assets/index-CbPEKHLG.js +93 -0
- package/packages/web/dist/index.html +17 -0
- package/packages/web/index.html +16 -0
- package/packages/web/package.json +29 -0
- package/packages/web/postcss.config.js +6 -0
- package/packages/web/src/App.tsx +827 -0
- package/packages/web/src/api.ts +242 -0
- package/packages/web/src/components/ChatWindow.tsx +864 -0
- package/packages/web/src/components/CronDetail.tsx +943 -0
- package/packages/web/src/components/CronSidebar.tsx +123 -0
- package/packages/web/src/components/DebugLogPanel.tsx +258 -0
- package/packages/web/src/components/IconRail.tsx +163 -0
- package/packages/web/src/components/JobList.tsx +120 -0
- package/packages/web/src/components/LoginPage.tsx +178 -0
- package/packages/web/src/components/MessageContent.tsx +1082 -0
- package/packages/web/src/components/ModelSelect.tsx +87 -0
- package/packages/web/src/components/ScheduleEditor.tsx +403 -0
- package/packages/web/src/components/SessionTabs.tsx +246 -0
- package/packages/web/src/components/Sidebar.tsx +331 -0
- package/packages/web/src/components/TaskBar.tsx +413 -0
- package/packages/web/src/components/ThreadPanel.tsx +212 -0
- package/packages/web/src/debug-log.ts +58 -0
- package/packages/web/src/index.css +170 -0
- package/packages/web/src/main.tsx +10 -0
- package/packages/web/src/store.ts +492 -0
- package/packages/web/src/ws.ts +99 -0
- package/packages/web/tailwind.config.js +65 -0
- package/packages/web/tsconfig.json +18 -0
- package/packages/web/vite.config.ts +20 -0
- package/scripts/dev.sh +122 -0
- package/tsconfig.json +18 -0
- package/wrangler.toml +40 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useAppState, useAppDispatch } from "../store";
|
|
3
|
+
import { tasksApi, type Task } from "../api";
|
|
4
|
+
|
|
5
|
+
const SCHEDULE_PRESETS = [
|
|
6
|
+
{ label: "Every 30 min", value: "every 30m" },
|
|
7
|
+
{ label: "Every hour", value: "every 1h" },
|
|
8
|
+
{ label: "Every 6 hours", value: "every 6h" },
|
|
9
|
+
{ label: "Daily at 9am", value: "cron 0 9 * * *" },
|
|
10
|
+
{ label: "Daily at 6pm", value: "cron 0 18 * * *" },
|
|
11
|
+
{ label: "Twice daily", value: "cron 0 9,18 * * *" },
|
|
12
|
+
{ label: "Weekly Monday 9am", value: "cron 0 9 * * 1" },
|
|
13
|
+
{ label: "Custom", value: "" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/** Task bar for channel-based agents – sits below the channel header */
|
|
17
|
+
export function TaskBar() {
|
|
18
|
+
const state = useAppState();
|
|
19
|
+
const dispatch = useAppDispatch();
|
|
20
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
21
|
+
const [newTaskName, setNewTaskName] = useState("");
|
|
22
|
+
const [newTaskKind, setNewTaskKind] = useState<"adhoc" | "background">("adhoc");
|
|
23
|
+
const [newSchedule, setNewSchedule] = useState("");
|
|
24
|
+
const [newInstructions, setNewInstructions] = useState("");
|
|
25
|
+
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
|
26
|
+
const [editSchedule, setEditSchedule] = useState("");
|
|
27
|
+
const [editInstructions, setEditInstructions] = useState("");
|
|
28
|
+
const [editEnabled, setEditEnabled] = useState(true);
|
|
29
|
+
|
|
30
|
+
const agent = state.agents.find((a) => a.id === state.selectedAgentId);
|
|
31
|
+
const channel = agent?.channelId
|
|
32
|
+
? state.channels.find((ch) => ch.id === agent.channelId)
|
|
33
|
+
: null;
|
|
34
|
+
if (!channel) return null;
|
|
35
|
+
|
|
36
|
+
const handleCreate = async () => {
|
|
37
|
+
if (!newTaskName.trim()) return;
|
|
38
|
+
try {
|
|
39
|
+
await tasksApi.create(channel.id, {
|
|
40
|
+
name: newTaskName,
|
|
41
|
+
kind: newTaskKind,
|
|
42
|
+
schedule: newTaskKind === "background" ? newSchedule : undefined,
|
|
43
|
+
instructions: newTaskKind === "background" ? newInstructions : undefined,
|
|
44
|
+
});
|
|
45
|
+
const { tasks } = await tasksApi.list(channel.id);
|
|
46
|
+
dispatch({ type: "SET_TASKS", tasks });
|
|
47
|
+
setShowCreate(false);
|
|
48
|
+
setNewTaskName("");
|
|
49
|
+
setNewSchedule("");
|
|
50
|
+
setNewInstructions("");
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("Failed to create task:", err);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleOpenConfig = (t: Task, e: React.MouseEvent) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setEditingTask(t);
|
|
59
|
+
setEditSchedule(t.schedule ?? "");
|
|
60
|
+
setEditInstructions(t.instructions ?? "");
|
|
61
|
+
setEditEnabled(t.enabled);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleSaveConfig = async () => {
|
|
65
|
+
if (!editingTask) return;
|
|
66
|
+
try {
|
|
67
|
+
await tasksApi.update(channel.id, editingTask.id, {
|
|
68
|
+
schedule: editSchedule,
|
|
69
|
+
instructions: editInstructions,
|
|
70
|
+
enabled: editEnabled,
|
|
71
|
+
});
|
|
72
|
+
const { tasks } = await tasksApi.list(channel.id);
|
|
73
|
+
dispatch({ type: "SET_TASKS", tasks });
|
|
74
|
+
setEditingTask(null);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Failed to update task:", err);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleToggleEnabled = async (t: Task, e: React.MouseEvent) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
try {
|
|
83
|
+
await tasksApi.update(channel.id, t.id, { enabled: !t.enabled });
|
|
84
|
+
const { tasks } = await tasksApi.list(channel.id);
|
|
85
|
+
dispatch({ type: "SET_TASKS", tasks });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("Failed to toggle task:", err);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleDeleteTask = async (t: Task, e: React.MouseEvent) => {
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
if (!confirm(`Delete task "${t.name}"?`)) return;
|
|
94
|
+
try {
|
|
95
|
+
await tasksApi.delete(channel.id, t.id);
|
|
96
|
+
const { tasks } = await tasksApi.list(channel.id);
|
|
97
|
+
dispatch({ type: "SET_TASKS", tasks });
|
|
98
|
+
if (state.selectedTaskId === t.id && tasks.length > 0) {
|
|
99
|
+
dispatch({ type: "SELECT_TASK", taskId: tasks[0].id, sessionKey: tasks[0].sessionKey });
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("Failed to delete task:", err);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<div
|
|
109
|
+
className="flex items-center gap-1 px-4 py-1.5 overflow-x-auto"
|
|
110
|
+
style={{
|
|
111
|
+
background: "var(--bg-surface)",
|
|
112
|
+
borderBottom: "1px solid var(--border)",
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{state.tasks.map((t) => (
|
|
116
|
+
<div key={t.id} className="flex items-center group">
|
|
117
|
+
<button
|
|
118
|
+
onClick={() =>
|
|
119
|
+
dispatch({
|
|
120
|
+
type: "SELECT_TASK",
|
|
121
|
+
taskId: t.id,
|
|
122
|
+
sessionKey: t.sessionKey ?? undefined,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
className={`flex items-center gap-1.5 px-3 py-1 text-caption rounded-lg whitespace-nowrap transition-colors ${
|
|
126
|
+
state.selectedTaskId === t.id
|
|
127
|
+
? "font-bold"
|
|
128
|
+
: "hover:bg-[--bg-hover]"
|
|
129
|
+
}`}
|
|
130
|
+
style={
|
|
131
|
+
state.selectedTaskId === t.id
|
|
132
|
+
? {
|
|
133
|
+
background: "var(--bg-hover)",
|
|
134
|
+
color: "var(--text-primary)",
|
|
135
|
+
boxShadow: "inset 0 -2px 0 var(--bg-active)",
|
|
136
|
+
}
|
|
137
|
+
: { color: "var(--text-secondary)" }
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
{t.kind === "background" ? (
|
|
141
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
142
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
143
|
+
</svg>
|
|
144
|
+
) : (
|
|
145
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
146
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
147
|
+
</svg>
|
|
148
|
+
)}
|
|
149
|
+
{t.name}
|
|
150
|
+
{t.kind === "background" && !t.enabled && (
|
|
151
|
+
<span className="text-tiny" style={{ color: "var(--text-muted)" }}>(paused)</span>
|
|
152
|
+
)}
|
|
153
|
+
{t.kind === "background" && t.schedule && t.enabled && (
|
|
154
|
+
<span className="text-tiny" style={{ color: "var(--text-muted)" }}>
|
|
155
|
+
{t.schedule}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</button>
|
|
159
|
+
{/* Config and toggle buttons for background tasks */}
|
|
160
|
+
{t.kind === "background" && state.selectedTaskId === t.id && (
|
|
161
|
+
<div className="flex items-center gap-0.5 ml-0.5">
|
|
162
|
+
<button
|
|
163
|
+
onClick={(e) => handleOpenConfig(t, e)}
|
|
164
|
+
title="Configure schedule"
|
|
165
|
+
className="p-1 rounded hover:bg-[--bg-hover] transition-colors"
|
|
166
|
+
style={{ color: "var(--text-muted)" }}
|
|
167
|
+
>
|
|
168
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
169
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
170
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
<button
|
|
174
|
+
onClick={(e) => handleToggleEnabled(t, e)}
|
|
175
|
+
title={t.enabled ? "Pause task" : "Resume task"}
|
|
176
|
+
className="p-1 rounded hover:bg-[--bg-hover] transition-colors"
|
|
177
|
+
style={{ color: t.enabled ? "var(--accent-green)" : "var(--accent-yellow)" }}
|
|
178
|
+
>
|
|
179
|
+
{t.enabled ? (
|
|
180
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
181
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
182
|
+
</svg>
|
|
183
|
+
) : (
|
|
184
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
185
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
186
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
187
|
+
</svg>
|
|
188
|
+
)}
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
|
|
195
|
+
{/* Add task */}
|
|
196
|
+
{showCreate ? (
|
|
197
|
+
<div className="flex items-center gap-1 ml-1">
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
placeholder="Task name"
|
|
201
|
+
value={newTaskName}
|
|
202
|
+
onChange={(e) => setNewTaskName(e.target.value)}
|
|
203
|
+
onKeyDown={(e) => e.key === "Enter" && !e.nativeEvent.isComposing && handleCreate()}
|
|
204
|
+
className="px-2 py-1 text-caption rounded-sm focus:outline-none w-36 placeholder:text-[--text-muted]"
|
|
205
|
+
style={{
|
|
206
|
+
background: "var(--bg-hover)",
|
|
207
|
+
color: "var(--text-primary)",
|
|
208
|
+
border: "1px solid var(--border)",
|
|
209
|
+
}}
|
|
210
|
+
autoFocus
|
|
211
|
+
/>
|
|
212
|
+
<select
|
|
213
|
+
value={newTaskKind}
|
|
214
|
+
onChange={(e) => setNewTaskKind(e.target.value as "adhoc" | "background")}
|
|
215
|
+
className="px-1 py-1 text-tiny rounded-sm"
|
|
216
|
+
style={{
|
|
217
|
+
background: "var(--bg-hover)",
|
|
218
|
+
color: "var(--text-primary)",
|
|
219
|
+
border: "1px solid var(--border)",
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<option value="adhoc">Ad Hoc</option>
|
|
223
|
+
<option value="background">Background</option>
|
|
224
|
+
</select>
|
|
225
|
+
<button
|
|
226
|
+
onClick={handleCreate}
|
|
227
|
+
className="px-2 py-1 text-tiny font-bold text-white rounded-sm"
|
|
228
|
+
style={{ background: "var(--bg-active)" }}
|
|
229
|
+
>
|
|
230
|
+
Add
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => { setShowCreate(false); setNewTaskName(""); setNewSchedule(""); setNewInstructions(""); }}
|
|
234
|
+
className="px-1 py-1 text-tiny"
|
|
235
|
+
style={{ color: "var(--text-muted)" }}
|
|
236
|
+
>
|
|
237
|
+
x
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
<button
|
|
242
|
+
onClick={() => setShowCreate(true)}
|
|
243
|
+
className="flex items-center gap-1 px-3 py-1 text-caption hover:bg-[--bg-hover] rounded-lg whitespace-nowrap transition-colors"
|
|
244
|
+
style={{ color: "var(--text-muted)" }}
|
|
245
|
+
>
|
|
246
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
247
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
|
248
|
+
</svg>
|
|
249
|
+
Add
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Schedule config strip for new background task */}
|
|
255
|
+
{showCreate && newTaskKind === "background" && (
|
|
256
|
+
<div
|
|
257
|
+
className="px-4 py-2 flex flex-wrap items-center gap-2"
|
|
258
|
+
style={{ background: "var(--bg-surface)", borderBottom: "1px solid var(--border)" }}
|
|
259
|
+
>
|
|
260
|
+
<select
|
|
261
|
+
value={SCHEDULE_PRESETS.find((p) => p.value === newSchedule) ? newSchedule : ""}
|
|
262
|
+
onChange={(e) => setNewSchedule(e.target.value)}
|
|
263
|
+
className="px-2 py-1 text-caption rounded-sm"
|
|
264
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
265
|
+
>
|
|
266
|
+
<option value="" disabled>Schedule...</option>
|
|
267
|
+
{SCHEDULE_PRESETS.map((p) => (
|
|
268
|
+
<option key={p.value || "custom"} value={p.value}>{p.label}</option>
|
|
269
|
+
))}
|
|
270
|
+
</select>
|
|
271
|
+
{(!SCHEDULE_PRESETS.find((p) => p.value === newSchedule) || newSchedule === "") && (
|
|
272
|
+
<input
|
|
273
|
+
type="text"
|
|
274
|
+
placeholder="e.g., every 6h or cron 0 */6 * * *"
|
|
275
|
+
value={newSchedule}
|
|
276
|
+
onChange={(e) => setNewSchedule(e.target.value)}
|
|
277
|
+
className="px-2 py-1 text-caption rounded-sm focus:outline-none flex-1 min-w-[200px] placeholder:text-[--text-muted]"
|
|
278
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
<input
|
|
282
|
+
type="text"
|
|
283
|
+
placeholder="Agent instructions per run..."
|
|
284
|
+
value={newInstructions}
|
|
285
|
+
onChange={(e) => setNewInstructions(e.target.value)}
|
|
286
|
+
className="px-2 py-1 text-caption rounded-sm focus:outline-none flex-1 min-w-[200px] placeholder:text-[--text-muted]"
|
|
287
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
{/* Task config editor modal */}
|
|
293
|
+
{editingTask && (
|
|
294
|
+
<div
|
|
295
|
+
className="fixed inset-0 flex items-center justify-center z-50"
|
|
296
|
+
style={{ background: "rgba(0,0,0,0.5)" }}
|
|
297
|
+
onClick={() => setEditingTask(null)}
|
|
298
|
+
>
|
|
299
|
+
<div
|
|
300
|
+
className="rounded-lg p-5 w-[480px] max-w-[90vw]"
|
|
301
|
+
style={{ background: "var(--bg-surface)", boxShadow: "var(--shadow-lg)" }}
|
|
302
|
+
onClick={(e) => e.stopPropagation()}
|
|
303
|
+
>
|
|
304
|
+
<div className="flex items-center justify-between mb-4">
|
|
305
|
+
<h2 className="text-h2 font-bold" style={{ color: "var(--text-primary)" }}>
|
|
306
|
+
Configure: {editingTask.name}
|
|
307
|
+
</h2>
|
|
308
|
+
<button
|
|
309
|
+
onClick={() => setEditingTask(null)}
|
|
310
|
+
className="p-1 hover:bg-[--bg-hover] rounded"
|
|
311
|
+
style={{ color: "var(--text-muted)" }}
|
|
312
|
+
>
|
|
313
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
314
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
315
|
+
</svg>
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div className="space-y-3">
|
|
320
|
+
{/* Schedule */}
|
|
321
|
+
<div>
|
|
322
|
+
<label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
|
|
323
|
+
Schedule
|
|
324
|
+
</label>
|
|
325
|
+
<select
|
|
326
|
+
value={SCHEDULE_PRESETS.find((p) => p.value === editSchedule) ? editSchedule : ""}
|
|
327
|
+
onChange={(e) => setEditSchedule(e.target.value)}
|
|
328
|
+
className="w-full px-3 py-2 text-body rounded-md mb-1"
|
|
329
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
330
|
+
>
|
|
331
|
+
<option value="">Custom</option>
|
|
332
|
+
{SCHEDULE_PRESETS.filter((p) => p.value).map((p) => (
|
|
333
|
+
<option key={p.value} value={p.value}>{p.label}</option>
|
|
334
|
+
))}
|
|
335
|
+
</select>
|
|
336
|
+
<input
|
|
337
|
+
type="text"
|
|
338
|
+
placeholder="e.g., every 6h or cron 0 */6 * * *"
|
|
339
|
+
value={editSchedule}
|
|
340
|
+
onChange={(e) => setEditSchedule(e.target.value)}
|
|
341
|
+
className="w-full px-3 py-2 text-body rounded-md focus:outline-none placeholder:text-[--text-muted]"
|
|
342
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Instructions */}
|
|
347
|
+
<div>
|
|
348
|
+
<label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
|
|
349
|
+
Agent Instructions (per run)
|
|
350
|
+
</label>
|
|
351
|
+
<textarea
|
|
352
|
+
placeholder="What should the agent do each time this task runs?"
|
|
353
|
+
value={editInstructions}
|
|
354
|
+
onChange={(e) => setEditInstructions(e.target.value)}
|
|
355
|
+
rows={4}
|
|
356
|
+
className="w-full px-3 py-2 text-body rounded-md focus:outline-none resize-y placeholder:text-[--text-muted]"
|
|
357
|
+
style={{ background: "var(--bg-hover)", color: "var(--text-primary)", border: "1px solid var(--border)" }}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
{/* Enabled toggle */}
|
|
362
|
+
<div className="flex items-center gap-3">
|
|
363
|
+
<label className="text-caption font-bold" style={{ color: "var(--text-secondary)" }}>
|
|
364
|
+
Enabled
|
|
365
|
+
</label>
|
|
366
|
+
<button
|
|
367
|
+
onClick={() => setEditEnabled(!editEnabled)}
|
|
368
|
+
className="relative w-10 h-5 rounded-full transition-colors"
|
|
369
|
+
style={{ background: editEnabled ? "var(--accent-green)" : "var(--border)" }}
|
|
370
|
+
>
|
|
371
|
+
<div
|
|
372
|
+
className="absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform"
|
|
373
|
+
style={{ left: editEnabled ? 20 : 2 }}
|
|
374
|
+
/>
|
|
375
|
+
</button>
|
|
376
|
+
<span className="text-caption" style={{ color: "var(--text-muted)" }}>
|
|
377
|
+
{editEnabled ? "Running on schedule" : "Paused"}
|
|
378
|
+
</span>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Actions */}
|
|
383
|
+
<div className="flex items-center justify-between mt-5 pt-3" style={{ borderTop: "1px solid var(--border)" }}>
|
|
384
|
+
<button
|
|
385
|
+
onClick={(e) => { handleDeleteTask(editingTask, e); setEditingTask(null); }}
|
|
386
|
+
className="px-3 py-1.5 text-caption rounded-md hover:bg-[--bg-hover] transition-colors"
|
|
387
|
+
style={{ color: "var(--accent-red)" }}
|
|
388
|
+
>
|
|
389
|
+
Delete task
|
|
390
|
+
</button>
|
|
391
|
+
<div className="flex gap-2">
|
|
392
|
+
<button
|
|
393
|
+
onClick={() => setEditingTask(null)}
|
|
394
|
+
className="px-4 py-1.5 text-caption rounded-md hover:bg-[--bg-hover] transition-colors"
|
|
395
|
+
style={{ color: "var(--text-secondary)" }}
|
|
396
|
+
>
|
|
397
|
+
Cancel
|
|
398
|
+
</button>
|
|
399
|
+
<button
|
|
400
|
+
onClick={handleSaveConfig}
|
|
401
|
+
className="px-4 py-1.5 text-caption font-bold text-white rounded-md"
|
|
402
|
+
style={{ background: "var(--bg-active)" }}
|
|
403
|
+
>
|
|
404
|
+
Save
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useAppState, useAppDispatch, type ChatMessage } from "../store";
|
|
3
|
+
import { messagesApi } from "../api";
|
|
4
|
+
import type { WSMessage } from "../ws";
|
|
5
|
+
import { MessageContent } from "./MessageContent";
|
|
6
|
+
import { dlog } from "../debug-log";
|
|
7
|
+
|
|
8
|
+
type ThreadPanelProps = {
|
|
9
|
+
sendMessage: (msg: WSMessage) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Detail Panel (section 5.5) – slides in from right */
|
|
13
|
+
export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
|
|
14
|
+
const state = useAppState();
|
|
15
|
+
const dispatch = useAppDispatch();
|
|
16
|
+
const [input, setInput] = useState("");
|
|
17
|
+
|
|
18
|
+
// Load thread message history when a thread is opened
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!state.activeThreadId || !state.selectedSessionKey || !state.user) return;
|
|
21
|
+
const threadSessionKey = `${state.selectedSessionKey}:thread:${state.activeThreadId}`;
|
|
22
|
+
dlog.info("Thread", `Loading history for thread ${state.activeThreadId}`);
|
|
23
|
+
messagesApi
|
|
24
|
+
.list(state.user.id, threadSessionKey, state.activeThreadId)
|
|
25
|
+
.then(({ messages }) => {
|
|
26
|
+
dlog.info("Thread", `Loaded ${messages.length} thread messages`);
|
|
27
|
+
if (messages.length > 0) {
|
|
28
|
+
dispatch({ type: "OPEN_THREAD", threadId: state.activeThreadId!, messages });
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.catch((err) => {
|
|
32
|
+
dlog.error("Thread", `Failed to load thread history: ${err}`);
|
|
33
|
+
});
|
|
34
|
+
}, [state.activeThreadId]);
|
|
35
|
+
|
|
36
|
+
if (!state.activeThreadId) return null;
|
|
37
|
+
|
|
38
|
+
const parentMessage = state.messages.find(
|
|
39
|
+
(m) => m.id === state.activeThreadId,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const handleSend = () => {
|
|
43
|
+
if (!input.trim() || !state.selectedSessionKey) return;
|
|
44
|
+
|
|
45
|
+
const trimmed = input.trim();
|
|
46
|
+
const threadSessionKey = `${state.selectedSessionKey}:thread:${state.activeThreadId}`;
|
|
47
|
+
dlog.info("Thread", `Send reply: ${trimmed.length > 120 ? trimmed.slice(0, 120) + "…" : trimmed}`, { threadId: state.activeThreadId });
|
|
48
|
+
|
|
49
|
+
const msg: ChatMessage = {
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
sender: "user",
|
|
52
|
+
text: trimmed,
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
threadId: state.activeThreadId ?? undefined,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
dispatch({ type: "ADD_THREAD_MESSAGE", message: msg });
|
|
58
|
+
|
|
59
|
+
sendMessage({
|
|
60
|
+
type: "user.message",
|
|
61
|
+
sessionKey: threadSessionKey,
|
|
62
|
+
text: trimmed,
|
|
63
|
+
userId: state.user?.id ?? "",
|
|
64
|
+
messageId: msg.id,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
setInput("");
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className="flex flex-col h-full"
|
|
73
|
+
style={{
|
|
74
|
+
width: 420,
|
|
75
|
+
minWidth: 320,
|
|
76
|
+
background: "var(--bg-surface)",
|
|
77
|
+
borderLeft: "1px solid var(--border)",
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{/* Header */}
|
|
81
|
+
<div
|
|
82
|
+
className="flex items-center justify-between px-4"
|
|
83
|
+
style={{ height: 44, borderBottom: "1px solid var(--border)" }}
|
|
84
|
+
>
|
|
85
|
+
<h3 className="text-h1" style={{ color: "var(--text-primary)" }}>Thread</h3>
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => dispatch({ type: "CLOSE_THREAD" })}
|
|
88
|
+
className="p-1 rounded hover:bg-[--bg-hover] transition-colors"
|
|
89
|
+
style={{ color: "var(--text-secondary)" }}
|
|
90
|
+
aria-label="Close thread"
|
|
91
|
+
>
|
|
92
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
93
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Scrollable area: parent message + replies */}
|
|
99
|
+
<div className="flex-1 overflow-y-auto">
|
|
100
|
+
{/* Parent message */}
|
|
101
|
+
{parentMessage && (
|
|
102
|
+
<div className="px-5 py-3" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
103
|
+
<div className="flex gap-2">
|
|
104
|
+
<div
|
|
105
|
+
className="w-9 h-9 rounded flex-shrink-0 flex items-center justify-center text-white text-caption font-bold"
|
|
106
|
+
style={{ background: parentMessage.sender === "user" ? "#9B59B6" : "#2BAC76" }}
|
|
107
|
+
>
|
|
108
|
+
{parentMessage.sender === "user" ? "U" : "A"}
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex-1 min-w-0">
|
|
111
|
+
<div className="flex items-baseline gap-2 mb-0.5">
|
|
112
|
+
<span className="text-h2" style={{ color: "var(--text-primary)" }}>
|
|
113
|
+
{parentMessage.sender === "user" ? "You" : "OpenClaw Agent"}
|
|
114
|
+
</span>
|
|
115
|
+
<span className="text-caption" style={{ color: "var(--text-secondary)" }}>
|
|
116
|
+
{new Date(parentMessage.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<MessageContent text={parentMessage.text} mediaUrl={parentMessage.mediaUrl} />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Reply count divider */}
|
|
126
|
+
<div className="px-5 py-2" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
127
|
+
<span className="text-caption font-bold" style={{ color: "var(--text-link)" }}>
|
|
128
|
+
{state.threadMessages.length} {state.threadMessages.length === 1 ? "reply" : "replies"}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Thread replies – flat rows like main content */}
|
|
133
|
+
{state.threadMessages.map((msg, i) => {
|
|
134
|
+
const prevMsg = i > 0 ? state.threadMessages[i - 1] : null;
|
|
135
|
+
const isGrouped = prevMsg?.sender === msg.sender
|
|
136
|
+
&& (msg.timestamp - prevMsg.timestamp) < 300000;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
key={msg.id}
|
|
141
|
+
className="px-5 hover:bg-[--bg-hover] transition-colors"
|
|
142
|
+
style={{ paddingTop: isGrouped ? 2 : 8, paddingBottom: 2 }}
|
|
143
|
+
>
|
|
144
|
+
<div className="flex gap-2">
|
|
145
|
+
<div className="flex-shrink-0" style={{ width: 36 }}>
|
|
146
|
+
{!isGrouped && (
|
|
147
|
+
<div
|
|
148
|
+
className="w-9 h-9 rounded flex items-center justify-center text-white text-caption font-bold"
|
|
149
|
+
style={{ background: msg.sender === "user" ? "#9B59B6" : "#2BAC76" }}
|
|
150
|
+
>
|
|
151
|
+
{msg.sender === "user" ? "U" : "A"}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex-1 min-w-0">
|
|
156
|
+
{!isGrouped && (
|
|
157
|
+
<div className="flex items-baseline gap-2 mb-0.5">
|
|
158
|
+
<span className="text-h2" style={{ color: "var(--text-primary)" }}>
|
|
159
|
+
{msg.sender === "user" ? "You" : "OpenClaw Agent"}
|
|
160
|
+
</span>
|
|
161
|
+
<span className="text-caption" style={{ color: "var(--text-secondary)" }}>
|
|
162
|
+
{new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
<MessageContent
|
|
167
|
+
text={msg.text}
|
|
168
|
+
mediaUrl={msg.mediaUrl}
|
|
169
|
+
a2ui={msg.a2ui}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</div> {/* end scrollable area */}
|
|
177
|
+
|
|
178
|
+
{/* Thread composer */}
|
|
179
|
+
<div className="px-4 pb-3 pt-2">
|
|
180
|
+
<div
|
|
181
|
+
className="rounded-md"
|
|
182
|
+
style={{ border: "1px solid var(--border)", background: "var(--bg-surface)" }}
|
|
183
|
+
>
|
|
184
|
+
<textarea
|
|
185
|
+
value={input}
|
|
186
|
+
onChange={(e) => setInput(e.target.value)}
|
|
187
|
+
onKeyDown={(e) => {
|
|
188
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
handleSend();
|
|
191
|
+
}
|
|
192
|
+
}}
|
|
193
|
+
placeholder="Reply…"
|
|
194
|
+
rows={1}
|
|
195
|
+
className="w-full px-3 py-2 text-body bg-transparent resize-none focus:outline-none placeholder:text-[--text-muted]"
|
|
196
|
+
style={{ color: "var(--text-primary)", minHeight: 36 }}
|
|
197
|
+
/>
|
|
198
|
+
<div className="flex justify-end px-3 pb-2">
|
|
199
|
+
<button
|
|
200
|
+
onClick={handleSend}
|
|
201
|
+
disabled={!input.trim()}
|
|
202
|
+
className="px-3 py-1 rounded-sm text-caption font-bold text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
203
|
+
style={{ background: "var(--bg-active)" }}
|
|
204
|
+
>
|
|
205
|
+
Send
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global debug log — captures WS messages, API calls, state changes, errors.
|
|
3
|
+
* Subscribers are notified on every new entry so React can re-render.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = "info" | "warn" | "error" | "ws-in" | "ws-out" | "api";
|
|
7
|
+
|
|
8
|
+
export type LogEntry = {
|
|
9
|
+
id: number;
|
|
10
|
+
ts: number;
|
|
11
|
+
level: LogLevel;
|
|
12
|
+
tag: string;
|
|
13
|
+
message: string;
|
|
14
|
+
detail?: string; // collapsed JSON / extra info
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MAX_ENTRIES = 500;
|
|
18
|
+
let _nextId = 1;
|
|
19
|
+
const _entries: LogEntry[] = [];
|
|
20
|
+
const _listeners = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
export function addLog(level: LogLevel, tag: string, message: string, detail?: unknown): void {
|
|
23
|
+
const entry: LogEntry = {
|
|
24
|
+
id: _nextId++,
|
|
25
|
+
ts: Date.now(),
|
|
26
|
+
level,
|
|
27
|
+
tag,
|
|
28
|
+
message,
|
|
29
|
+
detail: detail !== undefined ? (typeof detail === "string" ? detail : JSON.stringify(detail, null, 2)) : undefined,
|
|
30
|
+
};
|
|
31
|
+
_entries.push(entry);
|
|
32
|
+
if (_entries.length > MAX_ENTRIES) _entries.splice(0, _entries.length - MAX_ENTRIES);
|
|
33
|
+
for (const fn of _listeners) fn();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getLogEntries(): readonly LogEntry[] {
|
|
37
|
+
return _entries;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clearLog(): void {
|
|
41
|
+
_entries.length = 0;
|
|
42
|
+
for (const fn of _listeners) fn();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function subscribeLog(fn: () => void): () => void {
|
|
46
|
+
_listeners.add(fn);
|
|
47
|
+
return () => _listeners.delete(fn);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Convenience helpers
|
|
51
|
+
export const dlog = {
|
|
52
|
+
info: (tag: string, msg: string, detail?: unknown) => addLog("info", tag, msg, detail),
|
|
53
|
+
warn: (tag: string, msg: string, detail?: unknown) => addLog("warn", tag, msg, detail),
|
|
54
|
+
error: (tag: string, msg: string, detail?: unknown) => addLog("error", tag, msg, detail),
|
|
55
|
+
wsIn: (tag: string, msg: string, detail?: unknown) => addLog("ws-in", tag, msg, detail),
|
|
56
|
+
wsOut: (tag: string, msg: string, detail?: unknown) => addLog("ws-out", tag, msg, detail),
|
|
57
|
+
api: (tag: string, msg: string, detail?: unknown) => addLog("api", tag, msg, detail),
|
|
58
|
+
};
|