@agentprojectcontext/apx 1.56.2 → 1.58.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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/core/apc/paths.js +7 -0
  3. package/src/core/stores/conversations.js +10 -0
  4. package/src/core/stores/messages.js +37 -6
  5. package/src/core/stores/organization.js +152 -0
  6. package/src/core/stores/project-files.js +199 -0
  7. package/src/core/stores/tasks.js +36 -3
  8. package/src/host/daemon/api/agents.js +22 -2
  9. package/src/host/daemon/api/conversations.js +20 -2
  10. package/src/host/daemon/api/files-project.js +99 -0
  11. package/src/host/daemon/api/organization.js +88 -0
  12. package/src/host/daemon/api/shared.js +7 -0
  13. package/src/host/daemon/api/tasks.js +14 -0
  14. package/src/host/daemon/api.js +4 -0
  15. package/src/interfaces/cli/commands/org.js +77 -0
  16. package/src/interfaces/cli/index.js +48 -0
  17. package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +1 -0
  18. package/src/interfaces/web/dist/assets/index-DPAuXATr.js +705 -0
  19. package/src/interfaces/web/dist/assets/index-DPAuXATr.js.map +1 -0
  20. package/src/interfaces/web/dist/index.html +2 -2
  21. package/src/interfaces/web/package-lock.json +6 -6
  22. package/src/interfaces/web/src/App.tsx +1 -1
  23. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
  24. package/src/interfaces/web/src/components/chat/ChatList.tsx +86 -65
  25. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
  26. package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
  27. package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
  28. package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
  29. package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
  30. package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
  31. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
  32. package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
  33. package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
  34. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
  35. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
  36. package/src/interfaces/web/src/hooks/useChat.ts +39 -7
  37. package/src/interfaces/web/src/i18n/en.ts +113 -3
  38. package/src/interfaces/web/src/i18n/es.ts +113 -3
  39. package/src/interfaces/web/src/lib/api/conversations.ts +6 -0
  40. package/src/interfaces/web/src/lib/api/organization.ts +18 -0
  41. package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
  42. package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
  43. package/src/interfaces/web/src/lib/api.ts +2 -0
  44. package/src/interfaces/web/src/lib/slug.ts +11 -0
  45. package/src/interfaces/web/src/screens/ProjectScreen.tsx +22 -3
  46. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  47. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
  48. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
  49. package/src/interfaces/web/src/screens/project/ChatTab.tsx +136 -36
  50. package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
  51. package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
  52. package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
  53. package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
  54. package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
  55. package/src/interfaces/web/src/types/daemon.ts +68 -0
  56. package/src/interfaces/web/dist/assets/index-CAUezTBY.css +0 -1
  57. package/src/interfaces/web/dist/assets/index-DaE_memX.js +0 -651
  58. package/src/interfaces/web/dist/assets/index-DaE_memX.js.map +0 -1
