@agentprojectcontext/apx 1.74.2 → 1.76.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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/prompt-builder.js +26 -7
  3. package/src/core/agent/render-template.js +22 -0
  4. package/src/core/profiles/block.js +290 -0
  5. package/src/core/profiles/bundled/secretary/PROFILE.es.md +44 -0
  6. package/src/core/profiles/bundled/secretary/PROFILE.md +44 -0
  7. package/src/core/profiles/bundled/secretary/channels/routine.md +43 -0
  8. package/src/core/profiles/bundled/secretary/config.schema.json +49 -0
  9. package/src/core/profiles/bundled/secretary/profile.json +20 -0
  10. package/src/core/profiles/bundled/secretary/routines/day-close.json +10 -0
  11. package/src/core/profiles/bundled/secretary/routines/day-open.json +10 -0
  12. package/src/core/profiles/index.js +16 -0
  13. package/src/core/profiles/lifecycle.js +720 -0
  14. package/src/core/profiles/manifest.js +193 -0
  15. package/src/core/profiles/paths.js +51 -0
  16. package/src/core/profiles/store.js +184 -0
  17. package/src/core/runtime-skills/apx-profile/SKILL.md +126 -0
  18. package/src/core/runtime-skills/apx-task/SKILL.md +4 -0
  19. package/src/core/stores/routines.js +9 -1
  20. package/src/core/stores/tasks.js +70 -1
  21. package/src/host/daemon/api/profiles.js +179 -0
  22. package/src/host/daemon/api/tasks.js +36 -15
  23. package/src/host/daemon/api/web.js +1 -1
  24. package/src/host/daemon/api.js +2 -0
  25. package/src/interfaces/cli/commands/profile.js +252 -0
  26. package/src/interfaces/cli/commands/task.js +44 -13
  27. package/src/interfaces/cli/index.js +69 -1
  28. package/src/interfaces/web/dist/assets/{index-CQTIGYCu.js → index-Bjlk9ttU.js} +165 -160
  29. package/src/interfaces/web/dist/assets/index-Bjlk9ttU.js.map +1 -0
  30. package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +1 -0
  31. package/src/interfaces/web/dist/index.html +2 -2
  32. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +245 -0
  33. package/src/interfaces/web/src/hooks/useProfiles.ts +37 -0
  34. package/src/interfaces/web/src/i18n/en.ts +32 -0
  35. package/src/interfaces/web/src/i18n/es.ts +32 -0
  36. package/src/interfaces/web/src/lib/api/profiles.ts +86 -0
  37. package/src/interfaces/web/src/lib/api/tasks.ts +6 -2
  38. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  39. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +21 -3
  40. package/src/interfaces/web/dist/assets/index-COrRuBp1.css +0 -1
  41. package/src/interfaces/web/dist/assets/index-CQTIGYCu.js.map +0 -1
