@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,11 +1,21 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { t } from "../../i18n";
|
|
3
3
|
import { Empty } from "../ui";
|
|
4
|
+
import { Tip } from "../ui/tip";
|
|
4
5
|
import { computeMetrics, computeBreakdown } from "../../lib/code-context";
|
|
5
6
|
import type { CodeTurn } from "../../lib/api/code";
|
|
6
7
|
|
|
8
|
+
interface SessionInfo {
|
|
9
|
+
title: string;
|
|
10
|
+
mode: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
agentSlug: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface Props {
|
|
8
17
|
turns: CodeTurn[];
|
|
18
|
+
session?: SessionInfo | null;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
const SEG_COLOR: Record<string, string> = {
|
|
@@ -14,18 +24,25 @@ const SEG_COLOR: Record<string, string> = {
|
|
|
14
24
|
tool: "bg-amber-500",
|
|
15
25
|
};
|
|
16
26
|
|
|
17
|
-
function
|
|
27
|
+
function Row({ label, value }: { label: string; value: string | number }) {
|
|
18
28
|
return (
|
|
19
|
-
<div className="
|
|
20
|
-
<
|
|
21
|
-
<
|
|
29
|
+
<div className="flex items-baseline justify-between gap-2 py-0.5">
|
|
30
|
+
<span className="shrink-0 text-[10px] uppercase tracking-wide text-muted-foreground">{label}</span>
|
|
31
|
+
<span className="min-w-0 truncate text-right font-mono text-xs text-foreground">{value}</span>
|
|
22
32
|
</div>
|
|
23
33
|
);
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
function fmtDate(iso: string): string {
|
|
37
|
+
if (!iso) return "";
|
|
38
|
+
const d = new Date(iso);
|
|
39
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
40
|
+
return `${pad(d.getDate())} ${d.toLocaleString("es", { month: "short" })} ${d.getFullYear()}, ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
// Context tab: real token totals from the last assistant turn + a char/4
|
|
27
44
|
// estimate of where the conversation's weight sits.
|
|
28
|
-
export function CodeContextTab({ turns }: Props) {
|
|
45
|
+
export function CodeContextTab({ turns, session }: Props) {
|
|
29
46
|
const m = useMemo(() => computeMetrics(turns), [turns]);
|
|
30
47
|
const breakdown = useMemo(() => computeBreakdown(turns), [turns]);
|
|
31
48
|
|
|
@@ -38,13 +55,21 @@ export function CodeContextTab({ turns }: Props) {
|
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
return (
|
|
41
|
-
<div className="space-y-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
<div className="space-y-1 p-3" data-testid="code-context-tab">
|
|
59
|
+
<Row label={t("code_module.ctx_model")} value={m.model || "auto"} />
|
|
60
|
+
{session?.mode && <Row label="Modo" value={session.mode} />}
|
|
61
|
+
{session?.agentSlug && <Row label="Agente" value={session.agentSlug} />}
|
|
62
|
+
<Row
|
|
63
|
+
label={t("code_module.ctx_messages")}
|
|
64
|
+
value={`${m.userMsgs} usuario · ${m.assistantMsgs} asistente`}
|
|
65
|
+
/>
|
|
66
|
+
<Row label={t("code_module.ctx_input")} value={m.input.toLocaleString()} />
|
|
67
|
+
<Row label={t("code_module.ctx_output")} value={m.output.toLocaleString()} />
|
|
68
|
+
<Row label="Tokens Total" value={(m.input + m.output).toLocaleString()} />
|
|
69
|
+
{session?.createdAt && <Row label="Creado" value={fmtDate(session.createdAt)} />}
|
|
70
|
+
{session?.updatedAt && <Row label="Actividad" value={fmtDate(session.updatedAt)} />}
|
|
71
|
+
|
|
72
|
+
<hr className="border-border my-2" />
|
|
48
73
|
|
|
49
74
|
<div>
|
|
50
75
|
<div className="mb-1 text-[11px] font-semibold text-muted-foreground">
|
|
@@ -54,12 +79,12 @@ export function CodeContextTab({ turns }: Props) {
|
|
|
54
79
|
<>
|
|
55
80
|
<div className="flex h-2.5 w-full overflow-hidden rounded-full bg-muted">
|
|
56
81
|
{breakdown.map((s) => (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
<Tip key={s.key} content={`${s.key}: ${s.tokens} (${s.percent}%)`}>
|
|
83
|
+
<div
|
|
84
|
+
className={SEG_COLOR[s.key]}
|
|
85
|
+
style={{ width: `${s.percent}%` }}
|
|
86
|
+
/>
|
|
87
|
+
</Tip>
|
|
63
88
|
))}
|
|
64
89
|
</div>
|
|
65
90
|
<ul className="mt-2 space-y-1">
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { File, Folder, FolderOpen, ChevronRight, ChevronsUpDown, RefreshCw } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Empty, Spinner } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
6
|
+
import { http } from "../../lib/http";
|
|
7
|
+
|
|
8
|
+
interface FileNode {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string; // relative path from project root
|
|
11
|
+
type: "file" | "dir";
|
|
12
|
+
children?: FileNode[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildTree(paths: string[]): FileNode[] {
|
|
16
|
+
const root: FileNode[] = [];
|
|
17
|
+
for (const p of paths) {
|
|
18
|
+
const parts = p.split("/").filter(Boolean);
|
|
19
|
+
let level = root;
|
|
20
|
+
let cumPath = "";
|
|
21
|
+
for (let i = 0; i < parts.length; i++) {
|
|
22
|
+
cumPath = cumPath ? `${cumPath}/${parts[i]}` : parts[i];
|
|
23
|
+
const isLast = i === parts.length - 1;
|
|
24
|
+
let node = level.find((n) => n.name === parts[i]);
|
|
25
|
+
if (!node) {
|
|
26
|
+
node = { name: parts[i], path: cumPath, type: isLast ? "file" : "dir", children: isLast ? undefined : [] };
|
|
27
|
+
level.push(node);
|
|
28
|
+
}
|
|
29
|
+
if (!isLast) level = node.children!;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Sort: dirs first, then files, both alphabetically
|
|
33
|
+
const sort = (nodes: FileNode[]): FileNode[] => {
|
|
34
|
+
nodes.forEach((n) => { if (n.children) n.children = sort(n.children); });
|
|
35
|
+
return nodes.sort((a, b) => {
|
|
36
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
37
|
+
return a.name.localeCompare(b.name);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
return sort(root);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function TreeNode({
|
|
44
|
+
node,
|
|
45
|
+
depth,
|
|
46
|
+
onOpenFile,
|
|
47
|
+
openDirs,
|
|
48
|
+
toggleDir,
|
|
49
|
+
}: {
|
|
50
|
+
node: FileNode;
|
|
51
|
+
depth: number;
|
|
52
|
+
onOpenFile: (path: string) => void;
|
|
53
|
+
openDirs: Set<string>;
|
|
54
|
+
toggleDir: (path: string) => void;
|
|
55
|
+
}) {
|
|
56
|
+
const isDir = node.type === "dir";
|
|
57
|
+
const open = isDir && openDirs.has(node.path);
|
|
58
|
+
return (
|
|
59
|
+
<li>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => isDir ? toggleDir(node.path) : onOpenFile(node.path)}
|
|
63
|
+
style={{ paddingLeft: `${depth * 12 + 6}px` }}
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex w-full items-center gap-1.5 py-0.5 pr-2 text-left text-[11px] rounded transition-colors",
|
|
66
|
+
"hover:bg-accent/40",
|
|
67
|
+
isDir ? "text-foreground/80" : "text-foreground/70",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{isDir ? (
|
|
71
|
+
<>
|
|
72
|
+
<ChevronRight className={cn("size-3 shrink-0 transition-transform", open && "rotate-90")} />
|
|
73
|
+
{open ? <FolderOpen className="size-3.5 shrink-0 text-amber-400" /> : <Folder className="size-3.5 shrink-0 text-amber-400" />}
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
<span className="size-3 shrink-0" />
|
|
78
|
+
<File className="size-3.5 shrink-0 text-sky-400" />
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
<span className="truncate">{node.name}</span>
|
|
82
|
+
</button>
|
|
83
|
+
{isDir && open && node.children && node.children.length > 0 && (
|
|
84
|
+
<ul>
|
|
85
|
+
{node.children.map((child) => (
|
|
86
|
+
<TreeNode
|
|
87
|
+
key={child.path}
|
|
88
|
+
node={child}
|
|
89
|
+
depth={depth + 1}
|
|
90
|
+
onOpenFile={onOpenFile}
|
|
91
|
+
openDirs={openDirs}
|
|
92
|
+
toggleDir={toggleDir}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
)}
|
|
97
|
+
</li>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function CodeFileTree({
|
|
102
|
+
pid,
|
|
103
|
+
projectPath,
|
|
104
|
+
className,
|
|
105
|
+
onOpenFile,
|
|
106
|
+
}: {
|
|
107
|
+
pid: string;
|
|
108
|
+
projectPath?: string;
|
|
109
|
+
className?: string;
|
|
110
|
+
onOpenFile?: (path: string) => void;
|
|
111
|
+
}) {
|
|
112
|
+
const [files, setFiles] = useState<string[]>([]);
|
|
113
|
+
const [loading, setLoading] = useState(false);
|
|
114
|
+
const [loaded, setLoaded] = useState(false);
|
|
115
|
+
// Open-dir state lifted out of TreeNode so the parent can collapse all at once.
|
|
116
|
+
const [openDirs, setOpenDirs] = useState<Set<string>>(() => new Set());
|
|
117
|
+
|
|
118
|
+
const loadFiles = useCallback(async () => {
|
|
119
|
+
setLoading(true);
|
|
120
|
+
try {
|
|
121
|
+
const r = await http.post<{ ok: boolean; stdout: string; stderr: string }>(
|
|
122
|
+
"/run",
|
|
123
|
+
{
|
|
124
|
+
cmd: "find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.claude/*' | sed 's|^\\./||' | sort | head -500",
|
|
125
|
+
project: pid,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
const paths = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
129
|
+
setFiles(paths);
|
|
130
|
+
setLoaded(true);
|
|
131
|
+
} catch {
|
|
132
|
+
setLoaded(true);
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
}, [pid]);
|
|
137
|
+
|
|
138
|
+
// Load on first render and whenever the project changes. Also reset the
|
|
139
|
+
// expanded set so a fresh project starts fully collapsed.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setOpenDirs(new Set());
|
|
142
|
+
void loadFiles();
|
|
143
|
+
}, [loadFiles]);
|
|
144
|
+
|
|
145
|
+
const toggleDir = useCallback((path: string) => {
|
|
146
|
+
setOpenDirs((prev) => {
|
|
147
|
+
const next = new Set(prev);
|
|
148
|
+
if (next.has(path)) next.delete(path);
|
|
149
|
+
else next.add(path);
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const collapseAll = useCallback(() => {
|
|
155
|
+
setOpenDirs(new Set());
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const tree = buildTree(files);
|
|
159
|
+
const anyOpen = openDirs.size > 0;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className={cn("flex h-full flex-col", className)} data-testid="code-file-tree">
|
|
163
|
+
{/* Header */}
|
|
164
|
+
<div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
|
|
165
|
+
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Archivos</span>
|
|
166
|
+
<div className="flex items-center gap-0.5">
|
|
167
|
+
<Tip content="Colapsar todo">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={collapseAll}
|
|
171
|
+
disabled={!anyOpen}
|
|
172
|
+
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
|
|
173
|
+
>
|
|
174
|
+
<ChevronsUpDown className="size-3" />
|
|
175
|
+
</button>
|
|
176
|
+
</Tip>
|
|
177
|
+
<Tip content="Recargar">
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => void loadFiles()}
|
|
181
|
+
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
182
|
+
>
|
|
183
|
+
{loading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
|
|
184
|
+
</button>
|
|
185
|
+
</Tip>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* File tree */}
|
|
190
|
+
<div className="min-h-0 flex-1 overflow-y-auto py-1">
|
|
191
|
+
{!loaded ? (
|
|
192
|
+
<div className="flex justify-center pt-6"><Spinner size={14} /></div>
|
|
193
|
+
) : tree.length === 0 ? (
|
|
194
|
+
<div className="p-3"><Empty>Sin archivos</Empty></div>
|
|
195
|
+
) : (
|
|
196
|
+
<ul>
|
|
197
|
+
{tree.map((node) => (
|
|
198
|
+
<TreeNode
|
|
199
|
+
key={node.path}
|
|
200
|
+
node={node}
|
|
201
|
+
depth={0}
|
|
202
|
+
onOpenFile={onOpenFile ?? (() => {})}
|
|
203
|
+
openDirs={openDirs}
|
|
204
|
+
toggleDir={toggleDir}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Save, RotateCcw } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Spinner } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
6
|
+
|
|
7
|
+
export function CodeFileViewer({
|
|
8
|
+
path,
|
|
9
|
+
content,
|
|
10
|
+
loading,
|
|
11
|
+
onSave,
|
|
12
|
+
}: {
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
/** When provided, the viewer becomes an editor: textarea + Save button. */
|
|
17
|
+
onSave?: (content: string) => Promise<void> | void;
|
|
18
|
+
}) {
|
|
19
|
+
const editable = typeof onSave === "function";
|
|
20
|
+
const [draft, setDraft] = useState(content);
|
|
21
|
+
const [saving, setSaving] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Reset the draft whenever the upstream content changes (e.g. the file
|
|
24
|
+
// finished loading after the tab opened).
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setDraft(content);
|
|
27
|
+
}, [content]);
|
|
28
|
+
|
|
29
|
+
const dirty = editable && draft !== content;
|
|
30
|
+
|
|
31
|
+
const save = async () => {
|
|
32
|
+
if (!onSave || !dirty) return;
|
|
33
|
+
setSaving(true);
|
|
34
|
+
try {
|
|
35
|
+
await onSave(draft);
|
|
36
|
+
} finally {
|
|
37
|
+
setSaving(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex h-full min-h-0 flex-col bg-card/40" data-testid="code-file-viewer">
|
|
43
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
|
|
44
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
45
|
+
{path}
|
|
46
|
+
{dirty && <span className="ml-1 text-amber-400">•</span>}
|
|
47
|
+
</span>
|
|
48
|
+
{editable && (
|
|
49
|
+
<>
|
|
50
|
+
<Tip content="Descartar cambios">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => setDraft(content)}
|
|
54
|
+
disabled={!dirty || saving}
|
|
55
|
+
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40"
|
|
56
|
+
>
|
|
57
|
+
<RotateCcw className="size-3" />
|
|
58
|
+
Descartar
|
|
59
|
+
</button>
|
|
60
|
+
</Tip>
|
|
61
|
+
<Tip content="Guardar (Cmd/Ctrl+S)">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => void save()}
|
|
65
|
+
disabled={!dirty || saving}
|
|
66
|
+
className={cn(
|
|
67
|
+
"inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium transition-colors",
|
|
68
|
+
dirty && !saving
|
|
69
|
+
? "bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 dark:text-emerald-300"
|
|
70
|
+
: "bg-muted text-muted-foreground",
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{saving ? <Spinner size={10} /> : <Save className="size-3" />}
|
|
74
|
+
Guardar
|
|
75
|
+
</button>
|
|
76
|
+
</Tip>
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{loading ? (
|
|
82
|
+
<div className="flex flex-1 items-center justify-center">
|
|
83
|
+
<Spinner size={16} />
|
|
84
|
+
</div>
|
|
85
|
+
) : editable ? (
|
|
86
|
+
<textarea
|
|
87
|
+
value={draft}
|
|
88
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
89
|
+
onKeyDown={(e) => {
|
|
90
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
void save();
|
|
93
|
+
}
|
|
94
|
+
}}
|
|
95
|
+
className="min-h-0 flex-1 resize-none bg-transparent p-3 font-mono text-[12px] leading-[1.6] text-foreground/90 outline-none"
|
|
96
|
+
spellCheck={false}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
100
|
+
<table className="w-full border-collapse font-mono text-[12px] leading-[1.6]">
|
|
101
|
+
<tbody>
|
|
102
|
+
{content.split("\n").map((line, i) => (
|
|
103
|
+
<tr key={i} className="hover:bg-accent/20">
|
|
104
|
+
<td
|
|
105
|
+
className="w-12 select-none border-r border-border/30 px-3 py-0 text-right align-top text-[10px] text-muted-foreground/40"
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
>
|
|
108
|
+
{i + 1}
|
|
109
|
+
</td>
|
|
110
|
+
<td className="px-4 py-0 align-top text-foreground/90 whitespace-pre">
|
|
111
|
+
{line || " "}
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
))}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Plus, MessageSquare, Trash2, Pencil } from "lucide-react";
|
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { t } from "../../i18n";
|
|
4
4
|
import { Empty } from "../ui";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
5
6
|
import type { CodeSessionRow } from "../../lib/api/code";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -30,16 +31,17 @@ export function CodeSessionList({
|
|
|
30
31
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
31
32
|
{t("code_module.sessions")}
|
|
32
33
|
</span>
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
<Tip content={t("code_module.new_session")}>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={onCreate}
|
|
38
|
+
disabled={busy}
|
|
39
|
+
data-testid="code-new-session"
|
|
40
|
+
className="flex items-center gap-1 rounded-md border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-50"
|
|
41
|
+
>
|
|
42
|
+
<Plus className="size-3" /> {t("code_module.new_session")}
|
|
43
|
+
</button>
|
|
44
|
+
</Tip>
|
|
43
45
|
</div>
|
|
44
46
|
|
|
45
47
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
|
@@ -70,22 +72,24 @@ export function CodeSessionList({
|
|
|
70
72
|
</span>
|
|
71
73
|
</button>
|
|
72
74
|
<div className="absolute right-1 top-1 hidden items-center gap-0.5 group-hover/item:flex">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
<Tip content={t("code_module.rename")}>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => onRename(s.id, s.title)}
|
|
79
|
+
className="rounded p-1 text-muted-foreground hover:bg-background hover:text-foreground"
|
|
80
|
+
>
|
|
81
|
+
<Pencil className="size-3" />
|
|
82
|
+
</button>
|
|
83
|
+
</Tip>
|
|
84
|
+
<Tip content={t("code_module.delete")}>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => onDelete(s.id)}
|
|
88
|
+
className="rounded p-1 text-muted-foreground hover:bg-background hover:text-rose-500"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="size-3" />
|
|
91
|
+
</button>
|
|
92
|
+
</Tip>
|
|
89
93
|
</div>
|
|
90
94
|
</li>
|
|
91
95
|
))}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { Gauge, GitCompare, Package } from "lucide-react";
|
|
3
3
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
4
|
+
import { Tip } from "../ui/tip";
|
|
4
5
|
import { t } from "../../i18n";
|
|
5
6
|
import { CodeContextTab } from "./CodeContextTab";
|
|
6
7
|
import { CodeChangesTab } from "./CodeChangesTab";
|
|
@@ -13,6 +14,9 @@ interface Props {
|
|
|
13
14
|
changes: CodeChanges | undefined;
|
|
14
15
|
changesLoading: boolean;
|
|
15
16
|
onRefreshChanges: () => void;
|
|
17
|
+
session?: { title: string; mode: string; createdAt: string; updatedAt: string; agentSlug: string | null } | null;
|
|
18
|
+
onRunInTerminal?: (cmd: string) => void;
|
|
19
|
+
onEditArtifact?: (name: string) => void;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
const TABS = [
|
|
@@ -21,7 +25,7 @@ const TABS = [
|
|
|
21
25
|
{ value: "artifacts", icon: Package, label: "tab_artifacts" },
|
|
22
26
|
] as const;
|
|
23
27
|
|
|
24
|
-
export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshChanges }: Props) {
|
|
28
|
+
export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshChanges, session, onRunInTerminal, onEditArtifact }: Props) {
|
|
25
29
|
const [active, setActive] = useState<string>("context");
|
|
26
30
|
const changeCount = changes?.files.length || 0;
|
|
27
31
|
|
|
@@ -33,34 +37,34 @@ export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshCh
|
|
|
33
37
|
const isActive = active === value;
|
|
34
38
|
const fullLabel = t(`code_module.${label}` as never);
|
|
35
39
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</
|
|
40
|
+
<Tip key={value} content={fullLabel}>
|
|
41
|
+
<TabsTrigger
|
|
42
|
+
value={value}
|
|
43
|
+
className={isActive ? "flex-1 min-w-0" : "w-8 shrink-0"}
|
|
44
|
+
>
|
|
45
|
+
<Icon className="size-3.5 shrink-0" />
|
|
46
|
+
{isActive && (
|
|
47
|
+
<span className="truncate text-xs">{fullLabel}</span>
|
|
48
|
+
)}
|
|
49
|
+
{value === "changes" && changeCount > 0 && (
|
|
50
|
+
<span className="ml-0.5 rounded-full bg-muted px-1 text-[10px] text-muted-foreground leading-none py-0.5">
|
|
51
|
+
{changeCount}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</TabsTrigger>
|
|
55
|
+
</Tip>
|
|
52
56
|
);
|
|
53
57
|
})}
|
|
54
58
|
</TabsList>
|
|
55
59
|
</div>
|
|
56
60
|
<TabsContent value="context" className="min-h-0 flex-1 overflow-y-auto">
|
|
57
|
-
<CodeContextTab turns={turns} />
|
|
61
|
+
<CodeContextTab turns={turns} session={session} />
|
|
58
62
|
</TabsContent>
|
|
59
63
|
<TabsContent value="changes" className="min-h-0 flex-1 overflow-hidden">
|
|
60
64
|
<CodeChangesTab changes={changes} loading={changesLoading} onRefresh={onRefreshChanges} />
|
|
61
65
|
</TabsContent>
|
|
62
66
|
<TabsContent value="artifacts" className="min-h-0 flex-1 overflow-hidden">
|
|
63
|
-
<CodeArtifactsTab pid={pid} />
|
|
67
|
+
<CodeArtifactsTab pid={pid} onRunInTerminal={onRunInTerminal} onEditArtifact={onEditArtifact} />
|
|
64
68
|
</TabsContent>
|
|
65
69
|
</Tabs>
|
|
66
70
|
);
|