@agentprojectcontext/apx 1.56.2 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/apc/paths.js +7 -0
- package/src/core/stores/conversations.js +10 -0
- package/src/core/stores/messages.js +37 -6
- 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/conversations.js +20 -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/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/index.js +48 -0
- package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +1 -0
- package/src/interfaces/web/dist/assets/index-DPAuXATr.js +705 -0
- package/src/interfaces/web/dist/assets/index-DPAuXATr.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +6 -6
- package/src/interfaces/web/src/App.tsx +1 -1
- package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
- package/src/interfaces/web/src/components/chat/ChatList.tsx +86 -65
- package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
- package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
- 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/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/hooks/useChat.ts +39 -7
- package/src/interfaces/web/src/i18n/en.ts +113 -3
- package/src/interfaces/web/src/i18n/es.ts +113 -3
- package/src/interfaces/web/src/lib/api/conversations.ts +6 -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/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 +22 -3
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
- 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/ChatTab.tsx +136 -36
- 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/StructureTab.tsx +147 -0
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
- package/src/interfaces/web/src/types/daemon.ts +68 -0
- package/src/interfaces/web/dist/assets/index-CAUezTBY.css +0 -1
- package/src/interfaces/web/dist/assets/index-DaE_memX.js +0 -651
- package/src/interfaces/web/dist/assets/index-DaE_memX.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
|
+
}
|
|
@@ -130,6 +130,44 @@ function isErrorResult(result: unknown): boolean {
|
|
|
130
130
|
return "error" in r && !!r.error;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** Reconstruct chat turns from a persisted channel thread (global ledger).
|
|
134
|
+
* User rows start a new turn; consecutive assistant/tool rows collapse into a
|
|
135
|
+
* single assistant bubble with interleaved text + tool parts — mirroring how a
|
|
136
|
+
* live streamed turn is shaped, so tool executions render the same on reload as
|
|
137
|
+
* they did in real time. Persisted rows carry no live status, so it's derived
|
|
138
|
+
* from the stored result (error → "error", else "done"). */
|
|
139
|
+
function threadToChatMsgs(messages: ConversationMessage[]): ChatMsg[] {
|
|
140
|
+
const out: ChatMsg[] = [];
|
|
141
|
+
let turn: ChatMsg | null = null;
|
|
142
|
+
let toolSeq = 0;
|
|
143
|
+
for (const m of messages) {
|
|
144
|
+
const ts = m.ts || new Date().toISOString();
|
|
145
|
+
if (m.role === "user") {
|
|
146
|
+
turn = null;
|
|
147
|
+
out.push({ role: "user", parts: userPart(m.content), ts });
|
|
148
|
+
} else if (m.role === "assistant" || m.role === "tool") {
|
|
149
|
+
if (!turn) {
|
|
150
|
+
turn = { role: "assistant", parts: [], ts };
|
|
151
|
+
out.push(turn);
|
|
152
|
+
}
|
|
153
|
+
if (m.role === "tool") {
|
|
154
|
+
turn.parts.push({
|
|
155
|
+
kind: "tool",
|
|
156
|
+
id: `hist-${toolSeq++}`,
|
|
157
|
+
tool: m.tool || "tool",
|
|
158
|
+
args: m.args,
|
|
159
|
+
result: m.result,
|
|
160
|
+
status: isErrorResult(m.result) ? "error" : "done",
|
|
161
|
+
});
|
|
162
|
+
} else if (m.content) {
|
|
163
|
+
turn.parts.push({ kind: "text", text: m.content });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// system/compact rows are context-only; not rendered in the thread viewer.
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
/**
|
|
134
172
|
* Pure reducer: apply one NDJSON stream event to an assistant turn and return
|
|
135
173
|
* the next turn. Every surface that consumes the super-agent stream (ChatTab,
|
|
@@ -398,13 +436,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
398
436
|
try {
|
|
399
437
|
const detail = await Conversations.thread(pid, channel, threadId);
|
|
400
438
|
if (seq !== loadSeqRef.current) return; // superseded by a newer pick
|
|
401
|
-
const loaded
|
|
402
|
-
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
403
|
-
.map((m) => ({
|
|
404
|
-
role: m.role as "user" | "assistant",
|
|
405
|
-
parts: [{ kind: "text", text: m.content }],
|
|
406
|
-
ts: m.ts || new Date().toISOString(),
|
|
407
|
-
}));
|
|
439
|
+
const loaded = threadToChatMsgs(detail.messages ?? []);
|
|
408
440
|
// Ledger threads have no conversation file — sends continue as fresh
|
|
409
441
|
// web turns with this history as previousMessages.
|
|
410
442
|
convoRef.current = undefined;
|
|
@@ -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: {
|
|
@@ -319,6 +334,7 @@ export const en = {
|
|
|
319
334
|
chat: {
|
|
320
335
|
title: "Chat with agent",
|
|
321
336
|
subtitle: "Direct conversations with project agents. The super-agent does not intervene.",
|
|
337
|
+
live_title: "Chat with {agent}",
|
|
322
338
|
superagent_title: "Chat with {persona}",
|
|
323
339
|
superagent_subtitle: "Chat with {persona} — the APX super-agent. Can use tools (projects, tasks, mcps, agents).",
|
|
324
340
|
loaded_subtitle: "Loaded conversation with {slug}. Sending will append to this thread.",
|
|
@@ -327,7 +343,13 @@ export const en = {
|
|
|
327
343
|
placeholder: "Type something and press enter to send (shift+enter = new line)",
|
|
328
344
|
send: "Send",
|
|
329
345
|
stop: "Stop",
|
|
330
|
-
|
|
346
|
+
new_session: "New session",
|
|
347
|
+
delete: "Delete",
|
|
348
|
+
delete_confirm_title: "Delete chat",
|
|
349
|
+
delete_confirm_desc: "This can't be undone. This chat's history will be permanently deleted.",
|
|
350
|
+
deleted: "Chat deleted.",
|
|
351
|
+
meta_created: "Created {date} · {channel}",
|
|
352
|
+
meta_new: "New chat · {channel}",
|
|
331
353
|
copy: "copy",
|
|
332
354
|
copied: "Copied.",
|
|
333
355
|
stopped_marker: " [stopped]",
|
|
@@ -345,8 +367,7 @@ export const en = {
|
|
|
345
367
|
all_agents: "All agents",
|
|
346
368
|
empty: "No conversations yet. Start one from the right.",
|
|
347
369
|
count: "{n} total",
|
|
348
|
-
|
|
349
|
-
live_subtitle: "In-memory session",
|
|
370
|
+
pick_agent: "Pick an agent",
|
|
350
371
|
},
|
|
351
372
|
},
|
|
352
373
|
|
|
@@ -1295,6 +1316,95 @@ export const en = {
|
|
|
1295
1316
|
role_assigned: "{name} → {role}",
|
|
1296
1317
|
},
|
|
1297
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
|
+
|
|
1298
1408
|
agents_ui: {
|
|
1299
1409
|
model_router_default: "model: router default",
|
|
1300
1410
|
slug_kebab_hint: "kebab-case, e.g. reviewer, my-agent, content-writer",
|