apteva 0.4.17 → 0.4.19
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/dist/ActivityPage.9a1qg4bp.js +3 -0
- package/dist/ApiDocsPage.rfpf7ws1.js +4 -0
- package/dist/App.1nmg2h01.js +4 -0
- package/dist/App.5qw2dtxs.js +4 -0
- package/dist/App.6nc5acvk.js +4 -0
- package/dist/App.7vzbaz56.js +4 -0
- package/dist/App.8rfz30p1.js +4 -0
- package/dist/App.amwp54wf.js +4 -0
- package/dist/App.e4202qb4.js +267 -0
- package/dist/App.errxz2q4.js +4 -0
- package/dist/App.f8qsyhpr.js +4 -0
- package/dist/App.g8vq68n0.js +20 -0
- package/dist/App.kfyrnznw.js +13 -0
- package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
- package/dist/App.p93mmyqw.js +4 -0
- package/dist/App.qmg33p02.js +4 -0
- package/dist/App.sdsc0258.js +4 -0
- package/dist/ConnectionsPage.7zqba1r0.js +3 -0
- package/dist/McpPage.kf2g327t.js +3 -0
- package/dist/SettingsPage.472c15ep.js +3 -0
- package/dist/SkillsPage.xdxnh68a.js +3 -0
- package/dist/TasksPage.7g0b8xwc.js +3 -0
- package/dist/TelemetryPage.pr7rbz4r.js +3 -0
- package/dist/TestsPage.zhc6rqjm.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -4
- package/src/auth/middleware.ts +2 -0
- package/src/channels/index.ts +40 -0
- package/src/channels/telegram.ts +306 -0
- package/src/db.ts +342 -11
- package/src/integrations/agentdojo.ts +1 -1
- package/src/mcp-handler.ts +31 -24
- package/src/mcp-platform.ts +41 -1
- package/src/providers.ts +22 -9
- package/src/routes/api/agent-utils.ts +38 -2
- package/src/routes/api/agents.ts +65 -2
- package/src/routes/api/channels.ts +182 -0
- package/src/routes/api/integrations.ts +13 -5
- package/src/routes/api/mcp.ts +27 -9
- package/src/routes/api/projects.ts +19 -2
- package/src/routes/api/system.ts +26 -12
- package/src/routes/api/telemetry.ts +30 -0
- package/src/routes/api/triggers.ts +478 -0
- package/src/routes/api/webhooks.ts +171 -0
- package/src/routes/api.ts +7 -1
- package/src/routes/static.ts +12 -3
- package/src/server.ts +43 -6
- package/src/triggers/agentdojo.ts +253 -0
- package/src/triggers/composio.ts +264 -0
- package/src/triggers/index.ts +71 -0
- package/src/tui/AgentList.tsx +145 -0
- package/src/tui/App.tsx +102 -0
- package/src/tui/Login.tsx +104 -0
- package/src/tui/api.ts +72 -0
- package/src/tui/index.tsx +7 -0
- package/src/web/App.tsx +18 -11
- package/src/web/components/agents/AgentCard.tsx +14 -7
- package/src/web/components/agents/AgentPanel.tsx +94 -137
- package/src/web/components/common/Icons.tsx +16 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/connections/ConnectionsPage.tsx +54 -0
- package/src/web/components/connections/IntegrationsTab.tsx +144 -0
- package/src/web/components/connections/OverviewTab.tsx +137 -0
- package/src/web/components/connections/TriggersTab.tsx +1169 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +196 -4
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
- package/src/web/components/settings/SettingsPage.tsx +364 -2
- package/src/web/components/tasks/TasksPage.tsx +2 -2
- package/src/web/components/tests/TestsPage.tsx +1 -2
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/src/web/hooks/useAgents.ts +15 -11
- package/src/web/types.ts +1 -1
- package/dist/App.fq4xbpcz.js +0 -228
|
@@ -5,15 +5,17 @@ import { Select } from "../common/Select";
|
|
|
5
5
|
import { useProjects, useAuth, type Project } from "../../context";
|
|
6
6
|
import type { Provider } from "../../types";
|
|
7
7
|
|
|
8
|
-
type SettingsTab = "providers" | "projects" | "api-keys" | "account" | "updates" | "data";
|
|
8
|
+
type SettingsTab = "general" | "providers" | "projects" | "channels" | "api-keys" | "account" | "updates" | "data";
|
|
9
9
|
|
|
10
10
|
export function SettingsPage() {
|
|
11
11
|
const { projectsEnabled } = useProjects();
|
|
12
|
-
const [activeTab, setActiveTab] = useState<SettingsTab>("
|
|
12
|
+
const [activeTab, setActiveTab] = useState<SettingsTab>("general");
|
|
13
13
|
|
|
14
14
|
const tabs: { key: SettingsTab; label: string }[] = [
|
|
15
|
+
{ key: "general", label: "General" },
|
|
15
16
|
{ key: "providers", label: "Providers" },
|
|
16
17
|
...(projectsEnabled ? [{ key: "projects" as SettingsTab, label: "Projects" }] : []),
|
|
18
|
+
{ key: "channels", label: "Channels" },
|
|
17
19
|
{ key: "api-keys", label: "API Keys" },
|
|
18
20
|
{ key: "account", label: "Account" },
|
|
19
21
|
{ key: "updates", label: "Updates" },
|
|
@@ -58,8 +60,10 @@ export function SettingsPage() {
|
|
|
58
60
|
|
|
59
61
|
{/* Settings Content */}
|
|
60
62
|
<div className="flex-1 overflow-auto p-4 md:p-6">
|
|
63
|
+
{activeTab === "general" && <GeneralSettings />}
|
|
61
64
|
{activeTab === "providers" && <ProvidersSettings />}
|
|
62
65
|
{activeTab === "projects" && projectsEnabled && <ProjectsSettings />}
|
|
66
|
+
{activeTab === "channels" && <ChannelsSettings />}
|
|
63
67
|
{activeTab === "api-keys" && <ApiKeysSettings />}
|
|
64
68
|
{activeTab === "account" && <AccountSettings />}
|
|
65
69
|
{activeTab === "updates" && <UpdatesSettings />}
|
|
@@ -92,6 +96,98 @@ function SettingsNavItem({
|
|
|
92
96
|
);
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
function GeneralSettings() {
|
|
100
|
+
const { authFetch } = useAuth();
|
|
101
|
+
const [instanceUrl, setInstanceUrl] = useState("");
|
|
102
|
+
const [loading, setLoading] = useState(true);
|
|
103
|
+
const [saving, setSaving] = useState(false);
|
|
104
|
+
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const fetch = async () => {
|
|
108
|
+
try {
|
|
109
|
+
const res = await authFetch("/api/settings/instance-url");
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
setInstanceUrl(data.instance_url || "");
|
|
112
|
+
} catch {
|
|
113
|
+
// ignore
|
|
114
|
+
}
|
|
115
|
+
setLoading(false);
|
|
116
|
+
};
|
|
117
|
+
fetch();
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const handleSave = async () => {
|
|
121
|
+
setSaving(true);
|
|
122
|
+
setMessage(null);
|
|
123
|
+
try {
|
|
124
|
+
const res = await authFetch("/api/settings/instance-url", {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ instance_url: instanceUrl }),
|
|
128
|
+
});
|
|
129
|
+
const data = await res.json();
|
|
130
|
+
if (res.ok) {
|
|
131
|
+
setInstanceUrl(data.instance_url || "");
|
|
132
|
+
setMessage({ type: "success", text: "Instance URL saved" });
|
|
133
|
+
} else {
|
|
134
|
+
setMessage({ type: "error", text: data.error || "Failed to save" });
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
setMessage({ type: "error", text: "Failed to save" });
|
|
138
|
+
}
|
|
139
|
+
setSaving(false);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="max-w-4xl w-full">
|
|
144
|
+
<div className="mb-6">
|
|
145
|
+
<h1 className="text-2xl font-semibold mb-1">General</h1>
|
|
146
|
+
<p className="text-[#666]">Instance configuration.</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
150
|
+
<h3 className="font-medium mb-2">Instance URL</h3>
|
|
151
|
+
<p className="text-sm text-[#666] mb-4">
|
|
152
|
+
The public HTTPS URL for this instance. Used for webhook callbacks from external services like Composio.
|
|
153
|
+
</p>
|
|
154
|
+
|
|
155
|
+
{loading ? (
|
|
156
|
+
<div className="text-[#666] text-sm">Loading...</div>
|
|
157
|
+
) : (
|
|
158
|
+
<div className="space-y-3 max-w-lg">
|
|
159
|
+
<input
|
|
160
|
+
type="text"
|
|
161
|
+
value={instanceUrl}
|
|
162
|
+
onChange={e => setInstanceUrl(e.target.value)}
|
|
163
|
+
placeholder="https://your-domain.com"
|
|
164
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316] font-mono text-sm"
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
{message && (
|
|
168
|
+
<div className={`p-3 rounded text-sm ${
|
|
169
|
+
message.type === "success"
|
|
170
|
+
? "bg-green-500/10 text-green-400 border border-green-500/30"
|
|
171
|
+
: "bg-red-500/10 text-red-400 border border-red-500/30"
|
|
172
|
+
}`}>
|
|
173
|
+
{message.text}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleSave}
|
|
179
|
+
disabled={saving}
|
|
180
|
+
className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black rounded text-sm font-medium transition"
|
|
181
|
+
>
|
|
182
|
+
{saving ? "Saving..." : "Save"}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
95
191
|
function ProvidersSettings() {
|
|
96
192
|
const { authFetch } = useAuth();
|
|
97
193
|
const { projects, projectsEnabled } = useProjects();
|
|
@@ -1790,3 +1886,269 @@ function DataSettings() {
|
|
|
1790
1886
|
</>
|
|
1791
1887
|
);
|
|
1792
1888
|
}
|
|
1889
|
+
|
|
1890
|
+
// --- Channels Settings ---
|
|
1891
|
+
|
|
1892
|
+
interface ChannelInfo {
|
|
1893
|
+
id: string;
|
|
1894
|
+
type: string;
|
|
1895
|
+
name: string;
|
|
1896
|
+
agent_id: string;
|
|
1897
|
+
status: "stopped" | "running" | "error";
|
|
1898
|
+
error: string | null;
|
|
1899
|
+
created_at: string;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
interface AgentOption {
|
|
1903
|
+
id: string;
|
|
1904
|
+
name: string;
|
|
1905
|
+
status: string;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function ChannelsSettings() {
|
|
1909
|
+
const { authFetch } = useAuth();
|
|
1910
|
+
const [channels, setChannels] = useState<ChannelInfo[]>([]);
|
|
1911
|
+
const [agents, setAgents] = useState<AgentOption[]>([]);
|
|
1912
|
+
const [loading, setLoading] = useState(true);
|
|
1913
|
+
const [showForm, setShowForm] = useState(false);
|
|
1914
|
+
const [formData, setFormData] = useState({ name: "", agent_id: "", botToken: "" });
|
|
1915
|
+
const [creating, setCreating] = useState(false);
|
|
1916
|
+
const [error, setError] = useState<string | null>(null);
|
|
1917
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
1918
|
+
|
|
1919
|
+
const fetchChannels = async () => {
|
|
1920
|
+
try {
|
|
1921
|
+
const res = await authFetch("/api/channels");
|
|
1922
|
+
const data = await res.json();
|
|
1923
|
+
setChannels(data.channels || []);
|
|
1924
|
+
} catch {
|
|
1925
|
+
// Ignore
|
|
1926
|
+
} finally {
|
|
1927
|
+
setLoading(false);
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
const fetchAgents = async () => {
|
|
1932
|
+
try {
|
|
1933
|
+
const res = await authFetch("/api/agents");
|
|
1934
|
+
const data = await res.json();
|
|
1935
|
+
setAgents((data.agents || []).map((a: any) => ({ id: a.id, name: a.name, status: a.status })));
|
|
1936
|
+
} catch {
|
|
1937
|
+
// Ignore
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
useEffect(() => {
|
|
1942
|
+
fetchChannels();
|
|
1943
|
+
fetchAgents();
|
|
1944
|
+
}, []);
|
|
1945
|
+
|
|
1946
|
+
const createChannel = async () => {
|
|
1947
|
+
if (!formData.name || !formData.agent_id || !formData.botToken) return;
|
|
1948
|
+
setCreating(true);
|
|
1949
|
+
setError(null);
|
|
1950
|
+
|
|
1951
|
+
try {
|
|
1952
|
+
const res = await authFetch("/api/channels", {
|
|
1953
|
+
method: "POST",
|
|
1954
|
+
headers: { "Content-Type": "application/json" },
|
|
1955
|
+
body: JSON.stringify({
|
|
1956
|
+
type: "telegram",
|
|
1957
|
+
name: formData.name,
|
|
1958
|
+
agent_id: formData.agent_id,
|
|
1959
|
+
config: { botToken: formData.botToken },
|
|
1960
|
+
}),
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
if (!res.ok) {
|
|
1964
|
+
const data = await res.json();
|
|
1965
|
+
setError(data.error || "Failed to create channel");
|
|
1966
|
+
} else {
|
|
1967
|
+
setFormData({ name: "", agent_id: "", botToken: "" });
|
|
1968
|
+
setShowForm(false);
|
|
1969
|
+
await fetchChannels();
|
|
1970
|
+
}
|
|
1971
|
+
} catch (err: any) {
|
|
1972
|
+
setError(err.message);
|
|
1973
|
+
} finally {
|
|
1974
|
+
setCreating(false);
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
const toggleChannel = async (channel: ChannelInfo) => {
|
|
1979
|
+
const action = channel.status === "running" ? "stop" : "start";
|
|
1980
|
+
try {
|
|
1981
|
+
const res = await authFetch(`/api/channels/${channel.id}/${action}`, { method: "POST" });
|
|
1982
|
+
if (!res.ok) {
|
|
1983
|
+
const data = await res.json();
|
|
1984
|
+
setError(data.error || `Failed to ${action} channel`);
|
|
1985
|
+
}
|
|
1986
|
+
await fetchChannels();
|
|
1987
|
+
} catch {
|
|
1988
|
+
setError(`Failed to ${action} channel`);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
const deleteChannel = async (channel: ChannelInfo) => {
|
|
1993
|
+
const confirmed = await confirm(`Delete channel "${channel.name}"?`, {
|
|
1994
|
+
confirmText: "Delete",
|
|
1995
|
+
title: "Delete Channel",
|
|
1996
|
+
});
|
|
1997
|
+
if (!confirmed) return;
|
|
1998
|
+
|
|
1999
|
+
try {
|
|
2000
|
+
await authFetch(`/api/channels/${channel.id}`, { method: "DELETE" });
|
|
2001
|
+
await fetchChannels();
|
|
2002
|
+
} catch {
|
|
2003
|
+
// Ignore
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
const statusColors: Record<string, string> = {
|
|
2008
|
+
running: "bg-green-500/20 text-green-400",
|
|
2009
|
+
stopped: "bg-[#333] text-[#666]",
|
|
2010
|
+
error: "bg-red-500/20 text-red-400",
|
|
2011
|
+
};
|
|
2012
|
+
|
|
2013
|
+
const getAgentName = (agentId: string) => {
|
|
2014
|
+
return agents.find(a => a.id === agentId)?.name || agentId;
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
return (
|
|
2018
|
+
<>
|
|
2019
|
+
{ConfirmDialog}
|
|
2020
|
+
<div className="max-w-2xl">
|
|
2021
|
+
<div className="flex items-center justify-between mb-6">
|
|
2022
|
+
<div>
|
|
2023
|
+
<h2 className="text-xl font-semibold mb-1">Channels</h2>
|
|
2024
|
+
<p className="text-sm text-[#666]">Connect agents to external messaging platforms</p>
|
|
2025
|
+
</div>
|
|
2026
|
+
<button
|
|
2027
|
+
onClick={() => setShowForm(!showForm)}
|
|
2028
|
+
className="flex items-center gap-2 bg-[#f97316] hover:bg-[#fb923c] text-black px-3 py-1.5 rounded text-sm font-medium transition"
|
|
2029
|
+
>
|
|
2030
|
+
<PlusIcon /> Add Channel
|
|
2031
|
+
</button>
|
|
2032
|
+
</div>
|
|
2033
|
+
|
|
2034
|
+
{error && (
|
|
2035
|
+
<div className="mb-4 bg-red-500/10 text-red-400 border border-red-500/30 px-3 py-2 rounded text-sm flex items-center justify-between">
|
|
2036
|
+
<span>{error}</span>
|
|
2037
|
+
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300 ml-2">
|
|
2038
|
+
<CloseIcon />
|
|
2039
|
+
</button>
|
|
2040
|
+
</div>
|
|
2041
|
+
)}
|
|
2042
|
+
|
|
2043
|
+
{/* Create form */}
|
|
2044
|
+
{showForm && (
|
|
2045
|
+
<div className="mb-6 bg-[#111] border border-[#1a1a1a] rounded-lg p-4 space-y-3">
|
|
2046
|
+
<h3 className="text-sm font-medium text-[#888] mb-2">New Telegram Channel</h3>
|
|
2047
|
+
|
|
2048
|
+
<div>
|
|
2049
|
+
<label className="block text-xs text-[#666] mb-1">Channel Name</label>
|
|
2050
|
+
<input
|
|
2051
|
+
type="text"
|
|
2052
|
+
value={formData.name}
|
|
2053
|
+
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
2054
|
+
placeholder="e.g. My Telegram Bot"
|
|
2055
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
|
|
2056
|
+
/>
|
|
2057
|
+
</div>
|
|
2058
|
+
|
|
2059
|
+
<div>
|
|
2060
|
+
<label className="block text-xs text-[#666] mb-1">Agent</label>
|
|
2061
|
+
<Select
|
|
2062
|
+
value={formData.agent_id}
|
|
2063
|
+
options={agents.map(a => ({ value: a.id, label: a.name }))}
|
|
2064
|
+
onChange={value => setFormData(prev => ({ ...prev, agent_id: value }))}
|
|
2065
|
+
placeholder="Select an agent..."
|
|
2066
|
+
/>
|
|
2067
|
+
</div>
|
|
2068
|
+
|
|
2069
|
+
<div>
|
|
2070
|
+
<label className="block text-xs text-[#666] mb-1">Bot Token</label>
|
|
2071
|
+
<input
|
|
2072
|
+
type="password"
|
|
2073
|
+
value={formData.botToken}
|
|
2074
|
+
onChange={e => setFormData(prev => ({ ...prev, botToken: e.target.value }))}
|
|
2075
|
+
placeholder="From @BotFather on Telegram"
|
|
2076
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
|
|
2077
|
+
/>
|
|
2078
|
+
<p className="text-xs text-[#555] mt-1">
|
|
2079
|
+
Create a bot via <a href="https://t.me/BotFather" target="_blank" className="text-[#f97316] hover:underline">@BotFather</a> on Telegram to get a token.
|
|
2080
|
+
</p>
|
|
2081
|
+
</div>
|
|
2082
|
+
|
|
2083
|
+
<div className="flex gap-2 pt-1">
|
|
2084
|
+
<button
|
|
2085
|
+
onClick={createChannel}
|
|
2086
|
+
disabled={creating || !formData.name || !formData.agent_id || !formData.botToken}
|
|
2087
|
+
className="bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black px-4 py-1.5 rounded text-sm font-medium transition"
|
|
2088
|
+
>
|
|
2089
|
+
{creating ? "Creating..." : "Create"}
|
|
2090
|
+
</button>
|
|
2091
|
+
<button
|
|
2092
|
+
onClick={() => { setShowForm(false); setFormData({ name: "", agent_id: "", botToken: "" }); }}
|
|
2093
|
+
className="border border-[#333] hover:border-[#444] px-4 py-1.5 rounded text-sm transition"
|
|
2094
|
+
>
|
|
2095
|
+
Cancel
|
|
2096
|
+
</button>
|
|
2097
|
+
</div>
|
|
2098
|
+
</div>
|
|
2099
|
+
)}
|
|
2100
|
+
|
|
2101
|
+
{/* Channel list */}
|
|
2102
|
+
{loading ? (
|
|
2103
|
+
<p className="text-[#666] text-sm">Loading channels...</p>
|
|
2104
|
+
) : channels.length === 0 ? (
|
|
2105
|
+
<div className="text-center py-12 text-[#666]">
|
|
2106
|
+
<p className="text-lg mb-2">No channels configured</p>
|
|
2107
|
+
<p className="text-sm">Add a Telegram channel to let users message your agents directly.</p>
|
|
2108
|
+
</div>
|
|
2109
|
+
) : (
|
|
2110
|
+
<div className="space-y-3">
|
|
2111
|
+
{channels.map(channel => (
|
|
2112
|
+
<div key={channel.id} className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
|
|
2113
|
+
<div className="flex items-start justify-between">
|
|
2114
|
+
<div className="flex-1 min-w-0">
|
|
2115
|
+
<div className="flex items-center gap-2 mb-1">
|
|
2116
|
+
<h3 className="font-medium">{channel.name}</h3>
|
|
2117
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusColors[channel.status] || statusColors.stopped}`}>
|
|
2118
|
+
{channel.status}
|
|
2119
|
+
</span>
|
|
2120
|
+
</div>
|
|
2121
|
+
<p className="text-sm text-[#666]">
|
|
2122
|
+
{channel.type === "telegram" ? "Telegram" : channel.type} → {getAgentName(channel.agent_id)}
|
|
2123
|
+
</p>
|
|
2124
|
+
{channel.status === "error" && channel.error && (
|
|
2125
|
+
<p className="text-xs text-red-400 mt-1">{channel.error}</p>
|
|
2126
|
+
)}
|
|
2127
|
+
</div>
|
|
2128
|
+
<div className="flex items-center gap-2 ml-4">
|
|
2129
|
+
<button
|
|
2130
|
+
onClick={() => toggleChannel(channel)}
|
|
2131
|
+
className={`px-3 py-1 rounded text-xs font-medium transition ${
|
|
2132
|
+
channel.status === "running"
|
|
2133
|
+
? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
|
|
2134
|
+
: "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
|
|
2135
|
+
}`}
|
|
2136
|
+
>
|
|
2137
|
+
{channel.status === "running" ? "Stop" : "Start"}
|
|
2138
|
+
</button>
|
|
2139
|
+
<button
|
|
2140
|
+
onClick={() => deleteChannel(channel)}
|
|
2141
|
+
className="text-[#666] hover:text-red-400 transition text-sm"
|
|
2142
|
+
>
|
|
2143
|
+
×
|
|
2144
|
+
</button>
|
|
2145
|
+
</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
))}
|
|
2149
|
+
</div>
|
|
2150
|
+
)}
|
|
2151
|
+
</div>
|
|
2152
|
+
</>
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
@@ -500,7 +500,7 @@ function TrajectoryView({ trajectory }: { trajectory: TaskTrajectoryStep[] }) {
|
|
|
500
500
|
|
|
501
501
|
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
502
502
|
|
|
503
|
-
function formatCron(cron: string): string {
|
|
503
|
+
export function formatCron(cron: string): string {
|
|
504
504
|
try {
|
|
505
505
|
const parts = cron.trim().split(/\s+/);
|
|
506
506
|
if (parts.length !== 5) return cron;
|
|
@@ -558,7 +558,7 @@ function formatCron(cron: string): string {
|
|
|
558
558
|
}
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
function formatRelativeTime(dateStr: string): string {
|
|
561
|
+
export function formatRelativeTime(dateStr: string): string {
|
|
562
562
|
const date = new Date(dateStr);
|
|
563
563
|
const now = new Date();
|
|
564
564
|
const diffMs = date.getTime() - now.getTime();
|
|
@@ -21,6 +21,7 @@ interface TelemetryContextValue {
|
|
|
21
21
|
activeAgents: Record<string, { type: string; expiresAt: number }>;
|
|
22
22
|
statusChangeCounter: number;
|
|
23
23
|
taskChangeCounter: number;
|
|
24
|
+
notificationCounter: number;
|
|
24
25
|
clearEvents: () => void;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -35,6 +36,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
|
35
36
|
const [activeAgents, setActiveAgents] = useState<Record<string, { type: string; expiresAt: number }>>({});
|
|
36
37
|
const [statusChangeCounter, setStatusChangeCounter] = useState(0);
|
|
37
38
|
const [taskChangeCounter, setTaskChangeCounter] = useState(0);
|
|
39
|
+
const [notificationCounter, setNotificationCounter] = useState(0);
|
|
38
40
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
39
41
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
40
42
|
|
|
@@ -124,6 +126,11 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
|
124
126
|
if (data.some((e: TelemetryEvent) => e.category === "TASK" && (e.type === "task_created" || e.type === "task_updated" || e.type === "task_deleted"))) {
|
|
125
127
|
setTaskChangeCounter(c => c + 1);
|
|
126
128
|
}
|
|
129
|
+
|
|
130
|
+
// Detect notification-worthy events (errors, agent crashes)
|
|
131
|
+
if (data.some((e: TelemetryEvent) => e.level === "error" || e.category === "ERROR" || (e.category === "system" && e.type === "agent_stopped"))) {
|
|
132
|
+
setNotificationCounter(c => c + 1);
|
|
133
|
+
}
|
|
127
134
|
}
|
|
128
135
|
} catch {
|
|
129
136
|
// Ignore parse errors (likely keepalive or empty message)
|
|
@@ -169,7 +176,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
|
169
176
|
}, []);
|
|
170
177
|
|
|
171
178
|
return (
|
|
172
|
-
<TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, statusChangeCounter, taskChangeCounter, clearEvents }}>
|
|
179
|
+
<TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, statusChangeCounter, taskChangeCounter, notificationCounter, clearEvents }}>
|
|
173
180
|
{children}
|
|
174
181
|
</TelemetryContext.Provider>
|
|
175
182
|
);
|
|
@@ -248,3 +255,9 @@ export function useTaskChange(): number {
|
|
|
248
255
|
const { taskChangeCounter } = useTelemetryContext();
|
|
249
256
|
return taskChangeCounter;
|
|
250
257
|
}
|
|
258
|
+
|
|
259
|
+
// Hook to trigger notification badge refresh
|
|
260
|
+
export function useNotificationChange(): number {
|
|
261
|
+
const { notificationCounter } = useTelemetryContext();
|
|
262
|
+
return notificationCounter;
|
|
263
|
+
}
|
package/src/web/context/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity, useAgentStatusChange, useTaskChange } from "./TelemetryContext";
|
|
1
|
+
export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity, useAgentStatusChange, useTaskChange, useNotificationChange } from "./TelemetryContext";
|
|
2
2
|
export type { TelemetryEvent } from "./TelemetryContext";
|
|
3
3
|
|
|
4
4
|
export { AuthProvider, useAuth, useAuthHeaders } from "./AuthContext";
|
|
@@ -23,19 +23,13 @@ export function useAgents(enabled: boolean) {
|
|
|
23
23
|
setLoading(false);
|
|
24
24
|
}, [getHeaders]);
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Fetch on mount + auto-refetch when agents start/stop/crash (via SSE telemetry)
|
|
27
27
|
const statusChangeCounter = useAgentStatusChange();
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (enabled && statusChangeCounter > 0) {
|
|
30
|
-
fetchAgents();
|
|
31
|
-
}
|
|
32
|
-
}, [enabled, statusChangeCounter, fetchAgents]);
|
|
33
|
-
|
|
34
28
|
useEffect(() => {
|
|
35
29
|
if (enabled) {
|
|
36
30
|
fetchAgents();
|
|
37
31
|
}
|
|
38
|
-
}, [enabled, fetchAgents]);
|
|
32
|
+
}, [enabled, statusChangeCounter, fetchAgents]);
|
|
39
33
|
|
|
40
34
|
const createAgent = async (agent: {
|
|
41
35
|
name: string;
|
|
@@ -83,10 +77,20 @@ export function useAgents(enabled: boolean) {
|
|
|
83
77
|
|
|
84
78
|
const toggleAgent = async (agent: Agent): Promise<{ error?: string }> => {
|
|
85
79
|
const action = agent.status === "running" ? "stop" : "start";
|
|
80
|
+
|
|
81
|
+
// Optimistic UI update — show transitioning state immediately
|
|
82
|
+
setAgents(prev => prev.map(a =>
|
|
83
|
+
a.id === agent.id ? { ...a, status: action === "start" ? "starting" as any : "stopping" as any } : a
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
// Fire API call — telemetry SSE will trigger a refetch with the real status
|
|
86
87
|
const res = await fetch(`/api/agents/${agent.id}/${action}`, { method: "POST", headers: getHeaders() });
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
// Revert on error
|
|
91
|
+
setAgents(prev => prev.map(a =>
|
|
92
|
+
a.id === agent.id ? { ...a, status: agent.status } : a
|
|
93
|
+
));
|
|
90
94
|
return { error: data.error };
|
|
91
95
|
}
|
|
92
96
|
return {};
|
package/src/web/types.ts
CHANGED
|
@@ -143,7 +143,7 @@ export interface OnboardingStatus {
|
|
|
143
143
|
has_any_keys: boolean;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export type Route = "dashboard" | "activity" | "agents" | "tasks" | "mcp" | "skills" | "tests" | "telemetry" | "settings" | "api";
|
|
146
|
+
export type Route = "dashboard" | "activity" | "agents" | "tasks" | "connections" | "mcp" | "skills" | "tests" | "telemetry" | "settings" | "api";
|
|
147
147
|
|
|
148
148
|
// Tool use content block in trajectory
|
|
149
149
|
export interface ToolUseBlock {
|