@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
@@ -0,0 +1,13 @@
1
+ import { FileBrowser } from "../../components/files/FileBrowser";
2
+ import { t } from "../../i18n";
3
+
4
+ // Project documentation / specs. Rooted at the configured docs folder
5
+ // (config docs.root, default "docs") — folders per case, like Appsi's work/.
6
+ // Editable: create, edit (markdown split-preview) and delete docs.
7
+ export function DocsTab({ pid }: { pid: string }) {
8
+ return (
9
+ <div className="h-full">
10
+ <FileBrowser pid={pid} scope="docs" editable emptyHint={t("files.docs_empty")} />
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,12 @@
1
+ import { FileBrowser } from "../../components/files/FileBrowser";
2
+
3
+ // Read-only browser over the whole project tree (the same viewer the docs
4
+ // surface uses, scoped to the repo root). Type-aware: markdown renders,
5
+ // code shows with line numbers, images preview inline.
6
+ export function FilesTab({ pid }: { pid: string }) {
7
+ return (
8
+ <div className="h-full">
9
+ <FileBrowser pid={pid} scope="project" />
10
+ </div>
11
+ );
12
+ }
@@ -1,23 +1,135 @@
1
1
  import useSWR from "swr";
2
- import { NavLink } from "react-router-dom";
3
- import { Bot, FileCode2, Heart, MessagesSquare, Puzzle, Zap } from "lucide-react";
2
+ import { NavLink, useNavigate } from "react-router-dom";
3
+ import { Bot, FileCode2, Heart, MessagesSquare, Puzzle, Zap, Crown, Activity } from "lucide-react";
4
4
  import { Agents, Artifacts, Mcps, Routines, Tasks } from "../../lib/api";
5
+ import { Section } from "../../components/Section";
6
+ import { StatusIcon, StatusBadge, effectiveStatus, statusLabel, TASK_STATUS_ORDER } from "../../components/tasks/taskStatus";
7
+ import { cn } from "../../lib/cn";
5
8
  import { t } from "../../i18n";
9
+ import type { AgentEntry } from "../../types/daemon";
6
10
 
11
+ // Floor / mission control: a live per-project summary — what's here (agents,
12
+ // automation), what's in flight (task workflow), and what just happened.
7
13
  export function Overview({ pid }: { pid: string }) {
8
- const tasks = useSWR(`/projects/${pid}/tasks?state=open`, () => Tasks.list(pid));
14
+ const navigate = useNavigate();
15
+ const tasks = useSWR(`/projects/${pid}/tasks?state=open`, () => Tasks.list(pid), { refreshInterval: 20_000 });
16
+ const summary = useSWR(`/projects/${pid}/tasks-summary`, () => Tasks.summary(pid), { refreshInterval: 20_000 });
9
17
  const routines = useSWR(`/projects/${pid}/routines`, () => Routines.list(pid));
10
18
  const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
11
19
  const mcps = useSWR(`/projects/${pid}/mcps`, () => Mcps.list(pid));
12
20
  const artifacts = useSWR(`/projects/${pid}/artifacts`, () => Artifacts.list(pid));
21
+
22
+ const agentList = agents.data ?? [];
23
+ const orchestrators = agentList.filter((a) => a.is_master || a.type === "orchestrator");
24
+ const specialists = agentList.filter((a) => !(a.is_master || a.type === "orchestrator"));
25
+ const activeRoutines = (routines.data ?? []).filter((r) => r.enabled).length;
26
+ const openTasks = [...(tasks.data ?? [])].sort((a, b) => (b.created_at || "").localeCompare(a.created_at || "")).slice(0, 6);
27
+
13
28
  return (
14
- <div className="grid grid-cols-2 gap-4 md:grid-cols-3">
15
- <Card title={t("project.overview.tasks_open")} value={tasks.data?.length ?? "…"} href={`/p/${pid}/tasks`} icon={Zap} />
16
- <Card title={t("project.overview.routines")} value={routines.data?.length ?? "…"} href={`/p/${pid}/routines`} icon={Heart} />
17
- <Card title={t("project.overview.agents")} value={agents.data?.length ?? "…"} href={`/p/${pid}/agents`} icon={Bot} />
18
- <Card title={t("project.overview.mcps")} value={mcps.data?.length ?? "…"} href={`/p/${pid}/mcps`} icon={Puzzle} />
19
- <Card title={t("project.overview.artifacts")} value={artifacts.data?.length ?? "…"} href={`/p/${pid}/artifacts`} icon={FileCode2} />
20
- <Card title={t("project.overview.chat")} value={t("project.overview.chat_value")} href={`/p/${pid}/chat`} icon={MessagesSquare} />
29
+ <div className="space-y-6">
30
+ {/* Stat cards */}
31
+ <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
32
+ <Card title={t("project.overview.agents")} value={agentList.length} href={`/p/${pid}/agents`} icon={Bot} />
33
+ <Card title={t("project.overview.tasks_open")} value={summary.data?.open ?? tasks.data?.length ?? "…"} href={`/p/${pid}/tasks`} icon={Zap} />
34
+ <Card title={t("project.overview.routines_active")} value={activeRoutines} href={`/p/${pid}/routines`} icon={Heart} />
35
+ <Card title={t("project.overview.artifacts")} value={artifacts.data?.length ?? "…"} href={`/p/${pid}/artifacts`} icon={FileCode2} />
36
+ </div>
37
+
38
+ {/* Task workflow strip */}
39
+ {summary.data && summary.data.open > 0 && (
40
+ <div className="flex flex-wrap gap-2">
41
+ {TASK_STATUS_ORDER.map((s) => (
42
+ <NavLink
43
+ key={s}
44
+ to={`/p/${pid}/tasks`}
45
+ className="flex items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs hover:bg-accent/40"
46
+ >
47
+ <StatusIcon status={s} className="size-3.5" />
48
+ <span className="capitalize text-muted-foreground">{statusLabel(s)}</span>
49
+ <span className="font-semibold">{summary.data!.status?.[s] ?? 0}</span>
50
+ </NavLink>
51
+ ))}
52
+ </div>
53
+ )}
54
+
55
+ <div className="grid gap-4 lg:grid-cols-2">
56
+ {/* Agent roster */}
57
+ <Section title={t("project.overview.roster")} className="!p-4">
58
+ {agentList.length === 0 ? (
59
+ <p className="text-sm text-muted-fg">{t("project.overview.no_agents")}</p>
60
+ ) : (
61
+ <div className="space-y-3">
62
+ {orchestrators.length > 0 && (
63
+ <RosterRow label={t("project.overview.orchestrators")} icon={Crown} agents={orchestrators} pid={pid} navigate={navigate} />
64
+ )}
65
+ {specialists.length > 0 && (
66
+ <RosterRow label={t("project.overview.specialists")} icon={Bot} agents={specialists} pid={pid} navigate={navigate} />
67
+ )}
68
+ </div>
69
+ )}
70
+ </Section>
71
+
72
+ {/* Recent / in-flight tasks */}
73
+ <Section title={t("project.overview.recent_tasks")} className="!p-4"
74
+ action={<NavLink to={`/p/${pid}/tasks`} className="text-xs text-sky-500 hover:text-sky-400">{t("common.view_all")}</NavLink>}
75
+ >
76
+ {openTasks.length === 0 ? (
77
+ <p className="flex items-center gap-2 text-sm text-muted-fg"><Activity className="size-4" />{t("project.overview.no_activity")}</p>
78
+ ) : (
79
+ <ul className="space-y-1.5">
80
+ {openTasks.map((task) => (
81
+ <li key={task.id}>
82
+ <button
83
+ type="button"
84
+ onClick={() => navigate(`/p/${pid}/tasks?task=${task.id}`)}
85
+ className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left hover:bg-accent/40"
86
+ >
87
+ <StatusIcon status={effectiveStatus(task)} className="size-3.5 shrink-0" />
88
+ <span className="min-w-0 flex-1 truncate text-sm">{task.title}</span>
89
+ <StatusBadge status={effectiveStatus(task)} />
90
+ </button>
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ )}
95
+ </Section>
96
+ </div>
97
+
98
+ {/* Quick links */}
99
+ <div className="grid grid-cols-2 gap-4 md:grid-cols-3">
100
+ <Card title={t("project.overview.chat")} value={t("project.overview.chat_value")} href={`/p/${pid}/chat`} icon={MessagesSquare} />
101
+ <Card title={t("project.overview.mcps")} value={mcps.data?.length ?? "…"} href={`/p/${pid}/mcps`} icon={Puzzle} />
102
+ <Card title={t("project.overview.routines")} value={routines.data?.length ?? "…"} href={`/p/${pid}/routines`} icon={Heart} />
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function RosterRow({
109
+ label, icon: Icon, agents, pid, navigate,
110
+ }: {
111
+ label: string; icon: typeof Bot; agents: AgentEntry[]; pid: string; navigate: (to: string) => void;
112
+ }) {
113
+ return (
114
+ <div>
115
+ <div className="mb-1.5 flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
116
+ <Icon className="size-3.5" />{label} ({agents.length})
117
+ </div>
118
+ <div className="flex flex-wrap gap-1.5">
119
+ {agents.map((a) => (
120
+ <button
121
+ key={a.slug}
122
+ type="button"
123
+ onClick={() => navigate(`/p/${pid}/agents/${a.slug}`)}
124
+ className={cn(
125
+ "inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2 py-1 text-xs hover:border-muted-fg/50",
126
+ )}
127
+ >
128
+ <span className="text-sm leading-none">{a.emoji || "🤖"}</span>
129
+ <span className="truncate">{a.slug}</span>
130
+ </button>
131
+ ))}
132
+ </div>
21
133
  </div>
22
134
  );
23
135
  }
@@ -0,0 +1,147 @@
1
+ import { useState } from "react";
2
+ import useSWR from "swr";
3
+ import { FolderKanban, Briefcase, Plus, Pencil, Trash2, Info } from "lucide-react";
4
+ import { Section } from "../../components/Section";
5
+ import { Button, Empty, Loading } from "../../components/ui";
6
+ import { ConfirmDialog } from "../../components/common/ConfirmDialog";
7
+ import { AreaDialog, RoleDialog } from "../../components/structure/StructureDialogs";
8
+ import { Org } from "../../lib/api/organization";
9
+ import { useToast } from "../../components/Toast";
10
+ import { t } from "../../i18n";
11
+ import type { OrgArea, OrgRole } from "../../types/daemon";
12
+
13
+ export function StructureTab({ pid }: { pid: string }) {
14
+ const toast = useToast();
15
+ const org = useSWR(`/projects/${pid}/organization`, () => Org.get(pid));
16
+
17
+ const [areaDialog, setAreaDialog] = useState<{ editing?: OrgArea | null } | null>(null);
18
+ const [roleDialog, setRoleDialog] = useState<{ editing?: OrgRole | null; presetArea?: string | null } | null>(null);
19
+ const [confirm, setConfirm] = useState<{ kind: "area" | "role"; slug: string; name: string } | null>(null);
20
+
21
+ const refresh = () => void org.mutate();
22
+
23
+ const areas = org.data?.areas ?? [];
24
+ const roles = org.data?.roles ?? [];
25
+ const rolesByArea = (slug: string | null) => roles.filter((r) => r.area === slug);
26
+
27
+ const doDelete = async () => {
28
+ if (!confirm) return;
29
+ if (confirm.kind === "area") await Org.removeArea(pid, confirm.slug);
30
+ else await Org.removeRole(pid, confirm.slug);
31
+ toast.success(t("common.deleted"));
32
+ refresh();
33
+ };
34
+
35
+ return (
36
+ <div className="space-y-6">
37
+ <Section
38
+ title={t("structure.title")}
39
+ description={t("structure.subtitle")}
40
+ action={
41
+ <div className="flex gap-2">
42
+ <Button size="sm" variant="secondary" data-testid="structure-new-area" onClick={() => setAreaDialog({})}>
43
+ <Plus className="size-3.5" />{t("structure.new_area")}
44
+ </Button>
45
+ <Button size="sm" variant="primary" onClick={() => setRoleDialog({})}>
46
+ <Plus className="size-3.5" />{t("structure.new_role")}
47
+ </Button>
48
+ </div>
49
+ }
50
+ >
51
+ <div className="mb-4 flex items-start gap-2 rounded-lg border border-sky-500/20 bg-sky-500/5 px-3 py-2 text-[13px] text-muted-foreground">
52
+ <Info className="mt-0.5 size-4 shrink-0 text-sky-500" />
53
+ <span>{t("structure.info")}</span>
54
+ </div>
55
+
56
+ {org.isLoading ? (
57
+ <Loading />
58
+ ) : areas.length === 0 && roles.length === 0 ? (
59
+ <Empty>{t("structure.empty")}</Empty>
60
+ ) : (
61
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
62
+ {areas.map((area) => (
63
+ <div key={area.slug} className="group rounded-lg border border-border bg-card/50 p-3">
64
+ <div className="flex items-start gap-2">
65
+ <FolderKanban className="mt-0.5 size-4 shrink-0 text-emerald-500" />
66
+ <div className="min-w-0 flex-1">
67
+ <div className="flex items-center gap-2">
68
+ <span className="truncate text-sm font-semibold">{area.name}</span>
69
+ <span className="font-mono text-[10px] text-muted-foreground">{area.slug}</span>
70
+ </div>
71
+ {area.goal && <p className="mt-0.5 text-xs text-muted-foreground">{area.goal}</p>}
72
+ </div>
73
+ <div className="flex shrink-0 gap-1 opacity-0 transition-opacity group-hover:opacity-100">
74
+ <button type="button" onClick={() => setAreaDialog({ editing: area })} className="text-muted-foreground hover:text-foreground" aria-label={t("common.edit")}>
75
+ <Pencil className="size-3.5" />
76
+ </button>
77
+ <button type="button" onClick={() => setConfirm({ kind: "area", slug: area.slug, name: area.name })} className="text-muted-foreground hover:text-red-500" aria-label={t("common.delete")}>
78
+ <Trash2 className="size-3.5" />
79
+ </button>
80
+ </div>
81
+ </div>
82
+
83
+ {/* Roles nested inside the area (Panda pattern). */}
84
+ <div className="mt-3 border-t border-border/60 pt-2">
85
+ <div className="mb-1.5 flex items-center justify-between">
86
+ <span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
87
+ {t("structure.roles")} ({rolesByArea(area.slug).length})
88
+ </span>
89
+ <button type="button" onClick={() => setRoleDialog({ presetArea: area.slug })} className="text-[11px] text-sky-500 hover:text-sky-400">
90
+ + {t("structure.add_role")}
91
+ </button>
92
+ </div>
93
+ <div className="flex flex-wrap gap-1.5">
94
+ {rolesByArea(area.slug).map((role) => (
95
+ <RoleChip key={role.slug} role={role} onEdit={() => setRoleDialog({ editing: role })} onDelete={() => setConfirm({ kind: "role", slug: role.slug, name: role.name })} />
96
+ ))}
97
+ {rolesByArea(area.slug).length === 0 && <span className="text-[11px] text-muted-foreground/60">{t("structure.no_roles")}</span>}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ))}
102
+
103
+ {/* Unassigned roles (no area). */}
104
+ {rolesByArea(null).length > 0 && (
105
+ <div className="rounded-lg border border-dashed border-border bg-card/30 p-3">
106
+ <div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
107
+ <Briefcase className="size-3.5" />{t("structure.general_roles")}
108
+ </div>
109
+ <div className="flex flex-wrap gap-1.5">
110
+ {rolesByArea(null).map((role) => (
111
+ <RoleChip key={role.slug} role={role} onEdit={() => setRoleDialog({ editing: role })} onDelete={() => setConfirm({ kind: "role", slug: role.slug, name: role.name })} />
112
+ ))}
113
+ </div>
114
+ </div>
115
+ )}
116
+ </div>
117
+ )}
118
+ </Section>
119
+
120
+ <AreaDialog open={!!areaDialog} onClose={() => setAreaDialog(null)} pid={pid} editing={areaDialog?.editing} onSaved={refresh} />
121
+ <RoleDialog open={!!roleDialog} onClose={() => setRoleDialog(null)} pid={pid} areas={areas} editing={roleDialog?.editing} presetArea={roleDialog?.presetArea} onSaved={refresh} />
122
+ <ConfirmDialog
123
+ open={!!confirm}
124
+ onClose={() => setConfirm(null)}
125
+ onConfirm={doDelete}
126
+ title={confirm?.kind === "area" ? t("structure.delete_area") : t("structure.delete_role")}
127
+ description={confirm?.kind === "area" ? t("structure.delete_area_desc", { name: confirm?.name ?? "" }) : t("structure.delete_role_desc", { name: confirm?.name ?? "" })}
128
+ confirmLabel={t("common.delete")}
129
+ />
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function RoleChip({ role, onEdit, onDelete }: { role: OrgRole; onEdit: () => void; onDelete: () => void }) {
135
+ return (
136
+ <span className="group/chip inline-flex items-center gap-1 rounded-md border border-border bg-background px-1.5 py-0.5 text-[11px]">
137
+ <Briefcase className="size-3 text-muted-foreground" />
138
+ <span>{role.name}</span>
139
+ <button type="button" onClick={onEdit} className="opacity-0 transition-opacity group-hover/chip:opacity-100 text-muted-foreground hover:text-foreground" aria-label={t("common.edit")}>
140
+ <Pencil className="size-2.5" />
141
+ </button>
142
+ <button type="button" onClick={onDelete} className="opacity-0 transition-opacity group-hover/chip:opacity-100 text-muted-foreground hover:text-red-500" aria-label={t("common.delete")}>
143
+ <Trash2 className="size-2.5" />
144
+ </button>
145
+ </span>
146
+ );
147
+ }
@@ -1,14 +1,19 @@
1
1
  import { useState } from "react";
2
- import { Check, Plus, RotateCcw, Trash2 } from "lucide-react";
2
+ import { useSearchParams } from "react-router-dom";
3
+ import { Plus, ChevronDown, ChevronUp, Check, Trash2, RotateCcw } from "lucide-react";
3
4
  import { Tasks } from "../../lib/api";
4
5
  import { Section } from "../../components/Section";
5
6
  import { PagedList, usePagedQuery } from "../../components/Pager";
6
- import { Badge, Button, Empty, Field, Input, Loading } from "../../components/ui";
7
+ import { Badge, Button, Empty, Field, Input, Loading, Textarea } from "../../components/ui";
7
8
  import { useToast } from "../../components/Toast";
9
+ import { StatusIcon, StatusBadge, effectiveStatus } from "../../components/tasks/taskStatus";
10
+ import { TaskDetailPanel } from "../../components/tasks/TaskDetailPanel";
8
11
  import { t } from "../../i18n";
9
12
 
10
13
  export function TasksTab({ pid }: { pid: string }) {
11
14
  const [state, setState] = useState<"open" | "done" | "dropped">("open");
15
+ const [params, setParams] = useSearchParams();
16
+ const selected = params.get("task");
12
17
  const toast = useToast();
13
18
  // dedupingInterval:0 so switching the state filter always revalidates the
14
19
  // target page instead of showing the stale cached one from a prior switch.
@@ -19,14 +24,22 @@ export function TasksTab({ pid }: { pid: string }) {
19
24
  swr: { dedupingInterval: 0, revalidateOnFocus: true },
20
25
  });
21
26
  const [draft, setDraft] = useState("");
27
+ const [body, setBody] = useState("");
28
+ const [showBody, setShowBody] = useState(false);
22
29
  const [busy, setBusy] = useState(false);
23
30
 
31
+ const select = (id: string | null) => {
32
+ const next = new URLSearchParams(params);
33
+ if (id) next.set("task", id); else next.delete("task");
34
+ setParams(next, { replace: true });
35
+ };
36
+
24
37
  const add = async () => {
25
38
  if (!draft.trim()) return;
26
39
  setBusy(true);
27
40
  try {
28
- await Tasks.add(pid, { title: draft.trim() });
29
- setDraft("");
41
+ await Tasks.add(pid, { title: draft.trim(), body: body.trim() || null, source: "web" });
42
+ setDraft(""); setBody(""); setShowBody(false);
30
43
  toast.success(t("project.tasks.created"));
31
44
  paged.mutate();
32
45
  } catch (e: any) {
@@ -49,72 +62,98 @@ export function TasksTab({ pid }: { pid: string }) {
49
62
  <div className="flex gap-1">
50
63
  {(["open", "done", "dropped"] as const).map((s) => (
51
64
  <Button key={s} size="sm" data-testid={`task-filter-${s}`} variant={state === s ? "primary" : "ghost"} onClick={() => setState(s)}>
52
- {s}
65
+ {t(`tasks.state_${s}` as never)}
53
66
  </Button>
54
67
  ))}
55
68
  </div>
56
69
  }
57
70
  >
58
- <div className="mb-4 flex shrink-0 items-end gap-2">
59
- <Field label={t("project.tasks.add_label")}>
60
- <Input
61
- data-testid="task-input"
62
- placeholder={t("project.tasks.add_placeholder")}
63
- value={draft}
64
- onChange={(e) => setDraft(e.target.value)}
65
- onKeyDown={(e) => { if (e.key === "Enter") add(); }}
66
- />
67
- </Field>
68
- <Button variant="primary" data-testid="task-add" onClick={add} loading={busy}>
69
- <Plus size={14} /> {t("project.tasks.add")}
70
- </Button>
71
+ <div className="mb-4 shrink-0 space-y-2">
72
+ <div className="flex items-end gap-2">
73
+ <Field label={t("project.tasks.add_label")}>
74
+ <Input
75
+ data-testid="task-input"
76
+ placeholder={t("project.tasks.add_placeholder")}
77
+ value={draft}
78
+ onChange={(e) => setDraft(e.target.value)}
79
+ onKeyDown={(e) => { if (e.key === "Enter" && !showBody) add(); }}
80
+ />
81
+ </Field>
82
+ <Button variant="ghost" size="sm" onClick={() => setShowBody((v) => !v)} aria-label={t("tasks.toggle_prompt")}>
83
+ {showBody ? <ChevronUp size={14} /> : <ChevronDown size={14} />} {t("tasks.field_prompt")}
84
+ </Button>
85
+ <Button variant="primary" data-testid="task-add" onClick={add} loading={busy}>
86
+ <Plus size={14} /> {t("project.tasks.add")}
87
+ </Button>
88
+ </div>
89
+ {showBody && (
90
+ <Textarea rows={3} value={body} onChange={(e) => setBody(e.target.value)} placeholder={t("tasks.prompt_ph")} className="text-xs" />
91
+ )}
71
92
  </div>
72
93
 
73
- {paged.isLoading && <Loading />}
74
- {!paged.isLoading && paged.total === 0 && (
75
- <Empty>
76
- {state === "open"
77
- ? t("project.tasks.empty_open")
78
- : t("project.tasks.empty", { state })}
79
- {" "}<code>apx task add "…"</code>
80
- </Empty>
81
- )}
94
+ <div className="flex min-h-0 flex-1 gap-4">
95
+ <div className="flex min-w-0 flex-1 flex-col">
96
+ {paged.isLoading && <Loading />}
97
+ {!paged.isLoading && paged.total === 0 && (
98
+ <Empty>
99
+ {state === "open" ? t("project.tasks.empty_open") : t("project.tasks.empty", { state })}
100
+ {" "}<code>apx task add "…"</code>
101
+ </Empty>
102
+ )}
82
103
 
83
- <PagedList paged={paged} fullHeight>
84
- <ul className="space-y-2 text-sm" data-testid="task-list">
85
- {paged.items.map((task) => (
86
- <li key={task.id} data-testid={`task-${task.id}`} className="flex items-start gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
87
- <span className="mt-0.5 font-mono text-[10px] text-muted-fg">{task.id}</span>
88
- <div className="flex-1">
89
- <div className="font-medium">{task.title}</div>
90
- <div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-fg">
91
- {task.tags?.map((tag) => <Badge key={tag}>#{tag}</Badge>)}
92
- {task.agent && <Badge tone="info">@{task.agent}</Badge>}
93
- {task.source && <span>{t("project.tasks.via")} {task.source}</span>}
94
- {task.due && <span>{t("project.tasks.due")} {task.due}</span>}
95
- </div>
96
- </div>
97
- <div className="flex gap-1">
98
- {state === "open" && (
99
- <>
100
- <Button size="sm" variant="secondary" aria-label={t("project.tasks.aria_done")} data-testid={`task-done-${task.id}`} onClick={() => mark(() => Tasks.done(pid, task.id), t("project.tasks.done"))}>
101
- <Check size={13} />
102
- </Button>
103
- <Button size="sm" variant="destructive" aria-label={t("project.tasks.aria_drop")} data-testid={`task-drop-${task.id}`} onClick={() => mark(() => Tasks.drop(pid, task.id), t("project.tasks.drop"))}>
104
- <Trash2 size={13} />
105
- </Button>
106
- </>
107
- )}
108
- {state !== "open" && (
109
- <Button size="sm" variant="ghost" aria-label={t("project.tasks.aria_reopen")} data-testid={`task-reopen-${task.id}`} onClick={() => mark(() => Tasks.reopen(pid, task.id), t("project.tasks.reopen"))}>
110
- <RotateCcw size={13} />
111
- </Button>
112
- )}
113
- </div>
114
- </li>
115
- ))}
116
- </ul>
117
- </PagedList>
104
+ <PagedList paged={paged} fullHeight>
105
+ <ul className="space-y-2 text-sm" data-testid="task-list">
106
+ {paged.items.map((task) => {
107
+ const eff = effectiveStatus(task);
108
+ return (
109
+ <li
110
+ key={task.id}
111
+ data-testid={`task-${task.id}`}
112
+ onClick={() => select(task.id)}
113
+ className={`flex cursor-pointer items-start gap-3 rounded-md border px-3 py-2 hover:border-muted-fg/50 ${selected === task.id ? "border-primary/50 bg-primary/5" : "border-border bg-muted/30"}`}
114
+ >
115
+ <StatusIcon status={eff} className="mt-0.5 shrink-0" />
116
+ <div className="min-w-0 flex-1">
117
+ <div className="truncate font-medium">{task.title}</div>
118
+ <div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-fg">
119
+ {task.state === "open" && <StatusBadge status={eff} />}
120
+ {task.tags?.map((tag) => <Badge key={tag}>#{tag}</Badge>)}
121
+ {task.agent && <Badge tone="info">@{task.agent}</Badge>}
122
+ {task.due && <span>{t("project.tasks.due")} {task.due}</span>}
123
+ </div>
124
+ </div>
125
+ <div className="flex shrink-0 gap-1" onClick={(e) => e.stopPropagation()}>
126
+ {task.state === "open" ? (
127
+ <>
128
+ <Button size="sm" variant="secondary" aria-label={t("project.tasks.aria_done")} data-testid={`task-done-${task.id}`} onClick={() => mark(() => Tasks.done(pid, task.id), t("project.tasks.done"))}>
129
+ <Check size={13} />
130
+ </Button>
131
+ <Button size="sm" variant="destructive" aria-label={t("project.tasks.aria_drop")} data-testid={`task-drop-${task.id}`} onClick={() => mark(() => Tasks.drop(pid, task.id), t("project.tasks.drop"))}>
132
+ <Trash2 size={13} />
133
+ </Button>
134
+ </>
135
+ ) : (
136
+ <Button size="sm" variant="ghost" aria-label={t("project.tasks.aria_reopen")} data-testid={`task-reopen-${task.id}`} onClick={() => mark(() => Tasks.reopen(pid, task.id), t("project.tasks.reopen"))}>
137
+ <RotateCcw size={13} />
138
+ </Button>
139
+ )}
140
+ </div>
141
+ </li>
142
+ );
143
+ })}
144
+ </ul>
145
+ </PagedList>
146
+ </div>
147
+
148
+ {selected && (
149
+ <TaskDetailPanel
150
+ pid={pid}
151
+ taskId={selected}
152
+ onClose={() => select(null)}
153
+ onChanged={() => paged.mutate()}
154
+ />
155
+ )}
156
+ </div>
118
157
  </Section>
119
158
  );
120
159
  }
@@ -18,6 +18,9 @@ export interface ProjectEntry {
18
18
  storagePath?: string;
19
19
  }
20
20
 
21
+ // Autonomy mirrors the super-agent permission modes.
22
+ export type AgentAutonomy = "total" | "automatico" | "permiso";
23
+
21
24
  export interface AgentEntry {
22
25
  slug: string;
23
26
  role: string | null;
@@ -28,6 +31,8 @@ export interface AgentEntry {
28
31
  parent?: string | null;
29
32
  type?: string | null;
30
33
  area?: string | null;
34
+ emoji?: string | null;
35
+ autonomy?: AgentAutonomy | null;
31
36
  skills: string[];
32
37
  tools: string[];
33
38
  }
@@ -52,19 +57,77 @@ export interface RoutineEntry {
52
57
  post_commands?: string[];
53
58
  }
54
59
 
60
+ // Workflow sub-status for an open task (orthogonal to `state`).
61
+ export type TaskStatus = "pending" | "running" | "in_review" | "blocked";
62
+
55
63
  export interface TaskEntry {
56
64
  id: string;
57
65
  state: "open" | "done" | "dropped";
66
+ status?: TaskStatus;
58
67
  title: string;
59
68
  body: string | null;
60
69
  tags: string[];
61
70
  due: string | null;
62
71
  agent: string | null;
63
72
  source: string | null;
73
+ created_by?: string | null;
74
+ thread?: string | null;
75
+ done_at?: string | null;
76
+ done_by?: string | null;
77
+ dropped_at?: string | null;
64
78
  created_at: string;
65
79
  updated_at: string;
66
80
  }
67
81
 
82
+ export interface OrgArea {
83
+ slug: string;
84
+ name: string;
85
+ goal: string | null;
86
+ }
87
+
88
+ export interface OrgRole {
89
+ slug: string;
90
+ name: string;
91
+ area: string | null;
92
+ description: string | null;
93
+ }
94
+
95
+ export interface Organization {
96
+ areas: OrgArea[];
97
+ roles: OrgRole[];
98
+ }
99
+
100
+ // Project file browser (core/stores/project-files.js).
101
+ export type FileKind = "markdown" | "text" | "image" | "binary";
102
+
103
+ export interface FileNode {
104
+ name: string;
105
+ path: string;
106
+ type: "dir" | "file";
107
+ kind?: FileKind;
108
+ children?: FileNode[];
109
+ }
110
+
111
+ export interface FileTreeResponse {
112
+ scope: "project" | "docs";
113
+ root: string;
114
+ subdir: string;
115
+ tree: FileNode[];
116
+ truncated: boolean;
117
+ }
118
+
119
+ export interface FileContent {
120
+ path: string;
121
+ name: string;
122
+ kind: FileKind;
123
+ size: number;
124
+ modified: string;
125
+ encoding: "utf8" | "base64" | "binary";
126
+ mime?: string;
127
+ content: string | null;
128
+ too_large?: boolean;
129
+ }
130
+
68
131
  export interface McpEntry {
69
132
  name: string;
70
133
  source: "apc" | "runtime" | "global" | string;