@agentprojectcontext/apx 1.32.2 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/agent/prompts/action-discipline.md +12 -5
- package/src/core/agent/prompts/channels/telegram.md +9 -5
- package/src/core/stores/code-sessions.js +4 -1
- package/src/host/daemon/api/artifacts.js +25 -0
- package/src/host/daemon/api/code.js +14 -1
- package/src/host/daemon/api/exec.js +17 -2
- package/src/host/daemon/plugins/telegram/index.js +2 -14
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +1 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +602 -0
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.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/App.tsx +3 -1
- package/src/interfaces/web/src/components/UiSelect.tsx +12 -2
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +253 -111
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +10 -8
- package/src/interfaces/web/src/components/code/CodeComposer.tsx +20 -17
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +43 -18
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +212 -0
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +121 -0
- package/src/interfaces/web/src/components/code/CodeSessionList.tsx +30 -26
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +23 -19
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +140 -0
- package/src/interfaces/web/src/components/common/TabLayout.tsx +3 -3
- package/src/interfaces/web/src/components/ui/chat-input.tsx +17 -6
- package/src/interfaces/web/src/hooks/useChat.ts +1 -0
- package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +25 -1
- package/src/interfaces/web/src/i18n/es.ts +1 -1
- package/src/interfaces/web/src/lib/api/agents.ts +1 -1
- package/src/interfaces/web/src/lib/api/artifacts.ts +10 -0
- package/src/interfaces/web/src/lib/api/code.ts +4 -2
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +423 -79
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +7 -10
- package/src/core/util/text-similarity.js +0 -52
- package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +0 -1
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js +0 -570
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +0 -1
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import useSWR from "swr";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Bot, FolderTree, MessageSquare, PanelLeft, PanelRight, Terminal, X } from "lucide-react";
|
|
4
|
+
import { Group as PanelGroup, Panel, Separator as PanelResizeHandle } from "react-resizable-panels";
|
|
5
|
+
import { Code, Projects, Agents } from "../../lib/api";
|
|
6
|
+
import { Artifacts } from "../../lib/api/artifacts";
|
|
7
|
+
import { http } from "../../lib/http";
|
|
8
|
+
import { Empty, Loading } from "../../components/ui";
|
|
9
|
+
import { Tip } from "../../components/ui/tip";
|
|
10
|
+
import { UiSelect } from "../../components/UiSelect";
|
|
11
|
+
import { useSetPageLabel, useSetPageActions } from "../../hooks/useNavCollapseCtx";
|
|
6
12
|
import { MessageList } from "../../components/chat/MessageList";
|
|
7
13
|
import { CodeProjectPicker } from "../../components/code/CodeProjectPicker";
|
|
8
14
|
import { CodeSessionList } from "../../components/code/CodeSessionList";
|
|
9
15
|
import { CodeComposer } from "../../components/code/CodeComposer";
|
|
10
16
|
import { CodeSidePanel } from "../../components/code/CodeSidePanel";
|
|
17
|
+
import { CodeFileTree } from "../../components/code/CodeFileTree";
|
|
18
|
+
import { CodeFileViewer } from "../../components/code/CodeFileViewer";
|
|
19
|
+
import { CodeTerminal } from "../../components/code/CodeTerminal";
|
|
11
20
|
import { InlineAskPanel, pendingAskQuestions } from "../../components/chat/InlineAskPanel";
|
|
12
21
|
import { useToast } from "../../components/Toast";
|
|
13
22
|
import { t } from "../../i18n";
|
|
14
23
|
import { applyStreamEvent, textOf, type ChatMsg } from "../../hooks/useChat";
|
|
15
24
|
import type { CodeMode, CodeStreamEvent, CodeTurn } from "../../lib/api/code";
|
|
16
25
|
|
|
26
|
+
// Suppress unused import warning for textOf (kept for consumers)
|
|
27
|
+
void textOf;
|
|
28
|
+
|
|
29
|
+
const SUPER_AGENT_VALUE = "super-agent";
|
|
30
|
+
|
|
31
|
+
// Hit area is wider than the visible line so the handle is comfortable to
|
|
32
|
+
// grab — the inner ::before line is what the user sees.
|
|
33
|
+
function ResizeHandle() {
|
|
34
|
+
return (
|
|
35
|
+
<PanelResizeHandle className="relative z-10 w-px shrink-0 cursor-col-resize bg-border transition-colors hover:bg-primary/50 active:bg-primary/70" />
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ResizeHandleH() {
|
|
40
|
+
return (
|
|
41
|
+
<PanelResizeHandle className="relative z-10 h-px shrink-0 cursor-row-resize bg-border transition-colors hover:bg-primary/50 active:bg-primary/70" />
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
// Code module — OpenCode-style coding sessions in the APX web admin. Each
|
|
18
46
|
// project owns a list of persistent sessions; the daemon keeps the transcript
|
|
19
47
|
// server-side (api/code.js), so the UI just streams turns and renders them with
|
|
@@ -26,11 +54,34 @@ export function CodeScreen() {
|
|
|
26
54
|
|
|
27
55
|
const [pid, setPid] = useState<string>("");
|
|
28
56
|
const [sid, setSid] = useState<string | null>(null);
|
|
57
|
+
const [agentSlug, setAgentSlug] = useState<string>(SUPER_AGENT_VALUE);
|
|
29
58
|
const [msgs, setMsgs] = useState<ChatMsg[]>([]);
|
|
30
59
|
const [draft, setDraft] = useState("");
|
|
31
60
|
const [busy, setBusy] = useState(false);
|
|
61
|
+
const [leftOpen, setLeftOpen] = useState(true);
|
|
62
|
+
const [rightOpen, setRightOpen] = useState(true);
|
|
63
|
+
const [termOpen, setTermOpen] = useState(false);
|
|
64
|
+
const [termInitCmd, setTermInitCmd] = useState("");
|
|
65
|
+
const [worktreeOpen, setWorktreeOpen] = useState(false);
|
|
32
66
|
const abortRef = useRef<AbortController | null>(null);
|
|
33
67
|
|
|
68
|
+
// Open file tabs. `artifactName` marks an artifact opened for editing;
|
|
69
|
+
// saves route through Artifacts.write instead of being read-only.
|
|
70
|
+
type OpenFile = {
|
|
71
|
+
path: string;
|
|
72
|
+
content: string;
|
|
73
|
+
loading?: boolean;
|
|
74
|
+
artifactName?: string;
|
|
75
|
+
};
|
|
76
|
+
const [openFiles, setOpenFiles] = useState<OpenFile[]>([]);
|
|
77
|
+
// "chat" is the permanent tab, otherwise a file path
|
|
78
|
+
const [activeTab, setActiveTab] = useState<string>("chat");
|
|
79
|
+
|
|
80
|
+
const runInTerminal = useCallback((cmd: string) => {
|
|
81
|
+
setTermOpen(true);
|
|
82
|
+
setTermInitCmd(cmd);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
34
85
|
// Default to the first registered project once the list loads.
|
|
35
86
|
useEffect(() => {
|
|
36
87
|
if (!pid && projectList.length) setPid(String(projectList[0].id));
|
|
@@ -41,6 +92,9 @@ export function CodeScreen() {
|
|
|
41
92
|
Code.sessions.list(pid),
|
|
42
93
|
);
|
|
43
94
|
|
|
95
|
+
// Agents for the active project.
|
|
96
|
+
const agentsData = useSWR(pid ? ["agents", pid] : null, () => Agents.list(pid));
|
|
97
|
+
|
|
44
98
|
// Full transcript of the active session.
|
|
45
99
|
const session = useSWR(pid && sid ? ["code-session", pid, sid] : null, () =>
|
|
46
100
|
Code.sessions.get(pid, sid!),
|
|
@@ -58,6 +112,11 @@ export function CodeScreen() {
|
|
|
58
112
|
if (sid && list.length && !list.some((s) => s.id === sid)) setSid(list[0]?.id ?? null);
|
|
59
113
|
}, [sessions.data, sid]);
|
|
60
114
|
|
|
115
|
+
// Sync agentSlug from the active session when it loads.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (session.data) setAgentSlug(session.data.agentSlug || SUPER_AGENT_VALUE);
|
|
118
|
+
}, [session.data]);
|
|
119
|
+
|
|
61
120
|
// Hydrate the message list whenever the active session's transcript loads.
|
|
62
121
|
// (Not while streaming — we own the array then.)
|
|
63
122
|
useEffect(() => {
|
|
@@ -89,7 +148,10 @@ export function CodeScreen() {
|
|
|
89
148
|
const onCreateSession = async () => {
|
|
90
149
|
if (!pid || busy) return;
|
|
91
150
|
try {
|
|
92
|
-
const created = await Code.sessions.create(pid, {
|
|
151
|
+
const created = await Code.sessions.create(pid, {
|
|
152
|
+
title: t("code_module.untitled"),
|
|
153
|
+
agentSlug: agentSlug !== SUPER_AGENT_VALUE ? agentSlug : null,
|
|
154
|
+
});
|
|
93
155
|
await sessions.mutate();
|
|
94
156
|
setSid(created.id);
|
|
95
157
|
setMsgs([]);
|
|
@@ -126,6 +188,19 @@ export function CodeScreen() {
|
|
|
126
188
|
};
|
|
127
189
|
|
|
128
190
|
// Persist mode / model changes to the session (PATCH) + keep SWR in sync.
|
|
191
|
+
const onAgentChange = async (slug: string) => {
|
|
192
|
+
setAgentSlug(slug);
|
|
193
|
+
if (!sid) return;
|
|
194
|
+
try {
|
|
195
|
+
await Code.sessions.update(pid, sid, {
|
|
196
|
+
agentSlug: slug !== SUPER_AGENT_VALUE ? slug : null,
|
|
197
|
+
});
|
|
198
|
+
await Promise.all([session.mutate(), sessions.mutate()]);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
toast.error((e as Error).message);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
129
204
|
const patchSession = useCallback(
|
|
130
205
|
async (patch: { mode?: CodeMode; model?: string | null }) => {
|
|
131
206
|
if (!sid) return;
|
|
@@ -207,12 +282,106 @@ export function CodeScreen() {
|
|
|
207
282
|
}
|
|
208
283
|
};
|
|
209
284
|
|
|
285
|
+
const openFile = useCallback(
|
|
286
|
+
(path: string) => {
|
|
287
|
+
setActiveTab(path);
|
|
288
|
+
setOpenFiles((prev) => {
|
|
289
|
+
if (prev.some((f) => f.path === path)) return prev; // already open
|
|
290
|
+
return [...prev, { path, content: "", loading: true }];
|
|
291
|
+
});
|
|
292
|
+
// Fetch content async
|
|
293
|
+
http
|
|
294
|
+
.post<{ ok: boolean; stdout: string; stderr: string }>("/run", {
|
|
295
|
+
cmd: `cat "${path}"`,
|
|
296
|
+
project: pid,
|
|
297
|
+
})
|
|
298
|
+
.then((r) => {
|
|
299
|
+
const content = r.stdout || r.stderr || "(vacío)";
|
|
300
|
+
setOpenFiles((prev) =>
|
|
301
|
+
prev.map((f) => (f.path === path ? { ...f, content, loading: false } : f)),
|
|
302
|
+
);
|
|
303
|
+
})
|
|
304
|
+
.catch((e: Error) => {
|
|
305
|
+
setOpenFiles((prev) =>
|
|
306
|
+
prev.map((f) =>
|
|
307
|
+
f.path === path ? { ...f, content: `Error: ${e.message}`, loading: false } : f,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
[pid],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const closeFile = useCallback((path: string) => {
|
|
316
|
+
setOpenFiles((prev) => prev.filter((f) => f.path !== path));
|
|
317
|
+
setActiveTab((prev) => (prev === path ? "chat" : prev));
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
// Open an artifact as an EDITABLE tab. Reuses the file-tab UI but routes
|
|
321
|
+
// saves through Artifacts.write so the daemon persists the change.
|
|
322
|
+
const openArtifact = useCallback(
|
|
323
|
+
(name: string) => {
|
|
324
|
+
const tabPath = `artifacts/${name}`;
|
|
325
|
+
setActiveTab(tabPath);
|
|
326
|
+
setOpenFiles((prev) => {
|
|
327
|
+
if (prev.some((f) => f.path === tabPath)) return prev;
|
|
328
|
+
return [...prev, { path: tabPath, content: "", loading: true, artifactName: name }];
|
|
329
|
+
});
|
|
330
|
+
Artifacts.read(pid, name)
|
|
331
|
+
.then((r) => {
|
|
332
|
+
setOpenFiles((prev) =>
|
|
333
|
+
prev.map((f) =>
|
|
334
|
+
f.path === tabPath ? { ...f, content: r.content, loading: false } : f,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
})
|
|
338
|
+
.catch((e: Error) => {
|
|
339
|
+
setOpenFiles((prev) =>
|
|
340
|
+
prev.map((f) =>
|
|
341
|
+
f.path === tabPath ? { ...f, content: `Error: ${e.message}`, loading: false } : f,
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
[pid],
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const saveOpenFile = useCallback(
|
|
350
|
+
async (path: string, content: string) => {
|
|
351
|
+
const file = openFiles.find((f) => f.path === path);
|
|
352
|
+
if (!file?.artifactName) return;
|
|
353
|
+
try {
|
|
354
|
+
await Artifacts.write(pid, file.artifactName, content);
|
|
355
|
+
setOpenFiles((prev) =>
|
|
356
|
+
prev.map((f) => (f.path === path ? { ...f, content } : f)),
|
|
357
|
+
);
|
|
358
|
+
toast.info("Guardado.");
|
|
359
|
+
} catch (e) {
|
|
360
|
+
toast.error((e as Error).message);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
[openFiles, pid, toast],
|
|
364
|
+
);
|
|
365
|
+
|
|
210
366
|
const hasProjects = !projects.isLoading && projectList.length > 0;
|
|
367
|
+
|
|
368
|
+
const agentOptions = useMemo(() => {
|
|
369
|
+
const base = [{ value: SUPER_AGENT_VALUE, label: "super-agent", icon: Bot, description: "Agente principal con todas las herramientas" }];
|
|
370
|
+
const project = (agentsData.data || []).map((a) => ({
|
|
371
|
+
value: a.slug,
|
|
372
|
+
label: a.slug,
|
|
373
|
+
icon: Bot,
|
|
374
|
+
description: a.description || a.role || undefined,
|
|
375
|
+
}));
|
|
376
|
+
return [...base, ...project];
|
|
377
|
+
}, [agentsData.data]);
|
|
378
|
+
|
|
211
379
|
const turns: CodeTurn[] = useMemo(() => msgs as unknown as CodeTurn[], [msgs]);
|
|
212
380
|
const activeTitle = useMemo(
|
|
213
381
|
() => sessions.data?.find((s) => s.id === sid)?.title || "",
|
|
214
382
|
[sessions.data, sid],
|
|
215
383
|
);
|
|
384
|
+
const activeProject = useMemo(() => projectList.find((p) => String(p.id) === pid), [projectList, pid]);
|
|
216
385
|
useSetPageLabel(activeTitle);
|
|
217
386
|
|
|
218
387
|
// Detect unanswered ask_questions in the last assistant turn. Local "dismissed"
|
|
@@ -225,8 +394,42 @@ export function CodeScreen() {
|
|
|
225
394
|
void send(compiled);
|
|
226
395
|
};
|
|
227
396
|
|
|
397
|
+
// Stable toggle callbacks
|
|
398
|
+
const toggleLeft = useCallback(() => setLeftOpen((v) => !v), []);
|
|
399
|
+
const toggleTree = useCallback(() => setWorktreeOpen((v) => !v), []);
|
|
400
|
+
const toggleTerm = useCallback(() => setTermOpen((v) => !v), []);
|
|
401
|
+
const toggleRight = useCallback(() => setRightOpen((v) => !v), []);
|
|
402
|
+
|
|
403
|
+
// Inject panel toggle icons into TopBar
|
|
404
|
+
const pageActions = useMemo(
|
|
405
|
+
() =>
|
|
406
|
+
sid ? (
|
|
407
|
+
<div className="flex items-center gap-0.5">
|
|
408
|
+
{[
|
|
409
|
+
{ Icon: PanelLeft, open: leftOpen, toggle: toggleLeft, title: "Lista de sesiones" },
|
|
410
|
+
{ Icon: FolderTree, open: worktreeOpen, toggle: toggleTree, title: "Árbol de archivos" },
|
|
411
|
+
{ Icon: Terminal, open: termOpen, toggle: toggleTerm, title: "Terminal" },
|
|
412
|
+
{ Icon: PanelRight, open: rightOpen, toggle: toggleRight, title: "Panel de contexto" },
|
|
413
|
+
].map(({ Icon, open, toggle, title }) => (
|
|
414
|
+
<Tip key={title} content={title}>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={toggle}
|
|
418
|
+
data-active={open}
|
|
419
|
+
className="rounded p-1 text-muted-fg transition-colors hover:bg-accent hover:text-accent-fg data-[active=true]:bg-accent data-[active=true]:text-accent-fg"
|
|
420
|
+
>
|
|
421
|
+
<Icon className="size-3.5" />
|
|
422
|
+
</button>
|
|
423
|
+
</Tip>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
) : null,
|
|
427
|
+
[sid, leftOpen, worktreeOpen, termOpen, rightOpen, toggleLeft, toggleTree, toggleTerm, toggleRight],
|
|
428
|
+
);
|
|
429
|
+
useSetPageActions(pageActions);
|
|
430
|
+
|
|
228
431
|
return (
|
|
229
|
-
<div className="flex h-full min-h-0" data-testid="screen-code">
|
|
432
|
+
<div className="flex h-full min-h-0 flex-col" data-testid="screen-code">
|
|
230
433
|
{projects.isLoading ? (
|
|
231
434
|
<Loading />
|
|
232
435
|
) : !hasProjects ? (
|
|
@@ -234,82 +437,223 @@ export function CodeScreen() {
|
|
|
234
437
|
<Empty>{t("code_module.no_projects")}</Empty>
|
|
235
438
|
</div>
|
|
236
439
|
) : (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
440
|
+
<PanelGroup
|
|
441
|
+
orientation="vertical"
|
|
442
|
+
id="code-layout-v"
|
|
443
|
+
className="min-h-0 flex-1"
|
|
444
|
+
>
|
|
445
|
+
{/* TOP: horizontal split across [left | tree | main | right] */}
|
|
446
|
+
<Panel id="top" defaultSize={termOpen ? "55%" : "100%"} minSize="20%">
|
|
447
|
+
<PanelGroup orientation="horizontal" id="code-layout" className="h-full">
|
|
448
|
+
{/* Left panel: session list + agent selector */}
|
|
449
|
+
{leftOpen && (
|
|
450
|
+
<>
|
|
451
|
+
<Panel id="left" defaultSize="14%" minSize="8%">
|
|
452
|
+
<aside className="flex h-full flex-col">
|
|
453
|
+
<div className="shrink-0 border-b border-border p-2">
|
|
454
|
+
<CodeProjectPicker
|
|
455
|
+
projects={projectList}
|
|
456
|
+
value={pid}
|
|
457
|
+
onChange={onPickProject}
|
|
458
|
+
disabled={busy}
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
462
|
+
<CodeSessionList
|
|
463
|
+
sessions={sessions.data || []}
|
|
464
|
+
activeId={sid}
|
|
465
|
+
busy={busy}
|
|
466
|
+
onSelect={onSelectSession}
|
|
467
|
+
onCreate={onCreateSession}
|
|
468
|
+
onRename={onRenameSession}
|
|
469
|
+
onDelete={onDeleteSession}
|
|
470
|
+
/>
|
|
471
|
+
</div>
|
|
472
|
+
<div className="shrink-0 border-t border-border p-2">
|
|
473
|
+
<UiSelect
|
|
474
|
+
value={agentSlug}
|
|
475
|
+
onChange={onAgentChange}
|
|
476
|
+
options={agentOptions}
|
|
477
|
+
disabled={busy}
|
|
478
|
+
showIcon={true}
|
|
479
|
+
/>
|
|
480
|
+
</div>
|
|
481
|
+
</aside>
|
|
482
|
+
</Panel>
|
|
483
|
+
<ResizeHandle />
|
|
484
|
+
</>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{/* File tree panel */}
|
|
488
|
+
{worktreeOpen && (
|
|
489
|
+
<>
|
|
490
|
+
<Panel id="tree" defaultSize="13%" minSize="8%">
|
|
491
|
+
<div className="h-full">
|
|
492
|
+
<CodeFileTree pid={pid} projectPath={activeProject?.path} onOpenFile={openFile} />
|
|
493
|
+
</div>
|
|
494
|
+
</Panel>
|
|
495
|
+
<ResizeHandle />
|
|
496
|
+
</>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{/* Main panel: tab bar (only with files) + transcript/file viewer + composer */}
|
|
500
|
+
<Panel id="main" defaultSize="50%" minSize="20%">
|
|
501
|
+
<div className="flex h-full flex-col">
|
|
502
|
+
{/* Tab bar — only when files are open */}
|
|
503
|
+
{openFiles.length > 0 && (
|
|
504
|
+
<div className="flex shrink-0 items-center gap-0 overflow-x-auto border-b border-border">
|
|
505
|
+
{/* Chat tab */}
|
|
506
|
+
<button
|
|
507
|
+
type="button"
|
|
508
|
+
onClick={() => setActiveTab("chat")}
|
|
509
|
+
data-active={activeTab === "chat"}
|
|
510
|
+
className="flex shrink-0 items-center gap-1.5 border-r border-border px-3 py-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent/40 data-[active=true]:text-foreground"
|
|
511
|
+
>
|
|
512
|
+
<MessageSquare className="size-3 shrink-0" />
|
|
513
|
+
Chat
|
|
514
|
+
</button>
|
|
515
|
+
{/* File tabs */}
|
|
516
|
+
{openFiles.map((f) => {
|
|
517
|
+
const name = f.path.split("/").pop() ?? f.path;
|
|
518
|
+
const isActive = activeTab === f.path;
|
|
519
|
+
return (
|
|
520
|
+
<div
|
|
521
|
+
key={f.path}
|
|
522
|
+
data-active={isActive}
|
|
523
|
+
className="group flex shrink-0 items-center gap-1 border-r border-border px-2 py-2 text-[11px] text-muted-foreground transition-colors hover:bg-accent/40 data-[active=true]:text-foreground"
|
|
524
|
+
>
|
|
525
|
+
<Tip content={f.path}>
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
onClick={() => setActiveTab(f.path)}
|
|
529
|
+
className="min-w-0 max-w-[140px] truncate font-mono"
|
|
530
|
+
>
|
|
531
|
+
{name}
|
|
532
|
+
</button>
|
|
533
|
+
</Tip>
|
|
534
|
+
<Tip content="Cerrar">
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
onClick={() => closeFile(f.path)}
|
|
538
|
+
className="shrink-0 rounded p-0.5 opacity-60 hover:bg-accent hover:opacity-100"
|
|
539
|
+
>
|
|
540
|
+
<X className="size-2.5" />
|
|
541
|
+
</button>
|
|
542
|
+
</Tip>
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
})}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
|
|
549
|
+
{/* Tab content */}
|
|
550
|
+
{activeTab === "chat" ? (
|
|
551
|
+
<>
|
|
552
|
+
<div className="min-h-0 flex-1 overflow-y-auto" data-testid="code-transcript">
|
|
553
|
+
{!sid ? (
|
|
554
|
+
<div className="grid h-full place-items-center p-6">
|
|
555
|
+
<Empty>{t("code_module.pick_project")}</Empty>
|
|
556
|
+
</div>
|
|
557
|
+
) : msgs.length ? (
|
|
558
|
+
<MessageList msgs={msgs} onCopy={copyToClipboard} />
|
|
559
|
+
) : (
|
|
560
|
+
<div className="grid h-full place-items-center p-6">
|
|
561
|
+
<Empty>{t("code_module.empty_chat")}</Empty>
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
</div>
|
|
565
|
+
{askVisible && pending && (
|
|
566
|
+
<InlineAskPanel
|
|
567
|
+
turnKey={pending.turnKey}
|
|
568
|
+
questions={pending.questions}
|
|
569
|
+
onSubmit={submitAnswers}
|
|
570
|
+
onDismiss={() => setDismissedKey(pending.turnKey)}
|
|
571
|
+
disabled={busy}
|
|
572
|
+
/>
|
|
573
|
+
)}
|
|
574
|
+
</>
|
|
575
|
+
) : (
|
|
576
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
577
|
+
{(() => {
|
|
578
|
+
const file = openFiles.find((f) => f.path === activeTab);
|
|
579
|
+
if (!file) return null;
|
|
580
|
+
return (
|
|
581
|
+
<CodeFileViewer
|
|
582
|
+
path={file.path}
|
|
583
|
+
content={file.content}
|
|
584
|
+
loading={file.loading}
|
|
585
|
+
onSave={
|
|
586
|
+
file.artifactName
|
|
587
|
+
? (content) => saveOpenFile(file.path, content)
|
|
588
|
+
: undefined
|
|
589
|
+
}
|
|
590
|
+
/>
|
|
591
|
+
);
|
|
592
|
+
})()}
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
|
|
596
|
+
{/* Composer — always visible at the bottom of the main column */}
|
|
597
|
+
<div className="shrink-0 border-t border-border p-2" data-testid="code-input">
|
|
598
|
+
<CodeComposer
|
|
599
|
+
value={draft}
|
|
600
|
+
onValueChange={setDraft}
|
|
601
|
+
onSubmit={() => void send()}
|
|
602
|
+
onStop={stop}
|
|
603
|
+
busy={busy}
|
|
604
|
+
disabled={!sid}
|
|
605
|
+
mode={mode}
|
|
606
|
+
onModeChange={(m) => void patchSession({ mode: m })}
|
|
607
|
+
model={model}
|
|
608
|
+
onModelChange={(m) => void patchSession({ model: m || null })}
|
|
609
|
+
/>
|
|
610
|
+
</div>
|
|
274
611
|
</div>
|
|
612
|
+
</Panel>
|
|
613
|
+
|
|
614
|
+
{/* Right panel: context + changes + artifacts */}
|
|
615
|
+
{rightOpen && (
|
|
616
|
+
<>
|
|
617
|
+
<ResizeHandle />
|
|
618
|
+
<Panel id="right" defaultSize="22%" minSize="15%">
|
|
619
|
+
<aside className="flex h-full flex-col">
|
|
620
|
+
<CodeSidePanel
|
|
621
|
+
pid={pid}
|
|
622
|
+
turns={turns}
|
|
623
|
+
changes={changes.data}
|
|
624
|
+
changesLoading={changes.isLoading}
|
|
625
|
+
onRefreshChanges={() => void changes.mutate()}
|
|
626
|
+
session={
|
|
627
|
+
session.data
|
|
628
|
+
? {
|
|
629
|
+
title: session.data.title,
|
|
630
|
+
mode: session.data.mode,
|
|
631
|
+
createdAt: session.data.createdAt,
|
|
632
|
+
updatedAt: session.data.updatedAt,
|
|
633
|
+
agentSlug: session.data.agentSlug ?? null,
|
|
634
|
+
}
|
|
635
|
+
: null
|
|
636
|
+
}
|
|
637
|
+
onRunInTerminal={runInTerminal}
|
|
638
|
+
onEditArtifact={openArtifact}
|
|
639
|
+
/>
|
|
640
|
+
</aside>
|
|
641
|
+
</Panel>
|
|
642
|
+
</>
|
|
275
643
|
)}
|
|
276
|
-
</
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
onValueChange={setDraft}
|
|
290
|
-
onSubmit={() => void send()}
|
|
291
|
-
onStop={stop}
|
|
292
|
-
busy={busy}
|
|
293
|
-
disabled={!sid}
|
|
294
|
-
mode={mode}
|
|
295
|
-
onModeChange={(m) => void patchSession({ mode: m })}
|
|
296
|
-
model={model}
|
|
297
|
-
onModelChange={(m) => void patchSession({ model: m || null })}
|
|
298
|
-
/>
|
|
299
|
-
</div>
|
|
300
|
-
</main>
|
|
301
|
-
|
|
302
|
-
{/* Right: context + changes */}
|
|
303
|
-
<aside className="hidden w-72 shrink-0 flex-col border-l border-border lg:flex">
|
|
304
|
-
<CodeSidePanel
|
|
305
|
-
pid={pid}
|
|
306
|
-
turns={turns}
|
|
307
|
-
changes={changes.data}
|
|
308
|
-
changesLoading={changes.isLoading}
|
|
309
|
-
onRefreshChanges={() => void changes.mutate()}
|
|
310
|
-
/>
|
|
311
|
-
</aside>
|
|
312
|
-
</>
|
|
644
|
+
</PanelGroup>
|
|
645
|
+
</Panel>
|
|
646
|
+
|
|
647
|
+
{/* BOTTOM: terminal spanning the full width below all columns */}
|
|
648
|
+
{termOpen && pid && (
|
|
649
|
+
<>
|
|
650
|
+
<ResizeHandleH />
|
|
651
|
+
<Panel id="terminal" defaultSize="45%" minSize="10%" maxSize="80%">
|
|
652
|
+
<CodeTerminal pid={pid} initCmd={termInitCmd} onClose={toggleTerm} className="h-full" />
|
|
653
|
+
</Panel>
|
|
654
|
+
</>
|
|
655
|
+
)}
|
|
656
|
+
</PanelGroup>
|
|
313
657
|
)}
|
|
314
658
|
</div>
|
|
315
659
|
);
|
|
@@ -61,11 +61,9 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
61
61
|
const resetConversation = () => clear();
|
|
62
62
|
|
|
63
63
|
const send = async (text: string) => {
|
|
64
|
-
// Roby (super-agent) is always available; a real project agent requires
|
|
65
|
-
// that the project actually has one configured.
|
|
66
64
|
if (!activeIsRoby && !activeAgent) return;
|
|
67
65
|
await sendChat(text, {
|
|
68
|
-
model:
|
|
66
|
+
model: model || undefined,
|
|
69
67
|
agentSlug: activeIsRoby ? undefined : activeAgent!.slug,
|
|
70
68
|
});
|
|
71
69
|
};
|
|
@@ -77,15 +75,12 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
77
75
|
|
|
78
76
|
if (agents.isLoading) return <Loading />;
|
|
79
77
|
|
|
80
|
-
// Header subtitle differs: with the super-agent selected the chat goes
|
|
81
|
-
// through the super-agent loop (it CAN call tools); a project agent is a
|
|
82
|
-
// direct LLM call.
|
|
83
78
|
const headerSubtitle = activeIsRoby
|
|
84
79
|
? t("project.chat.superagent_subtitle", { persona })
|
|
85
80
|
: t("project.chat.subtitle");
|
|
86
81
|
|
|
87
82
|
return (
|
|
88
|
-
<div className="flex h-
|
|
83
|
+
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-card/40">
|
|
89
84
|
<header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
90
85
|
<div className="min-w-0">
|
|
91
86
|
<h2 className="text-sm font-semibold">
|
|
@@ -118,7 +113,9 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
118
113
|
{msgs.length ? (
|
|
119
114
|
<MessageList msgs={msgs} onCopy={copyToClipboard} />
|
|
120
115
|
) : (
|
|
121
|
-
<
|
|
116
|
+
<div className="flex h-full items-center justify-center p-8">
|
|
117
|
+
<p className="text-sm text-muted-fg">{t("project.chat.empty")}</p>
|
|
118
|
+
</div>
|
|
122
119
|
)}
|
|
123
120
|
</div>
|
|
124
121
|
<ContextBar msgs={msgs} />
|
|
@@ -139,8 +136,8 @@ export function ChatTab({ pid }: { pid: string }) {
|
|
|
139
136
|
onSend={send}
|
|
140
137
|
onStop={stop}
|
|
141
138
|
streaming={streaming}
|
|
142
|
-
model={
|
|
143
|
-
onModelChange={
|
|
139
|
+
model={model}
|
|
140
|
+
onModelChange={setModel}
|
|
144
141
|
/>
|
|
145
142
|
<CreateAgentDialog
|
|
146
143
|
open={creating}
|