@agentprojectcontext/apx 1.32.0 → 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 (230) hide show
  1. package/package.json +6 -1
  2. package/skills/apc-context/SKILL.md +5 -2
  3. package/skills/apx/SKILL.md +3 -3
  4. package/skills/apx-agency-agents/SKILL.md +5 -5
  5. package/skills/apx-agent/SKILL.md +7 -7
  6. package/skills/apx-mcp/SKILL.md +6 -4
  7. package/skills/apx-mcp-builder/SKILL.md +4 -7
  8. package/skills/apx-project/SKILL.md +4 -5
  9. package/skills/apx-routine/SKILL.md +14 -12
  10. package/skills/apx-runtime/SKILL.md +5 -3
  11. package/skills/apx-sessions/SKILL.md +5 -5
  12. package/skills/apx-skill-builder/SKILL.md +10 -6
  13. package/skills/apx-task/SKILL.md +8 -8
  14. package/skills/apx-telegram/SKILL.md +23 -7
  15. package/skills/apx-voice/SKILL.md +8 -6
  16. package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
  17. package/src/core/agent/index.js +0 -2
  18. package/src/core/{agent-memory.js → agent/memory.js} +2 -2
  19. package/src/core/agent/model-router.js +21 -43
  20. package/src/core/agent/prompt-builder.js +17 -63
  21. package/src/core/agent/prompts/action-discipline.md +24 -0
  22. package/src/core/agent/prompts/channels/code.md +8 -12
  23. package/src/core/agent/prompts/channels/desktop.md +6 -4
  24. package/src/core/agent/prompts/channels/routine.md +10 -1
  25. package/src/core/agent/prompts/channels/telegram.md +10 -1
  26. package/src/core/agent/prompts/channels/web_code.md +20 -0
  27. package/src/core/agent/prompts/modes/voice.md +2 -2
  28. package/src/core/agent/prompts/super-agent-base.md +2 -2
  29. package/src/core/agent/run-agent.js +37 -35
  30. package/src/core/agent/runtime-bridge.js +42 -0
  31. package/src/core/agent/self-memory.js +19 -9
  32. package/src/core/agent/skills/catalog.js +65 -0
  33. package/src/core/agent/skills/index.js +6 -0
  34. package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
  35. package/src/core/agent/skills/rag.js +91 -0
  36. package/src/core/agent/skills/trigger.js +71 -0
  37. package/src/{host/daemon → core/agent}/super-agent.js +5 -5
  38. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
  39. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
  40. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
  41. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
  42. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
  43. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
  44. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
  45. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
  46. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
  47. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
  48. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
  49. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
  50. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
  51. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
  52. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
  53. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
  54. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
  55. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
  56. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
  57. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
  58. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
  59. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
  60. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
  61. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
  62. package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
  63. package/src/core/agent/tools/helpers.js +74 -0
  64. package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
  65. package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
  66. package/src/core/apc/agents-vault.js +37 -0
  67. package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
  68. package/src/core/{config.js → config/index.js} +21 -27
  69. package/src/core/config/paths.js +32 -0
  70. package/src/core/constants/actors.js +8 -0
  71. package/src/core/constants/channels.js +19 -0
  72. package/src/core/constants/index.js +5 -0
  73. package/src/core/constants/permissions.js +17 -0
  74. package/src/core/constants/roles.js +9 -0
  75. package/src/core/engines/_streaming.js +63 -0
  76. package/src/core/engines/anthropic.js +11 -22
  77. package/src/core/engines/ollama.js +7 -16
  78. package/src/core/identity/index.js +8 -0
  79. package/src/core/{identity.js → identity/self.js} +5 -5
  80. package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
  81. package/src/core/logging.js +1 -1
  82. package/src/core/mascot.js +1 -1
  83. package/src/core/memory/active-threads.js +10 -10
  84. package/src/core/memory/broker.js +9 -9
  85. package/src/core/memory/compactor.js +2 -2
  86. package/src/core/memory/index.js +2 -2
  87. package/src/core/memory/indexer.js +1 -1
  88. package/src/core/{code-sessions-store.js → stores/code-sessions.js} +7 -8
  89. package/src/core/{messages-store.js → stores/messages.js} +6 -4
  90. package/src/core/stores/routine-memory.js +71 -0
  91. package/src/core/{routines-store.js → stores/routines.js} +1 -3
  92. package/src/core/stores/runtime-sessions.js +99 -0
  93. package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
  94. package/src/core/update-check.js +1 -1
  95. package/src/core/util/ids.js +14 -0
  96. package/src/core/util/index.js +2 -0
  97. package/src/core/util/time.js +9 -0
  98. package/src/core/voice/tts.js +1 -1
  99. package/src/host/daemon/api/admin-config.js +4 -3
  100. package/src/host/daemon/api/admin.js +1 -1
  101. package/src/host/daemon/api/agents.js +4 -25
  102. package/src/host/daemon/api/artifacts.js +26 -1
  103. package/src/host/daemon/api/code.js +62 -17
  104. package/src/host/daemon/api/confirm.js +1 -1
  105. package/src/host/daemon/api/connections.js +2 -2
  106. package/src/host/daemon/api/conversations.js +2 -2
  107. package/src/host/daemon/api/deck.js +1 -1
  108. package/src/host/daemon/api/desktop.js +1 -1
  109. package/src/host/daemon/api/embeddings.js +4 -4
  110. package/src/host/daemon/api/engines.js +2 -2
  111. package/src/host/daemon/api/exec.js +20 -5
  112. package/src/host/daemon/api/identity.js +1 -1
  113. package/src/host/daemon/api/mcps.js +1 -1
  114. package/src/host/daemon/api/messages.js +1 -1
  115. package/src/host/daemon/api/runtimes.js +9 -8
  116. package/src/host/daemon/api/sessions-search.js +1 -1
  117. package/src/host/daemon/api/sessions.js +2 -2
  118. package/src/host/daemon/api/shared.js +5 -4
  119. package/src/host/daemon/api/skills.js +30 -0
  120. package/src/host/daemon/api/super-agent.js +29 -9
  121. package/src/host/daemon/api/tasks.js +2 -2
  122. package/src/host/daemon/api/telegram.js +1 -1
  123. package/src/host/daemon/api/tools.js +6 -6
  124. package/src/host/daemon/api/tts.js +2 -2
  125. package/src/host/daemon/api/voice.js +14 -12
  126. package/src/host/daemon/api.js +2 -0
  127. package/src/host/daemon/compact.js +1 -1
  128. package/src/host/daemon/db.js +4 -4
  129. package/src/host/daemon/desktop-ws.js +1 -1
  130. package/src/host/daemon/index.js +4 -4
  131. package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +11 -6
  132. package/src/host/daemon/plugins/index.js +2 -2
  133. package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +52 -193
  134. package/src/host/daemon/plugins/telegram/media.js +162 -0
  135. package/src/host/daemon/projects-helpers.js +54 -0
  136. package/src/host/daemon/routines.js +28 -12
  137. package/src/host/daemon/smoke.js +2 -2
  138. package/src/host/daemon/token-store.js +1 -1
  139. package/src/host/daemon/transcription.js +2 -2
  140. package/src/host/daemon/wakeup.js +2 -2
  141. package/src/interfaces/cli/commands/agent.js +3 -3
  142. package/src/interfaces/cli/commands/command.js +1 -1
  143. package/src/interfaces/cli/commands/config.js +3 -2
  144. package/src/interfaces/cli/commands/desktop.js +1 -1
  145. package/src/interfaces/cli/commands/exec.js +2 -1
  146. package/src/interfaces/cli/commands/identity.js +2 -2
  147. package/src/interfaces/cli/commands/init.js +1 -1
  148. package/src/interfaces/cli/commands/mcp.js +1 -1
  149. package/src/interfaces/cli/commands/memory.js +2 -2
  150. package/src/interfaces/cli/commands/model.js +16 -6
  151. package/src/interfaces/cli/commands/project.js +1 -1
  152. package/src/interfaces/cli/commands/routine.js +58 -0
  153. package/src/interfaces/cli/commands/search.js +1 -1
  154. package/src/interfaces/cli/commands/session.js +4 -4
  155. package/src/interfaces/cli/commands/setup.js +4 -3
  156. package/src/interfaces/cli/commands/skills.js +25 -4
  157. package/src/interfaces/cli/commands/status.js +1 -1
  158. package/src/interfaces/cli/commands/sys.js +11 -4
  159. package/src/interfaces/cli/commands/update.js +1 -1
  160. package/src/interfaces/cli/index.js +4 -4
  161. package/src/interfaces/cli/postinstall.js +2 -2
  162. package/src/interfaces/mcp-server/index.js +1 -1
  163. package/src/interfaces/tui/component/prompt/index.tsx +3 -1
  164. package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
  165. package/src/interfaces/tui/context/sync-apx.tsx +20 -2
  166. package/src/interfaces/tui/context/sync.tsx +2 -1
  167. package/src/interfaces/tui/routes/session/index.tsx +151 -136
  168. package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
  169. package/src/interfaces/tui/run.ts +2 -0
  170. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +1 -0
  171. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +602 -0
  172. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +1 -0
  173. package/src/interfaces/web/dist/index.html +2 -2
  174. package/src/interfaces/web/package-lock.json +6 -6
  175. package/src/interfaces/web/src/App.tsx +53 -32
  176. package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
  177. package/src/interfaces/web/src/components/UiSelect.tsx +13 -3
  178. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
  179. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +253 -111
  180. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +10 -8
  181. package/src/interfaces/web/src/components/code/CodeComposer.tsx +20 -17
  182. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +43 -18
  183. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +212 -0
  184. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +121 -0
  185. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
  186. package/src/interfaces/web/src/components/code/CodeSessionList.tsx +30 -26
  187. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +40 -21
  188. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +140 -0
  189. package/src/interfaces/web/src/components/common/TabLayout.tsx +11 -7
  190. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
  191. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
  192. package/src/interfaces/web/src/components/ui/chat-input.tsx +17 -6
  193. package/src/interfaces/web/src/hooks/useChat.ts +48 -2
  194. package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +83 -0
  195. package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
  196. package/src/interfaces/web/src/i18n/en.ts +7 -7
  197. package/src/interfaces/web/src/i18n/es.ts +8 -8
  198. package/src/interfaces/web/src/lib/api/agents.ts +1 -1
  199. package/src/interfaces/web/src/lib/api/artifacts.ts +10 -0
  200. package/src/interfaces/web/src/lib/api/code.ts +4 -2
  201. package/src/interfaces/web/src/lib/api/skills.ts +25 -0
  202. package/src/interfaces/web/src/lib/api.ts +1 -0
  203. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +430 -86
  204. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
  205. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
  206. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
  207. package/src/interfaces/web/src/screens/project/ChatTab.tsx +16 -16
  208. package/src/skills/apc-context/SKILL.md +159 -0
  209. package/src/core/agent/ghost-guard.js +0 -24
  210. package/src/core/agent/prompts/channels/terminal.md +0 -16
  211. package/src/host/daemon/apc-runtime-context.js +0 -124
  212. package/src/host/daemon/super-agent-tools/helpers.js +0 -124
  213. package/src/host/daemon/tool-call-parser.js +0 -2
  214. package/src/interfaces/web/dist/assets/index-63P_ji1a.js +0 -571
  215. package/src/interfaces/web/dist/assets/index-63P_ji1a.js.map +0 -1
  216. package/src/interfaces/web/dist/assets/index-DLWy6dYz.css +0 -1
  217. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/ask-questions.js +0 -0
  218. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
  219. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
  220. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
  221. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
  222. /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
  223. /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
  224. /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
  225. /package/src/core/{parser.js → apc/parser.js} +0 -0
  226. /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
  227. /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
  228. /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
  229. /package/src/core/{session-store.js → stores/sessions.js} +0 -0
  230. /package/src/host/daemon/plugins/{telegram-ask.js → telegram/ask.js} +0 -0