@@ -33,6 +33,10 @@ export const es = {
33
33
  hide: "Ocultar",
34
34
  copy: "Copiar",
35
35
  run: "Ejecutar",
36
+ refresh: "Refrescar",
37
+ view_all: "Ver todo",
38
+ saved: "Guardado.",
39
+ deleted: "Eliminado.",
36
40
  pager_prev: "Anterior",
37
41
  pager_next: "Siguiente",
38
42
  pager_page: "Página {page} de {total}",
@@ -293,10 +297,14 @@ export const es = {
293
297
  vars: "Variables",
294
298
  logs: "Logs",
295
299
  memories: "Memorias",
300
+ structure: "Estructura",
301
+ docs: "Docs",
302
+ files: "Archivos",
296
303
  },
297
304
 
298
305
  sections: {
299
306
  workspace: "Workspace",
307
+ content: "Contenido",
300
308
  automation: "Automatización",
301
309
  knowledge: "Conversaciones",
302
310
  config: "Config",
@@ -305,11 +313,18 @@ export const es = {
305
313
  overview: {
306
314
  tasks_open: "Tasks abiertas",
307
315
  routines: "Rutinas",
316
+ routines_active: "Rutinas activas",
308
317
  agents: "Agents",
309
318
  mcps: "MCPs",
310
319
  artifacts: "Artifacts",
311
320
  chat: "Chat (super-agent)",
312
321
  chat_value: "abrir",
322
+ roster: "Equipo",
323
+ no_agents: "Todavía no hay agentes.",
324
+ orchestrators: "Orquestadores",
325
+ specialists: "Especialistas",
326
+ recent_tasks: "Tasks recientes",
327
+ no_activity: "No hay tasks abiertas.",
313
328
  },
314
329
 
315
330
  artifacts: {
@@ -320,6 +335,7 @@ export const es = {
320
335
  chat: {
321
336
  title: "Chat con agente",
322
337
  subtitle: "Chat directo con el agente del proyecto.",
338
+ live_title: "Chat con {agent}",
323
339
  superagent_title: "Chat con {persona}",
324
340
  superagent_subtitle: "Chat con {persona} — el super-agente APX. Puede usar tools (proyectos, tasks, mcps, agentes).",
325
341
  loaded_subtitle: "Conversación cargada con {slug}. Lo que mandes se agrega a este chat.",
@@ -328,7 +344,13 @@ export const es = {
328
344
  placeholder: "Escribí algo y enter para enviar (shift+enter = nueva línea)",
329
345
  send: "Enviar",
330
346
  stop: "Stop",
331
- clear: "Limpiar",
347
+ new_session: "Nueva sesión",
348
+ delete: "Borrar",
349
+ delete_confirm_title: "Borrar chat",
350
+ delete_confirm_desc: "Esta acción no se puede deshacer. Se elimina para siempre el historial de este chat.",
351
+ deleted: "Chat borrado.",
352
+ meta_created: "Creado {date} · {channel}",
353
+ meta_new: "Chat nuevo · {channel}",
332
354
  copy: "copiar",
333
355
  copied: "Copiado.",
334
356
  stopped_marker: " [detenido]",
@@ -346,8 +368,7 @@ export const es = {
346
368
  all_agents: "Todos los agentes",
347
369
  empty: "No hay conversaciones todavía. Arrancá una desde la derecha.",
348
370
  count: "{n} en total",
349
- live_with: "Live · {slug}",
350
- live_subtitle: "Sesión en memoria",
371
+ pick_agent: "Elegí un agente",
351
372
  },
352
373
  },
353
374
 
@@ -1293,6 +1314,95 @@ export const es = {
1293
1314
  role_assigned: "{name} → {role}",
1294
1315
  },
1295
1316
 
1317
+ agents_form: {
1318
+ emoji: "Emoji",
1319
+ area: "Área",
1320
+ role: "Rol",
1321
+ no_role: "— sin rol —",
1322
+ autonomy: "Autonomía",
1323
+ autonomy_hint: "Cuánto puede hacer el agente sin pedir confirmación.",
1324
+ auto_total: "Total",
1325
+ auto_automatico: "Auto",
1326
+ auto_permiso: "Permiso",
1327
+ },
1328
+
1329
+ structure: {
1330
+ title: "Estructura",
1331
+ subtitle: "Áreas y roles de la empresa. Las áreas agrupan agentes; los roles definen su función.",
1332
+ info: "Las áreas son agrupaciones opcionales. Los roles definen la función de un agente y pueden pertenecer a un área.",
1333
+ empty: "Todavía no hay áreas ni roles. Creá el primero arriba.",
1334
+ new_area: "Nueva área",
1335
+ new_role: "Nuevo rol",
1336
+ edit_area: "Editar área",
1337
+ edit_role: "Editar rol",
1338
+ create_area: "Crear área",
1339
+ create_role: "Crear rol",
1340
+ name: "Nombre",
1341
+ slug: "Slug",
1342
+ goal: "Objetivo",
1343
+ goal_hint: "Para qué existe esta área (opcional).",
1344
+ area: "Área",
1345
+ description: "Descripción",
1346
+ no_area: "— sin área —",
1347
+ roles: "Roles",
1348
+ add_role: "rol",
1349
+ no_roles: "sin roles",
1350
+ general_roles: "Roles generales",
1351
+ delete_area: "Borrar área",
1352
+ delete_role: "Borrar rol",
1353
+ delete_area_desc: "¿Borrar el área \"{name}\"? Sus roles quedan sin área, no se eliminan.",
1354
+ delete_role_desc: "¿Borrar el rol \"{name}\"?",
1355
+ },
1356
+
1357
+ files: {
1358
+ docs_label: "Docs",
1359
+ files_label: "Archivos",
1360
+ new_doc: "Nuevo documento",
1361
+ new_doc_hint: "Podés usar carpetas: cases/onboarding/spec.md",
1362
+ empty: "No hay archivos.",
1363
+ docs_empty: "Todavía no hay documentación. Creá el primer documento.",
1364
+ truncated: "Listado recortado (demasiados archivos).",
1365
+ select_prompt: "Elegí un archivo para verlo.",
1366
+ save: "Guardar",
1367
+ saved: "Guardado.",
1368
+ deleted: "Eliminado.",
1369
+ created: "Documento creado.",
1370
+ edit: "Editar",
1371
+ preview: "Preview",
1372
+ discard: "Descartar",
1373
+ no_preview: "No hay preview para este archivo.",
1374
+ too_large: "Archivo demasiado grande para mostrar.",
1375
+ path_label: "Ruta del archivo",
1376
+ path_example: "ej. cases/onboarding/spec.md",
1377
+ create: "Crear",
1378
+ },
1379
+
1380
+ tasks: {
1381
+ state_open: "abiertas",
1382
+ state_done: "hechas",
1383
+ state_dropped: "descartadas",
1384
+ status_pending: "pendiente",
1385
+ status_running: "corriendo",
1386
+ status_in_review: "en revisión",
1387
+ status_blocked: "bloqueada",
1388
+ done_label: "hecha",
1389
+ dropped_label: "descartada",
1390
+ detail_title: "Detalle de task",
1391
+ field_title: "Título",
1392
+ field_prompt: "Prompt",
1393
+ field_status: "Estado",
1394
+ field_agent: "Agente",
1395
+ field_creator: "Creada por",
1396
+ field_source: "Origen",
1397
+ field_created: "Creada",
1398
+ field_updated: "Actualizada",
1399
+ field_done: "Completada",
1400
+ prompt_ph: "Descripción / prompt de la task…",
1401
+ toggle_prompt: "Prompt",
1402
+ view_thread: "Ver conversación",
1403
+ mark_done: "Completar",
1404
+ },
1405
+
1296
1406
  agents_ui: {
1297
1407
  model_router_default: "modelo: default del router",
1298
1408
  slug_kebab_hint: "kebab-case, ej. reviewer, my-agent, content-writer",
@@ -12,6 +12,12 @@ export const Conversations = {
12
12
  http.get<ThreadListEntry[]>(`/projects/${pid}/super-agent/threads`),
13
13
  thread: (pid: string, channel: string, id: string) =>
14
14
  http.get<ThreadDetail>(`/projects/${pid}/super-agent/threads/${channel}/${id}`),
15
+ // Delete a persisted agent conversation (its `.md` file).
16
+ remove: (pid: string, slug: string, id: string) =>
17
+ http.del<{ ok: boolean }>(`/projects/${pid}/agents/${slug}/conversations/${id}`),
18
+ // Delete a super-agent channel thread (its channel+day ledger file).
19
+ removeThread: (pid: string, channel: string, id: string) =>
20
+ http.del<{ ok: boolean }>(`/projects/${pid}/super-agent/threads/${channel}/${id}`),
15
21
  compact: (pid: string, slug: string, id?: string) =>
16
22
  http.post<{ ok?: boolean }>(
17
23
  id
@@ -0,0 +1,18 @@
1
+ import { http } from "../http";
2
+ import type { Organization, OrgArea, OrgRole } from "../../types/daemon";
3
+
4
+ export const Org = {
5
+ get: (pid: string) => http.get<Organization>(`/projects/${pid}/organization`),
6
+ createArea: (pid: string, body: { name: string; slug?: string; goal?: string | null }) =>
7
+ http.post<OrgArea>(`/projects/${pid}/organization/areas`, body),
8
+ updateArea: (pid: string, slug: string, patch: { name?: string; goal?: string | null }) =>
9
+ http.patch<OrgArea>(`/projects/${pid}/organization/areas/${encodeURIComponent(slug)}`, patch),
10
+ removeArea: (pid: string, slug: string) =>
11
+ http.del<{ ok: boolean }>(`/projects/${pid}/organization/areas/${encodeURIComponent(slug)}`),
12
+ createRole: (pid: string, body: { name: string; slug?: string; area?: string | null; description?: string | null }) =>
13
+ http.post<OrgRole>(`/projects/${pid}/organization/roles`, body),
14
+ updateRole: (pid: string, slug: string, patch: { name?: string; area?: string | null; description?: string | null }) =>
15
+ http.patch<OrgRole>(`/projects/${pid}/organization/roles/${encodeURIComponent(slug)}`, patch),
16
+ removeRole: (pid: string, slug: string) =>
17
+ http.del<{ ok: boolean }>(`/projects/${pid}/organization/roles/${encodeURIComponent(slug)}`),
18
+ };
@@ -0,0 +1,19 @@
1
+ import { http } from "../http";
2
+ import type { FileTreeResponse, FileContent } from "../../types/daemon";
3
+
4
+ export type FileScope = "project" | "docs";
5
+
6
+ // Project file browser + docs editor. `scope` picks the sandbox: the whole
7
+ // repo ("project") or the docs subfolder ("docs"). Same store, different root.
8
+ export const ProjectFiles = {
9
+ tree: (pid: string, scope: FileScope = "project") =>
10
+ http.get<FileTreeResponse>(`/projects/${pid}/fs/tree?scope=${scope}`),
11
+ read: (pid: string, path: string, scope: FileScope = "project") =>
12
+ http.get<FileContent>(`/projects/${pid}/fs/file?scope=${scope}&path=${encodeURIComponent(path)}`),
13
+ write: (pid: string, path: string, content: string, scope: FileScope = "project") =>
14
+ http.put<{ ok: boolean; path: string; bytes: number }>(`/projects/${pid}/fs/file`, { scope, path, content }),
15
+ mkdir: (pid: string, path: string, scope: FileScope = "project") =>
16
+ http.post<{ ok: boolean; path: string }>(`/projects/${pid}/fs/dir`, { scope, path }),
17
+ remove: (pid: string, path: string, scope: FileScope = "project") =>
18
+ http.del<{ ok: boolean }>(`/projects/${pid}/fs/entry?scope=${scope}&path=${encodeURIComponent(path)}`),
19
+ };
@@ -1,11 +1,20 @@
1
1
  import { http, unwrapPage } from "../http";
2
- import type { TaskEntry } from "../../types/daemon";
2
+ import type { TaskEntry, TaskStatus } from "../../types/daemon";
3
3
 
4
4
  export interface GlobalTaskEntry extends TaskEntry {
5
5
  project_id: number;
6
6
  project_name: string;
7
7
  }
8
8
 
9
+ export interface TaskSummary {
10
+ open: number;
11
+ done: number;
12
+ dropped: number;
13
+ overdue: number;
14
+ total: number;
15
+ status: Record<TaskStatus, number>;
16
+ }
17
+
9
18
  export const Tasks = {
10
19
  // Full sets (no pagination) — unwrapped to plain arrays for non-paged callers.
11
20
  list: (pid: string, state: TaskEntry["state"] | "all" = "open") =>
@@ -18,9 +27,15 @@ export const Tasks = {
18
27
  http.get<unknown>(`/projects/${pid}/tasks?state=${state}&limit=${limit}&offset=${offset}`).then((b) => unwrapPage<TaskEntry>(b)),
19
28
  globalPage: ({ state, limit, offset }: { state: TaskEntry["state"] | "all"; limit: number; offset: number }) =>
20
29
  http.get<unknown>(`/tasks?state=${state}&limit=${limit}&offset=${offset}`).then((b) => unwrapPage<GlobalTaskEntry>(b)),
30
+ get: (pid: string, id: string) => http.get<TaskEntry>(`/projects/${pid}/tasks/${id}`),
21
31
  add: (pid: string, body: Partial<TaskEntry>) =>
22
32
  http.post<TaskEntry>(`/projects/${pid}/tasks`, body),
33
+ patch: (pid: string, id: string, patch: Partial<TaskEntry>) =>
34
+ http.patch<TaskEntry>(`/projects/${pid}/tasks/${id}`, { patch }),
35
+ status: (pid: string, id: string, status: TaskStatus) =>
36
+ http.post<TaskEntry>(`/projects/${pid}/tasks/${id}/status`, { status }),
23
37
  done: (pid: string, id: string) => http.post<TaskEntry>(`/projects/${pid}/tasks/${id}/done`),
24
38
  drop: (pid: string, id: string) => http.post<TaskEntry>(`/projects/${pid}/tasks/${id}/drop`),
25
39
  reopen: (pid: string, id: string) => http.post<TaskEntry>(`/projects/${pid}/tasks/${id}/reopen`),
40
+ summary: (pid: string) => http.get<TaskSummary>(`/projects/${pid}/tasks-summary`),
26
41
  };
@@ -24,6 +24,8 @@ export * from "./api/deck";
24
24
  export * from "./api/code";
25
25
  export * from "./api/artifacts";
26
26
  export * from "./api/skills";
27
+ export * from "./api/organization";
28
+ export * from "./api/projectFiles";
27
29
 
28
30
  // Re-export the daemon types so older imports of "../lib/api" still work.
29
31
  export type {
@@ -0,0 +1,11 @@
1
+ // Kebab-case a free-text name into a slug. Mirrors the backend's slugifyName
2
+ // (core/stores/organization.js) so a name typed in the web produces the same
3
+ // slug the daemon would derive.
4
+ export function slugify(name: string): string {
5
+ return String(name || "")
6
+ .trim()
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, "-")
9
+ .replace(/^-+|-+$/g, "")
10
+ .replace(/-{2,}/g, "-");
11
+ }
@@ -4,6 +4,7 @@ import {
4
4
  Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
5
5
  MessagesSquare, Send, KeyRound,
6
6
  LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2,
7
+ Building2, FileText, FolderTree,
7
8
  } from "lucide-react";
8
9
  import { useNavCollapse, type TabSection } from "../components/common/TabNav";
9
10
  import { TabLayout } from "../components/common/TabLayout";
@@ -30,10 +31,14 @@ import { TelegramTab } from "./project/TelegramTab";
30
31
  import { MemoriesTab } from "./project/MemoriesTab";
31
32
  import { ArtifactsTab } from "./project/ArtifactsTab";
32
33
  import { AgentDetailScreen } from "./project/AgentDetailScreen";
34
+ import { StructureTab } from "./project/StructureTab";
35
+ import { DocsTab } from "./project/DocsTab";
36
+ import { FilesTab } from "./project/FilesTab";
33
37
 
34
38
  type NavKey =
35
39
  | "" | "chat" | "config" | "telegram"
36
- | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts";
40
+ | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts"
41
+ | "structure" | "docs" | "files";
37
42
 
38
43
  export function ProjectScreen() {
39
44
  const navigate = useNavigate();
@@ -79,6 +84,9 @@ export function ProjectScreen() {
79
84
  },
80
85
  ];
81
86
  }
87
+ // Structure (org roles/areas) is only meaningful for company/enterprise
88
+ // projects — gate it on the project kind.
89
+ const isCompany = project?.kind === "company";
82
90
  return [
83
91
  {
84
92
  title: t("project.sections.workspace"),
@@ -87,9 +95,17 @@ export function ProjectScreen() {
87
95
  { key: "telegram", label: t("project.nav.telegram"), icon: Send },
88
96
  { key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
89
97
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
98
+ ...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
90
99
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
91
100
  ],
92
101
  },
102
+ {
103
+ title: t("project.sections.content"),
104
+ items: [
105
+ { key: "docs", label: t("project.nav.docs"), icon: FileText },
106
+ { key: "files", label: t("project.nav.files"), icon: FolderTree },
107
+ ],
108
+ },
93
109
  {
94
110
  title: t("project.sections.automation"),
95
111
  items: [
@@ -108,7 +124,7 @@ export function ProjectScreen() {
108
124
  ],
109
125
  },
110
126
  ];
111
- }, [isBase]);
127
+ }, [isBase, project?.kind]);
112
128
 
113
129
  // First path segment after /p/:pid — so deep routes like agents/:slug still
114
130
  // highlight the "agents" nav item.
@@ -141,7 +157,7 @@ export function ProjectScreen() {
141
157
  onChange={onTabChange}
142
158
  collapsed={collapsed}
143
159
  onToggleCollapse={toggle}
144
- contentClassName="w-full space-y-6 p-6 pt-3"
160
+ contentClassName="w-full space-y-6 py-6 pt-3 pr-6 pl-1"
145
161
  testId={`project-tab-${active || "overview"}`}
146
162
  >
147
163
  <Routes>
@@ -155,6 +171,9 @@ export function ProjectScreen() {
155
171
  <Route path="telegram" element={<TelegramTab pid={pid} />} />
156
172
  <Route path="agents" element={<AgentsTab pid={pid} />} />
157
173
  <Route path="agents/:slug" element={<AgentDetailScreen pid={pid} />} />
174
+ <Route path="structure" element={<StructureTab pid={pid} />} />
175
+ <Route path="docs" element={<DocsTab pid={pid} />} />
176
+ <Route path="files" element={<FilesTab pid={pid} />} />
158
177
  <Route path="memories" element={<MemoriesTab pid={pid} />} />
159
178
  <Route path="routines" element={<RoutinesTab pid={pid} />} />
160
179
  <Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
@@ -101,7 +101,7 @@ export function SettingsScreen() {
101
101
  onChange={(k) => navigate(k === "identity" ? "/settings" : `/settings/${pathFromTab(k as TabKey)}`)}
102
102
  collapsed={collapsed}
103
103
  onToggleCollapse={toggle}
104
- contentClassName={`w-full ${WIDE_TABS.has(active) ? "" : "mx-auto max-w-3xl"} space-y-6 p-6 pt-3`}
104
+ contentClassName={`w-full ${WIDE_TABS.has(active) ? "" : "mx-auto max-w-3xl"} space-y-6 py-6 pt-3 pr-6 pl-4`}
105
105
  testId={`settings-tab-${active}`}
106
106
  >
107
107
  <Panel />
@@ -12,8 +12,11 @@ import { Badge, Button, Field, Input, Loading, Switch, Textarea } from "../../co
12
12
  import { Tip } from "../../components/ui/tip";
13
13
  import { UiSelect } from "../../components/UiSelect";
14
14
  import { useToast } from "../../components/Toast";
15
+ import { ConfirmDialog } from "../../components/common/ConfirmDialog";
16
+ import { EmojiInput, AutonomyPicker, AreaRoleFields } from "../../components/agents/AgentFormFields";
15
17
  import { cn } from "../../lib/cn";
16
18
  import { t } from "../../i18n";
19
+ import type { AgentAutonomy } from "../../types/daemon";
17
20
  import { AgentBrainGraph, type BrainNode } from "./AgentBrainGraph";
18
21
 
19
22
  type TabKey = "overview" | "memories" | "records" | "sleep" | "brain" | "config";
@@ -28,7 +31,7 @@ function buildTabs(): { key: TabKey; label: string; icon: typeof Bot }[] {
28
31
  ];
29
32
  }
30
33
 
31
- function typeOptions() {
34
+ export function typeOptions() {
32
35
  return [
33
36
  { value: "", label: t("agents_ui.type_none") },
34
37
  { value: "orchestrator", label: t("agents_ui.type_orchestrator"), description: t("agents_ui.type_orchestrator_desc") },
@@ -205,9 +208,11 @@ function AgentConfigForm({
205
208
  onDeleted: () => void;
206
209
  }) {
207
210
  const toast = useToast();
211
+ const [emoji, setEmoji] = useState(agent.emoji || "");
208
212
  const [type, setType] = useState(agent.type || "");
209
213
  const [area, setArea] = useState(agent.area || "");
210
214
  const [role, setRole] = useState(agent.role || "");
215
+ const [autonomy, setAutonomy] = useState<AgentAutonomy | "">(agent.autonomy || "");
211
216
  const [model, setModel] = useState(agent.model || "");
212
217
  const [parent, setParent] = useState(agent.parent || "");
213
218
  const [isMaster, setIsMaster] = useState(!!agent.is_master);
@@ -216,14 +221,17 @@ function AgentConfigForm({
216
221
  const [description, setDescription] = useState(agent.description || "");
217
222
  const [system, setSystem] = useState(agent.system || "");
218
223
  const [busy, setBusy] = useState(false);
224
+ const [confirmDelete, setConfirmDelete] = useState(false);
219
225
 
220
226
  const save = async () => {
221
227
  setBusy(true);
222
228
  try {
223
229
  await Agents.update(pid, agent.slug, {
230
+ emoji: emoji || null,
224
231
  type: type || null,
225
232
  area: area || null,
226
233
  role: role || null,
234
+ autonomy: autonomy || null,
227
235
  model: model || null,
228
236
  parent: parent || null,
229
237
  is_master: isMaster || type === "orchestrator",
@@ -239,20 +247,23 @@ function AgentConfigForm({
239
247
  };
240
248
 
241
249
  const del = async () => {
242
- if (!confirm(t("project.agent_detail.delete_confirm", { slug: agent.slug }))) return;
243
- try { await Agents.remove(pid, agent.slug); toast.success(t("project.agent_detail.delete_success")); onDeleted(); }
244
- catch (e) { toast.error((e as Error).message); }
250
+ await Agents.remove(pid, agent.slug);
251
+ toast.success(t("project.agent_detail.delete_success"));
252
+ onDeleted();
245
253
  };
246
254
 
247
255
  return (
248
256
  <Section title={t("project.agent_detail.config_title")} description={`.apc/agents/${agent.slug}.md — ${t("agents_ui.config_def_desc")}`}>
249
257
  <div className="space-y-3">
250
- <div className="grid grid-cols-2 gap-3">
258
+ <div className="grid grid-cols-[80px_1fr] gap-3">
259
+ <Field label={t("agents_form.emoji")}><EmojiInput value={emoji} onChange={setEmoji} /></Field>
251
260
  <Field label={t("project.agent_detail.type_label")}><UiSelect value={type} onChange={setType} options={typeOptions()} /></Field>
252
- <Field label={t("project.agent_detail.area_label")} hint={t("project.agent_detail.area_hint")}><Input value={area} onChange={(e) => setArea(e.target.value)} placeholder={t("project.agent_detail.area_ph")} /></Field>
253
261
  </div>
262
+ <AreaRoleFields pid={pid} area={area} role={role} onArea={setArea} onRole={setRole} />
263
+ <Field label={t("agents_form.autonomy")} hint={t("agents_form.autonomy_hint")}>
264
+ <AutonomyPicker value={autonomy} onChange={setAutonomy} />
265
+ </Field>
254
266
  <div className="grid grid-cols-2 gap-3">
255
- <Field label={t("project.agent_detail.role_label")}><Input value={role} onChange={(e) => setRole(e.target.value)} placeholder="Operations Lead" /></Field>
256
267
  <Field label={t("project.agent_detail.parent_label")}>
257
268
  <UiSelect
258
269
  value={parent}
@@ -261,10 +272,10 @@ function AgentConfigForm({
261
272
  options={[{ value: "", label: t("project.agent_detail.none_parent") }, ...agents.filter((x) => x.slug !== agent.slug).map((x) => ({ value: x.slug, label: x.slug }))]}
262
273
  />
263
274
  </Field>
275
+ <Field label={t("project.agent_detail.model_label")} hint={t("project.agent_detail.model_hint")}>
276
+ <Input value={model} onChange={(e) => setModel(e.target.value)} placeholder={t("project.agent_detail.model_ph")} />
277
+ </Field>
264
278
  </div>
265
- <Field label={t("project.agent_detail.model_label")} hint={t("project.agent_detail.model_hint")}>
266
- <Input value={model} onChange={(e) => setModel(e.target.value)} placeholder={t("project.agent_detail.model_ph")} />
267
- </Field>
268
279
  <Field label={t("project.agent_detail.skills_label")}><Input value={skills} onChange={(e) => setSkills(e.target.value)} placeholder="skill-a, skill-b" /></Field>
269
280
  <ToolsPicker value={tools} onChange={setTools} />
270
281
  <Field label={t("project.agent_detail.bio_label")}><Textarea rows={2} value={description} onChange={(e) => setDescription(e.target.value)} /></Field>
@@ -274,10 +285,19 @@ function AgentConfigForm({
274
285
  <Switch checked={isMaster} onChange={setIsMaster} label={t("project.agent_detail.master_label")} />
275
286
 
276
287
  <div className="flex items-center justify-between border-t border-border pt-3">
277
- <Button variant="destructive" onClick={del}><Trash2 size={13} /> {t("project.agent_detail.delete_btn")}</Button>
288
+ <Button variant="destructive" onClick={() => setConfirmDelete(true)}><Trash2 size={13} /> {t("project.agent_detail.delete_btn")}</Button>
278
289
  <Button variant="primary" loading={busy} onClick={save}><Save size={13} /> {t("project.agent_detail.save_btn")}</Button>
279
290
  </div>
280
291
  </div>
292
+
293
+ <ConfirmDialog
294
+ open={confirmDelete}
295
+ onClose={() => setConfirmDelete(false)}
296
+ onConfirm={del}
297
+ title={t("project.agent_detail.delete_btn")}
298
+ description={t("project.agent_detail.delete_confirm", { slug: agent.slug })}
299
+ confirmLabel={t("project.agent_detail.delete_btn")}
300
+ />
281
301
  </Section>
282
302
  );
283
303
  }
@@ -8,8 +8,11 @@ import { Section } from "../../components/Section";
8
8
  import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch, Textarea } from "../../components/ui";
9
9
  import { UiSelect } from "../../components/UiSelect";
10
10
  import { useToast } from "../../components/Toast";
11
+ import { EmojiInput, AutonomyPicker, AreaRoleFields } from "../../components/agents/AgentFormFields";
11
12
  import { cn } from "../../lib/cn";
12
13
  import { t } from "../../i18n";
14
+ import type { AgentAutonomy } from "../../types/daemon";
15
+ import { typeOptions } from "./AgentDetailScreen";
13
16
 
14
17
  const LANGS = ["", "es", "en", "pt", "fr", "it", "de"];
15
18
  const csv = (s: string) => s.split(",").map((x) => x.trim()).filter(Boolean);
@@ -224,7 +227,7 @@ function AgentCard({
224
227
  >
225
228
  <div className="flex items-center gap-2">
226
229
  <div className={cn("flex size-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br", gradient)}>
227
- <Icon className="size-4 text-white" />
230
+ {agent.emoji ? <span className="text-base leading-none">{agent.emoji}</span> : <Icon className="size-4 text-white" />}
228
231
  </div>
229
232
  <span className="truncate text-sm font-semibold">{agent.slug}</span>
230
233
  </div>
@@ -250,7 +253,7 @@ function ListView({ agents, onOpen, onChat }: { agents: AgentEntry[]; onOpen: (s
250
253
  return (
251
254
  <div key={a.slug} data-testid={`agent-card-${a.slug}`} className="flex cursor-pointer items-center gap-4 rounded-xl border border-border bg-muted/30 p-3 hover:border-muted-fg/50" onClick={() => onOpen(a.slug)}>
252
255
  <div className={cn("flex size-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br", gradient)}>
253
- <Icon className="size-4 text-white" />
256
+ {a.emoji ? <span className="text-lg leading-none">{a.emoji}</span> : <Icon className="size-4 text-white" />}
254
257
  </div>
255
258
  <div className="min-w-0 flex-1">
256
259
  <div className="flex flex-wrap items-center gap-2">
@@ -282,7 +285,11 @@ function CreateAgentDialog({
282
285
  }: { open: boolean; onClose: () => void; onCreated: () => void; pid: string; agents: AgentEntry[] }) {
283
286
  const toast = useToast();
284
287
  const [slug, setSlug] = useState("");
288
+ const [emoji, setEmoji] = useState("");
289
+ const [type, setType] = useState("");
285
290
  const [role, setRole] = useState("");
291
+ const [area, setArea] = useState("");
292
+ const [autonomy, setAutonomy] = useState<AgentAutonomy | "">("");
286
293
  const [model, setModel] = useState("");
287
294
  const [language, setLanguage] = useState("");
288
295
  const [description, setDescription] = useState("");
@@ -293,8 +300,9 @@ function CreateAgentDialog({
293
300
  const [busy, setBusy] = useState(false);
294
301
 
295
302
  const reset = () => {
296
- setSlug(""); setRole(""); setModel(""); setLanguage("");
297
- setDescription(""); setSkills(""); setTools(""); setIsMaster(false); setParent("");
303
+ setSlug(""); setEmoji(""); setType(""); setRole(""); setArea(""); setAutonomy("");
304
+ setModel(""); setLanguage(""); setDescription(""); setSkills(""); setTools("");
305
+ setIsMaster(false); setParent("");
298
306
  };
299
307
 
300
308
  const submit = async () => {
@@ -303,13 +311,17 @@ function CreateAgentDialog({
303
311
  try {
304
312
  await Agents.create(pid, {
305
313
  slug,
314
+ emoji: emoji || undefined,
315
+ type: type || undefined,
306
316
  role: role || undefined,
317
+ area: area || undefined,
318
+ autonomy: autonomy || undefined,
307
319
  model: model || undefined,
308
320
  language: language || undefined,
309
321
  description: description || undefined,
310
322
  skills: csv(skills),
311
323
  tools: csv(tools),
312
- is_master: isMaster,
324
+ is_master: isMaster || type === "orchestrator",
313
325
  parent: parent || undefined,
314
326
  });
315
327
  toast.success(t("project.agents.create_success", { slug }));
@@ -334,10 +346,15 @@ function CreateAgentDialog({
334
346
  }
335
347
  >
336
348
  <div className="space-y-3">
337
- <div className="grid grid-cols-2 gap-3">
349
+ <div className="grid grid-cols-[80px_1fr_1fr] gap-3">
350
+ <Field label={t("agents_form.emoji")}><EmojiInput value={emoji} onChange={setEmoji} /></Field>
338
351
  <Field label={t("project.agents.slug_label")}><Input autoFocus data-testid="agent-slug" value={slug} onChange={(e) => setSlug(e.target.value)} placeholder={t("project.agents.slug_ph")} /></Field>
339
- <Field label={t("project.agents.role_label")}><Input value={role} onChange={(e) => setRole(e.target.value)} placeholder={t("project.agents.role_ph")} /></Field>
352
+ <Field label={t("project.agent_detail.type_label")}><UiSelect value={type} onChange={setType} options={typeOptions()} /></Field>
340
353
  </div>
354
+ <AreaRoleFields pid={pid} area={area} role={role} onArea={setArea} onRole={setRole} />
355
+ <Field label={t("agents_form.autonomy")} hint={t("agents_form.autonomy_hint")}>
356
+ <AutonomyPicker value={autonomy} onChange={setAutonomy} />
357
+ </Field>
341
358
  <div className="grid grid-cols-2 gap-3">
342
359
  <Field label={t("project.agents.model_label")} hint={t("project.agents.model_hint")}><Input value={model} onChange={(e) => setModel(e.target.value)} /></Field>
343
360
  <Field label={t("project.agents.lang_label")}><UiSelect value={language} onChange={setLanguage} options={LANGS.map((l) => ({ value: l, label: l || "—" }))} /></Field>