@agentprojectcontext/apx 1.57.0 → 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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/core/apc/paths.js +7 -0
  3. package/src/core/stores/organization.js +152 -0
  4. package/src/core/stores/project-files.js +199 -0
  5. package/src/core/stores/tasks.js +36 -3
  6. package/src/host/daemon/api/agents.js +22 -2
  7. package/src/host/daemon/api/files-project.js +99 -0
  8. package/src/host/daemon/api/organization.js +88 -0
  9. package/src/host/daemon/api/shared.js +7 -0
  10. package/src/host/daemon/api/tasks.js +14 -0
  11. package/src/host/daemon/api.js +4 -0
  12. package/src/interfaces/cli/commands/org.js +77 -0
  13. package/src/interfaces/cli/index.js +48 -0
  14. package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +1 -0
  15. package/src/interfaces/web/dist/assets/index-DPAuXATr.js +705 -0
  16. package/src/interfaces/web/dist/assets/index-DPAuXATr.js.map +1 -0
  17. package/src/interfaces/web/dist/index.html +2 -2
  18. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
  19. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
  20. package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
  21. package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
  22. package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
  23. package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
  24. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
  25. package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
  26. package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
  27. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
  28. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
  29. package/src/interfaces/web/src/i18n/en.ts +104 -0
  30. package/src/interfaces/web/src/i18n/es.ts +104 -0
  31. package/src/interfaces/web/src/lib/api/organization.ts +18 -0
  32. package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
  33. package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
  34. package/src/interfaces/web/src/lib/api.ts +2 -0
  35. package/src/interfaces/web/src/lib/slug.ts +11 -0
  36. package/src/interfaces/web/src/screens/ProjectScreen.tsx +21 -2
  37. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
  38. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
  39. package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
  40. package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
  41. package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
  42. package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
  43. package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
  44. package/src/interfaces/web/src/types/daemon.ts +63 -0
  45. package/src/interfaces/web/dist/assets/index-CEI8DfVg.css +0 -1
  46. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js +0 -651
  47. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js.map +0 -1