@@ -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
+ }
@@ -26,7 +26,7 @@ export function CodeProjectPicker({ projects, value, onChange, disabled }: Props
26
26
  });
27
27
 
28
28
  return (
29
- <div className="w-60" data-testid="code-project-select">
29
+ <div className="w-full" data-testid="code-project-select">
30
30
  <UiSelect
31
31
  value={value}
32
32
  onChange={onChange}
@@ -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,5 +1,7 @@
1
+ import { useState } from "react";
1
2
  import { Gauge, GitCompare, Package } from "lucide-react";
2
3
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
4
+ import { Tip } from "../ui/tip";
3
5
  import { t } from "../../i18n";
4
6
  import { CodeContextTab } from "./CodeContextTab";
5
7
  import { CodeChangesTab } from "./CodeChangesTab";
@@ -12,40 +14,57 @@ interface Props {
12
14
  changes: CodeChanges | undefined;
13
15
  changesLoading: boolean;
14
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;
15
20
  }
16
21
 
17
- // Right-hand panel: Context (token metrics), Changes (diffs vs the session's
18
- // git baseline), and Artifacts (managed files under <project>/artifacts/).
19
- export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshChanges }: Props) {
22
+ const TABS = [
23
+ { value: "context", icon: Gauge, label: "tab_context" },
24
+ { value: "changes", icon: GitCompare, label: "tab_changes" },
25
+ { value: "artifacts", icon: Package, label: "tab_artifacts" },
26
+ ] as const;
27
+
28
+ export function CodeSidePanel({ pid, turns, changes, changesLoading, onRefreshChanges, session, onRunInTerminal, onEditArtifact }: Props) {
29
+ const [active, setActive] = useState<string>("context");
20
30
  const changeCount = changes?.files.length || 0;
31
+
21
32
  return (
22
- <Tabs defaultValue="context" className="flex h-full flex-col gap-0" data-testid="code-side-panel">
23
- <div className="shrink-0 border-b border-border px-3 py-2">
33
+ <Tabs value={active} onValueChange={setActive} className="flex h-full flex-col gap-0" data-testid="code-side-panel">
34
+ <div className="shrink-0 border-b border-border px-2 py-2">
24
35
  <TabsList variant="line" className="w-full">
25
- <TabsTrigger value="context" className="flex-1">
26
- <Gauge className="size-3.5" /> {t("code_module.tab_context")}
27
- </TabsTrigger>
28
- <TabsTrigger value="changes" className="flex-1">
29
- <GitCompare className="size-3.5" /> {t("code_module.tab_changes")}
30
- {changeCount > 0 && (
31
- <span className="ml-1 rounded-full bg-muted px-1.5 text-[10px] text-muted-foreground">
32
- {changeCount}
33
- </span>
34
- )}
35
- </TabsTrigger>
36
- <TabsTrigger value="artifacts" className="flex-1">
37
- <Package className="size-3.5" /> {t("code_module.tab_artifacts")}
38
- </TabsTrigger>
36
+ {TABS.map(({ value, icon: Icon, label }) => {
37
+ const isActive = active === value;
38
+ const fullLabel = t(`code_module.${label}` as never);
39
+ return (
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>
56
+ );
57
+ })}
39
58
  </TabsList>
40
59
  </div>
41
60
  <TabsContent value="context" className="min-h-0 flex-1 overflow-y-auto">
42
- <CodeContextTab turns={turns} />
61
+ <CodeContextTab turns={turns} session={session} />
43
62
  </TabsContent>
44
63
  <TabsContent value="changes" className="min-h-0 flex-1 overflow-hidden">
45
64
  <CodeChangesTab changes={changes} loading={changesLoading} onRefresh={onRefreshChanges} />
46
65
  </TabsContent>
47
66
  <TabsContent value="artifacts" className="min-h-0 flex-1 overflow-hidden">
48
- <CodeArtifactsTab pid={pid} />
67
+ <CodeArtifactsTab pid={pid} onRunInTerminal={onRunInTerminal} onEditArtifact={onEditArtifact} />
49
68
  </TabsContent>
50
69
  </Tabs>
51
70
  );
@@ -0,0 +1,140 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { Terminal as TerminalIcon, Eraser, X } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { Tip } from "../ui/tip";
5
+ import { http } from "../../lib/http";
6
+
7
+ interface Line {
8
+ type: "cmd" | "out" | "err";
9
+ text: string;
10
+ }
11
+
12
+ export function CodeTerminal({
13
+ pid,
14
+ className,
15
+ initCmd,
16
+ onClose,
17
+ }: {
18
+ pid: string;
19
+ className?: string;
20
+ initCmd?: string;
21
+ /** Click handler for the header × button. Closes the terminal panel. */
22
+ onClose?: () => void;
23
+ }) {
24
+ const [lines, setLines] = useState<Line[]>([]);
25
+ const [input, setInput] = useState("");
26
+ const [busy, setBusy] = useState(false);
27
+ const [history, setHistory] = useState<string[]>([]);
28
+ const [histIdx, setHistIdx] = useState(-1);
29
+ const bottomRef = useRef<HTMLDivElement>(null);
30
+ const inputRef = useRef<HTMLInputElement>(null);
31
+
32
+ useEffect(() => {
33
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
34
+ }, [lines]);
35
+
36
+ // Pre-fill input when a command is pushed from parent (e.g. artifact run).
37
+ useEffect(() => {
38
+ if (!initCmd) return;
39
+ setInput(initCmd);
40
+ setTimeout(() => inputRef.current?.focus(), 50);
41
+ }, [initCmd]);
42
+
43
+ const run = async (cmd: string) => {
44
+ const trimmed = cmd.trim();
45
+ if (!trimmed) return;
46
+ setHistory((h) => [trimmed, ...h.slice(0, 49)]);
47
+ setHistIdx(-1);
48
+ setLines((l) => [...l, { type: "cmd", text: `$ ${trimmed}` }]);
49
+ setBusy(true);
50
+ try {
51
+ const r = await http.post<{ ok: boolean; stdout: string; stderr: string; exit_code: number; cwd: string }>(
52
+ "/run",
53
+ { cmd: trimmed, project: pid },
54
+ );
55
+ if (r.stdout) setLines((l) => [...l, { type: "out", text: r.stdout }]);
56
+ if (r.stderr) setLines((l) => [...l, { type: "err", text: r.stderr }]);
57
+ } catch (e) {
58
+ setLines((l) => [...l, { type: "err", text: String((e as Error).message) }]);
59
+ } finally {
60
+ setBusy(false);
61
+ }
62
+ };
63
+
64
+ const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
65
+ if (e.key === "Enter") {
66
+ void run(input);
67
+ setInput("");
68
+ } else if (e.key === "ArrowUp") {
69
+ e.preventDefault();
70
+ const next = Math.min(histIdx + 1, history.length - 1);
71
+ setHistIdx(next);
72
+ setInput(history[next] ?? "");
73
+ } else if (e.key === "ArrowDown") {
74
+ e.preventDefault();
75
+ const next = Math.max(histIdx - 1, -1);
76
+ setHistIdx(next);
77
+ setInput(next === -1 ? "" : (history[next] ?? ""));
78
+ }
79
+ };
80
+
81
+ return (
82
+ <div className={cn("flex h-full min-h-0 flex-col bg-card/60", className)} data-testid="code-terminal">
83
+ <div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1">
84
+ <TerminalIcon className="size-3 text-muted-foreground" />
85
+ <span className="flex-1 text-[11px] text-muted-foreground">Terminal</span>
86
+ <Tip content="Limpiar">
87
+ <button
88
+ type="button"
89
+ onClick={() => setLines([])}
90
+ className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
91
+ >
92
+ <Eraser className="size-3" />
93
+ </button>
94
+ </Tip>
95
+ <Tip content="Cerrar terminal">
96
+ <button
97
+ type="button"
98
+ onClick={() => onClose?.()}
99
+ className="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
100
+ >
101
+ <X className="size-3" />
102
+ </button>
103
+ </Tip>
104
+ </div>
105
+ <div
106
+ className="min-h-0 flex-1 overflow-y-auto px-3 py-1 font-mono text-[11px] leading-snug cursor-text"
107
+ onClick={() => inputRef.current?.focus()}
108
+ >
109
+ {lines.map((l, i) => (
110
+ <div
111
+ key={i}
112
+ className={cn(
113
+ "whitespace-pre-wrap break-all",
114
+ l.type === "cmd" && "text-emerald-400",
115
+ l.type === "err" && "text-rose-400",
116
+ l.type === "out" && "text-foreground/90",
117
+ )}
118
+ >
119
+ {l.text}
120
+ </div>
121
+ ))}
122
+ <div ref={bottomRef} />
123
+ </div>
124
+ <div className="flex shrink-0 items-center border-t border-border px-3 py-1">
125
+ <span className="mr-2 text-[11px] text-emerald-400 font-mono">$</span>
126
+ <input
127
+ ref={inputRef}
128
+ value={input}
129
+ onChange={(e) => setInput(e.target.value)}
130
+ onKeyDown={onKey}
131
+ disabled={busy}
132
+ placeholder={busy ? "ejecutando…" : "comando…"}
133
+ className="flex-1 bg-transparent font-mono text-[11px] text-foreground outline-none placeholder:text-muted-foreground/50 disabled:opacity-50"
134
+ spellCheck={false}
135
+ autoComplete="off"
136
+ />
137
+ </div>
138
+ </div>
139
+ );
140
+ }