@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
|
@@ -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-DPAuXATr.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cl0WXtxF.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -2162,9 +2162,9 @@
|
|
|
2162
2162
|
}
|
|
2163
2163
|
},
|
|
2164
2164
|
"node_modules/baseline-browser-mapping": {
|
|
2165
|
-
"version": "2.10.
|
|
2166
|
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.
|
|
2167
|
-
"integrity": "sha512-
|
|
2165
|
+
"version": "2.10.42",
|
|
2166
|
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
|
|
2167
|
+
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
|
|
2168
2168
|
"dev": true,
|
|
2169
2169
|
"license": "Apache-2.0",
|
|
2170
2170
|
"bin": {
|
|
@@ -2580,9 +2580,9 @@
|
|
|
2580
2580
|
"license": "MIT"
|
|
2581
2581
|
},
|
|
2582
2582
|
"node_modules/electron-to-chromium": {
|
|
2583
|
-
"version": "1.5.
|
|
2584
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
2585
|
-
"integrity": "sha512-
|
|
2583
|
+
"version": "1.5.387",
|
|
2584
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz",
|
|
2585
|
+
"integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==",
|
|
2586
2586
|
"dev": true,
|
|
2587
2587
|
"license": "ISC"
|
|
2588
2588
|
},
|
|
@@ -84,7 +84,7 @@ function Shell() {
|
|
|
84
84
|
onOpenRoby={() => setRobyOpen(true)}
|
|
85
85
|
onOpenAddProject={openAdd}
|
|
86
86
|
/>
|
|
87
|
-
<main className="m-2
|
|
87
|
+
<main className="m-2 flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm">
|
|
88
88
|
<TopBar onToggleTheme={toggle} isDark={theme === "dark"} pathname={location.pathname} />
|
|
89
89
|
<div className="flex-1 overflow-y-auto">
|
|
90
90
|
<Routes>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Field, Input } from "../ui";
|
|
6
|
+
import { UiSelect } from "../UiSelect";
|
|
7
|
+
import { Org } from "../../lib/api/organization";
|
|
8
|
+
import { t } from "../../i18n";
|
|
9
|
+
import type { AgentAutonomy } from "../../types/daemon";
|
|
10
|
+
import { AreaDialog, RoleDialog } from "../structure/StructureDialogs";
|
|
11
|
+
|
|
12
|
+
// ── Emoji ────────────────────────────────────────────────────────────────────
|
|
13
|
+
export function EmojiInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
14
|
+
return (
|
|
15
|
+
<Input
|
|
16
|
+
value={value}
|
|
17
|
+
onChange={(e) => onChange([...e.target.value].slice(-2).join(""))}
|
|
18
|
+
className="text-center text-lg"
|
|
19
|
+
placeholder="🤖"
|
|
20
|
+
aria-label={t("agents_form.emoji")}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Autonomy (permission mode) segmented control ─────────────────────────────
|
|
26
|
+
const AUTONOMY_OPTIONS: { value: AgentAutonomy; labelKey: "auto_total" | "auto_automatico" | "auto_permiso" }[] = [
|
|
27
|
+
{ value: "total", labelKey: "auto_total" },
|
|
28
|
+
{ value: "automatico", labelKey: "auto_automatico" },
|
|
29
|
+
{ value: "permiso", labelKey: "auto_permiso" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function AutonomyPicker({ value, onChange }: { value: string; onChange: (v: AgentAutonomy) => void }) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="inline-flex w-full rounded-lg border border-border p-0.5">
|
|
35
|
+
{AUTONOMY_OPTIONS.map((opt) => (
|
|
36
|
+
<button
|
|
37
|
+
key={opt.value}
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => onChange(opt.value)}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex-1 rounded-md px-2 py-1 text-[12px] font-medium capitalize transition-colors",
|
|
42
|
+
value === opt.value ? "bg-primary/15 text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{t(`agents_form.${opt.labelKey}`)}
|
|
46
|
+
</button>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Area + Role selects (fed by the project org structure) ──────────────────
|
|
53
|
+
// Quick-create area/role via floating dialogs (Panda's "org quick actions"),
|
|
54
|
+
// keeping the picker simple. On create we refetch and auto-select the new item.
|
|
55
|
+
export function AreaRoleFields({
|
|
56
|
+
pid, area, role, onArea, onRole,
|
|
57
|
+
}: {
|
|
58
|
+
pid: string;
|
|
59
|
+
area: string;
|
|
60
|
+
role: string;
|
|
61
|
+
onArea: (v: string) => void;
|
|
62
|
+
onRole: (v: string) => void;
|
|
63
|
+
}) {
|
|
64
|
+
const org = useSWR(`/projects/${pid}/organization`, () => Org.get(pid));
|
|
65
|
+
const [areaDialog, setAreaDialog] = useState(false);
|
|
66
|
+
const [roleDialog, setRoleDialog] = useState(false);
|
|
67
|
+
|
|
68
|
+
const areas = org.data?.areas ?? [];
|
|
69
|
+
const roles = org.data?.roles ?? [];
|
|
70
|
+
// Roles offered: those in the selected area, plus general (no-area) roles.
|
|
71
|
+
const roleChoices = roles.filter((r) => (area ? r.area === area || r.area === null : true));
|
|
72
|
+
|
|
73
|
+
const areaOptions = [{ value: "", label: t("structure.no_area") }, ...areas.map((a) => ({ value: a.slug, label: a.name }))];
|
|
74
|
+
const roleOptions = [{ value: "", label: t("agents_form.no_role") }, ...roleChoices.map((r) => ({ value: r.slug, label: r.name }))];
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="space-y-3">
|
|
78
|
+
<div className="grid grid-cols-2 gap-3">
|
|
79
|
+
<Field label={t("agents_form.area")}>
|
|
80
|
+
<div className="flex items-center gap-1">
|
|
81
|
+
<UiSelect value={area} onChange={(v) => { onArea(v); }} options={areaOptions} placeholder={t("structure.no_area")} className="flex-1" />
|
|
82
|
+
<QuickAdd label={t("structure.new_area")} onClick={() => setAreaDialog(true)} />
|
|
83
|
+
</div>
|
|
84
|
+
</Field>
|
|
85
|
+
<Field label={t("agents_form.role")}>
|
|
86
|
+
<div className="flex items-center gap-1">
|
|
87
|
+
<UiSelect value={role} onChange={onRole} options={roleOptions} placeholder={t("agents_form.no_role")} className="flex-1" />
|
|
88
|
+
<QuickAdd label={t("structure.new_role")} onClick={() => setRoleDialog(true)} />
|
|
89
|
+
</div>
|
|
90
|
+
</Field>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<AreaDialog
|
|
94
|
+
open={areaDialog}
|
|
95
|
+
onClose={() => setAreaDialog(false)}
|
|
96
|
+
pid={pid}
|
|
97
|
+
onSaved={() => void org.mutate()}
|
|
98
|
+
/>
|
|
99
|
+
<RoleDialog
|
|
100
|
+
open={roleDialog}
|
|
101
|
+
onClose={() => setRoleDialog(false)}
|
|
102
|
+
pid={pid}
|
|
103
|
+
areas={areas}
|
|
104
|
+
presetArea={area || null}
|
|
105
|
+
onSaved={() => void org.mutate()}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function QuickAdd({ label, onClick }: { label: string; onClick: () => void }) {
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onClick}
|
|
116
|
+
title={label}
|
|
117
|
+
aria-label={label}
|
|
118
|
+
className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
119
|
+
>
|
|
120
|
+
<Plus className="size-4" />
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
Monitor,
|
|
13
13
|
Plus,
|
|
14
14
|
Send,
|
|
15
|
-
Sparkles,
|
|
16
15
|
Timer,
|
|
17
16
|
User,
|
|
18
17
|
} from "lucide-react";
|
|
@@ -23,14 +22,15 @@ import { t } from "../../i18n";
|
|
|
23
22
|
import type { AgentEntry, ConversationListEntry, ThreadListEntry } from "../../types/daemon";
|
|
24
23
|
|
|
25
24
|
// Channel taxonomy — same channels the daemon writes ("web", "voice",
|
|
26
|
-
// "desktop", "telegram", …) folded into
|
|
27
|
-
// icon + a fixed display order so the sidebar is stable across reloads.
|
|
25
|
+
// "desktop", "telegram", …) folded into sidebar groups. Each group has an
|
|
26
|
+
// icon + a fixed display order so the sidebar is stable across reloads. Web is
|
|
27
|
+
// pinned first (it's where chats started from this UI land), then the remote
|
|
28
|
+
// channels, with the catch-all "other" last.
|
|
28
29
|
export type ChannelGroupKey =
|
|
29
|
-
| "
|
|
30
|
+
| "web"
|
|
30
31
|
| "telegram"
|
|
31
|
-
| "voice"
|
|
32
32
|
| "desktop"
|
|
33
|
-
| "
|
|
33
|
+
| "voice"
|
|
34
34
|
| "a2a"
|
|
35
35
|
| "schedule"
|
|
36
36
|
| "other";
|
|
@@ -42,14 +42,13 @@ interface GroupMeta {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const GROUP_META: Record<ChannelGroupKey, GroupMeta> = {
|
|
45
|
-
|
|
45
|
+
web: { label: "Web", icon: MessageSquare, order: 0 },
|
|
46
46
|
telegram: { label: "Telegram", icon: Send, order: 1 },
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
other: { label: "Other", icon: FolderOpen, order: 7 },
|
|
47
|
+
desktop: { label: "Desktop", icon: Monitor, order: 2 },
|
|
48
|
+
voice: { label: "Voice", icon: Mic, order: 3 },
|
|
49
|
+
a2a: { label: "Agent ↔ Agent", icon: Bot, order: 4 },
|
|
50
|
+
schedule: { label: "Schedule", icon: Timer, order: 5 },
|
|
51
|
+
other: { label: "Other", icon: FolderOpen, order: 6 },
|
|
53
52
|
};
|
|
54
53
|
|
|
55
54
|
function channelGroup(channel?: string): ChannelGroupKey {
|
|
@@ -78,6 +77,14 @@ export function chatKeyToString(k: ChatKey): string {
|
|
|
78
77
|
return `thread:${k.channel}:${k.threadId}`;
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
/** Display metadata carried alongside a selection so the right-pane header can
|
|
81
|
+
* show "Created {date} · {channel} · {agent}" without a second fetch. */
|
|
82
|
+
export interface ChatSelectionMeta {
|
|
83
|
+
channel?: string;
|
|
84
|
+
createdAt?: string;
|
|
85
|
+
title?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
interface Props {
|
|
82
89
|
pid: string;
|
|
83
90
|
agents: AgentEntry[];
|
|
@@ -85,8 +92,10 @@ interface Props {
|
|
|
85
92
|
superAgentSlug: string;
|
|
86
93
|
superAgentLabel: string;
|
|
87
94
|
selected: ChatKey;
|
|
88
|
-
onSelect: (key: ChatKey) => void;
|
|
89
|
-
|
|
95
|
+
onSelect: (key: ChatKey, meta?: ChatSelectionMeta) => void;
|
|
96
|
+
/** Start a fresh in-memory session with the chosen agent (super-agent or a
|
|
97
|
+
* project agent). It materialises in the Web group on the first message. */
|
|
98
|
+
onNewChat: (agentSlug: string) => void;
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
// Per-agent SWR fetcher. Lives in a child so adding/removing agents doesn't
|
|
@@ -125,6 +134,7 @@ export function ChatList({
|
|
|
125
134
|
const [agentFilter, setAgentFilter] = useState("");
|
|
126
135
|
const [collapsed, setCollapsed] = useState<Partial<Record<ChannelGroupKey, boolean>>>({});
|
|
127
136
|
const [byAgent, setByAgent] = useState<Record<string, ConversationListEntry[]>>({});
|
|
137
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
128
138
|
|
|
129
139
|
// Super-agent channel threads (telegram, web quick-chat, desktop …) come from
|
|
130
140
|
// the global message ledger, which is daemon-level and NOT project-scoped.
|
|
@@ -208,20 +218,16 @@ export function ChatList({
|
|
|
208
218
|
.sort((a, b) => GROUP_META[a.key].order - GROUP_META[b.key].order);
|
|
209
219
|
}, [filteredConvs, filteredThreads]);
|
|
210
220
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
out.push({ slug: a.slug, label: a.slug });
|
|
222
|
-
}
|
|
223
|
-
return out;
|
|
224
|
-
}, [agents, agentFilter, superAgentSlug, superAgentLabel]);
|
|
221
|
+
// Agents offered by the "+ New" picker: the super-agent (always available,
|
|
222
|
+
// listed first) followed by this project's own agents. Not affected by the
|
|
223
|
+
// sidebar filter — you can always start a fresh chat with any of them.
|
|
224
|
+
const newChatAgents = useMemo(
|
|
225
|
+
() => [
|
|
226
|
+
{ slug: superAgentSlug, label: superAgentLabel },
|
|
227
|
+
...agents.map((a) => ({ slug: a.slug, label: a.slug })),
|
|
228
|
+
],
|
|
229
|
+
[agents, superAgentSlug, superAgentLabel],
|
|
230
|
+
);
|
|
225
231
|
|
|
226
232
|
const agentOptions = useMemo(
|
|
227
233
|
() => [
|
|
@@ -232,7 +238,7 @@ export function ChatList({
|
|
|
232
238
|
[agents, superAgentSlug, superAgentLabel],
|
|
233
239
|
);
|
|
234
240
|
|
|
235
|
-
const totalCount = allConvs.length + (threadsQ.data?.length || 0)
|
|
241
|
+
const totalCount = allConvs.length + (threadsQ.data?.length || 0);
|
|
236
242
|
const anyLoaded =
|
|
237
243
|
Object.keys(byAgent).length > 0 || agents.length === 0 || !!threadsQ.data;
|
|
238
244
|
|
|
@@ -251,13 +257,48 @@ export function ChatList({
|
|
|
251
257
|
{t("project.chat.list.count", { n: totalCount })}
|
|
252
258
|
</p>
|
|
253
259
|
</div>
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
<div className="relative">
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={() => setPickerOpen((o) => !o)}
|
|
264
|
+
className="inline-flex items-center gap-1 rounded-md border border-border bg-accent/60 px-2 py-1 text-[11px] font-medium hover:bg-accent"
|
|
265
|
+
>
|
|
266
|
+
<Plus className="size-3" /> {t("project.chat.list.new")}
|
|
267
|
+
</button>
|
|
268
|
+
{pickerOpen && (
|
|
269
|
+
<>
|
|
270
|
+
{/* Click-away layer. */}
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
aria-hidden
|
|
274
|
+
tabIndex={-1}
|
|
275
|
+
className="fixed inset-0 z-10 cursor-default"
|
|
276
|
+
onClick={() => setPickerOpen(false)}
|
|
277
|
+
/>
|
|
278
|
+
<div className="absolute right-0 top-full z-20 mt-1 w-56 rounded-md border border-border bg-card p-1 shadow-lg">
|
|
279
|
+
<p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
|
|
280
|
+
{t("project.chat.list.pick_agent")}
|
|
281
|
+
</p>
|
|
282
|
+
<div className="max-h-64 overflow-y-auto">
|
|
283
|
+
{newChatAgents.map((a) => (
|
|
284
|
+
<button
|
|
285
|
+
key={`new-${a.slug}`}
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={() => {
|
|
288
|
+
setPickerOpen(false);
|
|
289
|
+
onNewChat(a.slug);
|
|
290
|
+
}}
|
|
291
|
+
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-accent/50"
|
|
292
|
+
>
|
|
293
|
+
<Bot className="size-3 shrink-0 text-muted-fg" />
|
|
294
|
+
<span className="truncate">{a.label}</span>
|
|
295
|
+
</button>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
261
302
|
</header>
|
|
262
303
|
|
|
263
304
|
<div className="space-y-2 border-b border-border p-2">
|
|
@@ -276,32 +317,6 @@ export function ChatList({
|
|
|
276
317
|
</div>
|
|
277
318
|
)}
|
|
278
319
|
|
|
279
|
-
{/* Live group — synthetic, always first. */}
|
|
280
|
-
{liveAgents.length > 0 && (
|
|
281
|
-
<ChannelGroup
|
|
282
|
-
keyName="live"
|
|
283
|
-
count={liveAgents.length}
|
|
284
|
-
collapsed={!!collapsed.live}
|
|
285
|
-
onToggle={() => setCollapsed((p) => ({ ...p, live: !p.live }))}
|
|
286
|
-
>
|
|
287
|
-
{liveAgents.map((a) => {
|
|
288
|
-
const isRoby = a.slug === superAgentSlug;
|
|
289
|
-
const active =
|
|
290
|
-
selected.kind === "live" && selected.agentSlug === a.slug;
|
|
291
|
-
return (
|
|
292
|
-
<ChatListItem
|
|
293
|
-
key={`live-${a.slug}`}
|
|
294
|
-
title={isRoby ? a.label : t("project.chat.list.live_with", { slug: a.slug })}
|
|
295
|
-
subtitle={t("project.chat.list.live_subtitle")}
|
|
296
|
-
badge={isRoby ? "super" : a.slug}
|
|
297
|
-
selected={active}
|
|
298
|
-
onClick={() => onSelect({ kind: "live", agentSlug: a.slug })}
|
|
299
|
-
/>
|
|
300
|
-
);
|
|
301
|
-
})}
|
|
302
|
-
</ChannelGroup>
|
|
303
|
-
)}
|
|
304
|
-
|
|
305
320
|
{/* Stored conversations + super-agent channel threads, grouped by channel. */}
|
|
306
321
|
{groups.map((g) => (
|
|
307
322
|
<ChannelGroup
|
|
@@ -327,7 +342,10 @@ export function ChatList({
|
|
|
327
342
|
timeAgo={th.last_ts}
|
|
328
343
|
selected={active}
|
|
329
344
|
onClick={() =>
|
|
330
|
-
onSelect(
|
|
345
|
+
onSelect(
|
|
346
|
+
{ kind: "thread", channel: th.channel, threadId: th.id },
|
|
347
|
+
{ channel: th.channel, createdAt: th.started_at, title: th.title },
|
|
348
|
+
)
|
|
331
349
|
}
|
|
332
350
|
/>
|
|
333
351
|
);
|
|
@@ -348,7 +366,10 @@ export function ChatList({
|
|
|
348
366
|
timeAgo={c.started_at}
|
|
349
367
|
selected={active}
|
|
350
368
|
onClick={() =>
|
|
351
|
-
onSelect(
|
|
369
|
+
onSelect(
|
|
370
|
+
{ kind: "conv", agentSlug: c.agent_slug, convId: c.id },
|
|
371
|
+
{ channel: c.channel, createdAt: c.started_at, title: c.title },
|
|
372
|
+
)
|
|
352
373
|
}
|
|
353
374
|
/>
|
|
354
375
|
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Dialog, Button } from "../ui";
|
|
3
|
+
import { t } from "../../i18n";
|
|
4
|
+
|
|
5
|
+
// Reusable confirm dialog for destructive/executing actions (project rule:
|
|
6
|
+
// never native confirm()). Shows a loading state while the action runs.
|
|
7
|
+
export function ConfirmDialog({
|
|
8
|
+
open,
|
|
9
|
+
onClose,
|
|
10
|
+
onConfirm,
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
confirmLabel,
|
|
14
|
+
destructive = true,
|
|
15
|
+
}: {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
onConfirm: () => Promise<void> | void;
|
|
19
|
+
title: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
confirmLabel?: string;
|
|
22
|
+
destructive?: boolean;
|
|
23
|
+
}) {
|
|
24
|
+
const [busy, setBusy] = useState(false);
|
|
25
|
+
const run = async () => {
|
|
26
|
+
setBusy(true);
|
|
27
|
+
try {
|
|
28
|
+
await onConfirm();
|
|
29
|
+
onClose();
|
|
30
|
+
} finally {
|
|
31
|
+
setBusy(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return (
|
|
35
|
+
<Dialog
|
|
36
|
+
open={open}
|
|
37
|
+
onClose={onClose}
|
|
38
|
+
title={title}
|
|
39
|
+
footer={
|
|
40
|
+
<>
|
|
41
|
+
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
42
|
+
<Button variant={destructive ? "destructive" : "primary"} onClick={() => void run()} loading={busy}>
|
|
43
|
+
{confirmLabel ?? t("common.confirm")}
|
|
44
|
+
</Button>
|
|
45
|
+
</>
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
49
|
+
</Dialog>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -65,7 +65,7 @@ export function TabNav({ sections, active, onChange, collapsed = false }: TabNav
|
|
|
65
65
|
<nav
|
|
66
66
|
className={cn(
|
|
67
67
|
"hidden md:flex shrink-0 flex-col gap-1 py-3 transition-all",
|
|
68
|
-
collapsed ? "w-12 items-center px-1" : "w-
|
|
68
|
+
collapsed ? "w-12 items-center px-1" : "w-44 px-2",
|
|
69
69
|
)}
|
|
70
70
|
>
|
|
71
71
|
{sections.map((section, si) => (
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { RefreshCw, FilePlus2, FolderOpen } from "lucide-react";
|
|
4
|
+
import { ProjectFiles, type FileScope } from "../../lib/api/projectFiles";
|
|
5
|
+
import type { FileNode, FileContent } from "../../types/daemon";
|
|
6
|
+
import { Spinner, Button, Empty } from "../ui";
|
|
7
|
+
import { useToast } from "../Toast";
|
|
8
|
+
import { t } from "../../i18n";
|
|
9
|
+
import { FileTree } from "./FileTree";
|
|
10
|
+
import { FileViewer } from "./FileViewer";
|
|
11
|
+
import { NewFileDialog } from "./NewFileDialog";
|
|
12
|
+
|
|
13
|
+
// Shared file browser used by both /files (scope=project) and /docs
|
|
14
|
+
// (scope=docs). One component, two roots — the docs surface is just the same
|
|
15
|
+
// browser with `editable` on and a "new document" affordance.
|
|
16
|
+
export function FileBrowser({
|
|
17
|
+
pid,
|
|
18
|
+
scope,
|
|
19
|
+
editable = false,
|
|
20
|
+
emptyHint,
|
|
21
|
+
}: {
|
|
22
|
+
pid: string;
|
|
23
|
+
scope: FileScope;
|
|
24
|
+
/** Allow editing text/markdown + creating/deleting files (docs surface). */
|
|
25
|
+
editable?: boolean;
|
|
26
|
+
emptyHint?: string;
|
|
27
|
+
}) {
|
|
28
|
+
const toast = useToast();
|
|
29
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
30
|
+
const [newOpen, setNewOpen] = useState(false);
|
|
31
|
+
|
|
32
|
+
const treeKey = `/projects/${pid}/fs/tree?scope=${scope}`;
|
|
33
|
+
const tree = useSWR(treeKey, () => ProjectFiles.tree(pid, scope));
|
|
34
|
+
|
|
35
|
+
const fileKey = selected ? `/projects/${pid}/fs/file?scope=${scope}&path=${selected}` : null;
|
|
36
|
+
const file = useSWR<FileContent | null>(fileKey, () => (selected ? ProjectFiles.read(pid, selected, scope) : null));
|
|
37
|
+
|
|
38
|
+
const onSelect = (node: FileNode) => setSelected(node.path);
|
|
39
|
+
|
|
40
|
+
const onSave = editable
|
|
41
|
+
? async (content: string) => {
|
|
42
|
+
if (!selected) return;
|
|
43
|
+
await ProjectFiles.write(pid, selected, content, scope);
|
|
44
|
+
toast.success(t("files.saved"));
|
|
45
|
+
void file.mutate();
|
|
46
|
+
}
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const onDelete = editable
|
|
50
|
+
? async (node: FileNode) => {
|
|
51
|
+
await ProjectFiles.remove(pid, node.path, scope);
|
|
52
|
+
if (selected === node.path) setSelected(null);
|
|
53
|
+
toast.success(t("files.deleted"));
|
|
54
|
+
void tree.mutate();
|
|
55
|
+
}
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
const onCreated = (path: string) => {
|
|
59
|
+
setNewOpen(false);
|
|
60
|
+
setSelected(path);
|
|
61
|
+
void tree.mutate();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const nodes = tree.data?.tree ?? [];
|
|
65
|
+
const isEmpty = !tree.isLoading && nodes.length === 0;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex h-full min-h-0 overflow-hidden rounded-xl border border-border bg-card">
|
|
69
|
+
{/* Sidebar: tree */}
|
|
70
|
+
<div className="flex w-64 shrink-0 flex-col border-r border-border">
|
|
71
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
|
72
|
+
<FolderOpen className="size-4 text-muted-foreground" />
|
|
73
|
+
<span className="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
74
|
+
{scope === "docs" ? t("files.docs_label") : t("files.files_label")}
|
|
75
|
+
</span>
|
|
76
|
+
{editable && (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
data-testid="docs-new"
|
|
80
|
+
onClick={() => setNewOpen(true)}
|
|
81
|
+
className="text-muted-foreground hover:text-foreground"
|
|
82
|
+
aria-label={t("files.new_doc")}
|
|
83
|
+
title={t("files.new_doc")}
|
|
84
|
+
>
|
|
85
|
+
<FilePlus2 className="size-4" />
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => void tree.mutate()}
|
|
91
|
+
className="text-muted-foreground hover:text-foreground"
|
|
92
|
+
aria-label={t("common.refresh")}
|
|
93
|
+
>
|
|
94
|
+
<RefreshCw className={tree.isValidating ? "size-3.5 animate-spin" : "size-3.5"} />
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
|
98
|
+
{tree.isLoading ? (
|
|
99
|
+
<div className="flex justify-center py-6"><Spinner size={14} /></div>
|
|
100
|
+
) : isEmpty ? (
|
|
101
|
+
<div className="p-3">
|
|
102
|
+
<Empty>
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<p>{emptyHint ?? t("files.empty")}</p>
|
|
105
|
+
{editable && (
|
|
106
|
+
<Button size="sm" variant="secondary" onClick={() => setNewOpen(true)}>
|
|
107
|
+
<FilePlus2 className="size-3.5" />{t("files.new_doc")}
|
|
108
|
+
</Button>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</Empty>
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<FileTree nodes={nodes} selectedPath={selected} onSelect={onSelect} onDelete={onDelete} />
|
|
115
|
+
)}
|
|
116
|
+
{tree.data?.truncated && (
|
|
117
|
+
<p className="px-2 py-1 text-[10px] text-muted-foreground/60">{t("files.truncated")}</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Main: viewer */}
|
|
123
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
124
|
+
<FileViewer file={file.data ?? null} loading={!!fileKey && file.isLoading} onSave={onSave} />
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{editable && (
|
|
128
|
+
<NewFileDialog
|
|
129
|
+
open={newOpen}
|
|
130
|
+
onClose={() => setNewOpen(false)}
|
|
131
|
+
pid={pid}
|
|
132
|
+
scope={scope}
|
|
133
|
+
onCreated={onCreated}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|