@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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/core/apc/paths.js +7 -0
  3. package/src/core/stores/conversations.js +10 -0
  4. package/src/core/stores/messages.js +37 -6
  5. package/src/core/stores/organization.js +152 -0
  6. package/src/core/stores/project-files.js +199 -0
  7. package/src/core/stores/tasks.js +36 -3
  8. package/src/host/daemon/api/agents.js +22 -2
  9. package/src/host/daemon/api/conversations.js +20 -2
  10. package/src/host/daemon/api/files-project.js +99 -0
  11. package/src/host/daemon/api/organization.js +88 -0
  12. package/src/host/daemon/api/shared.js +7 -0
  13. package/src/host/daemon/api/tasks.js +14 -0
  14. package/src/host/daemon/api.js +4 -0
  15. package/src/interfaces/cli/commands/org.js +77 -0
  16. package/src/interfaces/cli/index.js +48 -0
  17. package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +1 -0
  18. package/src/interfaces/web/dist/assets/index-DPAuXATr.js +705 -0
  19. package/src/interfaces/web/dist/assets/index-DPAuXATr.js.map +1 -0
  20. package/src/interfaces/web/dist/index.html +2 -2
  21. package/src/interfaces/web/package-lock.json +6 -6
  22. package/src/interfaces/web/src/App.tsx +1 -1
  23. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +123 -0
  24. package/src/interfaces/web/src/components/chat/ChatList.tsx +86 -65
  25. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +51 -0
  26. package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
  27. package/src/interfaces/web/src/components/files/FileBrowser.tsx +138 -0
  28. package/src/interfaces/web/src/components/files/FileTree.tsx +133 -0
  29. package/src/interfaces/web/src/components/files/FileViewer.tsx +167 -0
  30. package/src/interfaces/web/src/components/files/MarkdownEditor.tsx +48 -0
  31. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +146 -0
  32. package/src/interfaces/web/src/components/files/NewFileDialog.tsx +66 -0
  33. package/src/interfaces/web/src/components/structure/StructureDialogs.tsx +172 -0
  34. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +142 -0
  35. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +57 -0
  36. package/src/interfaces/web/src/hooks/useChat.ts +39 -7
  37. package/src/interfaces/web/src/i18n/en.ts +113 -3
  38. package/src/interfaces/web/src/i18n/es.ts +113 -3
  39. package/src/interfaces/web/src/lib/api/conversations.ts +6 -0
  40. package/src/interfaces/web/src/lib/api/organization.ts +18 -0
  41. package/src/interfaces/web/src/lib/api/projectFiles.ts +19 -0
  42. package/src/interfaces/web/src/lib/api/tasks.ts +16 -1
  43. package/src/interfaces/web/src/lib/api.ts +2 -0
  44. package/src/interfaces/web/src/lib/slug.ts +11 -0
  45. package/src/interfaces/web/src/screens/ProjectScreen.tsx +22 -3
  46. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  47. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +31 -11
  48. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +24 -7
  49. package/src/interfaces/web/src/screens/project/ChatTab.tsx +136 -36
  50. package/src/interfaces/web/src/screens/project/DocsTab.tsx +13 -0
  51. package/src/interfaces/web/src/screens/project/FilesTab.tsx +12 -0
  52. package/src/interfaces/web/src/screens/project/Overview.tsx +122 -10
  53. package/src/interfaces/web/src/screens/project/StructureTab.tsx +147 -0
  54. package/src/interfaces/web/src/screens/project/TasksTab.tsx +101 -62
  55. package/src/interfaces/web/src/types/daemon.ts +68 -0
  56. package/src/interfaces/web/dist/assets/index-CAUezTBY.css +0 -1
  57. package/src/interfaces/web/dist/assets/index-DaE_memX.js +0 -651
  58. 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: ChatMsg[] = (detail.messages ?? [])
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
- clear: "Clear",
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
- live_with: "Live · {slug}",
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",