@agentprojectcontext/apx 1.57.0 → 1.59.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/skills/index.js +9 -0
  3. package/src/core/agent/skills/inspector.js +7 -2
  4. package/src/core/agent/skills/policy.js +128 -0
  5. package/src/core/agent/super-agent.js +8 -1
  6. package/src/core/agent/tools/handlers/list-skills.js +6 -3
  7. package/src/core/agent/tools/handlers/load-skill.js +9 -2
  8. package/src/core/apc/paths.js +7 -0
  9. package/src/core/stores/organization.js +152 -0
  10. package/src/core/stores/project-files.js +199 -0
  11. package/src/core/stores/tasks.js +36 -3
  12. package/src/host/daemon/api/agents.js +22 -2
  13. package/src/host/daemon/api/files-project.js +99 -0
  14. package/src/host/daemon/api/organization.js +88 -0
  15. package/src/host/daemon/api/shared.js +7 -0
  16. package/src/host/daemon/api/skills.js +301 -17
  17. package/src/host/daemon/api/tasks.js +14 -0
  18. package/src/host/daemon/api.js +4 -0
  19. package/src/interfaces/cli/commands/org.js +77 -0
  20. package/src/interfaces/cli/commands/skills.js +3 -2
  21. package/src/interfaces/cli/index.js +48 -0
  22. package/src/interfaces/web/dist/assets/index-CnQb4N6C.js +731 -0
  23. package/src/interfaces/web/dist/assets/index-CnQb4N6C.js.map +1 -0
  24. package/src/interfaces/web/dist/assets/index-Dv3X-zpx.css +1 -0
  25. package/src/interfaces/web/dist/index.html +2 -2
  26. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
  27. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
  28. package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
  29. package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
  30. package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
  31. package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
  32. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
  33. package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
  34. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
  35. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
  36. package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
  37. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
  38. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
  39. package/src/interfaces/web/src/i18n/en.ts +171 -0
  40. package/src/interfaces/web/src/i18n/es.ts +171 -0
  41. package/src/interfaces/web/src/lib/api/organization.ts +18 -0
  42. package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
  43. package/src/interfaces/web/src/lib/api/skills.ts +79 -8
  44. package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
  45. package/src/interfaces/web/src/lib/api.ts +2 -0
  46. package/src/interfaces/web/src/lib/slug.ts +11 -0
  47. package/src/interfaces/web/src/screens/ProjectScreen.tsx +25 -2
  48. package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
  49. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
  50. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
  51. package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
  52. package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
  53. package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
  54. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
  55. package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
  56. package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
  57. package/src/interfaces/web/src/types/daemon.ts +63 -0
  58. package/src/interfaces/web/dist/assets/index-CEI8DfVg.css +0 -1
  59. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js +0 -651
  60. package/src/interfaces/web/dist/assets/index-DJ-ocXOR.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: {
@@ -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",
@@ -1579,6 +1683,73 @@ export const es = {
1579
1683
  cfg_apx_storage_id: "ID de storage APX",
1580
1684
  },
1581
1685
 
1686
+ skills_page: {
1687
+ title: "Skills",
1688
+ desc: "Activá o desactivá qué skills carga cada agente. Elegí el scope: el super-agent (global) o un proyecto puntual.",
1689
+ list_title: "Skills instaladas",
1690
+ list_desc: "Las privadas de APX están siempre activas y no se pueden tocar.",
1691
+ scope_label: "Scope",
1692
+ scope_super_agent: "Super-agent (global)",
1693
+ scope_hint: "El super-agent usa el scope global. Cada proyecto puede sobrescribir skills de forma independiente.",
1694
+ count_label: "{n} skills · {on} activas",
1695
+ empty: "No hay skills. Creá una abajo o instalá con la CLI.",
1696
+ source_builtin: "APX",
1697
+ source_global: "Global",
1698
+ source_project: "Proyecto",
1699
+ private_badge: "Privada",
1700
+ private_hint: "Skill interna de APX — siempre activa, no se puede desactivar ni borrar.",
1701
+ overridden_badge: "Override",
1702
+ inherited_hint: "Heredada del global",
1703
+ reset_to_global: "Volver al global",
1704
+ on: "activa",
1705
+ off: "inactiva",
1706
+ toggle_failed: "No se pudo cambiar el estado: {msg}",
1707
+ add_title: "Agregar skill",
1708
+ add_desc: "Crea una skill de usuario en ~/.apx/skills/<slug>/SKILL.md. Queda disponible para todos los scopes.",
1709
+ add_slug_label: "Slug",
1710
+ add_slug_ph: "mi-skill",
1711
+ add_desc_label: "Descripción",
1712
+ add_desc_ph: "Una línea que explique cuándo usarla",
1713
+ add_body_label: "Cuerpo (Markdown)",
1714
+ add_body_ph: "# Mi skill\n\nInstrucciones para el agente…",
1715
+ add_btn: "Crear skill",
1716
+ created_ok: "Skill \"{slug}\" creada.",
1717
+ create_failed: "No se pudo crear: {msg}",
1718
+ delete_btn: "Borrar",
1719
+ delete_confirm: "¿Borrar la skill \"{slug}\"? No se puede deshacer.",
1720
+ deleted_ok: "Skill \"{slug}\" borrada.",
1721
+ delete_failed: "No se pudo borrar: {msg}",
1722
+ inspector_section_title: "Skill Inspector (RAG por turno)",
1723
+ inspector_section_desc: "Config avanzada: RAG local que inyecta solo las skills que el mensaje necesita.",
1724
+ scope_ph: "— elegir scope —",
1725
+ select_a_skill: "Elegí una skill de la lista para ver su contenido.",
1726
+ added_by: "Agregado por",
1727
+ activator: "Activador",
1728
+ by_apx: "APX (built-in)",
1729
+ by_you: "Vos",
1730
+ activator_value: "Coincidencia semántica (RAG)",
1731
+ tab_preview: "Vista",
1732
+ tab_source: "Fuente",
1733
+ add_menu: "Agregar",
1734
+ add_online: "Crear con el editor",
1735
+ add_online_hint: "Escribí slug + descripción + contenido",
1736
+ add_zip: "Subir .zip",
1737
+ add_zip_hint: "Importar una skill empaquetada",
1738
+ add_repo: "Desde repo git",
1739
+ add_repo_hint: "Clonar desde una URL",
1740
+ create_dialog_title: "Crear skill",
1741
+ repo_dialog_title: "Importar desde repo git",
1742
+ repo_url_label: "URL del repo",
1743
+ repo_url_ph: "https://github.com/usuario/mi-skill.git",
1744
+ repo_url_hint: "El repo (o su subcarpeta) debe tener un SKILL.md.",
1745
+ import_btn: "Importar",
1746
+ imported_ok: "Skill \"{slug}\" importada.",
1747
+ import_failed: "No se pudo importar: {msg}",
1748
+ cancel: "Cancelar",
1749
+ manager_tab: "Skills",
1750
+ rag_tab: "Config (RAG)",
1751
+ },
1752
+
1582
1753
  shared_ui: {
1583
1754
  skill_inspector_title: "Skill Inspector ({embedder}) eligió estas skills para este turno",
1584
1755
  tools_count: "{n} tools",
@@ -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,16 +1,44 @@
1
1
  import { http } from "../http";
2
2
 
3
+ export type SkillSource = "builtin" | "global" | "project" | string;
4
+
3
5
  export type SkillEntry = {
4
6
  slug: string;
5
- source: "bundled" | "user" | "project" | string;
7
+ source: SkillSource;
6
8
  description: string;
9
+ /** Effective enabled state for the requested scope. */
10
+ enabled?: boolean;
11
+ /** Built-in APX skill — always active, never disableable. */
12
+ private?: boolean;
13
+ /** Whether THIS scope holds an explicit override (vs. inherited). */
14
+ overridden?: boolean;
7
15
  };
8
16
 
9
17
  export type SkillsList = {
10
18
  count: number;
19
+ /** Echoed scope key: "default" (super-agent) or a project path. */
20
+ scope?: string;
11
21
  skills: SkillEntry[];
12
22
  };
13
23
 
24
+ export interface SkillDetail {
25
+ slug: string;
26
+ source: SkillSource;
27
+ description: string;
28
+ frontmatter: Record<string, string>;
29
+ body: string;
30
+ file: string;
31
+ enabled: boolean;
32
+ private: boolean;
33
+ overridden: boolean;
34
+ }
35
+
36
+ export interface CreateResult {
37
+ ok: boolean;
38
+ slug: string;
39
+ source: string;
40
+ }
41
+
14
42
  export interface InspectorConfig {
15
43
  enabled: boolean;
16
44
  load_threshold: number;
@@ -62,16 +90,59 @@ export interface InspectResult {
62
90
 
63
91
  export const Skills = {
64
92
  /**
65
- * List installed skills (bundled + user + optional project-scoped). The
66
- * description is already condensed for one-line rendering.
93
+ * List installed skills (built-in + user + optional project-scoped), each
94
+ * annotated with `enabled`/`private` for the requested scope. Pass a project
95
+ * path to both scan that project's skills AND resolve enabled-state against
96
+ * it; omit it for the super-agent ("default") scope.
67
97
  */
68
- list: (projectPath?: string) =>
69
- http.get<SkillsList>(
70
- projectPath
71
- ? `/skills?project_path=${encodeURIComponent(projectPath)}`
72
- : "/skills",
98
+ list: (projectPath?: string) => {
99
+ const qs = new URLSearchParams();
100
+ if (projectPath) {
101
+ qs.set("project_path", projectPath);
102
+ qs.set("scope", projectPath);
103
+ }
104
+ const q = qs.toString();
105
+ return http.get<SkillsList>(q ? `/skills?${q}` : "/skills");
106
+ },
107
+
108
+ /** Full body + frontmatter of a skill, for the viewer. */
109
+ detail: (slug: string, projectPath?: string) => {
110
+ const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
111
+ return http.get<SkillDetail>(`/skills/${encodeURIComponent(slug)}/detail${qs}`);
112
+ },
113
+
114
+ /**
115
+ * Enable/disable a skill for a scope. `enabled: null` clears the override so
116
+ * the skill inherits the super-agent default again. `scope` is "default" or a
117
+ * project path.
118
+ */
119
+ setEnabled: (body: { slug: string; enabled: boolean | null; scope?: string }) =>
120
+ http.put<{ ok: boolean; slug: string; scope: string; enabled: boolean | null }>(
121
+ "/skills/enabled",
122
+ body,
73
123
  ),
74
124
 
125
+ /**
126
+ * Create a user skill from the online editor. With `project_path` it lands in
127
+ * that project's .apc/skills/; otherwise in ~/.apx/skills/.
128
+ */
129
+ create: (body: { slug: string; description?: string; body?: string; project_path?: string }) =>
130
+ http.post<CreateResult>("/skills", body),
131
+
132
+ /** Import a skill from an uploaded .zip (base64-encoded). */
133
+ importZip: (body: { data: string; project_path?: string }) =>
134
+ http.post<CreateResult>("/skills/import/zip", body),
135
+
136
+ /** Import a skill by cloning a git repo. */
137
+ importRepo: (body: { url: string; project_path?: string }) =>
138
+ http.post<CreateResult>("/skills/import/repo", body),
139
+
140
+ /** Delete a user skill (global or project-scoped). */
141
+ remove: (slug: string, projectPath?: string) => {
142
+ const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
143
+ return http.del<{ ok: boolean; slug: string }>(`/skills/${encodeURIComponent(slug)}${qs}`);
144
+ },
145
+
75
146
  /** Skill Inspector config + index status. */
76
147
  inspector: () => http.get<InspectorState>("/skills/inspector"),
77
148
 
@@ -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, Sparkles,
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,15 @@ 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";
37
+ import { SkillsTab } from "./project/SkillsTab";
33
38
 
34
39
  type NavKey =
35
40
  | "" | "chat" | "config" | "telegram"
36
- | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts";
41
+ | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts"
42
+ | "structure" | "docs" | "files" | "skills";
37
43
 
38
44
  export function ProjectScreen() {
39
45
  const navigate = useNavigate();
@@ -70,6 +76,7 @@ export function ProjectScreen() {
70
76
  items: [
71
77
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
72
78
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
79
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
73
80
  { key: "routines", label: t("project.nav.routines"), icon: Heart },
74
81
  { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
75
82
  { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
@@ -79,6 +86,9 @@ export function ProjectScreen() {
79
86
  },
80
87
  ];
81
88
  }
89
+ // Structure (org roles/areas) is only meaningful for company/enterprise
90
+ // projects — gate it on the project kind.
91
+ const isCompany = project?.kind === "company";
82
92
  return [
83
93
  {
84
94
  title: t("project.sections.workspace"),
@@ -87,7 +97,16 @@ export function ProjectScreen() {
87
97
  { key: "telegram", label: t("project.nav.telegram"), icon: Send },
88
98
  { key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
89
99
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
100
+ ...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
90
101
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
102
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
103
+ ],
104
+ },
105
+ {
106
+ title: t("project.sections.content"),
107
+ items: [
108
+ { key: "docs", label: t("project.nav.docs"), icon: FileText },
109
+ { key: "files", label: t("project.nav.files"), icon: FolderTree },
91
110
  ],
92
111
  },
93
112
  {
@@ -108,7 +127,7 @@ export function ProjectScreen() {
108
127
  ],
109
128
  },
110
129
  ];
111
- }, [isBase]);
130
+ }, [isBase, project?.kind]);
112
131
 
113
132
  // First path segment after /p/:pid — so deep routes like agents/:slug still
114
133
  // highlight the "agents" nav item.
@@ -155,7 +174,11 @@ export function ProjectScreen() {
155
174
  <Route path="telegram" element={<TelegramTab pid={pid} />} />
156
175
  <Route path="agents" element={<AgentsTab pid={pid} />} />
157
176
  <Route path="agents/:slug" element={<AgentDetailScreen pid={pid} />} />
177
+ <Route path="structure" element={<StructureTab pid={pid} />} />
178
+ <Route path="docs" element={<DocsTab pid={pid} />} />
179
+ <Route path="files" element={<FilesTab pid={pid} />} />
158
180
  <Route path="memories" element={<MemoriesTab pid={pid} />} />
181
+ <Route path="skills" element={<SkillsTab pid={pid} />} />
159
182
  <Route path="routines" element={<RoutinesTab pid={pid} />} />
160
183
  <Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
161
184
  <Route path="mcps" element={<McpsTab pid={pid} />} />
@@ -8,7 +8,7 @@ import { TabLayout } from "../components/common/TabLayout";
8
8
  import { IdentityPanel } from "../components/settings/IdentityPanel";
9
9
  import { SuperAgentPanel } from "../components/settings/SuperAgentPanel";
10
10
  import { MemoryPanel } from "../components/settings/MemoryPanel";
11
- import { SkillsInspectorPanel } from "../components/settings/SkillsInspectorPanel";
11
+ import { SkillsSettings } from "../components/settings/SkillsSettings";
12
12
  import { ModelsTab } from "./base/ModelsTab";
13
13
  import { TelegramSettingsTabs } from "../components/settings/TelegramSettingsTabs";
14
14
  import { DevicesPanel } from "../components/settings/DevicesPanel";
@@ -37,7 +37,7 @@ const SECTIONS: TabSection[] = [
37
37
  { key: "super_agent", label: t("settings.tabs.super_agent"), icon: Bot },
38
38
  { key: "engines", label: t("settings.tabs.engines"), icon: Cpu },
39
39
  { key: "memory", label: "Memory (RAG)", icon: Database },
40
- { key: "skills", label: "Skills (RAG)", icon: Sparkles },
40
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
41
41
  ],
42
42
  },
43
43
  {
@@ -75,7 +75,7 @@ const PANELS: Record<TabKey, () => ReactElement> = {
75
75
  super_agent: () => <SuperAgentPanel />,
76
76
  engines: () => <ModelsTab />,
77
77
  memory: () => <MemoryPanel />,
78
- skills: () => <SkillsInspectorPanel />,
78
+ skills: () => <SkillsSettings />,
79
79
  telegram: () => <TelegramSettingsTabs />,
80
80
  devices: () => <DevicesPanel />,
81
81
  voice: () => <VoiceScreen />,
@@ -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
  }