@agentprojectcontext/apx 1.39.0 → 1.40.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/package.json +1 -2
- package/src/core/agent/constants.js +7 -1
- package/src/core/agent/retry.js +9 -0
- package/src/core/agent/run-agent.js +56 -5
- package/src/core/agent/tools/pseudo-tools.js +13 -1
- package/src/core/channels/telegram/dispatch.js +23 -3
- package/src/core/engines/mock.js +33 -10
- package/src/core/i18n/en.js +2 -4
- package/src/core/i18n/es.js +1 -4
- package/src/core/i18n/index.js +5 -1
- package/src/core/i18n/pt.js +1 -3
- package/src/core/routines/runner.js +15 -3
- package/src/host/daemon/api/admin.js +29 -0
- package/src/interfaces/web/dist/assets/index-Cg-uHCex.js +646 -0
- package/src/interfaces/web/dist/assets/index-Cg-uHCex.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +11 -11
- package/src/interfaces/web/src/App.tsx +22 -11
- package/src/interfaces/web/src/components/AddProjectDialog.tsx +66 -34
- package/src/interfaces/web/src/components/ModelCombobox.tsx +6 -3
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +28 -25
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +19 -17
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +9 -7
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +21 -19
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +3 -2
- package/src/interfaces/web/src/components/routines/AvailableVarsCard.tsx +23 -0
- package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +189 -0
- package/src/interfaces/web/src/components/routines/ReadOnlyBlock.tsx +14 -0
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +86 -0
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +263 -0
- package/src/interfaces/web/src/components/routines/RoutineList.tsx +59 -0
- package/src/interfaces/web/src/components/routines/VarTextarea.tsx +70 -0
- package/src/interfaces/web/src/components/routines/shared.ts +89 -0
- package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +19 -16
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +10 -8
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +7 -4
- package/src/interfaces/web/src/components/ui/chat-input.tsx +24 -21
- package/src/interfaces/web/src/components/ui/sidebar.tsx +20 -18
- package/src/interfaces/web/src/components/ui.tsx +4 -0
- package/src/interfaces/web/src/i18n/en.ts +34 -11
- package/src/interfaces/web/src/i18n/es.ts +34 -11
- package/src/interfaces/web/src/lib/api/filesystem.ts +6 -0
- package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +11 -3
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +6 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -5
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +16 -9
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +126 -373
- package/src/interfaces/web/src/styles.css +5 -0
- package/src/interfaces/web/dist/assets/index-CAKEYko0.css +0 -1
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js +0 -639
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js.map +0 -1
|
@@ -1,159 +1,116 @@
|
|
|
1
|
-
import { useEffect,
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { UiSelect } from "../../components/UiSelect";
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import useSWR, { mutate as globalMutate } from "swr";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
import { Routines, type RoutineEntry } from "../../lib/api";
|
|
6
|
+
import { Button, Dialog, Empty, Loading } from "../../components/ui";
|
|
8
7
|
import { useToast } from "../../components/Toast";
|
|
9
|
-
import { cn } from "../../lib/cn";
|
|
10
8
|
import { t } from "../../i18n";
|
|
9
|
+
import { RoutineList } from "../../components/routines/RoutineList";
|
|
10
|
+
import { RoutineDetail } from "../../components/routines/RoutineDetail";
|
|
11
|
+
import { RoutineEditor } from "../../components/routines/RoutineEditor";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type Kind = RoutineEntry["kind"];
|
|
17
|
-
|
|
18
|
-
// Friendly action types (maps to routines.js kinds).
|
|
19
|
-
function kindMeta(): Record<Kind, { label: string; desc: string; icon: typeof Bot }> {
|
|
20
|
-
return {
|
|
21
|
-
exec_agent: { label: t("agents_ui.kind_exec_agent"), desc: t("agents_ui.kind_exec_agent_desc"), icon: Bot },
|
|
22
|
-
super_agent: { label: t("agents_ui.kind_super_agent"), desc: t("agents_ui.kind_super_agent_desc"), icon: Crown },
|
|
23
|
-
telegram: { label: t("agents_ui.kind_telegram"), desc: t("agents_ui.kind_telegram_desc"), icon: Send },
|
|
24
|
-
shell: { label: t("agents_ui.kind_shell"), desc: t("agents_ui.kind_shell_desc"), icon: Terminal },
|
|
25
|
-
heartbeat: { label: t("agents_ui.kind_heartbeat"), desc: t("agents_ui.kind_heartbeat_desc"), icon: Heart },
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function kindOptions() {
|
|
29
|
-
const meta = kindMeta();
|
|
30
|
-
return (Object.keys(meta) as Kind[]).map((k) => ({ value: k, label: meta[k].label, description: meta[k].desc, icon: meta[k].icon }));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// "every:10m" → "cada 10 minutos", cron/once → legible.
|
|
34
|
-
function scheduleHuman(s?: string): string {
|
|
35
|
-
if (!s) return "—";
|
|
36
|
-
if (s.startsWith("every:")) {
|
|
37
|
-
const v = s.slice(6);
|
|
38
|
-
const m = v.match(/^(\d+)(s|m|h|d)$/);
|
|
39
|
-
if (m) {
|
|
40
|
-
const n = m[1];
|
|
41
|
-
const unit = {
|
|
42
|
-
s: t("agents_ui.unit_seconds"),
|
|
43
|
-
m: t("agents_ui.unit_minutes"),
|
|
44
|
-
h: t("agents_ui.unit_hours"),
|
|
45
|
-
d: t("agents_ui.unit_days"),
|
|
46
|
-
}[m[2]] || m[2];
|
|
47
|
-
return t("agents_ui.every_n_unit", { n, unit });
|
|
48
|
-
}
|
|
49
|
-
return t("agents_ui.every_v", { v });
|
|
50
|
-
}
|
|
51
|
-
if (s.startsWith("once:")) return `once · ${new Date(s.slice(5)).toLocaleString()}`;
|
|
52
|
-
if (s.startsWith("cron ")) return `cron · ${s.slice(5)}`;
|
|
53
|
-
return s;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function schedPresets() {
|
|
57
|
-
return [
|
|
58
|
-
{ label: t("agents_ui.preset_every_10m"), value: "every:10m" },
|
|
59
|
-
{ label: t("agents_ui.preset_hourly"), value: "every:1h" },
|
|
60
|
-
{ label: t("agents_ui.preset_daily_9am"), value: "cron 0 9 * * *" },
|
|
61
|
-
{ label: t("agents_ui.preset_weekdays_9am"), value: "cron 0 9 * * 1-5" },
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Template/env vars the routine runner exposes (src/core/routines/runner.js).
|
|
66
|
-
function routineVars() {
|
|
67
|
-
return [
|
|
68
|
-
{ v: "{{pre_output}}", where: "prompt", desc: t("agents_ui.var_pre_output_prompt") },
|
|
69
|
-
{ v: "$APX_LLM_OUTPUT", where: "post", desc: t("agents_ui.var_llm_output") },
|
|
70
|
-
{ v: "$APX_STATUS", where: "post", desc: t("agents_ui.var_status") },
|
|
71
|
-
{ v: "$APX_SKIPPED", where: "post", desc: t("agents_ui.var_skipped") },
|
|
72
|
-
{ v: "$APX_PRE_OUTPUT", where: "post", desc: t("agents_ui.var_pre_output") },
|
|
73
|
-
{ v: "$APX_PRE_OUTPUT_FILE", where: "post", desc: t("agents_ui.var_pre_output_file") },
|
|
74
|
-
{ v: "$APX_PRE_EXIT", where: "post", desc: t("agents_ui.var_pre_exit") },
|
|
75
|
-
{ v: "$APX_ROUTINE", where: "pre/post", desc: t("agents_ui.var_routine") },
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function actionSummary(kind: Kind, spec: Record<string, unknown>): string {
|
|
80
|
-
switch (kind) {
|
|
81
|
-
case "exec_agent": return spec.agent ? t("agents_ui.summary_runs_agent", { agent: String(spec.agent) }) : t("agents_ui.summary_runs_agent_none");
|
|
82
|
-
case "super_agent": return t("agents_ui.summary_super_agent");
|
|
83
|
-
case "telegram": return t("agents_ui.summary_telegram", { channel: String(spec.channel || "default") });
|
|
84
|
-
case "shell": return spec.command ? t("agents_ui.summary_runs_cmd", { cmd: String(spec.command).slice(0, 40) }) : t("agents_ui.summary_shell");
|
|
85
|
-
case "heartbeat": return t("agents_ui.summary_heartbeat");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
13
|
+
// Full-height master-detail (like the Chat screen): scrollable routine list on
|
|
14
|
+
// the left, read-only detail on the right. Selection lives in the URL (?r_id),
|
|
15
|
+
// editing is behind a button, delete uses the shared <Dialog>.
|
|
89
16
|
export function RoutinesTab({ pid }: { pid: string }) {
|
|
90
17
|
const toast = useToast();
|
|
91
18
|
const list = useSWR(`/projects/${pid}/routines`, () => Routines.list(pid));
|
|
19
|
+
const [params, setParams] = useSearchParams();
|
|
92
20
|
const [editing, setEditing] = useState<Partial<RoutineEntry> | null>(null);
|
|
21
|
+
const [confirmDelete, setConfirmDelete] = useState<RoutineEntry | null>(null);
|
|
22
|
+
const [deleting, setDeleting] = useState(false);
|
|
23
|
+
const [confirmRun, setConfirmRun] = useState<RoutineEntry | null>(null);
|
|
24
|
+
const [running, setRunning] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
const rows = list.data || [];
|
|
27
|
+
const selectedName = params.get("r_id");
|
|
28
|
+
const selected = rows.find((r) => r.name === selectedName) || null;
|
|
29
|
+
|
|
30
|
+
const selectRoutine = (name: string | null) =>
|
|
31
|
+
setParams((prev) => {
|
|
32
|
+
const next = new URLSearchParams(prev);
|
|
33
|
+
if (name) next.set("r_id", name); else next.delete("r_id");
|
|
34
|
+
return next;
|
|
35
|
+
}, { replace: true });
|
|
36
|
+
|
|
37
|
+
// Keep the first routine selected by default, and heal a stale ?r_id.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (rows.length === 0) return;
|
|
40
|
+
if (selectedName && rows.some((r) => r.name === selectedName)) return;
|
|
41
|
+
selectRoutine(rows[0].name);
|
|
42
|
+
}, [rows, selectedName]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
93
43
|
|
|
94
44
|
const toggle = async (r: RoutineEntry) => {
|
|
95
45
|
try { await (r.enabled ? Routines.disable : Routines.enable)(pid, r.name); list.mutate(); }
|
|
96
46
|
catch (e: any) { toast.error(e?.message || t("project.routines.toggle_error")); }
|
|
97
47
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
48
|
+
const doRun = async () => {
|
|
49
|
+
if (!confirmRun) return;
|
|
50
|
+
const r = confirmRun;
|
|
51
|
+
setConfirmRun(null);
|
|
52
|
+
setRunning(r.name);
|
|
53
|
+
try {
|
|
54
|
+
await Routines.run(pid, r.name);
|
|
55
|
+
toast.success(t("project.routines.run_success", { name: r.name }));
|
|
56
|
+
// Refresh the routine list (last status) and its executions list.
|
|
57
|
+
await Promise.all([
|
|
58
|
+
list.mutate(),
|
|
59
|
+
globalMutate(`/projects/${pid}/routines/${r.name}/runs`),
|
|
60
|
+
]);
|
|
61
|
+
} catch (e: any) { toast.error(e?.message || t("project.routines.run_error")); }
|
|
62
|
+
finally { setRunning(null); }
|
|
101
63
|
};
|
|
102
|
-
const
|
|
103
|
-
if (!
|
|
104
|
-
|
|
105
|
-
|
|
64
|
+
const doDelete = async () => {
|
|
65
|
+
if (!confirmDelete) return;
|
|
66
|
+
setDeleting(true);
|
|
67
|
+
try {
|
|
68
|
+
await Routines.remove(pid, confirmDelete.name);
|
|
69
|
+
toast.success(t("project.routines.delete_success"));
|
|
70
|
+
if (selectedName === confirmDelete.name) selectRoutine(null);
|
|
71
|
+
setConfirmDelete(null);
|
|
72
|
+
list.mutate();
|
|
73
|
+
} catch (e: any) { toast.error(e?.message || t("project.routines.delete_error")); }
|
|
74
|
+
finally { setDeleting(false); }
|
|
106
75
|
};
|
|
107
76
|
|
|
108
77
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
<div className="flex h-full min-h-0 flex-col gap-3">
|
|
79
|
+
{/* header (title + new) */}
|
|
80
|
+
<div className="flex shrink-0 items-start justify-between gap-4">
|
|
81
|
+
<div>
|
|
82
|
+
<h2 className="text-lg font-semibold tracking-tight">{t("project.routines.title")}</h2>
|
|
83
|
+
<p className="mt-0.5 text-sm text-muted-fg">{t("project.routines.subtitle")}</p>
|
|
84
|
+
</div>
|
|
85
|
+
<Button size="sm" variant="primary" onClick={() => setEditing({ kind: "super_agent", schedule: "every:10m", enabled: true })}>
|
|
86
|
+
<Plus size={14} /> {t("project.routines.new_btn")}
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
116
90
|
{list.isLoading && <Loading />}
|
|
117
|
-
{!list.isLoading &&
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<Button size="sm" variant="secondary" onClick={() => runNow(row)}><Play size={13} /> {t("common.run")}</Button>
|
|
141
|
-
<Button size="sm" variant="destructive" onClick={() => remove(row)}><Trash2 size={13} /></Button>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
<div className="mt-1.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-fg">
|
|
145
|
-
<span>⏱ {scheduleHuman(row.schedule)}</span>
|
|
146
|
-
<span>{actionSummary(row.kind, row.spec || {})}</span>
|
|
147
|
-
{row.next_run_at && <span>{t("project.routines.next_run")} {new Date(row.next_run_at).toLocaleString()}</span>}
|
|
148
|
-
<span className={cn(row.last_status === "ok" && "text-emerald-500", err && "text-destructive")}>
|
|
149
|
-
{t("agents_ui.last_label")} {row.last_status || "—"}
|
|
150
|
-
</span>
|
|
151
|
-
</div>
|
|
152
|
-
{row.last_error && <div className="mt-2 rounded-md bg-destructive/10 px-2 py-1 text-xs text-destructive">{row.last_error}</div>}
|
|
153
|
-
</li>
|
|
154
|
-
);
|
|
155
|
-
})}
|
|
156
|
-
</ul>
|
|
91
|
+
{!list.isLoading && rows.length === 0 && <Empty>{t("project.routines.empty")}</Empty>}
|
|
92
|
+
|
|
93
|
+
{rows.length > 0 && (
|
|
94
|
+
<div className="grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)] grid-cols-[minmax(200px,260px)_1fr] overflow-hidden rounded-xl border border-border bg-card/40">
|
|
95
|
+
<RoutineList routines={rows} selectedName={selected?.name ?? null} onSelect={selectRoutine} />
|
|
96
|
+
<div className="min-h-0 min-w-0 overflow-hidden">
|
|
97
|
+
{selected
|
|
98
|
+
? <RoutineDetail
|
|
99
|
+
key={selected.name}
|
|
100
|
+
pid={pid}
|
|
101
|
+
routine={selected}
|
|
102
|
+
onEdit={() => setEditing({ ...selected })}
|
|
103
|
+
onRun={() => setConfirmRun(selected)}
|
|
104
|
+
onToggle={() => toggle(selected)}
|
|
105
|
+
onDelete={() => setConfirmDelete(selected)}
|
|
106
|
+
running={running === selected.name}
|
|
107
|
+
/>
|
|
108
|
+
: <div className="flex h-full items-center justify-center p-8">
|
|
109
|
+
<p className="text-sm text-muted-fg">{t("project.routines.detail_empty")}</p>
|
|
110
|
+
</div>}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
157
114
|
|
|
158
115
|
<RoutineEditor
|
|
159
116
|
draft={editing}
|
|
@@ -161,240 +118,36 @@ export function RoutinesTab({ pid }: { pid: string }) {
|
|
|
161
118
|
onSaved={() => { setEditing(null); list.mutate(); }}
|
|
162
119
|
pid={pid}
|
|
163
120
|
/>
|
|
164
|
-
</Section>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function RoutineEditor({
|
|
169
|
-
draft, onClose, onSaved, pid,
|
|
170
|
-
}: { draft: Partial<RoutineEntry> | null; onClose: () => void; onSaved: () => void; pid: string }) {
|
|
171
|
-
const toast = useToast();
|
|
172
|
-
const agentsList = useSWR(draft ? `/projects/${pid}/agents` : null, () => Agents.list(pid));
|
|
173
|
-
const [busy, setBusy] = useState(false);
|
|
174
|
-
|
|
175
|
-
const [name, setName] = useState("");
|
|
176
|
-
const [kind, setKind] = useState<Kind>("super_agent");
|
|
177
|
-
const [schedule, setSchedule] = useState("every:10m");
|
|
178
|
-
const [enabled, setEnabled] = useState(true);
|
|
179
|
-
// Per-kind fields
|
|
180
|
-
const [agent, setAgent] = useState("");
|
|
181
|
-
const [prompt, setPrompt] = useState("");
|
|
182
|
-
const [tgChannel, setTgChannel] = useState("default");
|
|
183
|
-
const [tgChatId, setTgChatId] = useState("");
|
|
184
|
-
const [tgText, setTgText] = useState("");
|
|
185
|
-
const [command, setCommand] = useState("");
|
|
186
|
-
const [hbChannel, setHbChannel] = useState("heartbeat");
|
|
187
|
-
const [hbMessage, setHbMessage] = useState("");
|
|
188
|
-
const [pre, setPre] = useState("");
|
|
189
|
-
const [post, setPost] = useState("");
|
|
190
121
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const submit = async () => {
|
|
222
|
-
if (!name) { toast.error(t("project.routines.name_required")); return; }
|
|
223
|
-
setBusy(true);
|
|
224
|
-
try {
|
|
225
|
-
const usePP = kind === "exec_agent" || kind === "super_agent";
|
|
226
|
-
await Routines.upsert(pid, {
|
|
227
|
-
name, kind, schedule, enabled,
|
|
228
|
-
spec: buildSpec(),
|
|
229
|
-
pre_commands: usePP ? splitLines(pre) : [],
|
|
230
|
-
post_commands: usePP ? splitLines(post) : [],
|
|
231
|
-
});
|
|
232
|
-
toast.success(t("project.routines.saved"));
|
|
233
|
-
onSaved();
|
|
234
|
-
} catch (e: any) { toast.error(e?.message || t("project.routines.save_error")); }
|
|
235
|
-
finally { setBusy(false); }
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Only the LLM kinds wrap the action with pre/post shell commands.
|
|
239
|
-
const usesPrePost = kind === "exec_agent" || kind === "super_agent";
|
|
240
|
-
// Timeline steps (pre → action → post).
|
|
241
|
-
const preSteps = usesPrePost ? splitLines(pre) : [];
|
|
242
|
-
const postSteps = usesPrePost ? splitLines(post) : [];
|
|
243
|
-
const actionLabel = (() => {
|
|
244
|
-
switch (kind) {
|
|
245
|
-
case "exec_agent": return agent ? t("agents_ui.action_agent_answers", { agent }) : t("agents_ui.action_agent_pick_answers");
|
|
246
|
-
case "super_agent": return t("agents_ui.action_super_answers");
|
|
247
|
-
case "telegram": return t("agents_ui.action_telegram_channel", { channel: tgChannel });
|
|
248
|
-
case "shell": return command ? t("agents_ui.summary_runs_cmd", { cmd: command.slice(0, 48) }) : t("agents_ui.action_runs_shell");
|
|
249
|
-
case "heartbeat": return t("agents_ui.summary_heartbeat");
|
|
250
|
-
}
|
|
251
|
-
})();
|
|
252
|
-
const usesPrompt = usesPrePost;
|
|
253
|
-
|
|
254
|
-
const ActionIcon = kindMeta()[kind].icon;
|
|
255
|
-
const steps = [
|
|
256
|
-
...preSteps.map((c, i) => ({ id: `pre-${i}`, icon: Terminal, label: t("agents_ui.step_pre"), detail: c, action: false })),
|
|
257
|
-
{ id: "action", icon: ActionIcon, label: actionLabel, detail: usesPrompt && prompt ? prompt.slice(0, 90) : undefined, action: true },
|
|
258
|
-
...postSteps.map((c, i) => ({ id: `post-${i}`, icon: Terminal, label: t("agents_ui.step_post"), detail: c, action: false })),
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
return (
|
|
262
|
-
<Dialog
|
|
263
|
-
open={!!draft}
|
|
264
|
-
onClose={onClose}
|
|
265
|
-
title={draft?.name ? t("project.routines.edit_title", { name: draft.name }) : t("project.routines.new_title")}
|
|
266
|
-
description={t("project.routines.dialog_desc")}
|
|
267
|
-
size="xl"
|
|
268
|
-
footer={
|
|
269
|
-
<>
|
|
270
|
-
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
271
|
-
<Button variant="primary" onClick={submit} loading={busy}>{t("common.save")}</Button>
|
|
272
|
-
</>
|
|
273
|
-
}
|
|
274
|
-
>
|
|
275
|
-
<div className="space-y-4">
|
|
276
|
-
{/* status */}
|
|
277
|
-
<div className="flex items-center justify-between rounded-lg border border-border bg-muted/20 px-3 py-2">
|
|
278
|
-
<Switch checked={enabled} onChange={setEnabled} label={t("project.routines.enabled_label")} />
|
|
279
|
-
<span className="text-[11px] text-muted-fg">{enabled ? t("project.routines.enabled_hint") : t("project.routines.disabled_hint")}</span>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<div className="grid gap-6 md:grid-cols-2">
|
|
283
|
-
{/* LEFT — qué y cuándo */}
|
|
284
|
-
<div className="space-y-3">
|
|
285
|
-
<Field label={t("project.routines.name_field")} hint={draft?.name ? t("project.routines.name_no_edit") : undefined}>
|
|
286
|
-
<Input value={name} disabled={!!draft?.name} onChange={(e) => setName(e.target.value)} placeholder="resumen-diario" />
|
|
287
|
-
</Field>
|
|
288
|
-
<Field label={t("project.routines.kind_field")}>
|
|
289
|
-
<UiSelect value={kind} onChange={(v) => setKind(v as Kind)} options={kindOptions()} />
|
|
290
|
-
</Field>
|
|
291
|
-
<p className="-mt-1 text-[11px] text-muted-fg">{kindMeta()[kind].desc}</p>
|
|
292
|
-
{kind === "exec_agent" && (
|
|
293
|
-
<Field label={t("project.routines.agent_field")} hint={t("project.routines.agent_hint")}>
|
|
294
|
-
<UiSelect value={agent} onChange={setAgent} placeholder={agentsList.isLoading ? t("project.routines.agent_loading") : t("project.routines.agent_pick")}
|
|
295
|
-
options={(agentsList.data || []).map((a) => ({ value: a.slug, label: a.slug, description: [a.role, a.model].filter(Boolean).join(" · ") || undefined }))} />
|
|
296
|
-
</Field>
|
|
297
|
-
)}
|
|
298
|
-
<Field label={t("project.routines.schedule_field")} hint={t("project.routines.schedule_hint")}>
|
|
299
|
-
<div className="space-y-2">
|
|
300
|
-
<div className="flex flex-wrap gap-1">
|
|
301
|
-
{schedPresets().map((s) => (
|
|
302
|
-
<button key={s.value} type="button" onClick={() => setSchedule(s.value)}
|
|
303
|
-
className={cn("rounded-md border px-2 py-0.5 text-[11px]", schedule === s.value ? "border-emerald-500/50 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
|
|
304
|
-
{s.label}
|
|
305
|
-
</button>
|
|
306
|
-
))}
|
|
307
|
-
<button type="button" onClick={() => setSchedule("manual")}
|
|
308
|
-
className={cn("rounded-md border px-2 py-0.5 text-[11px]", schedule === "manual" ? "border-emerald-500/50 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
|
|
309
|
-
{t("agents_ui.preset_manual")}
|
|
310
|
-
</button>
|
|
311
|
-
</div>
|
|
312
|
-
<Input value={schedule} onChange={(e) => setSchedule(e.target.value)} placeholder="every:10m · cron 0 9 * * 1-5 · once:ISO · manual" />
|
|
313
|
-
</div>
|
|
314
|
-
</Field>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
{/* RIGHT — lo que ejecuta, según el tipo */}
|
|
318
|
-
<div className="space-y-3">
|
|
319
|
-
{/* LLM: pre → prompt → post */}
|
|
320
|
-
{usesPrePost && (
|
|
321
|
-
<Field label={t("project.routines.pre_field")} hint={t("project.routines.pre_hint")}>
|
|
322
|
-
<Textarea rows={2} className="font-mono text-xs" value={pre} onChange={(e) => setPre(e.target.value)} placeholder="curl -s https://wttr.in/Bariloche" />
|
|
323
|
-
</Field>
|
|
324
|
-
)}
|
|
325
|
-
{kind === "exec_agent" && (
|
|
326
|
-
<Field label={t("project.routines.prompt_exec")}><Textarea rows={4} value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder={t("project.routines.prompt_exec_ph")} /></Field>
|
|
327
|
-
)}
|
|
328
|
-
{kind === "super_agent" && (
|
|
329
|
-
<Field label={t("project.routines.prompt_super")}><Textarea rows={4} value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder={t("project.routines.prompt_super_ph")} /></Field>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
|
-
{/* Telegram: solo manda un mensaje (sin LLM) */}
|
|
333
|
-
{kind === "telegram" && (
|
|
334
|
-
<>
|
|
335
|
-
<div className="grid grid-cols-2 gap-3">
|
|
336
|
-
<Field label={t("project.routines.tg_channel")}><Input value={tgChannel} onChange={(e) => setTgChannel(e.target.value)} placeholder="default" /></Field>
|
|
337
|
-
<Field label={t("project.routines.tg_chat_id")}><Input value={tgChatId} onChange={(e) => setTgChatId(e.target.value)} placeholder={t("agents_ui.tg_chat_id_ph")} /></Field>
|
|
338
|
-
</div>
|
|
339
|
-
<Field label={t("project.routines.tg_text")} hint={t("project.routines.tg_text_hint")}>
|
|
340
|
-
<Textarea rows={8} value={tgText} onChange={(e) => setTgText(e.target.value)} placeholder={t("agents_ui.tg_text_ph")} />
|
|
341
|
-
</Field>
|
|
342
|
-
</>
|
|
343
|
-
)}
|
|
344
|
-
|
|
345
|
-
{/* Shell: un comando, ocupando todo */}
|
|
346
|
-
{kind === "shell" && (
|
|
347
|
-
<Field label={t("project.routines.shell_field")} hint={t("project.routines.shell_hint")}>
|
|
348
|
-
<Textarea rows={11} className="font-mono text-xs" value={command} onChange={(e) => setCommand(e.target.value)} placeholder="cd /repo && git pull && npm test" />
|
|
349
|
-
</Field>
|
|
350
|
-
)}
|
|
351
|
-
|
|
352
|
-
{/* Heartbeat: solo loguea */}
|
|
353
|
-
{kind === "heartbeat" && (
|
|
354
|
-
<div className="grid grid-cols-2 gap-3">
|
|
355
|
-
<Field label={t("project.routines.hb_channel")}><Input value={hbChannel} onChange={(e) => setHbChannel(e.target.value)} placeholder="heartbeat" /></Field>
|
|
356
|
-
<Field label={t("project.routines.hb_message")}><Input value={hbMessage} onChange={(e) => setHbMessage(e.target.value)} placeholder={t("agents_ui.hb_message_ph")} /></Field>
|
|
357
|
-
</div>
|
|
358
|
-
)}
|
|
359
|
-
|
|
360
|
-
{usesPrePost && (
|
|
361
|
-
<Field label={t("project.routines.post_field")} hint={t("project.routines.post_hint")}>
|
|
362
|
-
<Textarea rows={2} className="font-mono text-xs" value={post} onChange={(e) => setPost(e.target.value)} placeholder={'apx telegram send "$APX_LLM_OUTPUT"'} />
|
|
363
|
-
</Field>
|
|
364
|
-
)}
|
|
365
|
-
</div>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
{/* Variables disponibles */}
|
|
369
|
-
<div className="rounded-lg border border-border bg-muted/10 p-3">
|
|
370
|
-
<div className="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-fg">{t("project.routines.vars_title")}</div>
|
|
371
|
-
<div className="flex flex-wrap gap-1.5">
|
|
372
|
-
{routineVars().map((v) => (
|
|
373
|
-
<span key={v.v} title={v.desc} className="inline-flex items-center gap-1 rounded-md border border-border bg-card px-1.5 py-0.5 font-mono text-[10px]">
|
|
374
|
-
{v.v}<span className="not-italic text-muted-fg">· {v.where}</span>
|
|
375
|
-
</span>
|
|
376
|
-
))}
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
|
|
380
|
-
{/* Qué va a pasar — full width */}
|
|
381
|
-
<div className="rounded-lg border border-border bg-muted/20 p-3">
|
|
382
|
-
<div className="mb-2 text-xs font-semibold text-muted-fg">{t("project.routines.what_happens")} <span className="font-normal text-muted-fg">· ⏱ {scheduleHuman(schedule)}</span></div>
|
|
383
|
-
<div className="flex flex-wrap items-stretch gap-2">
|
|
384
|
-
{steps.map((s, i) => (
|
|
385
|
-
<div key={s.id} className="flex items-stretch gap-2">
|
|
386
|
-
<div className={cn("flex max-w-[240px] flex-col gap-1 rounded-lg border px-2.5 py-2", s.action ? "border-emerald-500/40 bg-emerald-500/5" : "border-border bg-card")}>
|
|
387
|
-
<div className={cn("flex items-center gap-1.5 text-[11px] font-medium", s.action ? "text-emerald-400" : "text-muted-fg")}>
|
|
388
|
-
<s.icon size={12} /> {s.label}
|
|
389
|
-
</div>
|
|
390
|
-
{s.detail && <div className="line-clamp-2 font-mono text-[10px] text-muted-fg">{s.detail}</div>}
|
|
391
|
-
</div>
|
|
392
|
-
{i < steps.length - 1 && <ArrowRight size={14} className="shrink-0 self-center text-muted-fg" />}
|
|
393
|
-
</div>
|
|
394
|
-
))}
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
</Dialog>
|
|
122
|
+
<Dialog
|
|
123
|
+
open={!!confirmDelete}
|
|
124
|
+
onClose={() => (deleting ? null : setConfirmDelete(null))}
|
|
125
|
+
title={t("project.routines.delete_confirm", { name: confirmDelete?.name || "" })}
|
|
126
|
+
size="sm"
|
|
127
|
+
footer={
|
|
128
|
+
<>
|
|
129
|
+
<Button variant="ghost" onClick={() => setConfirmDelete(null)} disabled={deleting}>{t("common.cancel")}</Button>
|
|
130
|
+
<Button variant="destructive" onClick={doDelete} loading={deleting}>{t("common.delete")}</Button>
|
|
131
|
+
</>
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
<p className="text-sm text-muted-fg">{t("project.routines.delete_confirm_body")}</p>
|
|
135
|
+
</Dialog>
|
|
136
|
+
|
|
137
|
+
<Dialog
|
|
138
|
+
open={!!confirmRun}
|
|
139
|
+
onClose={() => setConfirmRun(null)}
|
|
140
|
+
title={t("project.routines.run_confirm", { name: confirmRun?.name || "" })}
|
|
141
|
+
size="sm"
|
|
142
|
+
footer={
|
|
143
|
+
<>
|
|
144
|
+
<Button variant="ghost" onClick={() => setConfirmRun(null)}>{t("common.cancel")}</Button>
|
|
145
|
+
<Button variant="primary" onClick={doRun}>{t("common.run")}</Button>
|
|
146
|
+
</>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<p className="text-sm text-muted-fg">{t("project.routines.run_confirm_body")}</p>
|
|
150
|
+
</Dialog>
|
|
151
|
+
</div>
|
|
399
152
|
);
|
|
400
153
|
}
|
|
@@ -120,6 +120,11 @@
|
|
|
120
120
|
* {
|
|
121
121
|
@apply border-border outline-ring/50;
|
|
122
122
|
}
|
|
123
|
+
/* Tailwind v4 Preflight resets buttons to cursor:default — restore the hand. */
|
|
124
|
+
button:not(:disabled),
|
|
125
|
+
[role="button"]:not(:disabled) {
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
/* Subtle scrollbar that doesn't fight the dark theme. */
|