@@ -0,0 +1,245 @@
1
+ import { useEffect, useState } from "react";
2
+ import { AlertTriangle, CheckCircle2, Info } from "lucide-react";
3
+ import { Section } from "../Section";
4
+ import { Badge, Button, Dialog, Empty, Field, Input, Loading } from "../ui";
5
+ import { UiSelect } from "../UiSelect";
6
+ import { useToast } from "../Toast";
7
+ import { useProfiles, useProfile, useProfileDoctor } from "../../hooks/useProfiles";
8
+ import { ProfilesApi, type ProfileSchemaProp } from "../../lib/api/profiles";
9
+ import { t } from "../../i18n";
10
+
11
+ /**
12
+ * Agent profiles — an installable line of work for the super-agent.
13
+ *
14
+ * Not to be confused with the agent's persona (its visible name, under
15
+ * Identity), nor with configuration profiles. With none active, APX behaves
16
+ * exactly as it always has, and the panel says so rather than looking broken.
17
+ */
18
+ export function ProfilePanel() {
19
+ const toast = useToast();
20
+ const { active, profiles, isLoading, mutate } = useProfiles();
21
+ const [selectedId, setSelectedId] = useState<string | null>(null);
22
+
23
+ const currentId = selectedId ?? active ?? profiles[0]?.id ?? null;
24
+ const { profile, mutate: mutateProfile } = useProfile(currentId);
25
+ const { doctor, mutate: mutateDoctor } = useProfileDoctor(active);
26
+
27
+ const [draft, setDraft] = useState<Record<string, string>>({});
28
+ const [busy, setBusy] = useState(false);
29
+ const [confirmOff, setConfirmOff] = useState(false);
30
+
31
+ useEffect(() => {
32
+ if (!profile) return;
33
+ const next: Record<string, string> = {};
34
+ for (const [k, v] of Object.entries(profile.config || {})) next[k] = String(v ?? "");
35
+ setDraft(next);
36
+ }, [profile?.id, profile?.config]);
37
+
38
+ const refreshAll = async () => {
39
+ await Promise.all([mutate(), mutateProfile(), mutateDoctor()]);
40
+ };
41
+
42
+ if (isLoading) return <Loading />;
43
+
44
+ const activate = async (id: string, force: boolean) => {
45
+ setBusy(true);
46
+ try {
47
+ const r = await ProfilesApi.use(id, force);
48
+ for (const w of r.warnings || []) toast.error(w);
49
+ await refreshAll();
50
+ toast.success(t("settings.profile.activated"));
51
+ } catch (e) {
52
+ toast.error((e as Error).message);
53
+ } finally {
54
+ setBusy(false);
55
+ }
56
+ };
57
+
58
+ const deactivate = async () => {
59
+ setBusy(true);
60
+ try {
61
+ await ProfilesApi.off();
62
+ await refreshAll();
63
+ toast.success(t("settings.profile.deactivated"));
64
+ } catch (e) {
65
+ toast.error((e as Error).message);
66
+ } finally {
67
+ setBusy(false);
68
+ setConfirmOff(false);
69
+ }
70
+ };
71
+
72
+ const saveConfig = async () => {
73
+ if (!profile) return;
74
+ setBusy(true);
75
+ try {
76
+ const r = await ProfilesApi.setConfig(draft, profile.id);
77
+ await refreshAll();
78
+ const moved = r.routines?.installed?.length ?? 0;
79
+ toast.success(
80
+ moved > 0 ? t("settings.profile.saved_with_routines") : t("settings.profile.saved"),
81
+ );
82
+ } catch (e) {
83
+ toast.error((e as Error).message);
84
+ } finally {
85
+ setBusy(false);
86
+ }
87
+ };
88
+
89
+ const props: Record<string, ProfileSchemaProp> = profile?.schema?.properties || {};
90
+ const overBudget = !!profile?.budget && !!profile?.tokens && profile.tokens > profile.budget;
91
+
92
+ return (
93
+ <div className="flex flex-col gap-4" data-testid="profile-panel">
94
+ <Section
95
+ title={t("settings.profile.title")}
96
+ description={t("settings.profile.subtitle")}
97
+ >
98
+ {!active ? (
99
+ <div
100
+ data-testid="profile-vanilla-hint"
101
+ className="mb-3 flex items-start gap-2 rounded-md border border-border bg-muted/40 p-3 text-sm"
102
+ >
103
+ <Info size={16} className="mt-0.5 shrink-0 opacity-70" />
104
+ <span>{t("settings.profile.vanilla_hint")}</span>
105
+ </div>
106
+ ) : null}
107
+
108
+ {!profiles.length ? (
109
+ <Empty>{t("settings.profile.none_available")}</Empty>
110
+ ) : (
111
+ <div className="flex flex-col gap-2">
112
+ {profiles.map((p) => (
113
+ <button
114
+ key={p.id}
115
+ data-testid={`profile-row-${p.id}`}
116
+ type="button"
117
+ onClick={() => setSelectedId(p.id)}
118
+ className={`flex items-start justify-between gap-3 rounded-md border p-3 text-left transition ${
119
+ p.id === currentId ? "border-primary bg-muted/40" : "border-border hover:bg-muted/20"
120
+ }`}
121
+ >
122
+ <span className="min-w-0">
123
+ <span className="flex items-center gap-2">
124
+ <span className="font-medium">{p.name}</span>
125
+ <Badge>{p.source}</Badge>
126
+ {p.active ? <Badge tone="success">{t("settings.profile.active")}</Badge> : null}
127
+ </span>
128
+ {p.description ? (
129
+ <span className="mt-0.5 block text-sm opacity-70">{p.description}</span>
130
+ ) : null}
131
+ </span>
132
+ <span className="shrink-0 text-xs opacity-60">{p.version ? `v${p.version}` : ""}</span>
133
+ </button>
134
+ ))}
135
+ </div>
136
+ )}
137
+
138
+ {profile ? (
139
+ <div className="mt-4 flex flex-wrap items-center gap-2">
140
+ {profile.active ? (
141
+ <Button variant="destructive" loading={busy} onClick={() => setConfirmOff(true)}>
142
+ {t("settings.profile.deactivate")}
143
+ </Button>
144
+ ) : (
145
+ <Button variant="primary" loading={busy} onClick={() => activate(profile.id, !!active)}>
146
+ {active ? t("settings.profile.replace_active") : t("settings.profile.activate")}
147
+ </Button>
148
+ )}
149
+ <span className="text-xs opacity-60">
150
+ {t("settings.profile.token_cost")}: ~{profile.tokens ?? 0}
151
+ {profile.budget ? ` / ${profile.budget}` : ""}
152
+ {overBudget ? ` — ${t("settings.profile.over_budget")}` : ""}
153
+ </span>
154
+ </div>
155
+ ) : null}
156
+ </Section>
157
+
158
+ {profile && Object.keys(props).length ? (
159
+ <Section
160
+ title={t("settings.profile.settings_title")}
161
+ description={t("settings.profile.settings_subtitle")}
162
+ >
163
+ <div className="grid grid-cols-2 gap-3">
164
+ {Object.entries(props).map(([key, def]) => (
165
+ <Field key={key} label={def.title || key} hint={def.description}>
166
+ {def.enum ? (
167
+ <UiSelect
168
+ value={draft[key] ?? String(def.default ?? "")}
169
+ onChange={(v) => setDraft({ ...draft, [key]: v })}
170
+ options={def.enum.map((o) => ({ value: String(o), label: String(o) }))}
171
+ />
172
+ ) : (
173
+ <Input
174
+ value={draft[key] ?? ""}
175
+ onChange={(e) => setDraft({ ...draft, [key]: e.target.value })}
176
+ />
177
+ )}
178
+ </Field>
179
+ ))}
180
+ </div>
181
+ <div className="mt-4">
182
+ <Button variant="primary" loading={busy} onClick={saveConfig}>
183
+ {t("common.save")}
184
+ </Button>
185
+ </div>
186
+ </Section>
187
+ ) : null}
188
+
189
+ {active && doctor ? (
190
+ <Section title={t("settings.profile.doctor_title")} description={doctor.summary}>
191
+ {!doctor.checks.length ? (
192
+ <div className="flex items-center gap-2 text-sm">
193
+ <CheckCircle2 size={16} className="text-emerald-500" />
194
+ {t("settings.profile.doctor_clean")}
195
+ </div>
196
+ ) : (
197
+ <ul className="flex flex-col gap-2">
198
+ {doctor.checks.map((c, i) => (
199
+ <li key={i} className="flex items-start gap-2 text-sm">
200
+ <AlertTriangle
201
+ size={16}
202
+ className={`mt-0.5 shrink-0 ${c.level === "error" ? "text-red-500" : "text-amber-500"}`}
203
+ />
204
+ <span className="min-w-0">
205
+ <span className="opacity-60">[{c.label}]</span> {c.detail}
206
+ {c.fix ? (
207
+ <code className="mt-1 block rounded bg-muted px-1.5 py-0.5 text-xs">{c.fix}</code>
208
+ ) : null}
209
+ </span>
210
+ </li>
211
+ ))}
212
+ </ul>
213
+ )}
214
+ </Section>
215
+ ) : null}
216
+
217
+ {profile ? (
218
+ <Section
219
+ title={t("settings.profile.preview_title")}
220
+ description={t("settings.profile.preview_subtitle")}
221
+ >
222
+ <pre data-testid="profile-preview" className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md border border-border bg-muted/30 p-3 text-xs leading-relaxed">
223
+ {profile.preview || t("settings.profile.preview_empty")}
224
+ </pre>
225
+ </Section>
226
+ ) : null}
227
+
228
+ <Dialog
229
+ open={confirmOff}
230
+ onClose={() => setConfirmOff(false)}
231
+ title={t("settings.profile.deactivate_title")}
232
+ footer={
233
+ <>
234
+ <Button onClick={() => setConfirmOff(false)}>{t("common.cancel")}</Button>
235
+ <Button variant="destructive" loading={busy} onClick={deactivate}>
236
+ {t("settings.profile.deactivate")}
237
+ </Button>
238
+ </>
239
+ }
240
+ >
241
+ {t("settings.profile.deactivate_confirm")}
242
+ </Dialog>
243
+ </div>
244
+ );
245
+ }
@@ -0,0 +1,37 @@
1
+ import useSWR from "swr";
2
+ import { ProfilesApi } from "../lib/api/profiles";
3
+ import type { ProfileDetail, ProfileDoctor, ProfileSummary } from "../lib/api/profiles";
4
+
5
+ /** The catalogue of agent profiles, plus which one is active. */
6
+ export function useProfiles() {
7
+ const { data, error, isLoading, mutate } = useSWR<{
8
+ active: string | null;
9
+ profiles: ProfileSummary[];
10
+ }>("/profiles", () => ProfilesApi.list());
11
+
12
+ return {
13
+ active: data?.active ?? null,
14
+ profiles: data?.profiles ?? [],
15
+ error,
16
+ isLoading,
17
+ mutate,
18
+ };
19
+ }
20
+
21
+ /** One profile, including its schema, settings and rendered prompt preview. */
22
+ export function useProfile(id: string | null) {
23
+ const { data, error, isLoading, mutate } = useSWR<ProfileDetail>(
24
+ id ? `/profiles/${id}` : null,
25
+ () => ProfilesApi.get(id as string),
26
+ );
27
+ return { profile: data, error, isLoading, mutate };
28
+ }
29
+
30
+ /** Health of the active profile (or a named one). */
31
+ export function useProfileDoctor(id: string | null) {
32
+ const { data, error, isLoading, mutate } = useSWR<ProfileDoctor>(
33
+ id ? `/profiles/doctor?id=${id}` : "/profiles/doctor",
34
+ () => ProfilesApi.doctor(id || undefined),
35
+ );
36
+ return { doctor: data, error, isLoading, mutate };
37
+ }
@@ -192,12 +192,43 @@ export const en = {
192
192
  tabs: {
193
193
  identity: "Identity",
194
194
  super_agent: "Super-agent",
195
+ profile: "Agent profile",
195
196
  engines: "Engines & models",
196
197
  telegram: "Telegram",
197
198
  devices: "Devices",
198
199
  advanced: "Advanced",
199
200
  },
200
201
 
202
+ profile: {
203
+ title: "Agent profile",
204
+ subtitle:
205
+ "An installable line of work for the super-agent: what it does with its day and when it speaks to you. Distinct from the agent's name (that lives under Identity).",
206
+ vanilla_hint:
207
+ "No profile is active. APX behaves exactly as it always has — the super-agent prompt is identical to a clean install.",
208
+ none_available: "No profiles available yet.",
209
+ active: "active",
210
+ activate: "Activate",
211
+ replace_active: "Replace the active one",
212
+ deactivate: "Deactivate",
213
+ deactivate_title: "Deactivate the profile?",
214
+ deactivate_confirm:
215
+ "APX goes back to vanilla. The profile's routines are disabled but not deleted, and your settings, tasks and memory are untouched — activating it again restores everything.",
216
+ activated: "Profile active",
217
+ deactivated: "Profile off — APX is back to vanilla",
218
+ token_cost: "Prompt cost",
219
+ over_budget: "over its declared budget",
220
+ settings_title: "Profile settings",
221
+ settings_subtitle:
222
+ "Blank fields fall back to the package default. Changing a time really reschedules the routine.",
223
+ saved: "Settings saved",
224
+ saved_with_routines: "Settings saved and routines rescheduled",
225
+ doctor_title: "Doctor",
226
+ doctor_clean: "All good.",
227
+ preview_title: "Prompt block",
228
+ preview_subtitle: "Exactly what reaches the model, with your values substituted.",
229
+ preview_empty: "(empty)",
230
+ },
231
+
201
232
  identity: {
202
233
  title: "Identity",
203
234
  subtitle: "User data. Agent configuration goes in Super-agent.",
@@ -395,6 +426,7 @@ export const en = {
395
426
  },
396
427
 
397
428
  global_tasks: {
429
+ any_status: "any status",
398
430
  title: "Tasks (all projects)",
399
431
  subtitle: "Aggregated tasks from all registered projects.",
400
432
  empty: "No tasks.",
@@ -193,12 +193,43 @@ export const es = {
193
193
  tabs: {
194
194
  identity: "Identidad",
195
195
  super_agent: "Super-agente",
196
+ profile: "Perfil del agente",
196
197
  engines: "Engines & modelos",
197
198
  telegram: "Telegram",
198
199
  devices: "Dispositivos",
199
200
  advanced: "Avanzado",
200
201
  },
201
202
 
203
+ profile: {
204
+ title: "Perfil del agente",
205
+ subtitle:
206
+ "Un oficio instalable para el super-agente: qué hace con su día y cuándo te habla. Distinto del nombre del agente (eso está en Identidad).",
207
+ vanilla_hint:
208
+ "No hay ningún perfil activo. APX se comporta exactamente como siempre — el prompt del super-agente es idéntico al de una instalación limpia.",
209
+ none_available: "No hay perfiles disponibles todavía.",
210
+ active: "activo",
211
+ activate: "Activar",
212
+ replace_active: "Reemplazar el activo",
213
+ deactivate: "Desactivar",
214
+ deactivate_title: "¿Desactivar el perfil?",
215
+ deactivate_confirm:
216
+ "APX vuelve a vanilla. Las rutinas del perfil se deshabilitan pero no se borran, y tu configuración, tareas y memoria quedan intactas: volver a activarlo restaura todo.",
217
+ activated: "Perfil activo",
218
+ deactivated: "Perfil desactivado — APX está en vanilla",
219
+ token_cost: "Costo de prompt",
220
+ over_budget: "excede su presupuesto declarado",
221
+ settings_title: "Configuración del perfil",
222
+ settings_subtitle:
223
+ "Los valores en blanco toman el default del paquete. Cambiar un horario reprograma la rutina de verdad.",
224
+ saved: "Configuración guardada",
225
+ saved_with_routines: "Configuración guardada y rutinas reprogramadas",
226
+ doctor_title: "Diagnóstico",
227
+ doctor_clean: "Todo en orden.",
228
+ preview_title: "Bloque de prompt",
229
+ preview_subtitle: "Exactamente lo que recibe el modelo, con tus valores ya sustituidos.",
230
+ preview_empty: "(vacío)",
231
+ },
232
+
202
233
  identity: {
203
234
  title: "Identidad",
204
235
  subtitle: "Datos del usuario. Configuración del agente va en Super-agente.",
@@ -396,6 +427,7 @@ export const es = {
396
427
  },
397
428
 
398
429
  global_tasks: {
430
+ any_status: "cualquier estado",
399
431
  title: "Tasks (todos los proyectos)",
400
432
  subtitle: "Tareas agregadas de todos los proyectos registrados.",
401
433
  empty: "Sin tasks.",
@@ -0,0 +1,86 @@
1
+ import { http } from "../http";
2
+
3
+ /** A property of a profile's config.schema.json (the supported subset). */
4
+ export type ProfileSchemaProp = {
5
+ type?: "string" | "integer" | "number" | "boolean";
6
+ enum?: (string | number | boolean)[];
7
+ default?: string | number | boolean;
8
+ title?: string;
9
+ description?: string;
10
+ };
11
+
12
+ export type ProfileSchema = {
13
+ type?: string;
14
+ properties?: Record<string, ProfileSchemaProp>;
15
+ };
16
+
17
+ export type ProfileSummary = {
18
+ id: string;
19
+ name: string;
20
+ version: string | null;
21
+ description: string;
22
+ author: string | null;
23
+ languages: string[];
24
+ source: "bundled" | "user" | "user-override";
25
+ active: boolean;
26
+ dir: string;
27
+ };
28
+
29
+ export type ProfileDetail = ProfileSummary & {
30
+ provides: Record<string, string[]>;
31
+ requires: Record<string, string[]>;
32
+ schema: ProfileSchema | null;
33
+ defaults: Record<string, unknown>;
34
+ config: Record<string, unknown>;
35
+ budget: number | null;
36
+ tokens: number | null;
37
+ /** The rendered prompt block, exactly as it reaches the model. */
38
+ preview: string;
39
+ };
40
+
41
+ export type ProfileCheck = {
42
+ level: "error" | "warn";
43
+ label: string;
44
+ detail: string;
45
+ fix: string | null;
46
+ };
47
+
48
+ export type ProfileDoctor = {
49
+ id: string | null;
50
+ active: boolean;
51
+ ok: boolean;
52
+ tokens?: number;
53
+ budget?: number | null;
54
+ checks: ProfileCheck[];
55
+ summary: string;
56
+ };
57
+
58
+ export type ProfileRoutineSync = {
59
+ installed: string[];
60
+ skipped: { name: string; reason: string }[];
61
+ };
62
+
63
+ export const ProfilesApi = {
64
+ list: () => http.get<{ active: string | null; profiles: ProfileSummary[] }>("/profiles"),
65
+ get: (id: string) => http.get<ProfileDetail>(`/profiles/${encodeURIComponent(id)}`),
66
+ doctor: (id?: string) =>
67
+ http.get<ProfileDoctor>(`/profiles/doctor${id ? `?id=${encodeURIComponent(id)}` : ""}`),
68
+ install: (source: string, force = false) =>
69
+ http.post<{ ok: true; profile: ProfileDetail; warnings: string[]; tokens: number }>(
70
+ "/profiles/install",
71
+ { source, force }
72
+ ),
73
+ use: (id: string, force = false) =>
74
+ http.post<{ ok: true; profile: ProfileDetail; routines: ProfileRoutineSync; warnings: string[] }>(
75
+ "/profiles/use",
76
+ { id, force }
77
+ ),
78
+ off: () => http.post<{ ok: true; was: string | null; routines: string[] }>("/profiles/off", {}),
79
+ setConfig: (values: Record<string, unknown>, id?: string) =>
80
+ http.patch<{ ok: true; config: Record<string, unknown>; changed: string[]; routines: ProfileRoutineSync }>(
81
+ "/profiles/config",
82
+ { values, id }
83
+ ),
84
+ uninstall: (id: string) =>
85
+ http.del<{ ok: true; id: string; source: string }>(`/profiles/${encodeURIComponent(id)}`),
86
+ };
@@ -25,8 +25,12 @@ export const Tasks = {
25
25
  // (globalPage). Each returns the requested window plus the full total.
26
26
  listPage: (pid: string, { state, limit, offset }: { state: TaskEntry["state"] | "all"; limit: number; offset: number }) =>
27
27
  http.get<unknown>(`/projects/${pid}/tasks?state=${state}&limit=${limit}&offset=${offset}`).then((b) => unwrapPage<TaskEntry>(b)),
28
- globalPage: ({ state, limit, offset }: { state: TaskEntry["state"] | "all"; limit: number; offset: number }) =>
29
- http.get<unknown>(`/tasks?state=${state}&limit=${limit}&offset=${offset}`).then((b) => unwrapPage<GlobalTaskEntry>(b)),
28
+ globalPage: ({ state, limit, offset, status }: { state: TaskEntry["state"] | "all"; limit: number; offset: number; status?: TaskStatus | "" }) =>
29
+ http
30
+ .get<unknown>(
31
+ `/tasks?state=${state}&limit=${limit}&offset=${offset}` + (status ? `&status=${status}` : ""),
32
+ )
33
+ .then((b) => unwrapPage<GlobalTaskEntry>(b)),
30
34
  get: (pid: string, id: string) => http.get<TaskEntry>(`/projects/${pid}/tasks/${id}`),
31
35
  add: (pid: string, body: Partial<TaskEntry>) =>
32
36
  http.post<TaskEntry>(`/projects/${pid}/tasks`, body),
@@ -1,12 +1,13 @@
1
1
  import { type ReactElement } from "react";
2
2
  import { useLocation, useNavigate } from "react-router-dom";
3
3
  import {
4
- Bot, Cpu, Database, Globe, KeyRound, LayoutGrid, MessageCircle, Mic, Monitor, ScrollText, Send, Smartphone, Sparkles, User,
4
+ Bot, Cpu, Database, Globe, IdCard, KeyRound, LayoutGrid, MessageCircle, Mic, Monitor, ScrollText, Send, Smartphone, Sparkles, User,
5
5
  } from "lucide-react";
6
6
  import { useNavCollapse, type TabSection } from "../components/common/TabNav";
7
7
  import { TabLayout } from "../components/common/TabLayout";
8
8
  import { IdentityPanel } from "../components/settings/IdentityPanel";
9
9
  import { SuperAgentPanel } from "../components/settings/SuperAgentPanel";
10
+ import { ProfilePanel } from "../components/settings/ProfilePanel";
10
11
  import { MemoryPanel } from "../components/settings/MemoryPanel";
11
12
  import { SkillsSettings } from "../components/settings/SkillsSettings";
12
13
  import { ModelsTab } from "./base/ModelsTab";
@@ -21,7 +22,7 @@ import { STORAGE } from "../constants";
21
22
  import { t } from "../i18n";
22
23
 
23
24
  type TabKey =
24
- | "identity" | "super_agent" | "engines" | "memory" | "skills" | "telegram" | "devices"
25
+ | "identity" | "super_agent" | "profile" | "engines" | "memory" | "skills" | "telegram" | "devices"
25
26
  | "voice" | "deck" | "desktop" | "web" | "advanced";
26
27
 
27
28
  const SECTIONS: TabSection[] = [
@@ -35,6 +36,7 @@ const SECTIONS: TabSection[] = [
35
36
  title: t("settings.agents_section"),
36
37
  items: [
37
38
  { key: "super_agent", label: t("settings.tabs.super_agent"), icon: Bot },
39
+ { key: "profile", label: t("settings.tabs.profile"), icon: IdCard },
38
40
  { key: "engines", label: t("settings.tabs.engines"), icon: Cpu },
39
41
  { key: "memory", label: "Memory (RAG)", icon: Database },
40
42
  { key: "skills", label: t("skills_page.title"), icon: Sparkles },
@@ -73,6 +75,7 @@ const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "w
73
75
  const PANELS: Record<TabKey, () => ReactElement> = {
74
76
  identity: () => <IdentityPanel />,
75
77
  super_agent: () => <SuperAgentPanel />,
78
+ profile: () => <ProfilePanel />,
76
79
  engines: () => <ModelsTab />,
77
80
  memory: () => <MemoryPanel />,
78
81
  skills: () => <SkillsSettings />,
@@ -113,6 +116,7 @@ function tabFromPath(pathname: string): TabKey {
113
116
  const raw = pathname.split("/").filter(Boolean)[1] || "identity";
114
117
  switch (raw) {
115
118
  case "super-agent": return "super_agent";
119
+ case "profile": return "profile";
116
120
  case "engines": return "engines";
117
121
  case "memory": return "memory";
118
122
  case "skills": return "skills";
@@ -1,6 +1,7 @@
1
1
  import { useState } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
3
  import { Tasks } from "../../lib/api";
4
+ import type { TaskStatus } from "../../types/daemon";
4
5
  import { Section } from "../../components/Section";
5
6
  import { PagedList, usePagedQuery } from "../../components/Pager";
6
7
  import { Badge, Button, Empty, Loading } from "../../components/ui";
@@ -10,10 +11,14 @@ import { t } from "../../i18n";
10
11
  export function GlobalTasksTab() {
11
12
  const navigate = useNavigate();
12
13
  const [state, setState] = useState<"open" | "done" | "dropped" | "all">("open");
14
+ // Workflow sub-status is a different question from state: "what is blocked
15
+ // right now" is not "what is open". Only meaningful for open tasks.
16
+ const [status, setStatus] = useState<TaskStatus | "">("");
17
+ const effectiveStatus = state === "open" ? status : "";
13
18
  const paged = usePagedQuery({
14
- key: `/tasks?state=${state}`,
15
- fetchPage: (limit, offset) => Tasks.globalPage({ state, limit, offset }),
16
- resetKey: state,
19
+ key: `/tasks?state=${state}&status=${effectiveStatus}`,
20
+ fetchPage: (limit, offset) => Tasks.globalPage({ state, limit, offset, status: effectiveStatus }),
21
+ resetKey: `${state}|${effectiveStatus}`,
17
22
  });
18
23
 
19
24
  return (
@@ -29,6 +34,19 @@ export function GlobalTasksTab() {
29
34
  </div>
30
35
  }
31
36
  >
37
+ {state === "open" ? (
38
+ <div className="mb-3 flex flex-wrap gap-1">
39
+ <Button size="sm" variant={status === "" ? "primary" : "ghost"} onClick={() => setStatus("")}>
40
+ {t("project.global_tasks.any_status")}
41
+ </Button>
42
+ {(["pending", "running", "in_review", "blocked"] as const).map((s) => (
43
+ <Button key={s} size="sm" variant={status === s ? "primary" : "ghost"} onClick={() => setStatus(s)}>
44
+ {s.replace("_", " ")}
45
+ </Button>
46
+ ))}
47
+ </div>
48
+ ) : null}
49
+
32
50
  {paged.isLoading && <Loading />}
33
51
  {!paged.isLoading && paged.total === 0 && <Empty>{t("project.global_tasks.empty")}</Empty>}
34
52
  <PagedList paged={paged} fullHeight>