@@ -32,6 +32,10 @@ export const en = {
32
32
  hide: "Hide",
33
33
  copy: "Copy",
34
34
  run: "Run",
35
+ refresh: "Refresh",
36
+ view_all: "View all",
37
+ saved: "Saved.",
38
+ deleted: "Deleted.",
35
39
  pager_prev: "Previous",
36
40
  pager_next: "Next",
37
41
  pager_page: "Page {page} of {total}",
@@ -292,10 +296,14 @@ export const en = {
292
296
  vars: "Variables",
293
297
  logs: "Logs",
294
298
  memories: "Memories",
299
+ structure: "Structure",
300
+ docs: "Docs",
301
+ files: "Files",
295
302
  },
296
303
 
297
304
  sections: {
298
305
  workspace: "Workspace",
306
+ content: "Content",
299
307
  automation: "Automation",
300
308
  knowledge: "Conversations",
301
309
  config: "Config",
@@ -304,11 +312,18 @@ export const en = {
304
312
  overview: {
305
313
  tasks_open: "Open tasks",
306
314
  routines: "Routines",
315
+ routines_active: "Active routines",
307
316
  agents: "Agents",
308
317
  mcps: "MCPs",
309
318
  artifacts: "Artifacts",
310
319
  chat: "Chat (super-agent)",
311
320
  chat_value: "open",
321
+ roster: "Team",
322
+ no_agents: "No agents yet.",
323
+ orchestrators: "Orchestrators",
324
+ specialists: "Specialists",
325
+ recent_tasks: "Recent tasks",
326
+ no_activity: "No open tasks.",
312
327
  },
313
328
 
314
329
  artifacts: {
@@ -1301,6 +1316,95 @@ export const en = {
1301
1316
  role_assigned: "{name} → {role}",
1302
1317
  },
1303
1318
 
1319
+ agents_form: {
1320
+ emoji: "Emoji",
1321
+ area: "Area",
1322
+ role: "Role",
1323
+ no_role: "— no role —",
1324
+ autonomy: "Autonomy",
1325
+ autonomy_hint: "How much the agent can do without asking for confirmation.",
1326
+ auto_total: "Total",
1327
+ auto_automatico: "Auto",
1328
+ auto_permiso: "Permission",
1329
+ },
1330
+
1331
+ structure: {
1332
+ title: "Structure",
1333
+ subtitle: "Company areas and roles. Areas group agents; roles define their function.",
1334
+ info: "Areas are optional groupings. Roles define an agent's function and may belong to an area.",
1335
+ empty: "No areas or roles yet. Create the first one above.",
1336
+ new_area: "New area",
1337
+ new_role: "New role",
1338
+ edit_area: "Edit area",
1339
+ edit_role: "Edit role",
1340
+ create_area: "Create area",
1341
+ create_role: "Create role",
1342
+ name: "Name",
1343
+ slug: "Slug",
1344
+ goal: "Goal",
1345
+ goal_hint: "What this area exists for (optional).",
1346
+ area: "Area",
1347
+ description: "Description",
1348
+ no_area: "— no area —",
1349
+ roles: "Roles",
1350
+ add_role: "role",
1351
+ no_roles: "no roles",
1352
+ general_roles: "General roles",
1353
+ delete_area: "Delete area",
1354
+ delete_role: "Delete role",
1355
+ delete_area_desc: "Delete area \"{name}\"? Its roles are detached, not deleted.",
1356
+ delete_role_desc: "Delete role \"{name}\"?",
1357
+ },
1358
+
1359
+ files: {
1360
+ docs_label: "Docs",
1361
+ files_label: "Files",
1362
+ new_doc: "New document",
1363
+ new_doc_hint: "Folders allowed: cases/onboarding/spec.md",
1364
+ empty: "No files.",
1365
+ docs_empty: "No documentation yet. Create the first document.",
1366
+ truncated: "Listing truncated (too many files).",
1367
+ select_prompt: "Pick a file to view it.",
1368
+ save: "Save",
1369
+ saved: "Saved.",
1370
+ deleted: "Deleted.",
1371
+ created: "Document created.",
1372
+ edit: "Edit",
1373
+ preview: "Preview",
1374
+ discard: "Discard",
1375
+ no_preview: "No preview for this file.",
1376
+ too_large: "File too large to display.",
1377
+ path_label: "File path",
1378
+ path_example: "e.g. cases/onboarding/spec.md",
1379
+ create: "Create",
1380
+ },
1381
+
1382
+ tasks: {
1383
+ state_open: "open",
1384
+ state_done: "done",
1385
+ state_dropped: "dropped",
1386
+ status_pending: "pending",
1387
+ status_running: "running",
1388
+ status_in_review: "in review",
1389
+ status_blocked: "blocked",
1390
+ done_label: "done",
1391
+ dropped_label: "dropped",
1392
+ detail_title: "Task detail",
1393
+ field_title: "Title",
1394
+ field_prompt: "Prompt",
1395
+ field_status: "Status",
1396
+ field_agent: "Agent",
1397
+ field_creator: "Created by",
1398
+ field_source: "Source",
1399
+ field_created: "Created",
1400
+ field_updated: "Updated",
1401
+ field_done: "Completed",
1402
+ prompt_ph: "Task description / prompt…",
1403
+ toggle_prompt: "Prompt",
1404
+ view_thread: "View thread",
1405
+ mark_done: "Complete",
1406
+ },
1407
+
1304
1408
  agents_ui: {
1305
1409
  model_router_default: "model: router default",
1306
1410
  slug_kebab_hint: "kebab-case, e.g. reviewer, my-agent, content-writer",
@@ -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: {
@@ -1299,6 +1314,95 @@ export const es = {
1299
1314
  role_assigned: "{name} → {role}",
1300
1315
  },
1301
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
+
1302
1406
  agents_ui: {
1303
1407
  model_router_default: "modelo: default del router",
1304
1408
  slug_kebab_hint: "kebab-case, ej. reviewer, my-agent, content-writer",
@@ -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.
@@ -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} />} />
@@ -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>