@raingor/pi-web-switch 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +17 -1
- package/README.md +17 -1
- package/README.zh-CN.md +17 -1
- package/package.json +46 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/pi.svg +47 -4
- package/server/pi-reader.ts +750 -4
- package/src/App.tsx +2 -0
- package/src/components/layout/Sidebar.tsx +3 -5
- package/src/components/providers/ProvidersModelsPage.tsx +1171 -124
- package/src/components/settings/SettingsPage.tsx +74 -55
- package/src/components/subagents/SubagentsPage.tsx +502 -0
- package/src/data/builtin-providers.ts +15 -7
- package/src/data/model-catalog.ts +967 -0
- package/src/index.css +5 -1
- package/src/lib/config.ts +61 -0
- package/src/lib/translations/en.ts +91 -1
- package/src/lib/translations/ja.ts +91 -1
- package/src/lib/translations/zh-CN.ts +91 -1
- package/src/lib/translations/zh-TW.ts +91 -1
- package/src/main.tsx +9 -0
- package/src/store/config-store.ts +75 -28
- package/src/types/index.ts +52 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +58 -2
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useTranslation } from "@/lib/i18n";
|
|
3
|
+
import { Badge } from "@/components/ui/Badge";
|
|
4
|
+
import { EmptyState } from "@/components/ui/EmptyState";
|
|
5
|
+
import { formatTokens } from "@/lib/utils";
|
|
6
|
+
import type { AgentDef, ChainDef, ChainStep, RunRecord, SubagentsData } from "@/types";
|
|
7
|
+
import {
|
|
8
|
+
Brain,
|
|
9
|
+
GitBranch,
|
|
10
|
+
History,
|
|
11
|
+
Box,
|
|
12
|
+
Users,
|
|
13
|
+
FileCode,
|
|
14
|
+
CheckCircle2,
|
|
15
|
+
XCircle,
|
|
16
|
+
Loader2,
|
|
17
|
+
Search,
|
|
18
|
+
ExternalLink,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
|
|
21
|
+
const API_BASE = "/api/pi";
|
|
22
|
+
|
|
23
|
+
type Tab = "agents" | "chains" | "history";
|
|
24
|
+
|
|
25
|
+
export function SubagentsPage() {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const [data, setData] = useState<SubagentsData | null>(null);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
const [tab, setTab] = useState<Tab>("agents");
|
|
31
|
+
const [search, setSearch] = useState("");
|
|
32
|
+
|
|
33
|
+
const loadData = async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${API_BASE}/subagents`);
|
|
38
|
+
if (!res.ok) throw new Error(`API /subagents: ${res.status}`);
|
|
39
|
+
const json = await res.json();
|
|
40
|
+
setData(json);
|
|
41
|
+
} catch (e: any) {
|
|
42
|
+
setError(e.message || "Failed to load subagents data");
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
loadData();
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const q = search.trim().toLowerCase();
|
|
53
|
+
|
|
54
|
+
const filteredAgents = data?.agents.filter(
|
|
55
|
+
(a) => !q || a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const filteredChains = data?.chains.filter(
|
|
59
|
+
(c) => !q || c.name.toLowerCase().includes(q) || c.description.toLowerCase().includes(q)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const filteredHistory = data?.runHistory.filter(
|
|
63
|
+
(r) => !q || r.agent.toLowerCase().includes(q) || r.status.toLowerCase().includes(q)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (loading && !data) {
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex h-60 items-center justify-center">
|
|
69
|
+
<Loader2 className="h-6 w-6 animate-spin text-gray-500" />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (error && !data) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex h-60 flex-col items-center justify-center gap-3">
|
|
77
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
78
|
+
<button
|
|
79
|
+
onClick={loadData}
|
|
80
|
+
className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-500"
|
|
81
|
+
>
|
|
82
|
+
{t("loading.retry")}
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="space-y-6">
|
|
90
|
+
{/* Header */}
|
|
91
|
+
<div className="flex items-center justify-between">
|
|
92
|
+
<div>
|
|
93
|
+
<h1 className="text-2xl font-bold" style={{ color: "var(--page-text)" }}>
|
|
94
|
+
{t("nav.subagents")}
|
|
95
|
+
</h1>
|
|
96
|
+
<p className="mt-1 text-sm" style={{ color: "var(--muted-text)" }}>
|
|
97
|
+
{data && t("subagents.summary", String(data.agents.length), String(data.chains.length))}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<button
|
|
101
|
+
onClick={loadData}
|
|
102
|
+
className="flex items-center gap-2 rounded-lg border border-gray-700 px-3 py-2 text-sm text-gray-300 hover:bg-gray-800"
|
|
103
|
+
>
|
|
104
|
+
<Loader2 className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
|
105
|
+
{t("dashboard.refresh_now")}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Tabs */}
|
|
110
|
+
<div className="flex gap-1 rounded-lg bg-gray-900 p-1">
|
|
111
|
+
{(["agents", "chains", "history"] as Tab[]).map((tKey) => (
|
|
112
|
+
<button
|
|
113
|
+
key={tKey}
|
|
114
|
+
onClick={() => setTab(tKey)}
|
|
115
|
+
className={`flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
|
116
|
+
tab === tKey
|
|
117
|
+
? "bg-blue-600/10 text-blue-400"
|
|
118
|
+
: "text-gray-400 hover:text-gray-200"
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
{tKey === "agents" && <Brain className="h-4 w-4" />}
|
|
122
|
+
{tKey === "chains" && <GitBranch className="h-4 w-4" />}
|
|
123
|
+
{tKey === "history" && <History className="h-4 w-4" />}
|
|
124
|
+
{t(`subagents.tab_${tKey}`)}
|
|
125
|
+
{data && tKey === "agents" && (
|
|
126
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${tab === "agents" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.agents.length}</span>
|
|
127
|
+
)}
|
|
128
|
+
{data && tKey === "chains" && (
|
|
129
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${tab === "chains" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.chains.length}</span>
|
|
130
|
+
)}
|
|
131
|
+
{data && tKey === "history" && (
|
|
132
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${tab === "history" ? "bg-blue-600/20 text-blue-300" : "bg-blue-600/10 text-blue-400"}`}>{data.runHistory.length}</span>
|
|
133
|
+
)}
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
{data && (data.agents.length > 5 || data.chains.length > 5) && (
|
|
137
|
+
<div className="relative ml-auto">
|
|
138
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
value={search}
|
|
142
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
143
|
+
placeholder={t("models.search_placeholder")}
|
|
144
|
+
className="w-48 rounded-lg border border-gray-700 bg-gray-800 py-2 pl-9 pr-3 text-sm text-white"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Tab Content */}
|
|
151
|
+
<div>
|
|
152
|
+
{tab === "agents" && (
|
|
153
|
+
<AgentList
|
|
154
|
+
agents={filteredAgents ?? []}
|
|
155
|
+
onRefresh={loadData}
|
|
156
|
+
searchActive={!!q}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
{tab === "chains" && (
|
|
160
|
+
<ChainList
|
|
161
|
+
chains={filteredChains ?? []}
|
|
162
|
+
searchActive={!!q}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
{tab === "history" && (
|
|
166
|
+
<RunHistoryList
|
|
167
|
+
records={filteredHistory ?? []}
|
|
168
|
+
searchActive={!!q}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Agent List ────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function AgentList({
|
|
179
|
+
agents,
|
|
180
|
+
onRefresh,
|
|
181
|
+
searchActive,
|
|
182
|
+
}: {
|
|
183
|
+
agents: AgentDef[];
|
|
184
|
+
onRefresh: () => void;
|
|
185
|
+
searchActive: boolean;
|
|
186
|
+
}) {
|
|
187
|
+
const { t } = useTranslation();
|
|
188
|
+
const [selected, setSelected] = useState<AgentDef | null>(null);
|
|
189
|
+
|
|
190
|
+
if (agents.length === 0) {
|
|
191
|
+
return (
|
|
192
|
+
<EmptyState
|
|
193
|
+
icon={<Brain className="h-8 w-8" />}
|
|
194
|
+
title={t("subagents.no_agents")}
|
|
195
|
+
description={t("subagents.no_agents_desc")}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className="flex overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50">
|
|
202
|
+
{/* Left: Agent cards */}
|
|
203
|
+
<div className="w-72 shrink-0 border-r border-gray-800 p-3 space-y-2 overflow-y-auto max-h-[70vh]">
|
|
204
|
+
{agents.map((agent) => (
|
|
205
|
+
<button
|
|
206
|
+
key={agent.fileName}
|
|
207
|
+
onClick={() => setSelected(agent)}
|
|
208
|
+
className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
|
|
209
|
+
selected?.fileName === agent.fileName
|
|
210
|
+
? "border-blue-500/30 bg-gray-800 text-white"
|
|
211
|
+
: "border-gray-700 text-gray-300 hover:bg-gray-800 hover:text-white"
|
|
212
|
+
}`}
|
|
213
|
+
>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<Brain className="h-4 w-4 shrink-0 text-blue-400" />
|
|
216
|
+
<span className="truncate text-sm font-medium">{agent.name}</span>
|
|
217
|
+
<Badge variant={agent.package === "custom" ? "default" : "info"}>
|
|
218
|
+
{agent.package}
|
|
219
|
+
</Badge>
|
|
220
|
+
</div>
|
|
221
|
+
<p className="mt-1 line-clamp-2 text-xs text-gray-500">{agent.description}</p>
|
|
222
|
+
</button>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Right: Agent detail */}
|
|
227
|
+
<div className="min-w-0 flex-1 p-6">
|
|
228
|
+
{selected ? (
|
|
229
|
+
<AgentDetail agent={selected} />
|
|
230
|
+
) : (
|
|
231
|
+
<div className="flex h-40 items-center justify-center text-sm text-gray-500">
|
|
232
|
+
{t("providers_models.select_hint")}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function AgentDetail({ agent }: { agent: AgentDef }) {
|
|
241
|
+
const { t } = useTranslation();
|
|
242
|
+
return (
|
|
243
|
+
<div className="space-y-4">
|
|
244
|
+
<div className="flex items-center gap-3">
|
|
245
|
+
<h2 className="text-lg font-semibold text-white">{agent.name}</h2>
|
|
246
|
+
<Badge variant={agent.package === "custom" ? "default" : "info"}>
|
|
247
|
+
{agent.package}
|
|
248
|
+
</Badge>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<p className="text-sm text-gray-400">{agent.description}</p>
|
|
252
|
+
|
|
253
|
+
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-800 bg-gray-900/70 p-4">
|
|
254
|
+
{agent.model && (
|
|
255
|
+
<div>
|
|
256
|
+
<span className="text-xs text-gray-500">{t("subagents.model")}</span>
|
|
257
|
+
<p className="mt-0.5 text-sm text-gray-200 font-mono">{agent.model}</p>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
{agent.thinking && (
|
|
261
|
+
<div>
|
|
262
|
+
<span className="text-xs text-gray-500">{t("subagents.thinking")}</span>
|
|
263
|
+
<p className="mt-0.5 text-sm text-gray-200">{agent.thinking}</p>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
{agent.tools && (
|
|
267
|
+
<div className="col-span-2">
|
|
268
|
+
<span className="text-xs text-gray-500">{t("subagents.tools")}</span>
|
|
269
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
270
|
+
{agent.tools.map((tool) => (
|
|
271
|
+
<span
|
|
272
|
+
key={tool}
|
|
273
|
+
className="rounded-md border border-gray-700 bg-gray-800 px-2 py-0.5 font-mono text-xs text-gray-300"
|
|
274
|
+
>
|
|
275
|
+
{tool}
|
|
276
|
+
</span>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
<div>
|
|
282
|
+
<span className="text-xs text-gray-500">{t("subagents.system_prompt_mode")}</span>
|
|
283
|
+
<p className="mt-0.5 text-sm text-gray-200">{agent.systemPromptMode || "replace"}</p>
|
|
284
|
+
</div>
|
|
285
|
+
<div>
|
|
286
|
+
<span className="text-xs text-gray-500">{t("subagents.input")}</span>
|
|
287
|
+
<p className="mt-0.5 text-sm text-gray-200">{(agent.input ?? ["text"]).join(", ")}</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Body (system prompt preview) */}
|
|
292
|
+
<div>
|
|
293
|
+
<span className="text-xs text-gray-500">{t("subagents.system_prompt")}</span>
|
|
294
|
+
<pre className="mt-1.5 max-h-48 overflow-y-auto rounded-lg border border-gray-800 bg-gray-950 p-3 text-xs text-gray-400 whitespace-pre-wrap font-mono">
|
|
295
|
+
{agent.body || t("subagents.empty_prompt")}
|
|
296
|
+
</pre>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Chain List ────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
function ChainList({
|
|
305
|
+
chains,
|
|
306
|
+
searchActive,
|
|
307
|
+
}: {
|
|
308
|
+
chains: ChainDef[];
|
|
309
|
+
searchActive: boolean;
|
|
310
|
+
}) {
|
|
311
|
+
const { t } = useTranslation();
|
|
312
|
+
const [selected, setSelected] = useState<ChainDef | null>(null);
|
|
313
|
+
|
|
314
|
+
if (chains.length === 0) {
|
|
315
|
+
return (
|
|
316
|
+
<EmptyState
|
|
317
|
+
icon={<GitBranch className="h-8 w-8" />}
|
|
318
|
+
title={t("subagents.no_chains")}
|
|
319
|
+
description={t("subagents.no_chains_desc")}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div className="flex overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50">
|
|
326
|
+
<div className="w-72 shrink-0 border-r border-gray-800 p-3 space-y-2 overflow-y-auto max-h-[70vh]">
|
|
327
|
+
{chains.map((chain) => (
|
|
328
|
+
<button
|
|
329
|
+
key={chain.fileName}
|
|
330
|
+
onClick={() => setSelected(chain)}
|
|
331
|
+
className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
|
|
332
|
+
selected?.fileName === chain.fileName
|
|
333
|
+
? "border-blue-500/30 bg-gray-800 text-white"
|
|
334
|
+
: "border-gray-700 text-gray-300 hover:bg-gray-800 hover:text-white"
|
|
335
|
+
}`}
|
|
336
|
+
>
|
|
337
|
+
<div className="flex items-center gap-2">
|
|
338
|
+
<GitBranch className="h-4 w-4 shrink-0 text-emerald-400" />
|
|
339
|
+
<span className="truncate text-sm font-medium">{chain.name}</span>
|
|
340
|
+
</div>
|
|
341
|
+
<p className="mt-1 line-clamp-2 text-xs text-gray-500">{chain.description}</p>
|
|
342
|
+
<p className="mt-1 text-xs text-gray-600">
|
|
343
|
+
{chain.steps.length} {t("subagents.steps_count").toLowerCase()}
|
|
344
|
+
</p>
|
|
345
|
+
</button>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div className="min-w-0 flex-1 p-6">
|
|
350
|
+
{selected ? (
|
|
351
|
+
<ChainDetail chain={selected} />
|
|
352
|
+
) : (
|
|
353
|
+
<div className="flex h-40 items-center justify-center text-sm text-gray-500">
|
|
354
|
+
{t("providers_models.select_hint")}
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function StepIcon({ agent }: { agent: string }) {
|
|
363
|
+
const isParallel = agent.includes("|");
|
|
364
|
+
if (isParallel) return <Users className="h-4 w-4 text-purple-400" />;
|
|
365
|
+
return <Box className="h-4 w-4 text-blue-400" />;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function ChainDetail({ chain }: { chain: ChainDef }) {
|
|
369
|
+
const { t } = useTranslation();
|
|
370
|
+
return (
|
|
371
|
+
<div className="space-y-4">
|
|
372
|
+
<h2 className="text-lg font-semibold text-white">{chain.name}</h2>
|
|
373
|
+
<p className="text-sm text-gray-400">{chain.description}</p>
|
|
374
|
+
|
|
375
|
+
<div className="space-y-3">
|
|
376
|
+
<span className="text-xs font-medium text-gray-500">{t("subagents.pipeline")}</span>
|
|
377
|
+
<div className="relative">
|
|
378
|
+
{/* Vertical line connector */}
|
|
379
|
+
<div className="absolute left-4 top-2 bottom-2 w-0.5 bg-gray-700" />
|
|
380
|
+
|
|
381
|
+
{chain.steps.map((step, i) => (
|
|
382
|
+
<div key={i} className="relative flex items-start gap-4 pb-4 last:pb-0">
|
|
383
|
+
<div className="z-10 flex h-8 w-8 items-center justify-center rounded-full border border-gray-600 bg-gray-800">
|
|
384
|
+
<StepIcon agent={step.agent} />
|
|
385
|
+
</div>
|
|
386
|
+
<div className="min-w-0 flex-1 pt-1">
|
|
387
|
+
<p className="text-sm text-gray-200">
|
|
388
|
+
{step.agent.split("|").map((a, j) => (
|
|
389
|
+
<span key={j}>
|
|
390
|
+
{j > 0 && <span className="text-gray-500 mx-1">|</span>}
|
|
391
|
+
<code className="text-blue-300">{a.trim()}</code>
|
|
392
|
+
</span>
|
|
393
|
+
))}
|
|
394
|
+
</p>
|
|
395
|
+
<div className="mt-1 flex flex-wrap gap-2 text-xs text-gray-500">
|
|
396
|
+
{step.phase && <span>{t("subagents.phase")}: {step.phase}</span>}
|
|
397
|
+
{step.label && <span>{t("subagents.label")}: {step.label}</span>}
|
|
398
|
+
{step.output && <span>{t("subagents.output")}: {step.output}</span>}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
<span className="shrink-0 text-xs text-gray-600">#{i + 1}</span>
|
|
402
|
+
</div>
|
|
403
|
+
))}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Run History ───────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function RunHistoryList({
|
|
413
|
+
records,
|
|
414
|
+
searchActive,
|
|
415
|
+
}: {
|
|
416
|
+
records: RunRecord[];
|
|
417
|
+
searchActive: boolean;
|
|
418
|
+
}) {
|
|
419
|
+
const { t } = useTranslation();
|
|
420
|
+
|
|
421
|
+
if (records.length === 0) {
|
|
422
|
+
return (
|
|
423
|
+
<EmptyState
|
|
424
|
+
icon={<History className="h-8 w-8" />}
|
|
425
|
+
title={t("subagents.no_history")}
|
|
426
|
+
description={t("subagents.no_history_desc")}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div className="overflow-hidden rounded-xl border border-gray-800">
|
|
433
|
+
<table className="w-full text-sm">
|
|
434
|
+
<thead>
|
|
435
|
+
<tr className="border-b border-gray-800 bg-gray-900/70">
|
|
436
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.agent")}</th>
|
|
437
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.time")}</th>
|
|
438
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500">{t("subagents.status")}</th>
|
|
439
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500">{t("subagents.duration")}</th>
|
|
440
|
+
</tr>
|
|
441
|
+
</thead>
|
|
442
|
+
<tbody className="divide-y divide-gray-800">
|
|
443
|
+
{records.map((r, i) => (
|
|
444
|
+
<tr key={`${r.taskHash}-${i}`} className="hover:bg-gray-800/40">
|
|
445
|
+
<td className="px-4 py-3">
|
|
446
|
+
<code className="text-sm text-blue-300">{r.agent}</code>
|
|
447
|
+
</td>
|
|
448
|
+
<td className="px-4 py-3 text-gray-400">
|
|
449
|
+
{formatTimestamp(r.ts)}
|
|
450
|
+
</td>
|
|
451
|
+
<td className="px-4 py-3">
|
|
452
|
+
{r.status === "ok" ? (
|
|
453
|
+
<span className="flex items-center gap-1 text-emerald-400">
|
|
454
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
455
|
+
{t("subagents.status_ok")}
|
|
456
|
+
</span>
|
|
457
|
+
) : (
|
|
458
|
+
<span className="flex items-center gap-1 text-red-400">
|
|
459
|
+
<XCircle className="h-3.5 w-3.5" />
|
|
460
|
+
{t("subagents.status_error")}
|
|
461
|
+
{r.exit != null && <span className="text-xs text-gray-500">(exit {r.exit})</span>}
|
|
462
|
+
</span>
|
|
463
|
+
)}
|
|
464
|
+
</td>
|
|
465
|
+
<td className="px-4 py-3 text-right text-gray-400">
|
|
466
|
+
{r.duration != null ? formatDuration(r.duration) : "—"}
|
|
467
|
+
</td>
|
|
468
|
+
</tr>
|
|
469
|
+
))}
|
|
470
|
+
</tbody>
|
|
471
|
+
</table>
|
|
472
|
+
{records.length >= 100 && (
|
|
473
|
+
<p className="border-t border-gray-800 px-4 py-2 text-xs text-gray-500">
|
|
474
|
+
{t("subagents.showing_recent")}
|
|
475
|
+
</p>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
function formatTimestamp(ts: number): string {
|
|
484
|
+
const d = new Date(ts * 1000);
|
|
485
|
+
const now = new Date();
|
|
486
|
+
const diffMs = now.getTime() - d.getTime();
|
|
487
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
488
|
+
if (diffMin < 1) return "just now";
|
|
489
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
490
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
491
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
492
|
+
return d.toLocaleDateString();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function formatDuration(ms: number): string {
|
|
496
|
+
if (ms < 1000) return `${ms}ms`;
|
|
497
|
+
const sec = Math.floor(ms / 1000);
|
|
498
|
+
if (sec < 60) return `${sec}s`;
|
|
499
|
+
const min = Math.floor(sec / 60);
|
|
500
|
+
const rem = sec % 60;
|
|
501
|
+
return `${min}m ${rem}s`;
|
|
502
|
+
}
|
|
@@ -10,12 +10,13 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
10
10
|
name: "Anthropic",
|
|
11
11
|
type: "builtin",
|
|
12
12
|
api: "anthropic-messages",
|
|
13
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
13
14
|
hasAuth: true,
|
|
14
15
|
authMethod: "env",
|
|
15
16
|
models: [
|
|
16
|
-
{ id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens:
|
|
17
|
-
{ id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens:
|
|
18
|
-
{ id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens:
|
|
17
|
+
{ id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
|
|
18
|
+
{ id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
|
|
19
|
+
{ id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 32000, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
|
|
19
20
|
{ id: "claude-haiku-3-5", name: "Claude 3.5 Haiku", reasoning: false, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }, enabled: true },
|
|
20
21
|
],
|
|
21
22
|
},
|
|
@@ -24,6 +25,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
24
25
|
name: "OpenAI",
|
|
25
26
|
type: "builtin",
|
|
26
27
|
api: "openai-completions",
|
|
28
|
+
baseUrl: "https://api.openai.com/v1",
|
|
27
29
|
hasAuth: true,
|
|
28
30
|
authMethod: "env",
|
|
29
31
|
models: [
|
|
@@ -38,6 +40,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
38
40
|
name: "DeepSeek",
|
|
39
41
|
type: "builtin",
|
|
40
42
|
api: "openai-completions",
|
|
43
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
41
44
|
hasAuth: true,
|
|
42
45
|
authMethod: "env",
|
|
43
46
|
models: [
|
|
@@ -77,11 +80,12 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
77
80
|
name: "Google Gemini",
|
|
78
81
|
type: "builtin",
|
|
79
82
|
api: "google-generative-ai",
|
|
83
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
80
84
|
hasAuth: true,
|
|
81
85
|
authMethod: "env",
|
|
82
86
|
models: [
|
|
83
|
-
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens:
|
|
84
|
-
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens:
|
|
87
|
+
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
|
|
88
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false },
|
|
85
89
|
],
|
|
86
90
|
},
|
|
87
91
|
{
|
|
@@ -89,10 +93,11 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
89
93
|
name: "OpenRouter",
|
|
90
94
|
type: "builtin",
|
|
91
95
|
api: "openai-completions",
|
|
96
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
92
97
|
hasAuth: false,
|
|
93
98
|
authMethod: "none",
|
|
94
99
|
models: [
|
|
95
|
-
{ id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens:
|
|
100
|
+
{ id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
|
|
96
101
|
{ id: "openrouter/deepseek/deepseek-r1", name: "DeepSeek R1 (OpenRouter)", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: false },
|
|
97
102
|
],
|
|
98
103
|
},
|
|
@@ -101,6 +106,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
101
106
|
name: "Mistral",
|
|
102
107
|
type: "builtin",
|
|
103
108
|
api: "mistral-conversations",
|
|
109
|
+
baseUrl: "https://api.mistral.ai/v1",
|
|
104
110
|
hasAuth: false,
|
|
105
111
|
authMethod: "none",
|
|
106
112
|
models: [
|
|
@@ -112,6 +118,7 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
112
118
|
name: "GitHub Copilot",
|
|
113
119
|
type: "builtin",
|
|
114
120
|
api: "openai-completions",
|
|
121
|
+
baseUrl: "https://api.githubcopilot.com",
|
|
115
122
|
hasAuth: false,
|
|
116
123
|
authMethod: "none",
|
|
117
124
|
models: [
|
|
@@ -123,10 +130,11 @@ export const BUILTIN_PROVIDERS: Provider[] = [
|
|
|
123
130
|
name: "Groq",
|
|
124
131
|
type: "builtin",
|
|
125
132
|
api: "openai-completions",
|
|
133
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
126
134
|
hasAuth: false,
|
|
127
135
|
authMethod: "none",
|
|
128
136
|
models: [
|
|
129
|
-
{ id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens:
|
|
137
|
+
{ id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 4096, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false },
|
|
130
138
|
],
|
|
131
139
|
},
|
|
132
140
|
];
|