@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.
- package/package.json +1 -1
- package/src/core/agent/skills/index.js +9 -0
- package/src/core/agent/skills/inspector.js +7 -2
- package/src/core/agent/skills/policy.js +128 -0
- package/src/core/agent/super-agent.js +8 -1
- package/src/core/agent/tools/handlers/list-skills.js +6 -3
- package/src/core/agent/tools/handlers/load-skill.js +9 -2
- package/src/core/apc/paths.js +7 -0
- package/src/core/stores/organization.js +152 -0
- package/src/core/stores/project-files.js +199 -0
- package/src/core/stores/tasks.js +36 -3
- package/src/host/daemon/api/agents.js +22 -2
- package/src/host/daemon/api/files-project.js +99 -0
- package/src/host/daemon/api/organization.js +88 -0
- package/src/host/daemon/api/shared.js +7 -0
- package/src/host/daemon/api/skills.js +301 -17
- package/src/host/daemon/api/tasks.js +14 -0
- package/src/host/daemon/api.js +4 -0
- package/src/interfaces/cli/commands/org.js +77 -0
- package/src/interfaces/cli/commands/skills.js +3 -2
- package/src/interfaces/cli/index.js +48 -0
- package/src/interfaces/web/dist/assets/index-CnQb4N6C.js +731 -0
- package/src/interfaces/web/dist/assets/index-CnQb4N6C.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-Dv3X-zpx.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
- package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
- package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
- package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
- package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
- package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
- package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
- package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
- package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
- package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
- package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
- package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
- package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
- package/src/interfaces/web/src/i18n/en.ts +171 -0
- package/src/interfaces/web/src/i18n/es.ts +171 -0
- package/src/interfaces/web/src/lib/api/organization.ts +18 -0
- package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
- package/src/interfaces/web/src/lib/api/skills.ts +79 -8
- package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
- package/src/interfaces/web/src/lib/api.ts +2 -0
- package/src/interfaces/web/src/lib/slug.ts +11 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +25 -2
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
- package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
- package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
- package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
- package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
- package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
- package/src/interfaces/web/src/types/daemon.ts +63 -0
- package/src/interfaces/web/dist/assets/index-CEI8DfVg.css +0 -1
- package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js +0 -651
- package/src/interfaces/web/dist/assets/index-DJ-ocXOR.js.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Dialog, Button, Field, Input, Textarea } from "../ui";
|
|
3
|
+
import { UiSelect } from "../UiSelect";
|
|
4
|
+
import { useToast } from "../Toast";
|
|
5
|
+
import { Org } from "../../lib/api/organization";
|
|
6
|
+
import { slugify } from "../../lib/slug";
|
|
7
|
+
import { t } from "../../i18n";
|
|
8
|
+
import type { OrgArea, OrgRole } from "../../types/daemon";
|
|
9
|
+
|
|
10
|
+
// Create or edit an area. `editing` present → edit mode (slug is immutable).
|
|
11
|
+
export function AreaDialog({
|
|
12
|
+
open, onClose, pid, editing, onSaved,
|
|
13
|
+
}: {
|
|
14
|
+
open: boolean;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
pid: string;
|
|
17
|
+
editing?: OrgArea | null;
|
|
18
|
+
onSaved: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
const toast = useToast();
|
|
21
|
+
const [name, setName] = useState("");
|
|
22
|
+
const [slug, setSlug] = useState("");
|
|
23
|
+
const [goal, setGoal] = useState("");
|
|
24
|
+
const [busy, setBusy] = useState(false);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
setName(editing?.name ?? "");
|
|
29
|
+
setSlug(editing?.slug ?? "");
|
|
30
|
+
setGoal(editing?.goal ?? "");
|
|
31
|
+
}, [open, editing]);
|
|
32
|
+
|
|
33
|
+
const save = async () => {
|
|
34
|
+
if (!name.trim()) return;
|
|
35
|
+
setBusy(true);
|
|
36
|
+
try {
|
|
37
|
+
if (editing) await Org.updateArea(pid, editing.slug, { name, goal });
|
|
38
|
+
else await Org.createArea(pid, { name, slug: slug || slugify(name), goal });
|
|
39
|
+
toast.success(t("common.saved"));
|
|
40
|
+
onSaved();
|
|
41
|
+
onClose();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
toast.error(e instanceof Error ? e.message : String(e));
|
|
44
|
+
} finally {
|
|
45
|
+
setBusy(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Dialog
|
|
51
|
+
open={open}
|
|
52
|
+
onClose={onClose}
|
|
53
|
+
title={editing ? t("structure.edit_area") : t("structure.new_area")}
|
|
54
|
+
footer={
|
|
55
|
+
<>
|
|
56
|
+
<Button variant="ghost" onClick={onClose}>{t("common.cancel")}</Button>
|
|
57
|
+
<Button variant="primary" data-testid="area-create" onClick={() => void save()} loading={busy} disabled={!name.trim()}>
|
|
58
|
+
{editing ? t("common.save") : t("structure.create_area")}
|
|
59
|
+
</Button>
|
|
60
|
+
</>
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
<div className="space-y-3">
|
|
64
|
+
<Field label={t("structure.name")}>
|
|
65
|
+
<Input
|
|
66
|
+
autoFocus
|
|
67
|
+
data-testid="area-name"
|
|
68
|
+
value={name}
|
|
69
|
+
onChange={(e) => { setName(e.target.value); if (!editing) setSlug(slugify(e.target.value)); }}
|
|
70
|
+
placeholder="Engineering"
|
|
71
|
+
/>
|
|
72
|
+
</Field>
|
|
73
|
+
{!editing && (
|
|
74
|
+
<Field label={t("structure.slug")}>
|
|
75
|
+
<Input value={slug} onChange={(e) => setSlug(slugify(e.target.value))} className="font-mono" placeholder="engineering" />
|
|
76
|
+
</Field>
|
|
77
|
+
)}
|
|
78
|
+
<Field label={t("structure.goal")} hint={t("structure.goal_hint")}>
|
|
79
|
+
<Textarea value={goal} onChange={(e) => setGoal(e.target.value)} rows={2} />
|
|
80
|
+
</Field>
|
|
81
|
+
</div>
|
|
82
|
+
</Dialog>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create or edit a role. `presetArea` pre-selects an area (quick-create from an
|
|
87
|
+
// area card).
|
|
88
|
+
export function RoleDialog({
|
|
89
|
+
open, onClose, pid, areas, editing, presetArea, onSaved,
|
|
90
|
+
}: {
|
|
91
|
+
open: boolean;
|
|
92
|
+
onClose: () => void;
|
|
93
|
+
pid: string;
|
|
94
|
+
areas: OrgArea[];
|
|
95
|
+
editing?: OrgRole | null;
|
|
96
|
+
presetArea?: string | null;
|
|
97
|
+
onSaved: () => void;
|
|
98
|
+
}) {
|
|
99
|
+
const toast = useToast();
|
|
100
|
+
const [name, setName] = useState("");
|
|
101
|
+
const [slug, setSlug] = useState("");
|
|
102
|
+
const [area, setArea] = useState("");
|
|
103
|
+
const [description, setDescription] = useState("");
|
|
104
|
+
const [busy, setBusy] = useState(false);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!open) return;
|
|
108
|
+
setName(editing?.name ?? "");
|
|
109
|
+
setSlug(editing?.slug ?? "");
|
|
110
|
+
setArea(editing?.area ?? presetArea ?? "");
|
|
111
|
+
setDescription(editing?.description ?? "");
|
|
112
|
+
}, [open, editing, presetArea]);
|
|
113
|
+
|
|
114
|
+
const save = async () => {
|
|
115
|
+
if (!name.trim()) return;
|
|
116
|
+
setBusy(true);
|
|
117
|
+
try {
|
|
118
|
+
if (editing) await Org.updateRole(pid, editing.slug, { name, area: area || null, description });
|
|
119
|
+
else await Org.createRole(pid, { name, slug: slug || slugify(name), area: area || null, description });
|
|
120
|
+
toast.success(t("common.saved"));
|
|
121
|
+
onSaved();
|
|
122
|
+
onClose();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
toast.error(e instanceof Error ? e.message : String(e));
|
|
125
|
+
} finally {
|
|
126
|
+
setBusy(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const areaOptions = [
|
|
131
|
+
{ value: "", label: t("structure.no_area") },
|
|
132
|
+
...areas.map((a) => ({ value: a.slug, label: a.name })),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Dialog
|
|
137
|
+
open={open}
|
|
138
|
+
onClose={onClose}
|
|
139
|
+
title={editing ? t("structure.edit_role") : t("structure.new_role")}
|
|
140
|
+
footer={
|
|
141
|
+
<>
|
|
142
|
+
<Button variant="ghost" onClick={onClose}>{t("common.cancel")}</Button>
|
|
143
|
+
<Button variant="primary" onClick={() => void save()} loading={busy} disabled={!name.trim()}>
|
|
144
|
+
{editing ? t("common.save") : t("structure.create_role")}
|
|
145
|
+
</Button>
|
|
146
|
+
</>
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<div className="space-y-3">
|
|
150
|
+
<Field label={t("structure.name")}>
|
|
151
|
+
<Input
|
|
152
|
+
autoFocus
|
|
153
|
+
value={name}
|
|
154
|
+
onChange={(e) => { setName(e.target.value); if (!editing) setSlug(slugify(e.target.value)); }}
|
|
155
|
+
placeholder="Tech Lead"
|
|
156
|
+
/>
|
|
157
|
+
</Field>
|
|
158
|
+
{!editing && (
|
|
159
|
+
<Field label={t("structure.slug")}>
|
|
160
|
+
<Input value={slug} onChange={(e) => setSlug(slugify(e.target.value))} className="font-mono" placeholder="tech-lead" />
|
|
161
|
+
</Field>
|
|
162
|
+
)}
|
|
163
|
+
<Field label={t("structure.area")}>
|
|
164
|
+
<UiSelect value={area} onChange={setArea} options={areaOptions} placeholder={t("structure.no_area")} />
|
|
165
|
+
</Field>
|
|
166
|
+
<Field label={t("structure.description")}>
|
|
167
|
+
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2} />
|
|
168
|
+
</Field>
|
|
169
|
+
</div>
|
|
170
|
+
</Dialog>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { X, Check, Trash2, RotateCcw, ExternalLink, Save } from "lucide-react";
|
|
4
|
+
import { useNavigate } from "react-router-dom";
|
|
5
|
+
import { Tasks } from "../../lib/api";
|
|
6
|
+
import { Button, Spinner, Textarea } from "../ui";
|
|
7
|
+
import { UiSelect } from "../UiSelect";
|
|
8
|
+
import { useToast } from "../Toast";
|
|
9
|
+
import { StatusBadge, effectiveStatus, TASK_STATUS_ORDER, statusLabel } from "./taskStatus";
|
|
10
|
+
import { t } from "../../i18n";
|
|
11
|
+
import type { TaskStatus } from "../../types/daemon";
|
|
12
|
+
|
|
13
|
+
function Row({ label, children }: { label: string; children: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-baseline justify-between gap-3 text-xs">
|
|
16
|
+
<span className="text-muted-foreground">{label}</span>
|
|
17
|
+
<span className="text-right font-mono text-foreground/90">{children}</span>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Right-hand task inspector: prompt/body, workflow status, who created it, the
|
|
23
|
+
// linked thread, timestamps, and lifecycle actions. Mirrors Panda's detail
|
|
24
|
+
// panel but wired to APX's task store.
|
|
25
|
+
export function TaskDetailPanel({
|
|
26
|
+
pid, taskId, onClose, onChanged,
|
|
27
|
+
}: {
|
|
28
|
+
pid: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
onChanged: () => void;
|
|
32
|
+
}) {
|
|
33
|
+
const toast = useToast();
|
|
34
|
+
const navigate = useNavigate();
|
|
35
|
+
const { data: task, isLoading, mutate } = useSWR(`/projects/${pid}/tasks/${taskId}`, () => Tasks.get(pid, taskId));
|
|
36
|
+
const [body, setBody] = useState("");
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => { setBody(task?.body ?? ""); }, [task?.id, task?.body]);
|
|
40
|
+
|
|
41
|
+
const refresh = () => { void mutate(); onChanged(); };
|
|
42
|
+
const act = async (fn: () => Promise<unknown>) => {
|
|
43
|
+
setBusy(true);
|
|
44
|
+
try { await fn(); refresh(); }
|
|
45
|
+
catch (e) { toast.error(e instanceof Error ? e.message : String(e)); }
|
|
46
|
+
finally { setBusy(false); }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (isLoading) return <div className="flex w-80 items-center justify-center border-l border-border"><Spinner /></div>;
|
|
50
|
+
if (!task) return null;
|
|
51
|
+
|
|
52
|
+
const eff = effectiveStatus(task);
|
|
53
|
+
const isOpen = task.state === "open";
|
|
54
|
+
const bodyDirty = body !== (task.body ?? "");
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex w-80 shrink-0 flex-col border-l border-border bg-card/40" data-testid="task-detail">
|
|
58
|
+
<div className="flex shrink-0 items-center justify-between border-b border-border px-4 py-2.5">
|
|
59
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("tasks.detail_title")}</span>
|
|
60
|
+
<button type="button" onClick={onClose} aria-label={t("common.close")} className="text-muted-foreground hover:text-foreground">
|
|
61
|
+
<X className="size-4" />
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4">
|
|
66
|
+
<div>
|
|
67
|
+
<div className="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">{t("tasks.field_title")}</div>
|
|
68
|
+
<div className="text-sm font-semibold">{task.title}</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
<StatusBadge status={eff} />
|
|
73
|
+
<span className="font-mono text-[10px] text-muted-foreground">{task.id}</span>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Prompt / body — editable */}
|
|
77
|
+
<div>
|
|
78
|
+
<div className="mb-1 flex items-center justify-between">
|
|
79
|
+
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">{t("tasks.field_prompt")}</span>
|
|
80
|
+
{bodyDirty && (
|
|
81
|
+
<button type="button" onClick={() => act(async () => { await Tasks.patch(pid, task.id, { body }); toast.success(t("common.saved")); })} className="flex items-center gap-1 text-[10px] text-emerald-500 hover:text-emerald-400">
|
|
82
|
+
<Save className="size-3" />{t("files.save")}
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
<Textarea rows={4} value={body} onChange={(e) => setBody(e.target.value)} placeholder={t("tasks.prompt_ph")} className="text-xs" />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Workflow status control (open tasks) */}
|
|
90
|
+
{isOpen && (
|
|
91
|
+
<div>
|
|
92
|
+
<div className="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">{t("tasks.field_status")}</div>
|
|
93
|
+
<UiSelect
|
|
94
|
+
value={task.status ?? "pending"}
|
|
95
|
+
onChange={(v) => act(() => Tasks.status(pid, task.id, v as TaskStatus))}
|
|
96
|
+
options={TASK_STATUS_ORDER.map((s) => ({ value: s, label: statusLabel(s) }))}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
<div className="space-y-1.5 rounded-lg border border-border bg-background/40 p-2.5">
|
|
102
|
+
{task.agent && <Row label={t("tasks.field_agent")}>@{task.agent}</Row>}
|
|
103
|
+
{task.created_by && <Row label={t("tasks.field_creator")}>{task.created_by}</Row>}
|
|
104
|
+
{task.source && <Row label={t("tasks.field_source")}>{task.source}</Row>}
|
|
105
|
+
{task.due && <Row label={t("project.tasks.due")}>{task.due}</Row>}
|
|
106
|
+
<Row label={t("tasks.field_created")}>{new Date(task.created_at).toLocaleString()}</Row>
|
|
107
|
+
<Row label={t("tasks.field_updated")}>{new Date(task.updated_at).toLocaleString()}</Row>
|
|
108
|
+
{task.done_at && <Row label={t("tasks.field_done")}>{new Date(task.done_at).toLocaleString()}</Row>}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Thread link */}
|
|
112
|
+
{task.thread && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={() => navigate(`/p/${pid}/chat?thread=${task.thread}`)}
|
|
116
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-sky-500/30 bg-sky-500/5 px-3 py-2 text-xs text-sky-500 hover:bg-sky-500/10"
|
|
117
|
+
>
|
|
118
|
+
<ExternalLink className="size-3.5" />{t("tasks.view_thread")}
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Actions */}
|
|
124
|
+
<div className="flex shrink-0 gap-2 border-t border-border px-4 py-3">
|
|
125
|
+
{isOpen ? (
|
|
126
|
+
<>
|
|
127
|
+
<Button size="sm" variant="primary" className="flex-1" loading={busy} onClick={() => act(() => Tasks.done(pid, task.id))}>
|
|
128
|
+
<Check size={13} />{t("tasks.mark_done")}
|
|
129
|
+
</Button>
|
|
130
|
+
<Button size="sm" variant="destructive" loading={busy} onClick={() => act(() => Tasks.drop(pid, task.id))} aria-label={t("project.tasks.aria_drop")}>
|
|
131
|
+
<Trash2 size={13} />
|
|
132
|
+
</Button>
|
|
133
|
+
</>
|
|
134
|
+
) : (
|
|
135
|
+
<Button size="sm" variant="secondary" className="flex-1" loading={busy} onClick={() => act(() => Tasks.reopen(pid, task.id))}>
|
|
136
|
+
<RotateCcw size={13} />{t("project.tasks.reopen")}
|
|
137
|
+
</Button>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Loader2, CheckCircle2, XCircle, Clock, CircleDot, HelpCircle } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { t } from "../../i18n";
|
|
4
|
+
import type { TaskEntry, TaskStatus } from "../../types/daemon";
|
|
5
|
+
|
|
6
|
+
type Meta = { labelKey: string; color: string; dot: string; Icon: typeof Clock; spin?: boolean };
|
|
7
|
+
|
|
8
|
+
// Single source of truth for how each workflow status looks. Reused by the
|
|
9
|
+
// task list, detail panel and the floor/overview.
|
|
10
|
+
const STATUS_META: Record<TaskStatus, Meta> = {
|
|
11
|
+
pending: { labelKey: "tasks.status_pending", color: "text-amber-500", dot: "bg-amber-400", Icon: CircleDot },
|
|
12
|
+
running: { labelKey: "tasks.status_running", color: "text-sky-500", dot: "bg-sky-400", Icon: Loader2, spin: true },
|
|
13
|
+
in_review: { labelKey: "tasks.status_in_review", color: "text-violet-500", dot: "bg-violet-400", Icon: Clock },
|
|
14
|
+
blocked: { labelKey: "tasks.status_blocked", color: "text-slate-400", dot: "bg-slate-400", Icon: HelpCircle },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const TASK_STATUS_ORDER: TaskStatus[] = ["pending", "running", "in_review", "blocked"];
|
|
18
|
+
|
|
19
|
+
// Effective status for display: closed tasks render as done/dropped regardless
|
|
20
|
+
// of their last open sub-status.
|
|
21
|
+
export function effectiveStatus(task: TaskEntry): TaskStatus | "done" | "dropped" {
|
|
22
|
+
if (task.state === "done") return "done";
|
|
23
|
+
if (task.state === "dropped") return "dropped";
|
|
24
|
+
return task.status ?? "pending";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function statusLabel(status: TaskStatus): string {
|
|
28
|
+
return t(STATUS_META[status].labelKey as never);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function StatusIcon({ status, className }: { status: TaskStatus | "done" | "dropped"; className?: string }) {
|
|
32
|
+
if (status === "done") return <CheckCircle2 className={cn("size-4 text-emerald-500", className)} />;
|
|
33
|
+
if (status === "dropped") return <XCircle className={cn("size-4 text-muted-foreground", className)} />;
|
|
34
|
+
const m = STATUS_META[status];
|
|
35
|
+
return <m.Icon className={cn("size-4", m.color, m.spin && "animate-spin", className)} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function StatusBadge({ status }: { status: TaskStatus | "done" | "dropped" }) {
|
|
39
|
+
const label =
|
|
40
|
+
status === "done" ? t("tasks.done_label")
|
|
41
|
+
: status === "dropped" ? t("tasks.dropped_label")
|
|
42
|
+
: statusLabel(status);
|
|
43
|
+
const color =
|
|
44
|
+
status === "done" ? "text-emerald-500 border-emerald-500/30"
|
|
45
|
+
: status === "dropped" ? "text-muted-foreground border-border"
|
|
46
|
+
: `${STATUS_META[status].color} border-current/30`;
|
|
47
|
+
return (
|
|
48
|
+
<span className={cn("inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] font-medium capitalize", color)}>
|
|
49
|
+
<StatusIcon status={status} className="size-3" />
|
|
50
|
+
{label}
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function StatusDot({ status }: { status: TaskStatus }) {
|
|
56
|
+
return <span className={cn("size-2 rounded-full", STATUS_META[status].dot)} />;
|
|
57
|
+
}
|
|
@@ -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",
|
|
@@ -1581,6 +1685,73 @@ export const en = {
|
|
|
1581
1685
|
cfg_apx_storage_id: "APX storage id",
|
|
1582
1686
|
},
|
|
1583
1687
|
|
|
1688
|
+
skills_page: {
|
|
1689
|
+
title: "Skills",
|
|
1690
|
+
desc: "Turn skills on or off per agent. Pick a scope: the super-agent (global) or a specific project.",
|
|
1691
|
+
list_title: "Installed skills",
|
|
1692
|
+
list_desc: "APX's private skills are always active and can't be changed.",
|
|
1693
|
+
scope_label: "Scope",
|
|
1694
|
+
scope_super_agent: "Super-agent (global)",
|
|
1695
|
+
scope_hint: "The super-agent uses the global scope. Each project can override skills independently.",
|
|
1696
|
+
count_label: "{n} skills · {on} on",
|
|
1697
|
+
empty: "No skills yet. Create one below or install via the CLI.",
|
|
1698
|
+
source_builtin: "APX",
|
|
1699
|
+
source_global: "Global",
|
|
1700
|
+
source_project: "Project",
|
|
1701
|
+
private_badge: "Private",
|
|
1702
|
+
private_hint: "Built-in APX skill — always active, can't be disabled or deleted.",
|
|
1703
|
+
overridden_badge: "Override",
|
|
1704
|
+
inherited_hint: "Inherited from global",
|
|
1705
|
+
reset_to_global: "Reset to global",
|
|
1706
|
+
on: "on",
|
|
1707
|
+
off: "off",
|
|
1708
|
+
toggle_failed: "Could not change state: {msg}",
|
|
1709
|
+
add_title: "Add a skill",
|
|
1710
|
+
add_desc: "Creates a user skill at ~/.apx/skills/<slug>/SKILL.md. Available across all scopes.",
|
|
1711
|
+
add_slug_label: "Slug",
|
|
1712
|
+
add_slug_ph: "my-skill",
|
|
1713
|
+
add_desc_label: "Description",
|
|
1714
|
+
add_desc_ph: "One line describing when to use it",
|
|
1715
|
+
add_body_label: "Body (Markdown)",
|
|
1716
|
+
add_body_ph: "# My skill\n\nInstructions for the agent…",
|
|
1717
|
+
add_btn: "Create skill",
|
|
1718
|
+
created_ok: "Skill \"{slug}\" created.",
|
|
1719
|
+
create_failed: "Could not create: {msg}",
|
|
1720
|
+
delete_btn: "Delete",
|
|
1721
|
+
delete_confirm: "Delete skill \"{slug}\"? This can't be undone.",
|
|
1722
|
+
deleted_ok: "Skill \"{slug}\" deleted.",
|
|
1723
|
+
delete_failed: "Could not delete: {msg}",
|
|
1724
|
+
inspector_section_title: "Skill Inspector (per-turn RAG)",
|
|
1725
|
+
inspector_section_desc: "Advanced: local RAG that injects only the skills a message needs.",
|
|
1726
|
+
scope_ph: "— choose scope —",
|
|
1727
|
+
select_a_skill: "Pick a skill from the list to see its content.",
|
|
1728
|
+
added_by: "Added by",
|
|
1729
|
+
activator: "Activator",
|
|
1730
|
+
by_apx: "APX (built-in)",
|
|
1731
|
+
by_you: "You",
|
|
1732
|
+
activator_value: "Semantic match (RAG)",
|
|
1733
|
+
tab_preview: "Preview",
|
|
1734
|
+
tab_source: "Source",
|
|
1735
|
+
add_menu: "Add",
|
|
1736
|
+
add_online: "Create with editor",
|
|
1737
|
+
add_online_hint: "Write slug + description + content",
|
|
1738
|
+
add_zip: "Upload .zip",
|
|
1739
|
+
add_zip_hint: "Import a packaged skill",
|
|
1740
|
+
add_repo: "From git repo",
|
|
1741
|
+
add_repo_hint: "Clone from a URL",
|
|
1742
|
+
create_dialog_title: "Create skill",
|
|
1743
|
+
repo_dialog_title: "Import from git repo",
|
|
1744
|
+
repo_url_label: "Repo URL",
|
|
1745
|
+
repo_url_ph: "https://github.com/user/my-skill.git",
|
|
1746
|
+
repo_url_hint: "The repo (or its subfolder) must contain a SKILL.md.",
|
|
1747
|
+
import_btn: "Import",
|
|
1748
|
+
imported_ok: "Skill \"{slug}\" imported.",
|
|
1749
|
+
import_failed: "Could not import: {msg}",
|
|
1750
|
+
cancel: "Cancel",
|
|
1751
|
+
manager_tab: "Skills",
|
|
1752
|
+
rag_tab: "Config (RAG)",
|
|
1753
|
+
},
|
|
1754
|
+
|
|
1584
1755
|
shared_ui: {
|
|
1585
1756
|
skill_inspector_title: "Skill Inspector ({embedder}) chose these skills for this turn",
|
|
1586
1757
|
tools_count: "{n} tools",
|