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