@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
@@ -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-BkybwwRn.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-34U_Mp1M.css">
21
+ <script type="module" crossorigin src="/assets/index-DWsE_8Nz.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-7dVT2O1S.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -2236,9 +2236,9 @@
2236
2236
  }
2237
2237
  },
2238
2238
  "node_modules/caniuse-lite": {
2239
- "version": "1.0.30001797",
2240
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
2241
- "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
2239
+ "version": "1.0.30001799",
2240
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
2241
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
2242
2242
  "dev": true,
2243
2243
  "funding": [
2244
2244
  {
@@ -17,7 +17,7 @@ import { TooltipProvider } from "./components/ui/tooltip";
17
17
  import { useTheme } from "./hooks/useTheme";
18
18
  import { useProjects } from "./hooks/useProjects";
19
19
  import { useTokenBootstrap } from "./hooks/useTokenBootstrap";
20
- import { NavCollapseProvider, useNavCollapseCtx, usePageLabel } from "./hooks/useNavCollapseCtx";
20
+ import { NavCollapseProvider, useNavCollapseCtx, usePageActions, usePageLabel } from "./hooks/useNavCollapseCtx";
21
21
  import { NavToggle } from "./components/common/TabNav";
22
22
  import { t } from "./i18n";
23
23
 
@@ -131,6 +131,7 @@ function TopBar({
131
131
  : project ? `${projectKindLabel(project.kind)} · ${project.path}` : "")
132
132
  : "";
133
133
  const nav = useNavCollapseCtx();
134
+ const pageActions = usePageActions();
134
135
  return (
135
136
  <header className="flex h-10 shrink-0 items-center gap-2 border-b border-border/50 px-3">
136
137
  {nav && <NavToggle collapsed={nav.collapsed} onToggle={nav.toggle} />}
@@ -138,6 +139,7 @@ function TopBar({
138
139
  {crumb}
139
140
  {subtitle && <span className="text-muted-fg/50"> · {subtitle}</span>}
140
141
  </span>
142
+ {pageActions}
141
143
  <button
142
144
  type="button"
143
145
  onClick={onToggleTheme}
@@ -25,6 +25,7 @@ export function UiSelect({
25
25
  placeholder = "— elegir —",
26
26
  disabled,
27
27
  className,
28
+ showIcon = false,
28
29
  }: {
29
30
  value: string;
30
31
  onChange: (value: string) => void;
@@ -32,13 +33,22 @@ export function UiSelect({
32
33
  placeholder?: string;
33
34
  disabled?: boolean;
34
35
  className?: string;
36
+ showIcon?: boolean;
35
37
  }) {
36
38
  return (
37
39
  <Select value={value} onValueChange={(v) => onChange((v as string) ?? "")} disabled={disabled}>
38
40
  <SelectTrigger className={cn("h-9 w-full", className)}>
39
- {/* Show the option's label in the trigger, not the raw value key. */}
40
41
  <SelectValue placeholder={placeholder}>
41
- {(val) => options.find((o) => o.value === val)?.label ?? (val as string)}
42
+ {(val) => {
43
+ const opt = options.find((o) => o.value === val);
44
+ const Icon = showIcon ? opt?.icon : undefined;
45
+ return (
46
+ <span className="flex min-w-0 items-center gap-1.5">
47
+ {Icon && <Icon className="size-3.5 shrink-0" />}
48
+ <span className="truncate">{opt?.label ?? (val as string)}</span>
49
+ </span>
50
+ );
51
+ }}
42
52
  </SelectValue>
43
53
  </SelectTrigger>
44
54
  {/* side=bottom + alignItemWithTrigger=false → dropdown sits BELOW the
@@ -1,37 +1,63 @@
1
- import { useState } from "react";
1
+ import { useRef, useState } from "react";
2
2
  import useSWR from "swr";
3
- import { ChevronRight, Copy, RefreshCw, Trash2, FileCode2, Play } from "lucide-react";
3
+ import { Copy, RefreshCw, Trash2, FileCode2, Play, Pencil, Eye, SquarePen } from "lucide-react";
4
4
  import { cn } from "../../lib/cn";
5
5
  import { t } from "../../i18n";
6
6
  import { Empty, Spinner } from "../ui";
7
7
  import { Artifacts, type ArtifactEntry, type ArtifactRunResult } from "../../lib/api/artifacts";
8
8
  import { useToast } from "../Toast";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogFooter,
15
+ DialogClose,
16
+ } from "../ui/dialog";
17
+ import { Tip } from "../ui/tip";
9
18
 
10
19
  interface Props {
11
20
  pid: string;
21
+ onRunInTerminal?: (cmd: string) => void;
22
+ onEditArtifact?: (name: string) => void;
12
23
  }
13
24
 
14
25
  function ArtifactRow({
15
26
  pid,
16
27
  entry,
17
28
  onDeleted,
29
+ onRenamed,
30
+ onRunInTerminal,
31
+ onEditArtifact,
18
32
  }: {
19
33
  pid: string;
20
34
  entry: ArtifactEntry;
21
35
  onDeleted: () => void;
36
+ onRenamed: () => void;
37
+ onRunInTerminal?: (cmd: string) => void;
38
+ onEditArtifact?: (name: string) => void;
22
39
  }) {
23
- const [open, setOpen] = useState(false);
24
40
  const [running, setRunning] = useState(false);
25
41
  const [runResult, setRunResult] = useState<ArtifactRunResult | null>(null);
26
42
  const toast = useToast();
27
- const detail = useSWR(open ? ["artifact", pid, entry.name] : null, () =>
28
- Artifacts.read(pid, entry.name),
29
- );
30
43
 
31
- // Daemon-side detection: a file is runnable if it has the exec bit OR
32
- // starts with a shebang. Locally we can only check the shebang from the
33
- // fetched content; if it's missing we still show Run (the daemon will
34
- // 400 cleanly and the toast surfaces the reason).
44
+ // Rename state
45
+ const [renaming, setRenaming] = useState(false);
46
+ const [renameValue, setRenameValue] = useState(entry.name);
47
+ const renameInputRef = useRef<HTMLInputElement>(null);
48
+
49
+ // View dialog state
50
+ const [viewOpen, setViewOpen] = useState(false);
51
+ // Delete confirmation dialog
52
+ const [deleteOpen, setDeleteOpen] = useState(false);
53
+ const [deleting, setDeleting] = useState(false);
54
+
55
+ // Load detail only when the view dialog is open
56
+ const detailKey = viewOpen ? ["artifact", pid, entry.name] : null;
57
+ const detail = useSWR(detailKey, () => Artifacts.read(pid, entry.name), {
58
+ revalidateOnFocus: false,
59
+ });
60
+
35
61
  const looksRunnable = !detail.data?.content || detail.data.content.startsWith("#!");
36
62
 
37
63
  const copy = async (text: string) => {
@@ -59,137 +85,249 @@ function ArtifactRow({
59
85
  };
60
86
 
61
87
  const remove = async () => {
62
- if (!window.confirm(t("code_module.artifacts_delete_confirm"))) return;
88
+ setDeleting(true);
63
89
  try {
64
90
  await Artifacts.remove(pid, entry.name);
91
+ setDeleteOpen(false);
65
92
  onDeleted();
66
93
  } catch (e) {
67
94
  toast.error((e as Error).message);
95
+ } finally {
96
+ setDeleting(false);
97
+ }
98
+ };
99
+
100
+ const startRename = () => {
101
+ setRenameValue(entry.name);
102
+ setRenaming(true);
103
+ // Focus after paint
104
+ requestAnimationFrame(() => renameInputRef.current?.select());
105
+ };
106
+
107
+ const commitRename = async () => {
108
+ const trimmed = renameValue.trim();
109
+ setRenaming(false);
110
+ if (!trimmed || trimmed === entry.name) return;
111
+ try {
112
+ await Artifacts.rename(pid, entry.name, trimmed);
113
+ onRenamed();
114
+ } catch (e) {
115
+ toast.error((e as Error).message);
68
116
  }
69
117
  };
70
118
 
71
119
  return (
72
120
  <li className="rounded-md border border-border">
73
- <button
74
- type="button"
75
- onClick={() => setOpen((v) => !v)}
76
- className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-accent/40"
77
- >
78
- <ChevronRight
79
- className={cn(
80
- "size-3 shrink-0 transition-transform",
81
- open && "rotate-90",
82
- )}
83
- />
121
+ {/* Row header: file icon + name (or rename input) + size */}
122
+ <div className="flex w-full items-center gap-2 px-2 py-1.5 text-xs">
84
123
  <FileCode2 className="size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
85
- <span className="min-w-0 flex-1 truncate font-mono">{entry.name}</span>
124
+ {renaming ? (
125
+ <input
126
+ ref={renameInputRef}
127
+ value={renameValue}
128
+ onChange={(e) => setRenameValue(e.target.value)}
129
+ onBlur={() => void commitRename()}
130
+ onKeyDown={(e) => {
131
+ if (e.key === "Enter") void commitRename();
132
+ if (e.key === "Escape") setRenaming(false);
133
+ }}
134
+ autoFocus
135
+ className="min-w-0 flex-1 rounded border border-border bg-background px-1 py-0.5 font-mono text-xs outline-none focus:ring-1 focus:ring-ring"
136
+ />
137
+ ) : (
138
+ <span className="min-w-0 flex-1 truncate font-mono">{entry.name}</span>
139
+ )}
140
+ <Tip content="Renombrar">
141
+ <button
142
+ type="button"
143
+ onClick={startRename}
144
+ className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
145
+ >
146
+ <Pencil className="size-3" />
147
+ </button>
148
+ </Tip>
86
149
  <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
87
150
  {entry.size}b
88
151
  </span>
89
- </button>
90
- {open && (
91
- <div className="space-y-2 border-t border-border p-2">
92
- <div className="flex flex-wrap items-center gap-1">
93
- <code className="min-w-0 flex-1 truncate rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
94
- {entry.path}
95
- </code>
96
- {looksRunnable && (
97
- <button
98
- type="button"
99
- onClick={() => void run()}
100
- disabled={running}
101
- title={t("code_module.artifacts_run")}
102
- className={cn(
103
- "inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium",
104
- running
105
- ? "bg-muted text-muted-foreground"
106
- : "bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 dark:text-emerald-300",
107
- )}
108
- >
109
- {running ? <Spinner size={10} /> : <Play className="size-3" />}
110
- {t("code_module.artifacts_run")}
111
- </button>
112
- )}
152
+ </div>
153
+
154
+ {/* Action bar always visible */}
155
+ <div className="space-y-2 border-t border-border p-2">
156
+ <div className="flex w-full min-w-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5">
157
+ <code className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
158
+ {entry.path}
159
+ </code>
160
+ <Tip content={t("code_module.artifacts_copy_path")}>
113
161
  <button
114
162
  type="button"
115
163
  onClick={() => void copy(entry.path)}
116
- title={t("code_module.artifacts_copy_path")}
117
- className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
164
+ className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
118
165
  >
119
166
  <Copy className="size-3" />
120
167
  </button>
168
+ </Tip>
169
+ </div>
170
+ <div className="flex flex-wrap items-center gap-1 mt-1">
171
+ {/* Ver button */}
172
+ <Dialog open={viewOpen} onOpenChange={setViewOpen}>
173
+ <Tip content="Ver contenido">
174
+ <button
175
+ type="button"
176
+ onClick={() => setViewOpen(true)}
177
+ className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-blue-500/15 text-blue-700 hover:bg-blue-500/25 dark:text-blue-300"
178
+ >
179
+ <Eye className="size-3" />
180
+ Ver
181
+ </button>
182
+ </Tip>
183
+ <DialogContent className="sm:max-w-lg">
184
+ <DialogHeader>
185
+ <DialogTitle className="font-mono text-sm">{entry.name}</DialogTitle>
186
+ </DialogHeader>
187
+ {detail.isLoading ? (
188
+ <div className="flex justify-center py-6">
189
+ <Spinner size={16} />
190
+ </div>
191
+ ) : (
192
+ <pre className="max-h-96 overflow-auto rounded bg-muted/50 p-3 font-mono text-[11px] leading-tight whitespace-pre-wrap break-all">
193
+ {detail.data?.content ?? ""}
194
+ </pre>
195
+ )}
196
+ <DialogFooter showCloseButton />
197
+ </DialogContent>
198
+ </Dialog>
199
+
200
+ {/* Editar — opens as a file tab in the main panel */}
201
+ <Tip content="Editar contenido">
121
202
  <button
122
203
  type="button"
123
- onClick={() => void remove()}
124
- title={t("code_module.artifacts_delete")}
125
- className="rounded p-1 text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950"
204
+ onClick={() => onEditArtifact?.(entry.name)}
205
+ className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-violet-500/15 text-violet-700 hover:bg-violet-500/25 dark:text-violet-300"
126
206
  >
127
- <Trash2 className="size-3" />
207
+ <SquarePen className="size-3" />
208
+ Editar
128
209
  </button>
129
- </div>
130
- <div className="text-[10px] text-muted-foreground">
131
- {t("code_module.artifacts_run_hint")}{" "}
132
- <code className="rounded bg-muted px-1 font-mono">
133
- apx artifact run {entry.name}
134
- </code>
135
- </div>
136
- {runResult && (
137
- <div className="space-y-1">
138
- <div className="flex items-center gap-2 text-[10px]">
139
- <span
210
+ </Tip>
211
+
212
+ {/* Run button */}
213
+ {looksRunnable && (
214
+ <Tip content={t("code_module.artifacts_run")}>
215
+ <button
216
+ type="button"
217
+ onClick={() => onRunInTerminal?.(`apx artifact run ${entry.name}`)}
218
+ className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 dark:text-emerald-300"
219
+ >
220
+ <Play className="size-3" />
221
+ {t("code_module.artifacts_run")}
222
+ </button>
223
+ </Tip>
224
+ )}
225
+
226
+ {/* Eliminar — confirmation dialog */}
227
+ <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
228
+ <Tip content={t("code_module.artifacts_delete")}>
229
+ <button
230
+ type="button"
231
+ onClick={() => setDeleteOpen(true)}
232
+ className="ml-auto rounded p-1 text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950"
233
+ >
234
+ <Trash2 className="size-3" />
235
+ </button>
236
+ </Tip>
237
+ <DialogContent className="sm:max-w-sm">
238
+ <DialogHeader>
239
+ <DialogTitle className="font-mono text-sm">
240
+ {t("code_module.artifacts_delete")} — {entry.name}
241
+ </DialogTitle>
242
+ </DialogHeader>
243
+ <p className="px-1 text-sm text-muted-foreground">
244
+ {t("code_module.artifacts_delete_confirm")}
245
+ </p>
246
+ <DialogFooter>
247
+ <DialogClose
248
+ render={
249
+ <button
250
+ type="button"
251
+ className="rounded px-3 py-1.5 text-xs font-medium hover:bg-accent"
252
+ />
253
+ }
254
+ >
255
+ Cancelar
256
+ </DialogClose>
257
+ <button
258
+ type="button"
259
+ onClick={() => void remove()}
260
+ disabled={deleting}
140
261
  className={cn(
141
- "rounded px-1.5 py-0.5 font-mono",
142
- runResult.ok
143
- ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"
144
- : "bg-rose-500/15 text-rose-700 dark:text-rose-300",
262
+ "inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-xs font-medium",
263
+ deleting
264
+ ? "bg-muted text-muted-foreground"
265
+ : "bg-rose-500/15 text-rose-700 hover:bg-rose-500/25 dark:text-rose-300",
145
266
  )}
146
267
  >
147
- exit {runResult.exitCode ?? runResult.signal ?? "?"}
148
- </span>
149
- {runResult.timedOut && (
150
- <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
151
- timeout
152
- </span>
153
- )}
154
- {runResult.truncated && (
155
- <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
156
- truncated
157
- </span>
268
+ {deleting && <Spinner size={10} />}
269
+ Eliminar
270
+ </button>
271
+ </DialogFooter>
272
+ </DialogContent>
273
+ </Dialog>
274
+ </div>
275
+
276
+ <div className="mt-1 text-[10px] text-muted-foreground">
277
+ {t("code_module.artifacts_run_hint")}{" "}
278
+ <code className="rounded bg-muted px-1 font-mono">
279
+ apx artifact run {entry.name}
280
+ </code>
281
+ </div>
282
+
283
+ {/* Run result display */}
284
+ {runResult && (
285
+ <div className="space-y-1">
286
+ <div className="flex items-center gap-2 text-[10px]">
287
+ <span
288
+ className={cn(
289
+ "rounded px-1.5 py-0.5 font-mono",
290
+ runResult.ok
291
+ ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"
292
+ : "bg-rose-500/15 text-rose-700 dark:text-rose-300",
158
293
  )}
159
- <span className="font-mono text-muted-foreground">
160
- {runResult.durationMs}ms
294
+ >
295
+ exit {runResult.exitCode ?? runResult.signal ?? "?"}
296
+ </span>
297
+ {runResult.timedOut && (
298
+ <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
299
+ timeout
161
300
  </span>
162
- </div>
163
- {runResult.stdout && (
164
- <pre className="max-h-32 overflow-auto rounded bg-background/60 p-2 text-[10px] leading-tight">
165
- {runResult.stdout}
166
- </pre>
167
301
  )}
168
- {runResult.stderr && (
169
- <pre className="max-h-32 overflow-auto rounded bg-rose-500/5 p-2 text-[10px] leading-tight text-rose-700 dark:text-rose-300">
170
- {runResult.stderr}
171
- </pre>
302
+ {runResult.truncated && (
303
+ <span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
304
+ truncated
305
+ </span>
172
306
  )}
307
+ <span className="font-mono text-muted-foreground">
308
+ {runResult.durationMs}ms
309
+ </span>
173
310
  </div>
174
- )}
175
- {detail.isLoading ? (
176
- <div className="flex justify-center py-2">
177
- <Spinner size={12} />
178
- </div>
179
- ) : detail.data?.content ? (
180
- <pre className="max-h-64 overflow-auto rounded bg-muted/50 p-2 text-[10px] leading-tight">
181
- {detail.data.content}
182
- </pre>
183
- ) : null}
184
- </div>
185
- )}
311
+ {runResult.stdout && (
312
+ <pre className="max-h-32 overflow-auto rounded bg-background/60 p-2 text-[10px] leading-tight">
313
+ {runResult.stdout}
314
+ </pre>
315
+ )}
316
+ {runResult.stderr && (
317
+ <pre className="max-h-32 overflow-auto rounded bg-rose-500/5 p-2 text-[10px] leading-tight text-rose-700 dark:text-rose-300">
318
+ {runResult.stderr}
319
+ </pre>
320
+ )}
321
+ </div>
322
+ )}
323
+ </div>
186
324
  </li>
187
325
  );
188
326
  }
189
327
 
190
328
  // Artifacts tab: managed files stored under <project>/artifacts/. The agent
191
329
  // puts reusable scripts here so the user can run them from a terminal.
192
- export function CodeArtifactsTab({ pid }: Props) {
330
+ export function CodeArtifactsTab({ pid, onRunInTerminal, onEditArtifact }: Props) {
193
331
  const list = useSWR(pid ? ["artifacts", pid] : null, () => Artifacts.list(pid));
194
332
  const entries = list.data || [];
195
333
  return (
@@ -200,14 +338,15 @@ export function CodeArtifactsTab({ pid }: Props) {
200
338
  ? t("code_module.artifacts_count", { n: entries.length })
201
339
  : ""}
202
340
  </span>
203
- <button
204
- type="button"
205
- onClick={() => void list.mutate()}
206
- title="↻"
207
- className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
208
- >
209
- {list.isLoading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
210
- </button>
341
+ <Tip content="Recargar">
342
+ <button
343
+ type="button"
344
+ onClick={() => void list.mutate()}
345
+ className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
346
+ >
347
+ {list.isLoading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
348
+ </button>
349
+ </Tip>
211
350
  </div>
212
351
  <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-3">
213
352
  {entries.length === 0 ? (
@@ -220,6 +359,9 @@ export function CodeArtifactsTab({ pid }: Props) {
220
359
  pid={pid}
221
360
  entry={a}
222
361
  onDeleted={() => void list.mutate()}
362
+ onRenamed={() => void list.mutate()}
363
+ onRunInTerminal={onRunInTerminal}
364
+ onEditArtifact={onEditArtifact}
223
365
  />
224
366
  ))}
225
367
  </ul>
@@ -3,6 +3,7 @@ import { ChevronRight, FilePlus2, FilePen, FileX2, RefreshCw } from "lucide-reac
3
3
  import { cn } from "../../lib/cn";
4
4
  import { t } from "../../i18n";
5
5
  import { Empty, Spinner } from "../ui";
6
+ import { Tip } from "../ui/tip";
6
7
  import { DiffView } from "./DiffView";
7
8
  import type { CodeChanges, CodeFileChange } from "../../lib/api/code";
8
9
 
@@ -60,14 +61,15 @@ export function CodeChangesTab({ changes, loading, onRefresh }: Props) {
60
61
  <span className="text-[11px] text-muted-foreground">
61
62
  {files.length > 0 ? t("code_module.changes_files", { n: files.length }) : ""}
62
63
  </span>
63
- <button
64
- type="button"
65
- onClick={onRefresh}
66
- title="↻"
67
- className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
68
- >
69
- {loading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
70
- </button>
64
+ <Tip content="Recargar">
65
+ <button
66
+ type="button"
67
+ onClick={onRefresh}
68
+ className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
69
+ >
70
+ {loading ? <Spinner size={12} /> : <RefreshCw className="size-3" />}
71
+ </button>
72
+ </Tip>
71
73
  </div>
72
74
  <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-3">
73
75
  {changes && !changes.git ? (
@@ -2,6 +2,7 @@ import { Hammer, ClipboardList } from "lucide-react";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { t } from "../../i18n";
4
4
  import { ChatInput } from "../ui/chat-input";
5
+ import { Tip } from "../ui/tip";
5
6
  import { ModelPicker } from "../chat/ModelPicker";
6
7
  import type { CodeMode } from "../../lib/api/code";
7
8
 
@@ -29,22 +30,23 @@ function ModeToggle({
29
30
  disabled?: boolean;
30
31
  }) {
31
32
  const item = (m: CodeMode, label: string, hint: string, Icon: typeof Hammer) => (
32
- <button
33
- type="button"
34
- disabled={disabled}
35
- title={hint}
36
- data-testid={`code-mode-${m}`}
37
- aria-pressed={mode === m}
38
- onClick={() => onChange(m)}
39
- className={cn(
40
- "flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-medium transition-colors disabled:opacity-50",
41
- mode === m
42
- ? "bg-background text-foreground shadow-sm"
43
- : "text-muted-foreground hover:text-foreground",
44
- )}
45
- >
46
- <Icon className="size-3" /> {label}
47
- </button>
33
+ <Tip content={hint}>
34
+ <button
35
+ type="button"
36
+ disabled={disabled}
37
+ data-testid={`code-mode-${m}`}
38
+ aria-pressed={mode === m}
39
+ onClick={() => onChange(m)}
40
+ className={cn(
41
+ "flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-50",
42
+ mode === m
43
+ ? "bg-background text-foreground shadow-sm"
44
+ : "text-muted-foreground hover:text-foreground",
45
+ )}
46
+ >
47
+ <Icon className="size-3.5" /> {label}
48
+ </button>
49
+ </Tip>
48
50
  );
49
51
  return (
50
52
  <div className="flex items-center gap-0.5 rounded-lg border border-border bg-muted/60 p-0.5">
@@ -75,7 +77,8 @@ export function CodeComposer({
75
77
  busy={busy}
76
78
  disabled={disabled}
77
79
  placeholder={t("code_module.placeholder")}
78
- maxRows={12}
80
+ minRows={1}
81
+ maxRows={6}
79
82
  footer={
80
83
  <div className="flex items-center gap-2">
81
84
  <ModeToggle mode={mode} onChange={onModeChange} disabled={busy} />