@agentprojectcontext/apx 1.67.0 → 1.68.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/config/redact.js +44 -0
- package/src/host/daemon/api/config.js +17 -5
- package/src/host/daemon/api/sessions.js +9 -0
- package/src/interfaces/web/dist/assets/index-D4BmWoDM.css +1 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js +803 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
- package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
- package/src/interfaces/web/src/i18n/en.ts +6 -0
- package/src/interfaces/web/src/i18n/es.ts +6 -0
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
- package/src/interfaces/web/src/screens/project/Overview.tsx +3 -2
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js +0 -803
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-BPGECxzm.css +0 -1
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
|
|
19
19
|
<link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
|
|
20
20
|
<link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-vwd6yQVw.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D4BmWoDM.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -3195,9 +3195,9 @@
|
|
|
3195
3195
|
}
|
|
3196
3196
|
},
|
|
3197
3197
|
"node_modules/node-releases": {
|
|
3198
|
-
"version": "2.0.
|
|
3199
|
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.
|
|
3200
|
-
"integrity": "sha512-
|
|
3198
|
+
"version": "2.0.51",
|
|
3199
|
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
|
3200
|
+
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
|
|
3201
3201
|
"dev": true,
|
|
3202
3202
|
"license": "MIT",
|
|
3203
3203
|
"engines": {
|
|
@@ -32,6 +32,7 @@ export function ConfigTabsEditor({
|
|
|
32
32
|
onSaveFields,
|
|
33
33
|
onSaveJson,
|
|
34
34
|
busy,
|
|
35
|
+
hideJson = false,
|
|
35
36
|
}: {
|
|
36
37
|
sections: ConfigSection[];
|
|
37
38
|
source: Record<string, unknown>;
|
|
@@ -42,6 +43,8 @@ export function ConfigTabsEditor({
|
|
|
42
43
|
onSaveFields: (set: Record<string, unknown>, unset: string[]) => Promise<void>;
|
|
43
44
|
onSaveJson: (next: Record<string, unknown>) => Promise<void>;
|
|
44
45
|
busy?: boolean;
|
|
46
|
+
/** Hide the raw-JSON tab (used when JSON editing lives in its own top tab). */
|
|
47
|
+
hideJson?: boolean;
|
|
45
48
|
}) {
|
|
46
49
|
const firstKey = sections[0]?.key || "json";
|
|
47
50
|
const [draft, setDraft] = useState<Record<string, unknown>>({});
|
|
@@ -92,51 +95,63 @@ export function ConfigTabsEditor({
|
|
|
92
95
|
}
|
|
93
96
|
};
|
|
94
97
|
|
|
98
|
+
const sectionBody = (section: ConfigSection) => (
|
|
99
|
+
<div className="space-y-4">
|
|
100
|
+
{section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
|
|
101
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
102
|
+
{section.fields.map((field) => (
|
|
103
|
+
<ConfigFieldControl
|
|
104
|
+
key={field.path}
|
|
105
|
+
field={field}
|
|
106
|
+
value={draft[field.path]}
|
|
107
|
+
inherited={getDotted(placeholderSource, field.path)}
|
|
108
|
+
onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
<Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Single section with no JSON tab → render the fields flat (no inner tab bar),
|
|
117
|
+
// so the parent's top-level tab is the only tab the user sees.
|
|
118
|
+
if (hideJson && sections.length <= 1) {
|
|
119
|
+
return sections[0] ? sectionBody(sections[0]) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
return (
|
|
96
123
|
<Tabs defaultValue={firstKey} className="space-y-4">
|
|
97
124
|
<TabsList className="flex flex-wrap">
|
|
98
125
|
{sections.map((section) => (
|
|
99
126
|
<TabsTrigger key={section.key} value={section.key}>{section.label}</TabsTrigger>
|
|
100
127
|
))}
|
|
101
|
-
<TabsTrigger value="json">JSON</TabsTrigger>
|
|
128
|
+
{!hideJson && <TabsTrigger value="json">JSON</TabsTrigger>}
|
|
102
129
|
</TabsList>
|
|
103
130
|
|
|
104
131
|
{sections.map((section) => (
|
|
105
132
|
<TabsContent key={section.key} value={section.key}>
|
|
106
|
-
|
|
107
|
-
{section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
|
|
108
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
109
|
-
{section.fields.map((field) => (
|
|
110
|
-
<ConfigFieldControl
|
|
111
|
-
key={field.path}
|
|
112
|
-
field={field}
|
|
113
|
-
value={draft[field.path]}
|
|
114
|
-
inherited={getDotted(placeholderSource, field.path)}
|
|
115
|
-
onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
|
|
116
|
-
/>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
<Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
|
|
120
|
-
</div>
|
|
133
|
+
{sectionBody(section)}
|
|
121
134
|
</TabsContent>
|
|
122
135
|
))}
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
<div>
|
|
127
|
-
<
|
|
128
|
-
|
|
137
|
+
{!hideJson && (
|
|
138
|
+
<TabsContent value="json">
|
|
139
|
+
<div className="space-y-3">
|
|
140
|
+
<div>
|
|
141
|
+
<h3 className="text-sm font-medium">{jsonTitle}</h3>
|
|
142
|
+
{jsonDescription && <p className="text-xs text-muted-fg">{jsonDescription}</p>}
|
|
143
|
+
</div>
|
|
144
|
+
<Textarea
|
|
145
|
+
rows={18}
|
|
146
|
+
className="font-mono text-xs"
|
|
147
|
+
value={raw}
|
|
148
|
+
onChange={(event) => setRaw(event.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
|
151
|
+
<Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
|
|
129
152
|
</div>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
className="font-mono text-xs"
|
|
133
|
-
value={raw}
|
|
134
|
-
onChange={(event) => setRaw(event.target.value)}
|
|
135
|
-
/>
|
|
136
|
-
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
|
137
|
-
<Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
|
|
138
|
-
</div>
|
|
139
|
-
</TabsContent>
|
|
153
|
+
</TabsContent>
|
|
154
|
+
)}
|
|
140
155
|
</Tabs>
|
|
141
156
|
);
|
|
142
157
|
}
|
|
@@ -4,7 +4,10 @@ import { t } from "../../i18n";
|
|
|
4
4
|
|
|
5
5
|
// These are functions (not module-level consts) so t() runs per-render with the
|
|
6
6
|
// active locale — a frozen const would lock the strings to the locale at import.
|
|
7
|
-
|
|
7
|
+
// Config is organized by concept: Settings (routing / super-agent behaviour) and
|
|
8
|
+
// Engines (provider keys) are separate top-level tabs. Telegram is edited on its
|
|
9
|
+
// own channel page (the canonical mechanism), not here.
|
|
10
|
+
export function projectSettingsSections(): ConfigSection[] {
|
|
8
11
|
return [
|
|
9
12
|
{
|
|
10
13
|
key: "routing",
|
|
@@ -22,16 +25,11 @@ export function projectOverrideSections(): ConfigSection[] {
|
|
|
22
25
|
{ path: "super_agent.system", label: t("settings_ui.cfg_extra_prompt"), kind: "textarea" },
|
|
23
26
|
],
|
|
24
27
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ path: "telegram.chat_id", label: t("settings_ui.cfg_chat_id") },
|
|
31
|
-
{ path: "telegram.bot_token", label: t("settings_ui.cfg_bot_token"), kind: "password" },
|
|
32
|
-
{ path: "telegram.respond_with_engine", label: t("settings_ui.cfg_respond_with_engine"), kind: "boolean" },
|
|
33
|
-
],
|
|
34
|
-
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function projectEnginesSections(): ConfigSection[] {
|
|
32
|
+
return [
|
|
35
33
|
{
|
|
36
34
|
key: "engines",
|
|
37
35
|
label: t("settings_ui.cfg_engines_label"),
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Brain, Bot, Crown, RefreshCw } from "lucide-react";
|
|
4
|
+
import { Agents, Projects } from "../../lib/api";
|
|
5
|
+
import type { AgentEntry, FileContent } from "../../types/daemon";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import { Spinner, Empty } from "../ui";
|
|
8
|
+
import { useToast } from "../Toast";
|
|
9
|
+
import { t } from "../../i18n";
|
|
10
|
+
import { FileViewer } from "../files/FileViewer";
|
|
11
|
+
|
|
12
|
+
// Which memory is open in the right pane.
|
|
13
|
+
type Sel = { kind: "project" } | { kind: "agent"; slug: string };
|
|
14
|
+
|
|
15
|
+
function selId(s: Sel): string {
|
|
16
|
+
return s.kind === "project" ? "project" : `agent:${s.slug}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// On-disk-ish path shown in the viewer header (mirrors the real memory.md
|
|
20
|
+
// locations so it reads familiarly, like the docs surface).
|
|
21
|
+
function selPath(s: Sel): string {
|
|
22
|
+
return s.kind === "project" ? ".apc/memory.md" : `agents/${s.slug}/memory.md`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadBody(pid: string, s: Sel): Promise<string> {
|
|
26
|
+
return s.kind === "project"
|
|
27
|
+
? Projects.memory.get(pid).then((r) => r.body)
|
|
28
|
+
: Agents.memory.get(pid, s.slug).then((r) => r.body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveBody(pid: string, s: Sel, body: string): Promise<void> {
|
|
32
|
+
return s.kind === "project"
|
|
33
|
+
? Projects.memory.put(pid, body).then(() => {})
|
|
34
|
+
: Agents.memory.put(pid, s.slug, body).then(() => {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SidebarItem({
|
|
38
|
+
active, onClick, icon: Icon, iconClass, label, sub,
|
|
39
|
+
}: {
|
|
40
|
+
active: boolean;
|
|
41
|
+
onClick: () => void;
|
|
42
|
+
icon: typeof Bot;
|
|
43
|
+
iconClass?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
sub?: string;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-[13px]",
|
|
53
|
+
active ? "bg-primary/15 text-foreground" : "text-foreground/80 hover:bg-accent/40",
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<Icon className={cn("size-3.5 shrink-0", iconClass ?? "text-muted-foreground")} />
|
|
57
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
58
|
+
{sub && <span className="shrink-0 truncate text-[11px] text-muted-foreground">{sub}</span>}
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Docs-style two-pane browser for durable memory: a sidebar listing the project
|
|
64
|
+
// ("General") memory plus every agent's memory, and the shared FileViewer on the
|
|
65
|
+
// right for markdown edit / split-preview / save — the same surface as /docs.
|
|
66
|
+
export function MemoryBrowser({ pid }: { pid: string }) {
|
|
67
|
+
const toast = useToast();
|
|
68
|
+
const [sel, setSel] = useState<Sel>({ kind: "project" });
|
|
69
|
+
|
|
70
|
+
const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
|
|
71
|
+
|
|
72
|
+
const bodyKey = `/memory/${pid}/${selId(sel)}`;
|
|
73
|
+
const body = useSWR(bodyKey, () => loadBody(pid, sel));
|
|
74
|
+
|
|
75
|
+
// Adapt the raw memory body into the FileContent shape FileViewer expects.
|
|
76
|
+
const file = useMemo<FileContent | null>(() => {
|
|
77
|
+
if (body.data === undefined) return null;
|
|
78
|
+
const content = body.data ?? "";
|
|
79
|
+
return {
|
|
80
|
+
path: selPath(sel),
|
|
81
|
+
name: "memory.md",
|
|
82
|
+
kind: "markdown",
|
|
83
|
+
size: content.length,
|
|
84
|
+
modified: "",
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
content,
|
|
87
|
+
};
|
|
88
|
+
}, [body.data, sel]);
|
|
89
|
+
|
|
90
|
+
const onSave = async (content: string) => {
|
|
91
|
+
await saveBody(pid, sel, content);
|
|
92
|
+
toast.success(t("project.memories.saved"));
|
|
93
|
+
void body.mutate(content, { revalidate: false });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const list = (agents.data || []) as AgentEntry[];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex h-full min-h-0 overflow-hidden rounded-xl border border-border bg-card">
|
|
100
|
+
{/* Sidebar */}
|
|
101
|
+
<div className="flex w-64 shrink-0 flex-col border-r border-border">
|
|
102
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
|
103
|
+
<Brain className="size-4 text-muted-foreground" />
|
|
104
|
+
<span className="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
105
|
+
{t("project.memories.sidebar_title")}
|
|
106
|
+
</span>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => { void agents.mutate(); void body.mutate(); }}
|
|
110
|
+
className="text-muted-foreground hover:text-foreground"
|
|
111
|
+
aria-label={t("common.refresh")}
|
|
112
|
+
>
|
|
113
|
+
<RefreshCw className={agents.isValidating ? "size-3.5 animate-spin" : "size-3.5"} />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
|
118
|
+
{/* General / project memory */}
|
|
119
|
+
<p className="px-1.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
120
|
+
{t("project.memories.general_group")}
|
|
121
|
+
</p>
|
|
122
|
+
<SidebarItem
|
|
123
|
+
active={sel.kind === "project"}
|
|
124
|
+
onClick={() => setSel({ kind: "project" })}
|
|
125
|
+
icon={Brain}
|
|
126
|
+
iconClass="text-sky-500"
|
|
127
|
+
label={t("project.memories.general_item")}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Agent memories */}
|
|
131
|
+
<p className="px-1.5 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
132
|
+
{t("project.memories.agents_title")}
|
|
133
|
+
</p>
|
|
134
|
+
{agents.isLoading ? (
|
|
135
|
+
<div className="flex justify-center py-4"><Spinner size={14} /></div>
|
|
136
|
+
) : list.length === 0 ? (
|
|
137
|
+
<div className="px-1.5 py-2">
|
|
138
|
+
<Empty>{t("project.memories.no_agents")}</Empty>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
list.map((a) => (
|
|
142
|
+
<SidebarItem
|
|
143
|
+
key={a.slug}
|
|
144
|
+
active={sel.kind === "agent" && sel.slug === a.slug}
|
|
145
|
+
onClick={() => setSel({ kind: "agent", slug: a.slug })}
|
|
146
|
+
icon={a.is_master ? Crown : Bot}
|
|
147
|
+
iconClass={a.is_master ? "text-violet-400" : "text-muted-foreground"}
|
|
148
|
+
label={a.slug}
|
|
149
|
+
sub={a.role || undefined}
|
|
150
|
+
/>
|
|
151
|
+
))
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Viewer */}
|
|
157
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
158
|
+
<FileViewer file={file} loading={body.isLoading} onSave={onSave} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -711,6 +711,8 @@ export const en = {
|
|
|
711
711
|
save_fields_success: "Overrides saved.",
|
|
712
712
|
save_meta_success: "Project metadata saved.",
|
|
713
713
|
no_data: "No data.",
|
|
714
|
+
tab_settings: "Settings",
|
|
715
|
+
tab_project: "Project",
|
|
714
716
|
},
|
|
715
717
|
|
|
716
718
|
telegram: {
|
|
@@ -731,6 +733,9 @@ export const en = {
|
|
|
731
733
|
},
|
|
732
734
|
|
|
733
735
|
memories: {
|
|
736
|
+
sidebar_title: "Memories",
|
|
737
|
+
general_group: "General",
|
|
738
|
+
general_item: "Project memory",
|
|
734
739
|
project_title: "Project memory",
|
|
735
740
|
project_desc: "Durable facts at the project level. .apc/memory.md — read by agents and the super-agent.",
|
|
736
741
|
project_ph: "# Project Memory\n\nStable facts that any agent should know…",
|
|
@@ -756,6 +761,7 @@ export const en = {
|
|
|
756
761
|
workspaces_empty: "No projects. Add one with the button above.",
|
|
757
762
|
sessions_title: "Sessions",
|
|
758
763
|
sessions_desc: "Sessions from all engines (apx · claude · codex), newest first.",
|
|
764
|
+
sessions_desc_scoped: "Sessions in this project's folder ({path}), all engines, newest first.",
|
|
759
765
|
sessions_all: "All engines",
|
|
760
766
|
sessions_empty: "No sessions.",
|
|
761
767
|
sessions_error: "Could not read sessions: {msg}",
|
|
@@ -709,6 +709,8 @@ export const es = {
|
|
|
709
709
|
save_fields_success: "Overrides guardados.",
|
|
710
710
|
save_meta_success: "Project metadata guardado.",
|
|
711
711
|
no_data: "Sin datos.",
|
|
712
|
+
tab_settings: "Settings",
|
|
713
|
+
tab_project: "Project",
|
|
712
714
|
},
|
|
713
715
|
|
|
714
716
|
telegram: {
|
|
@@ -729,6 +731,9 @@ export const es = {
|
|
|
729
731
|
},
|
|
730
732
|
|
|
731
733
|
memories: {
|
|
734
|
+
sidebar_title: "Memorias",
|
|
735
|
+
general_group: "General",
|
|
736
|
+
general_item: "Memoria del proyecto",
|
|
732
737
|
project_title: "Memoria del proyecto",
|
|
733
738
|
project_desc: "Hechos durables a nivel proyecto. .apc/memory.md — la leen los agentes y el super-agente.",
|
|
734
739
|
project_ph: "# Memoria del proyecto\n\nHechos estables que cualquier agente debería saber…",
|
|
@@ -754,6 +759,7 @@ export const es = {
|
|
|
754
759
|
workspaces_empty: "Sin proyectos. Agregá uno con el botón de arriba.",
|
|
755
760
|
sessions_title: "Sessions",
|
|
756
761
|
sessions_desc: "Sesiones de todos los engines (apx · claude · codex), más nuevas primero.",
|
|
762
|
+
sessions_desc_scoped: "Sesiones en la carpeta de este proyecto ({path}), todos los engines, más nuevas primero.",
|
|
757
763
|
sessions_all: "Todos los engines",
|
|
758
764
|
sessions_empty: "Sin sesiones.",
|
|
759
765
|
sessions_error: "No pude leer las sesiones: {msg}",
|
|
@@ -19,11 +19,12 @@ export const Sessions = {
|
|
|
19
19
|
.then((b) => ({ sessions: unwrapPage<SessionRow>(b).items })),
|
|
20
20
|
// Server-paginated page. Optional `q` runs the same search core as
|
|
21
21
|
// `apx session find` (title; + transcript content when `deep`).
|
|
22
|
-
page: ({ engine, q, deep, limit, offset }: { engine?: string; q?: string; deep?: boolean; limit: number; offset: number }) => {
|
|
22
|
+
page: ({ engine, q, deep, cwd, limit, offset }: { engine?: string; q?: string; deep?: boolean; cwd?: string; limit: number; offset: number }) => {
|
|
23
23
|
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
|
|
24
24
|
if (engine) params.set("engine", engine);
|
|
25
25
|
if (q?.trim()) params.set("q", q.trim());
|
|
26
26
|
if (deep) params.set("deep", "1");
|
|
27
|
+
if (cwd?.trim()) params.set("cwd", cwd.trim());
|
|
27
28
|
return http.get<unknown>(`/sessions?${params.toString()}`).then((b) => unwrapPage<SessionRow>(b));
|
|
28
29
|
},
|
|
29
30
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
-
Bot, Heart, Zap, Puzzle,
|
|
5
|
-
MessagesSquare,
|
|
4
|
+
Bot, Heart, Zap, Puzzle, Settings,
|
|
5
|
+
MessagesSquare, KeyRound,
|
|
6
6
|
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2, Cable,
|
|
7
7
|
Building2, FileText, FolderTree, Sparkles,
|
|
8
8
|
} from "lucide-react";
|
|
@@ -40,7 +40,7 @@ import { SkillsTab } from "./project/SkillsTab";
|
|
|
40
40
|
type NavKey =
|
|
41
41
|
| "" | "chat" | "config" | "telegram"
|
|
42
42
|
| "agents" | "routines" | "tasks" | "mcps" | "integrations" | "vars" | "logs" | "memories" | "artifacts"
|
|
43
|
-
| "structure" | "docs" | "files" | "skills";
|
|
43
|
+
| "structure" | "docs" | "files" | "skills" | "sessions";
|
|
44
44
|
|
|
45
45
|
export function ProjectScreen() {
|
|
46
46
|
const navigate = useNavigate();
|
|
@@ -51,85 +51,75 @@ export function ProjectScreen() {
|
|
|
51
51
|
|
|
52
52
|
const isBase = String(pid) === "0";
|
|
53
53
|
const sections: TabSection[] = useMemo(() => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
title: t("base.nav_system"),
|
|
77
|
-
items: [
|
|
78
|
-
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
79
|
-
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
80
|
-
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
81
|
-
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
82
|
-
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
83
|
-
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
84
|
-
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
85
|
-
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
86
|
-
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
// Structure (org roles/areas) is only meaningful for company/enterprise
|
|
92
|
-
// projects — gate it on the project kind.
|
|
93
|
-
const isCompany = project?.kind === "company";
|
|
94
|
-
return [
|
|
54
|
+
// One shared taxonomy for both Base and projects, in the same order, so the
|
|
55
|
+
// two menus mirror each other. Base additionally gets a "General" admin
|
|
56
|
+
// section (workspaces / engines / agent defaults) and drops "Content"
|
|
57
|
+
// (no docs/files surface). "Workspace" and "Automation" are identical on
|
|
58
|
+
// both. Structure (org roles/areas) only makes sense for company projects.
|
|
59
|
+
const isCompany = !isBase && project?.kind === "company";
|
|
60
|
+
|
|
61
|
+
const out: (TabSection | null)[] = [
|
|
62
|
+
// General — Base-only daemon admin.
|
|
63
|
+
isBase ? {
|
|
64
|
+
title: t("base.nav_general"),
|
|
65
|
+
items: [
|
|
66
|
+
{ key: "workspaces", label: t("base.workspaces_title"), icon: Boxes },
|
|
67
|
+
{ key: "models", label: t("settings.tabs.engines"), icon: Cpu },
|
|
68
|
+
{ key: "agent-defaults", label: t("base.defaults_title"), icon: Bot },
|
|
69
|
+
],
|
|
70
|
+
} : null,
|
|
71
|
+
// Workspace — the overview plus the team's own building blocks
|
|
72
|
+
// (agents / memories / skills / artifacts). "Overview" on both sides.
|
|
95
73
|
{
|
|
96
74
|
title: t("project.sections.workspace"),
|
|
97
75
|
items: [
|
|
98
|
-
{ key: "",
|
|
99
|
-
{ key: "telegram", label: t("project.nav.telegram"), icon: Send },
|
|
100
|
-
{ key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
|
|
101
|
-
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
76
|
+
{ key: "", label: t("project.nav.overview"), icon: LayoutDashboard },
|
|
102
77
|
...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
|
|
103
|
-
{ key: "
|
|
104
|
-
{ key: "
|
|
78
|
+
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
79
|
+
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
80
|
+
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
81
|
+
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
105
82
|
],
|
|
106
83
|
},
|
|
84
|
+
// Activity — chat / sessions / logs.
|
|
107
85
|
{
|
|
86
|
+
title: t("base.nav_activity"),
|
|
87
|
+
items: [
|
|
88
|
+
{ key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
|
|
89
|
+
{ key: "sessions", label: t("base.sessions_title"), icon: History },
|
|
90
|
+
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
// Content — docs / files. Project-only (Base has no such surface).
|
|
94
|
+
!isBase ? {
|
|
108
95
|
title: t("project.sections.content"),
|
|
109
96
|
items: [
|
|
110
97
|
{ key: "docs", label: t("project.nav.docs"), icon: FileText },
|
|
111
98
|
{ key: "files", label: t("project.nav.files"), icon: FolderTree },
|
|
112
99
|
],
|
|
113
|
-
},
|
|
100
|
+
} : null,
|
|
101
|
+
// Automation — routines / tasks / mcps / integrations / vars.
|
|
102
|
+
// Identical on both sides.
|
|
114
103
|
{
|
|
115
104
|
title: t("project.sections.automation"),
|
|
116
105
|
items: [
|
|
117
|
-
{ key: "routines",
|
|
118
|
-
{ key: "tasks",
|
|
119
|
-
{ key: "mcps",
|
|
120
|
-
{ key: "integrations", label: "Integrations",
|
|
121
|
-
{ key: "vars",
|
|
122
|
-
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
123
|
-
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
106
|
+
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
107
|
+
{ key: "tasks", label: t("project.nav.tasks"), icon: Zap },
|
|
108
|
+
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
109
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
110
|
+
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
124
111
|
],
|
|
125
112
|
},
|
|
113
|
+
// Config — the general project/daemon config.
|
|
126
114
|
{
|
|
127
115
|
title: t("project.sections.config"),
|
|
128
116
|
items: [
|
|
129
|
-
{ key: "config",
|
|
117
|
+
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
130
118
|
],
|
|
131
119
|
},
|
|
132
120
|
];
|
|
121
|
+
|
|
122
|
+
return out.filter(Boolean) as TabSection[];
|
|
133
123
|
}, [isBase, project?.kind]);
|
|
134
124
|
|
|
135
125
|
// First path segment after /p/:pid — so deep routes like agents/:slug still
|
|
@@ -171,7 +161,7 @@ export function ProjectScreen() {
|
|
|
171
161
|
<Route path="workspaces" element={<WorkspacesTab />} />
|
|
172
162
|
<Route path="models" element={<ModelsTab />} />
|
|
173
163
|
<Route path="agent-defaults" element={<AgentDefaultsTab />} />
|
|
174
|
-
<Route path="sessions" element={<SessionsTab />} />
|
|
164
|
+
<Route path="sessions" element={<SessionsTab pid={pid} />} />
|
|
175
165
|
<Route path="logs" element={<LogsTab pid={pid} />} />
|
|
176
166
|
<Route path="config" element={<ConfigTab pid={pid} />} />
|
|
177
167
|
<Route path="telegram" element={<TelegramTab pid={pid} />} />
|
|
@@ -7,15 +7,21 @@ import { Badge, Button, Empty, Input, Loading, Tip } from "../../components/ui";
|
|
|
7
7
|
import { UiSelect } from "../../components/UiSelect";
|
|
8
8
|
import { useToast } from "../../components/Toast";
|
|
9
9
|
import { usePersonaName } from "../../hooks/usePersonaName";
|
|
10
|
+
import { useProject } from "../../hooks/useProjects";
|
|
10
11
|
import { t } from "../../i18n";
|
|
11
12
|
|
|
12
13
|
const ENGINE_TONE: Record<string, "success" | "info" | "warning" | "muted"> = {
|
|
13
14
|
apx: "success", claude: "info", codex: "warning",
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// `pid` present + not base → scope to that project's local folder. Base (or no
|
|
18
|
+
// pid) shows every session across engines and folders.
|
|
19
|
+
export function SessionsTab({ pid }: { pid?: string } = {}) {
|
|
17
20
|
const toast = useToast();
|
|
18
21
|
const persona = usePersonaName();
|
|
22
|
+
const isBase = !pid || String(pid) === "0";
|
|
23
|
+
const { project } = useProject(isBase ? "" : pid);
|
|
24
|
+
const cwd = isBase ? undefined : project?.path || undefined;
|
|
19
25
|
const [engine, setEngine] = useState("");
|
|
20
26
|
const [input, setInput] = useState("");
|
|
21
27
|
const [query, setQuery] = useState("");
|
|
@@ -28,10 +34,10 @@ export function SessionsTab() {
|
|
|
28
34
|
}, [input]);
|
|
29
35
|
|
|
30
36
|
const paged = usePagedQuery<SessionRow>({
|
|
31
|
-
key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}`,
|
|
37
|
+
key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}&cwd=${cwd || ""}`,
|
|
32
38
|
fetchPage: (limit, offset) =>
|
|
33
|
-
Sessions.page({ engine: engine || undefined, q: query || undefined, deep, limit, offset }),
|
|
34
|
-
resetKey: `${engine}|${query}|${deep ? 1 : 0}`,
|
|
39
|
+
Sessions.page({ engine: engine || undefined, q: query || undefined, deep, cwd, limit, offset }),
|
|
40
|
+
resetKey: `${engine}|${query}|${deep ? 1 : 0}|${cwd || ""}`,
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
const clear = () => { setInput(""); setQuery(""); setEngine(""); setDeep(false); };
|
|
@@ -72,7 +78,7 @@ export function SessionsTab() {
|
|
|
72
78
|
<Section
|
|
73
79
|
fullHeight
|
|
74
80
|
title={t("base.sessions_title")}
|
|
75
|
-
description={t("base.sessions_desc")}
|
|
81
|
+
description={isBase ? t("base.sessions_desc") : t("base.sessions_desc_scoped", { path: cwd || "…" })}
|
|
76
82
|
action={
|
|
77
83
|
<Tip content={t("base.sessions_refresh")}>
|
|
78
84
|
<Button size="sm" variant="secondary" onClick={() => paged.mutate()}><RefreshCw size={13} /></Button>
|