@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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/prompts/action-discipline.md +12 -5
  3. package/src/core/agent/prompts/channels/telegram.md +9 -5
  4. package/src/core/stores/code-sessions.js +4 -1
  5. package/src/host/daemon/api/artifacts.js +25 -0
  6. package/src/host/daemon/api/code.js +14 -1
  7. package/src/host/daemon/api/exec.js +17 -2
  8. package/src/host/daemon/plugins/telegram/index.js +2 -14
  9. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +1 -0
  10. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +602 -0
  11. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +1 -0
  12. package/src/interfaces/web/dist/index.html +2 -2
  13. package/src/interfaces/web/package-lock.json +3 -3
  14. package/src/interfaces/web/src/App.tsx +3 -1
  15. package/src/interfaces/web/src/components/UiSelect.tsx +12 -2
  16. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +253 -111
  17. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +10 -8
  18. package/src/interfaces/web/src/components/code/CodeComposer.tsx +20 -17
  19. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +43 -18
  20. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +212 -0
  21. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +121 -0
  22. package/src/interfaces/web/src/components/code/CodeSessionList.tsx +30 -26
  23. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +23 -19
  24. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +140 -0
  25. package/src/interfaces/web/src/components/common/TabLayout.tsx +3 -3
  26. package/src/interfaces/web/src/components/ui/chat-input.tsx +17 -6
  27. package/src/interfaces/web/src/hooks/useChat.ts +1 -0
  28. package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +25 -1
  29. package/src/interfaces/web/src/i18n/es.ts +1 -1
  30. package/src/interfaces/web/src/lib/api/agents.ts +1 -1
  31. package/src/interfaces/web/src/lib/api/artifacts.ts +10 -0
  32. package/src/interfaces/web/src/lib/api/code.ts +4 -2
  33. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +423 -79
  34. package/src/interfaces/web/src/screens/project/ChatTab.tsx +7 -10
  35. package/src/core/util/text-similarity.js +0 -52
  36. package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +0 -1
  37. package/src/interfaces/web/dist/assets/index-BkybwwRn.js +0 -570
  38. 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 Stat({ label, value }: { label: string; value: string | number }) {
27
+ function Row({ label, value }: { label: string; value: string | number }) {
18
28
  return (
19
- <div className="rounded-md border border-border bg-background/50 px-2.5 py-2">
20
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</div>
21
- <div className="mt-0.5 truncate font-mono text-sm">{value}</div>
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-3 p-3" data-testid="code-context-tab">
42
- <div className="grid grid-cols-2 gap-2">
43
- <Stat label={t("code_module.ctx_model")} value={m.model || "auto"} />
44
- <Stat label={t("code_module.ctx_messages")} value={`${m.userMsgs}/${m.assistantMsgs}`} />
45
- <Stat label={t("code_module.ctx_input")} value={m.input.toLocaleString()} />
46
- <Stat label={t("code_module.ctx_output")} value={m.output.toLocaleString()} />
47
- </div>
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
- <div
58
- key={s.key}
59
- className={SEG_COLOR[s.key]}
60
- style={{ width: `${s.percent}%` }}
61
- title={`${s.key}: ${s.tokens} (${s.percent}%)`}
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
- <button
34
- type="button"
35
- onClick={onCreate}
36
- disabled={busy}
37
- title={t("code_module.new_session")}
38
- data-testid="code-new-session"
39
- 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"
40
- >
41
- <Plus className="size-3" /> {t("code_module.new_session")}
42
- </button>
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
- <button
74
- type="button"
75
- onClick={() => onRename(s.id, s.title)}
76
- title={t("code_module.rename")}
77
- className="rounded p-1 text-muted-foreground hover:bg-background hover:text-foreground"
78
- >
79
- <Pencil className="size-3" />
80
- </button>
81
- <button
82
- type="button"
83
- onClick={() => onDelete(s.id)}
84
- title={t("code_module.delete")}
85
- className="rounded p-1 text-muted-foreground hover:bg-background hover:text-rose-500"
86
- >
87
- <Trash2 className="size-3" />
88
- </button>
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
- <TabsTrigger
37
- key={value}
38
- value={value}
39
- title={fullLabel}
40
- className={isActive ? "flex-1 min-w-0" : "w-8 shrink-0"}
41
- >
42
- <Icon className="size-3.5 shrink-0" />
43
- {isActive && (
44
- <span className="truncate text-xs">{fullLabel}</span>
45
- )}
46
- {value === "changes" && changeCount > 0 && (
47
- <span className="ml-0.5 rounded-full bg-muted px-1 text-[10px] text-muted-foreground leading-none py-0.5">
48
- {changeCount}
49
- </span>
50
- )}
51
- </TabsTrigger>
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
  );