@agentprojectcontext/apx 1.77.3 → 1.79.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 (356) hide show
  1. package/README.md +23 -9
  2. package/package.json +12 -12
  3. package/src/core/agent/build-agent-system.js +3 -2
  4. package/src/core/agent/constants.js +3 -2
  5. package/src/core/agent/loop/greeting-guard.js +55 -0
  6. package/src/core/agent/loop/side-effects.js +52 -0
  7. package/src/core/agent/prompt-builder.js +49 -8
  8. package/src/core/agent/run-agent.js +43 -53
  9. package/src/core/agent/security.js +1 -1
  10. package/src/core/agent/self-memory.js +4 -2
  11. package/src/core/agent/skills/index-store.js +2 -2
  12. package/src/core/agent/skills/loader.js +3 -22
  13. package/src/core/agent/super-agent.js +0 -2
  14. package/src/core/agent/tool-summary.js +65 -0
  15. package/src/core/agent/tools/handlers/call-runtime.js +4 -4
  16. package/src/core/agent/tools/handlers/list-commitments.js +80 -0
  17. package/src/core/agent/tools/handlers/list-tasks.js +66 -27
  18. package/src/core/agent/tools/handlers/record-commitment.js +68 -0
  19. package/src/core/agent/tools/handlers/search-sessions.js +1 -1
  20. package/src/core/agent/tools/handlers/send-telegram.js +68 -2
  21. package/src/core/agent/tools/handlers/set-permission-mode.js +11 -4
  22. package/src/core/agent/tools/names.js +64 -0
  23. package/src/core/agent/tools/registry.js +9 -0
  24. package/src/core/agent/tools/tool-call-parser.js +70 -1
  25. package/src/core/apc/context-copy.js +1 -1
  26. package/src/core/apc/frontmatter.js +85 -0
  27. package/src/core/apc/parser.js +9 -21
  28. package/src/core/apc/paths.js +8 -8
  29. package/src/core/apc/scaffold.js +5 -4
  30. package/src/core/channels/telegram/ask-callbacks.js +35 -0
  31. package/src/core/channels/telegram/config.js +201 -0
  32. package/src/core/channels/telegram/dispatch.js +3 -0
  33. package/src/core/channels/telegram/reply.js +30 -5
  34. package/src/core/config/index.js +25 -183
  35. package/src/core/config/paths.js +57 -1
  36. package/src/core/config/redact.js +22 -0
  37. package/src/core/confirmation/adapters/web.js +1 -1
  38. package/src/core/daemon/service.js +238 -0
  39. package/src/core/deck/manifest.js +12 -12
  40. package/src/core/desktop/autostart.js +3 -2
  41. package/src/core/desktop/process.js +4 -4
  42. package/src/core/engines/gemini.js +322 -60
  43. package/src/core/engines/openai-compatible.js +21 -2
  44. package/src/core/engines/presets.js +2 -2
  45. package/src/core/http-tools/browser.js +1 -1
  46. package/src/core/http-tools/catalog.js +551 -0
  47. package/src/core/http-tools/fetch.js +1 -1
  48. package/src/core/http-tools/glob.js +3 -3
  49. package/src/core/http-tools/grep.js +2 -9
  50. package/src/core/http-tools/inline-handlers.js +59 -0
  51. package/src/core/http-tools/registry.js +31 -615
  52. package/src/core/http-tools/search.js +2 -2
  53. package/src/core/identity/self.js +6 -7
  54. package/src/core/identity/telegram.js +3 -3
  55. package/src/core/logging.js +6 -5
  56. package/src/core/mcp/sources.js +12 -16
  57. package/src/core/memory/consolidate.js +225 -0
  58. package/src/core/memory/indexer.js +2 -5
  59. package/src/core/nudge/index.js +192 -0
  60. package/src/core/nudge/policy.js +143 -0
  61. package/src/core/nudge/store.js +141 -0
  62. package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
  63. package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
  64. package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
  65. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
  66. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
  67. package/src/core/profiles/lifecycle.js +0 -2
  68. package/src/core/profiles/store.js +2 -7
  69. package/src/core/routines/runner.js +103 -4
  70. package/src/core/routines/signals.js +270 -0
  71. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
  72. package/src/core/runtime-skills/apx-profile/SKILL.md +3 -3
  73. package/src/core/runtime-skills/apx-project/SKILL.md +1 -1
  74. package/src/core/runtime-skills/apx-task/SKILL.md +8 -8
  75. package/src/core/runtime-skills/apx-telegram/SKILL.md +4 -4
  76. package/src/core/runtime-skills/apx-voice/SKILL.md +4 -4
  77. package/src/core/runtimes/detect.js +2 -2
  78. package/src/core/sessions/index.js +800 -0
  79. package/src/core/stores/code-sessions.js +3 -14
  80. package/src/core/stores/commitments.js +331 -0
  81. package/src/core/stores/engine-sessions.js +0 -4
  82. package/src/core/stores/messages.js +4 -5
  83. package/src/core/stores/organization.js +9 -11
  84. package/src/core/stores/routines.js +23 -14
  85. package/src/core/stores/runtime-callbacks.js +2 -1
  86. package/src/core/util/json-file.js +128 -0
  87. package/src/core/util/thinking.js +51 -0
  88. package/src/core/vars/sources.js +14 -21
  89. package/src/core/voice/stt-models.js +1 -1
  90. package/src/core/voice/tts.js +5 -4
  91. package/src/host/daemon/api/admin-config.js +5 -5
  92. package/src/host/daemon/api/admin.js +8 -7
  93. package/src/host/daemon/api/agents.js +16 -16
  94. package/src/host/daemon/api/artifact-preview.js +8 -8
  95. package/src/host/daemon/api/artifacts.js +7 -7
  96. package/src/host/daemon/api/code.js +8 -8
  97. package/src/host/daemon/api/commitments.js +135 -0
  98. package/src/host/daemon/api/config.js +6 -6
  99. package/src/host/daemon/api/confirm.js +2 -2
  100. package/src/host/daemon/api/connections.js +2 -2
  101. package/src/host/daemon/api/conversations.js +12 -12
  102. package/src/host/daemon/api/deck.js +6 -6
  103. package/src/host/daemon/api/desktop.js +8 -8
  104. package/src/host/daemon/api/embeddings.js +4 -4
  105. package/src/host/daemon/api/engines.js +8 -7
  106. package/src/host/daemon/api/exec.js +5 -4
  107. package/src/host/daemon/api/files-project.js +6 -6
  108. package/src/host/daemon/api/health.js +2 -2
  109. package/src/host/daemon/api/identity.js +3 -3
  110. package/src/host/daemon/api/inbox.js +2 -2
  111. package/src/host/daemon/api/integrations.js +23 -19
  112. package/src/host/daemon/api/mcps.js +16 -12
  113. package/src/host/daemon/api/messages.js +5 -5
  114. package/src/host/daemon/api/nudges.js +112 -0
  115. package/src/host/daemon/api/organization.js +8 -8
  116. package/src/host/daemon/api/pairing.js +6 -6
  117. package/src/host/daemon/api/plugins.js +3 -3
  118. package/src/host/daemon/api/prefix.js +20 -0
  119. package/src/host/daemon/api/profiles.js +9 -9
  120. package/src/host/daemon/api/projects.js +5 -5
  121. package/src/host/daemon/api/routines.js +32 -8
  122. package/src/host/daemon/api/run.js +2 -2
  123. package/src/host/daemon/api/runtimes.js +6 -6
  124. package/src/host/daemon/api/self-memory.js +50 -0
  125. package/src/host/daemon/api/sessions-search.js +18 -11
  126. package/src/host/daemon/api/sessions.js +6 -6
  127. package/src/host/daemon/api/shared.js +59 -13
  128. package/src/host/daemon/api/skills.js +12 -12
  129. package/src/host/daemon/api/super-agent.js +4 -4
  130. package/src/host/daemon/api/tasks.js +11 -11
  131. package/src/host/daemon/api/telegram.js +61 -24
  132. package/src/host/daemon/api/tools.js +7 -7
  133. package/src/host/daemon/api/top-level.js +7 -7
  134. package/src/host/daemon/api/transcribe.js +6 -6
  135. package/src/host/daemon/api/tts.js +3 -3
  136. package/src/host/daemon/api/vars.js +11 -8
  137. package/src/host/daemon/api/voice.js +9 -7
  138. package/src/host/daemon/api/web.js +28 -32
  139. package/src/host/daemon/api.js +83 -48
  140. package/src/host/daemon/callback-reconciler.js +16 -0
  141. package/src/host/daemon/db.js +1 -1
  142. package/src/host/daemon/desktop-ws.js +7 -3
  143. package/src/host/daemon/index.js +11 -5
  144. package/src/host/daemon/plugins/desktop/index.js +8 -2
  145. package/src/host/daemon/plugins/telegram/index.js +7 -2
  146. package/src/host/daemon/stt-venv.js +20 -81
  147. package/src/host/daemon/wakeup.js +18 -4
  148. package/src/interfaces/acp/index.js +3 -3
  149. package/src/interfaces/acp/session.js +2 -2
  150. package/src/interfaces/cli/commands/a2a.js +2 -2
  151. package/src/interfaces/cli/commands/agent.js +3 -3
  152. package/src/interfaces/cli/commands/artifact.js +13 -13
  153. package/src/interfaces/cli/commands/chat.js +3 -3
  154. package/src/interfaces/cli/commands/command.js +2 -2
  155. package/src/interfaces/cli/commands/commitment.js +154 -0
  156. package/src/interfaces/cli/commands/config.js +4 -4
  157. package/src/interfaces/cli/commands/daemon.js +74 -18
  158. package/src/interfaces/cli/commands/desktop.js +7 -14
  159. package/src/interfaces/cli/commands/exec.js +2 -2
  160. package/src/interfaces/cli/commands/log.js +3 -4
  161. package/src/interfaces/cli/commands/mcp.js +9 -9
  162. package/src/interfaces/cli/commands/memory.js +75 -2
  163. package/src/interfaces/cli/commands/messages.js +5 -5
  164. package/src/interfaces/cli/commands/model.js +0 -18
  165. package/src/interfaces/cli/commands/nudge.js +130 -0
  166. package/src/interfaces/cli/commands/obsidian.js +5 -5
  167. package/src/interfaces/cli/commands/org.js +5 -5
  168. package/src/interfaces/cli/commands/pair.js +6 -6
  169. package/src/interfaces/cli/commands/plugins.js +2 -2
  170. package/src/interfaces/cli/commands/profile.js +10 -10
  171. package/src/interfaces/cli/commands/project-config.js +6 -6
  172. package/src/interfaces/cli/commands/project.js +10 -10
  173. package/src/interfaces/cli/commands/routine.js +8 -9
  174. package/src/interfaces/cli/commands/runtime.js +2 -2
  175. package/src/interfaces/cli/commands/search.js +0 -1
  176. package/src/interfaces/cli/commands/session.js +12 -40
  177. package/src/interfaces/cli/commands/sessions.js +21 -798
  178. package/src/interfaces/cli/commands/setup.js +16 -6
  179. package/src/interfaces/cli/commands/skills.js +1 -2
  180. package/src/interfaces/cli/commands/status.js +3 -3
  181. package/src/interfaces/cli/commands/task.js +8 -8
  182. package/src/interfaces/cli/commands/telegram.js +21 -21
  183. package/src/interfaces/cli/commands/voice.js +6 -6
  184. package/src/interfaces/cli/help/index.js +2245 -0
  185. package/src/interfaces/cli/http.js +9 -4
  186. package/src/interfaces/cli/index.js +14 -2857
  187. package/src/interfaces/cli/routes/acp.js +13 -0
  188. package/src/interfaces/cli/routes/agent.js +27 -0
  189. package/src/interfaces/cli/routes/artifact.js +28 -0
  190. package/src/interfaces/cli/routes/chat.js +11 -0
  191. package/src/interfaces/cli/routes/code.js +11 -0
  192. package/src/interfaces/cli/routes/command.js +21 -0
  193. package/src/interfaces/cli/routes/commitment.js +19 -0
  194. package/src/interfaces/cli/routes/config.js +16 -0
  195. package/src/interfaces/cli/routes/connections.js +11 -0
  196. package/src/interfaces/cli/routes/conversations.js +21 -0
  197. package/src/interfaces/cli/routes/daemon.js +25 -0
  198. package/src/interfaces/cli/routes/desktop.js +20 -0
  199. package/src/interfaces/cli/routes/env.js +13 -0
  200. package/src/interfaces/cli/routes/exec.js +11 -0
  201. package/src/interfaces/cli/routes/identity.js +11 -0
  202. package/src/interfaces/cli/routes/index.js +75 -0
  203. package/src/interfaces/cli/routes/init.js +11 -0
  204. package/src/interfaces/cli/routes/log.js +20 -0
  205. package/src/interfaces/cli/routes/mcp.js +22 -0
  206. package/src/interfaces/cli/routes/memory.js +19 -0
  207. package/src/interfaces/cli/routes/messages.js +16 -0
  208. package/src/interfaces/cli/routes/model.js +11 -0
  209. package/src/interfaces/cli/routes/nudge.js +17 -0
  210. package/src/interfaces/cli/routes/obsidian.js +17 -0
  211. package/src/interfaces/cli/routes/org.js +34 -0
  212. package/src/interfaces/cli/routes/overlay.js +10 -0
  213. package/src/interfaces/cli/routes/pair.js +17 -0
  214. package/src/interfaces/cli/routes/panel.js +16 -0
  215. package/src/interfaces/cli/routes/permission.js +11 -0
  216. package/src/interfaces/cli/routes/plugins.js +21 -0
  217. package/src/interfaces/cli/routes/profile.js +27 -0
  218. package/src/interfaces/cli/routes/project.js +41 -0
  219. package/src/interfaces/cli/routes/restart.js +15 -0
  220. package/src/interfaces/cli/routes/routine.js +28 -0
  221. package/src/interfaces/cli/routes/run.js +11 -0
  222. package/src/interfaces/cli/routes/search.js +11 -0
  223. package/src/interfaces/cli/routes/send.js +11 -0
  224. package/src/interfaces/cli/routes/session.js +26 -0
  225. package/src/interfaces/cli/routes/sessions.js +15 -0
  226. package/src/interfaces/cli/routes/setup.js +18 -0
  227. package/src/interfaces/cli/routes/skills.js +20 -0
  228. package/src/interfaces/cli/routes/status.js +12 -0
  229. package/src/interfaces/cli/routes/task.js +26 -0
  230. package/src/interfaces/cli/routes/telegram.js +39 -0
  231. package/src/interfaces/cli/routes/update.js +24 -0
  232. package/src/interfaces/cli/routes/voice.js +17 -0
  233. package/src/interfaces/desktop/main.js +6 -7
  234. package/src/interfaces/desktop/renderer.js +0 -5
  235. package/src/interfaces/mcp-server/index.js +15 -14
  236. package/src/interfaces/tui/_shims/globals.d.ts +8 -0
  237. package/src/interfaces/tui/tsconfig.json +4 -1
  238. package/src/interfaces/web/README.md +6 -6
  239. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
  240. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
  241. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
  242. package/src/interfaces/web/dist/index.html +2 -2
  243. package/src/interfaces/web/package-lock.json +11 -10
  244. package/src/interfaces/web/src/components/AddProjectDialog.tsx +1 -1
  245. package/src/interfaces/web/src/components/Section.tsx +18 -3
  246. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +1 -1
  247. package/src/interfaces/web/src/components/chat/ChatList.tsx +2 -2
  248. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
  249. package/src/interfaces/web/src/components/chat/SkillPicker.tsx +1 -1
  250. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +1 -1
  251. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +1 -1
  252. package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
  253. package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +1 -1
  254. package/src/interfaces/web/src/components/files/FileBrowser.tsx +2 -2
  255. package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
  256. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +36 -6
  257. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +1 -1
  258. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
  259. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +14 -4
  260. package/src/interfaces/web/src/components/routines/shared.ts +14 -5
  261. package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +1 -1
  262. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
  263. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
  264. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
  265. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
  266. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +2 -2
  267. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +1 -1
  268. package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
  269. package/src/interfaces/web/src/components/ui.tsx +1 -0
  270. package/src/interfaces/web/src/constants/index.ts +1 -0
  271. package/src/interfaces/web/src/hooks/useChat.ts +5 -1
  272. package/src/interfaces/web/src/hooks/useDaemonStatus.ts +1 -1
  273. package/src/interfaces/web/src/hooks/useDevices.ts +1 -1
  274. package/src/interfaces/web/src/hooks/useEngines.ts +1 -1
  275. package/src/interfaces/web/src/hooks/useGlobalConfig.ts +2 -2
  276. package/src/interfaces/web/src/hooks/useIdentity.ts +1 -1
  277. package/src/interfaces/web/src/hooks/useInbox.ts +1 -1
  278. package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
  279. package/src/interfaces/web/src/hooks/useProfiles.ts +3 -3
  280. package/src/interfaces/web/src/hooks/useProjects.ts +1 -1
  281. package/src/interfaces/web/src/hooks/useTelegram.ts +3 -3
  282. package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +3 -3
  283. package/src/interfaces/web/src/i18n/en.ts +127 -0
  284. package/src/interfaces/web/src/i18n/es.ts +127 -0
  285. package/src/interfaces/web/src/lib/api/admin.ts +11 -11
  286. package/src/interfaces/web/src/lib/api/agents.ts +14 -14
  287. package/src/interfaces/web/src/lib/api/artifacts.ts +11 -11
  288. package/src/interfaces/web/src/lib/api/code.ts +1 -1
  289. package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
  290. package/src/interfaces/web/src/lib/api/conversations.ts +8 -8
  291. package/src/interfaces/web/src/lib/api/deck.ts +3 -3
  292. package/src/interfaces/web/src/lib/api/desktop.ts +8 -8
  293. package/src/interfaces/web/src/lib/api/embeddings.ts +3 -3
  294. package/src/interfaces/web/src/lib/api/engines.ts +3 -3
  295. package/src/interfaces/web/src/lib/api/filesystem.ts +2 -2
  296. package/src/interfaces/web/src/lib/api/health.ts +1 -1
  297. package/src/interfaces/web/src/lib/api/identity.ts +2 -2
  298. package/src/interfaces/web/src/lib/api/inbox.ts +1 -1
  299. package/src/interfaces/web/src/lib/api/integrations.ts +8 -8
  300. package/src/interfaces/web/src/lib/api/mcps.ts +6 -6
  301. package/src/interfaces/web/src/lib/api/messages.ts +3 -3
  302. package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
  303. package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
  304. package/src/interfaces/web/src/lib/api/organization.ts +7 -7
  305. package/src/interfaces/web/src/lib/api/profiles.ts +8 -8
  306. package/src/interfaces/web/src/lib/api/projectFiles.ts +5 -5
  307. package/src/interfaces/web/src/lib/api/projects.ts +12 -12
  308. package/src/interfaces/web/src/lib/api/routines.ts +7 -7
  309. package/src/interfaces/web/src/lib/api/sessions.ts +2 -2
  310. package/src/interfaces/web/src/lib/api/skills.ts +11 -11
  311. package/src/interfaces/web/src/lib/api/super_agent.ts +3 -3
  312. package/src/interfaces/web/src/lib/api/tasks.ts +12 -12
  313. package/src/interfaces/web/src/lib/api/telegram.ts +14 -14
  314. package/src/interfaces/web/src/lib/api/tools.ts +1 -1
  315. package/src/interfaces/web/src/lib/api/vars.ts +4 -4
  316. package/src/interfaces/web/src/lib/api/voice.ts +6 -6
  317. package/src/interfaces/web/src/lib/cron.ts +196 -0
  318. package/src/interfaces/web/src/lib/when.ts +32 -0
  319. package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
  320. package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
  321. package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
  322. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +1 -1
  323. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
  324. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +103 -20
  325. package/src/interfaces/web/src/screens/base/LogsTab.tsx +18 -3
  326. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +1 -1
  327. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  328. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +1 -1
  329. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -1
  330. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  331. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -8
  332. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +2 -2
  333. package/src/interfaces/web/src/screens/project/ChatTab.tsx +24 -6
  334. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +1 -1
  335. package/src/interfaces/web/src/screens/project/McpsTab.tsx +3 -3
  336. package/src/interfaces/web/src/screens/project/Overview.tsx +6 -6
  337. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +15 -13
  338. package/src/interfaces/web/src/screens/project/StructureTab.tsx +1 -1
  339. package/src/interfaces/web/src/screens/project/TasksTab.tsx +1 -1
  340. package/src/interfaces/web/src/screens/project/VarsTab.tsx +1 -1
  341. package/src/interfaces/web/src/types/daemon.ts +10 -1
  342. package/src/interfaces/web/vite.config.ts +13 -18
  343. package/src/interfaces/web/dist/assets/index-BptlbKjU.js +0 -824
  344. package/src/interfaces/web/dist/assets/index-BptlbKjU.js.map +0 -1
  345. package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
  346. package/src/skills/apc-context/SKILL.md +0 -159
  347. /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
  348. /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
  349. /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
  350. /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
  351. /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
  352. /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
  353. /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
  354. /package/src/{host/daemon → core}/runtimes/index.js +0 -0
  355. /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
  356. /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
@@ -110,7 +110,7 @@ function AgentConvFetcher({
110
110
  onLoaded: (slug: string, data: ConversationListEntry[] | undefined) => void;
111
111
  }) {
112
112
  const { data } = useSWR(
113
- `/projects/${pid}/agents/${slug}/conversations`,
113
+ `/api/projects/${pid}/agents/${slug}/conversations`,
114
114
  () => Conversations.list(pid, slug),
115
115
  { revalidateOnFocus: false },
116
116
  );
@@ -142,7 +142,7 @@ export function ChatList({
142
142
  // sidebar shows just that project's own agent conversations.
143
143
  const isBase = String(pid) === "0";
144
144
  const threadsQ = useSWR(
145
- isBase ? `/projects/${pid}/super-agent/threads` : null,
145
+ isBase ? `/api/projects/${pid}/super-agent/threads` : null,
146
146
  () => Conversations.threads(pid),
147
147
  { revalidateOnFocus: false },
148
148
  );
@@ -133,6 +133,19 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
133
133
  {!mine && hasTools && (
134
134
  <span>· {t("shared_ui.tools_count", { n: msg.parts.filter((p) => p.kind === "tool").length })}</span>
135
135
  )}
136
+ {/* Replayed turns have no tool parts — the live events are gone — but
137
+ they do carry the summary recorded at the time. Show that instead,
138
+ so history does not look like the agent just answered from
139
+ nothing. Failures are named: "it tried and could not" is the half
140
+ worth surfacing. */}
141
+ {!mine && !hasTools && msg.toolSummary?.tools?.length ? (
142
+ <span title={msg.toolSummary.tools.map((x) => `${x.name}×${x.count}`).join(", ")}>
143
+ · {t("shared_ui.tools_count", { n: msg.toolSummary.total })}
144
+ {msg.toolSummary.failed
145
+ ? ` (${t("shared_ui.tools_failed", { n: msg.toolSummary.failed })})`
146
+ : ""}
147
+ </span>
148
+ ) : null}
136
149
  {onCopy && copyText && (
137
150
  <Tip content={t("chat_ui.copy")}>
138
151
  <button
@@ -27,7 +27,7 @@ export function SkillPicker({ value, projectPath, onPick }: SkillPickerProps) {
27
27
  const query = open ? extractQuery(value) : "";
28
28
 
29
29
  const { data } = useSWR(
30
- open ? ["/skills", projectPath || ""] : null,
30
+ open ? ["/api/skills", projectPath || ""] : null,
31
31
  () => Skills.list(projectPath),
32
32
  );
33
33
 
@@ -120,7 +120,7 @@ export function CodeFileTree({
120
120
  setLoading(true);
121
121
  try {
122
122
  const r = await http.post<{ ok: boolean; stdout: string; stderr: string }>(
123
- "/run",
123
+ "/api/run",
124
124
  {
125
125
  cmd: "find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.claude/*' | sed 's|^\\./||' | sort | head -500",
126
126
  project: pid,
@@ -50,7 +50,7 @@ export function CodeTerminal({
50
50
  setBusy(true);
51
51
  try {
52
52
  const r = await http.post<{ ok: boolean; stdout: string; stderr: string; exit_code: number; cwd: string }>(
53
- "/run",
53
+ "/api/run",
54
54
  { cmd: trimmed, project: pid },
55
55
  );
56
56
  if (r.stdout) setLines((l) => [...l, { type: "out", text: r.stdout }]);
@@ -0,0 +1,196 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { Clock, Code2 } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+ import { t } from "../../i18n";
5
+ import {
6
+ DAY_KEYS, cronToSelection, selectionToCron, describeCron, isPickerFriendly,
7
+ normalizeCron, type CronSelection,
8
+ } from "../../lib/cron";
9
+
10
+ /**
11
+ * Read-only sentence for a cron expression. Falls back to the raw expression
12
+ * when the schedule is beyond what lib/cron understands — a confident wrong
13
+ * reading of when something runs is worse than showing the source.
14
+ */
15
+ export function CronSummary({ expr, className }: { expr: string; className?: string }) {
16
+ const text = describeCron(expr, t as never);
17
+ return (
18
+ <span className={cn("inline-flex items-center gap-1.5", className)} title={expr}>
19
+ <Clock size={12} className="shrink-0 opacity-60" />
20
+ {text ? <span>{text}</span> : <code className="font-mono text-[11px]">{expr}</code>}
21
+ </span>
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Time + day checkboxes, or every-N-hours, with the cron expression as an
27
+ * escape hatch rather than the only interface.
28
+ *
29
+ * The raw field stays reachable on purpose: cron can express things this
30
+ * picker cannot, and a form that silently narrows what the user is allowed to
31
+ * schedule is worse than one that asks them to type. When the current value is
32
+ * beyond the picker, the raw field opens by itself — the alternative is a
33
+ * picker quietly showing something the expression does not say.
34
+ */
35
+ export function CronPicker({
36
+ value,
37
+ onChange,
38
+ disabled,
39
+ allowEveryHours = true,
40
+ }: {
41
+ value: string;
42
+ onChange: (expr: string) => void;
43
+ disabled?: boolean;
44
+ allowEveryHours?: boolean;
45
+ }) {
46
+ const friendly = useMemo(() => isPickerFriendly(value), [value]);
47
+ const [raw, setRaw] = useState(!friendly);
48
+ const [rawDraft, setRawDraft] = useState(value);
49
+ const sel = useMemo<CronSelection>(() => cronToSelection(value), [value]);
50
+
51
+ // An expression arriving from outside (a profile default, another edit) that
52
+ // the picker cannot represent flips to raw rather than misrepresenting it.
53
+ useEffect(() => {
54
+ setRawDraft(value);
55
+ if (!isPickerFriendly(value)) setRaw(true);
56
+ }, [value]);
57
+
58
+ const patch = (over: Partial<CronSelection>) =>
59
+ onChange(selectionToCron({ ...sel, ...over }));
60
+
61
+ const toggleDay = (d: number) => {
62
+ const next = sel.days.includes(d) ? sel.days.filter((x) => x !== d) : [...sel.days, d];
63
+ patch({ days: next });
64
+ };
65
+
66
+ const preset = (days: number[]) => patch({ days });
67
+
68
+ return (
69
+ <div className="flex flex-col gap-2" data-testid="cron-picker">
70
+ {raw ? (
71
+ <input
72
+ value={rawDraft}
73
+ disabled={disabled}
74
+ onChange={(e) => setRawDraft(e.target.value)}
75
+ onBlur={() => onChange(rawDraft.trim())}
76
+ onKeyDown={(e) => { if (e.key === "Enter") onChange(rawDraft.trim()); }}
77
+ placeholder="30 8 * * 1-5"
78
+ className="w-full rounded-lg border border-border bg-muted/30 px-2.5 py-1.5 font-mono text-sm outline-none focus:border-primary/60"
79
+ data-testid="cron-raw"
80
+ />
81
+ ) : (
82
+ <>
83
+ <div className="flex flex-wrap items-center gap-2">
84
+ <input
85
+ type="time"
86
+ value={sel.everyHours > 0 ? "00:00" : sel.time}
87
+ disabled={disabled || sel.everyHours > 0}
88
+ onChange={(e) => patch({ time: e.target.value, everyHours: 0 })}
89
+ className="rounded-lg border border-border bg-muted/30 px-2.5 py-1.5 text-sm outline-none focus:border-primary/60 disabled:opacity-50"
90
+ data-testid="cron-time"
91
+ />
92
+ {allowEveryHours ? (
93
+ <label className="flex items-center gap-1.5 text-xs text-muted-fg">
94
+ <input
95
+ type="checkbox"
96
+ checked={sel.everyHours > 0}
97
+ disabled={disabled}
98
+ onChange={(e) => patch({ everyHours: e.target.checked ? 2 : 0 })}
99
+ className="size-3.5 accent-[var(--primary)]"
100
+ />
101
+ {t("cron.every_label")}
102
+ </label>
103
+ ) : null}
104
+ {sel.everyHours > 0 ? (
105
+ <input
106
+ type="number"
107
+ min={1}
108
+ max={23}
109
+ value={sel.everyHours}
110
+ disabled={disabled}
111
+ onChange={(e) => patch({ everyHours: Number(e.target.value) })}
112
+ className="w-16 rounded-lg border border-border bg-muted/30 px-2 py-1.5 text-sm outline-none focus:border-primary/60"
113
+ aria-label={t("cron.every_label")}
114
+ />
115
+ ) : null}
116
+ </div>
117
+
118
+ {sel.everyHours === 0 ? (
119
+ <>
120
+ <div className="flex flex-wrap gap-1" data-testid="cron-days">
121
+ {DAY_KEYS.map((key, d) => {
122
+ const on = sel.days.length === 0 || sel.days.includes(d);
123
+ return (
124
+ <button
125
+ key={key}
126
+ type="button"
127
+ disabled={disabled}
128
+ aria-pressed={sel.days.includes(d)}
129
+ onClick={() => toggleDay(d)}
130
+ className={cn(
131
+ "min-w-9 rounded-md border px-2 py-1 text-[11px] font-medium transition-colors",
132
+ sel.days.includes(d)
133
+ ? "border-primary bg-primary/15 text-foreground"
134
+ : on
135
+ // "every day" means all seven are in effect even
136
+ // though none is individually ticked — show that
137
+ // rather than seven blank boxes.
138
+ ? "border-border bg-muted/40 text-muted-fg"
139
+ : "border-border text-muted-fg/60 hover:bg-muted/40",
140
+ )}
141
+ >
142
+ {t(`cron.day_short.${key}`)}
143
+ </button>
144
+ );
145
+ })}
146
+ </div>
147
+ <div className="flex flex-wrap gap-1 text-[11px]">
148
+ {([
149
+ ["cron.preset_every_day", [] as number[]],
150
+ ["cron.preset_weekdays", [1, 2, 3, 4, 5]],
151
+ ["cron.preset_weekends", [0, 6]],
152
+ ] as const).map(([label, days]) => (
153
+ <button
154
+ key={label}
155
+ type="button"
156
+ disabled={disabled}
157
+ onClick={() => preset([...days])}
158
+ className="rounded-md px-1.5 py-0.5 text-muted-fg underline-offset-2 hover:text-foreground hover:underline"
159
+ >
160
+ {t(label)}
161
+ </button>
162
+ ))}
163
+ </div>
164
+ </>
165
+ ) : null}
166
+ </>
167
+ )}
168
+
169
+ <div className="flex items-center justify-between gap-2">
170
+ {/* The sentence, always — including in raw mode, where it is the only
171
+ confirmation that what was typed means what was intended. */}
172
+ <CronSummary expr={raw ? rawDraft : value} className="text-[11px] text-muted-fg" />
173
+ <button
174
+ type="button"
175
+ disabled={disabled}
176
+ onClick={() => {
177
+ if (raw) {
178
+ // Leaving raw mode must not silently rewrite a schedule the
179
+ // picker cannot hold.
180
+ const next = rawDraft.trim();
181
+ onChange(next);
182
+ if (isPickerFriendly(next)) setRaw(false);
183
+ } else {
184
+ setRawDraft(normalizeCron(value));
185
+ setRaw(true);
186
+ }
187
+ }}
188
+ className="inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-fg hover:text-foreground"
189
+ data-testid="cron-toggle-raw"
190
+ >
191
+ <Code2 size={11} /> {raw ? t("cron.use_picker") : t("cron.use_cron")}
192
+ </button>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
@@ -17,7 +17,7 @@ export function DesktopStatusCard({ showConfigLink = false }: { showConfigLink?:
17
17
  const toast = useToast();
18
18
 
19
19
  const { data: status, isLoading: stLoading, mutate: mutateStatus } = useSWR(
20
- "/desktop/status",
20
+ "/api/desktop/status",
21
21
  () => Desktop.status(),
22
22
  { refreshInterval: 5000 },
23
23
  );
@@ -29,10 +29,10 @@ export function FileBrowser({
29
29
  const [selected, setSelected] = useState<string | null>(null);
30
30
  const [newOpen, setNewOpen] = useState(false);
31
31
 
32
- const treeKey = `/projects/${pid}/fs/tree?scope=${scope}`;
32
+ const treeKey = `/api/projects/${pid}/fs/tree?scope=${scope}`;
33
33
  const tree = useSWR(treeKey, () => ProjectFiles.tree(pid, scope));
34
34
 
35
- const fileKey = selected ? `/projects/${pid}/fs/file?scope=${scope}&path=${selected}` : null;
35
+ const fileKey = selected ? `/api/projects/${pid}/fs/file?scope=${scope}&path=${selected}` : null;
36
36
  const file = useSWR<FileContent | null>(fileKey, () => (selected ? ProjectFiles.read(pid, selected, scope) : null));
37
37
 
38
38
  const onSelect = (node: FileNode) => setSelected(node.path);
@@ -0,0 +1,145 @@
1
+ import { useMemo, useState } from "react";
2
+ import { Search } from "lucide-react";
3
+ import type { InboxRow } from "../../lib/api/inbox";
4
+ import { cn } from "../../lib/cn";
5
+ import { t } from "../../i18n";
6
+
7
+ /**
8
+ * The conversation rail: every agent as a chat, most recent first.
9
+ *
10
+ * Messaging-app shape rather than a data table, because that is what this is —
11
+ * a list of people-ish things that said something, ordered by when. The
12
+ * previous version was a full-width list of cards that navigated AWAY on
13
+ * click, which meant losing the list to read one row.
14
+ */
15
+
16
+ /** Stable colour per agent, so the same agent is the same dot every time. */
17
+ const DOT_COLOURS = [
18
+ "bg-emerald-500", "bg-orange-500", "bg-violet-500", "bg-sky-500",
19
+ "bg-rose-500", "bg-amber-500", "bg-teal-500", "bg-fuchsia-500",
20
+ ];
21
+
22
+ function colourFor(seed: string): string {
23
+ let h = 0;
24
+ for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
25
+ return DOT_COLOURS[h % DOT_COLOURS.length];
26
+ }
27
+
28
+ export function rowKey(row: InboxRow): string {
29
+ return `${row.project_id ?? "global"}::${row.agent_slug}`;
30
+ }
31
+
32
+ /** Today → time of day; older → date. Same idea as a messaging app. */
33
+ function shortTime(iso: string): string {
34
+ if (!iso) return "";
35
+ const d = new Date(iso);
36
+ if (Number.isNaN(d.getTime())) return "";
37
+ const sameDay = new Date().toDateString() === d.toDateString();
38
+ return sameDay
39
+ ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
40
+ : d.toLocaleDateString(undefined, { day: "numeric", month: "short" });
41
+ }
42
+
43
+ export function InboxList({
44
+ rows,
45
+ selectedKey,
46
+ onSelect,
47
+ action,
48
+ }: {
49
+ rows: InboxRow[];
50
+ selectedKey: string | null;
51
+ onSelect: (row: InboxRow) => void;
52
+ action?: React.ReactNode;
53
+ }) {
54
+ const [q, setQ] = useState("");
55
+
56
+ const filtered = useMemo(() => {
57
+ const needle = q.trim().toLowerCase();
58
+ if (!needle) return rows;
59
+ return rows.filter((r) =>
60
+ [r.agent_name, r.agent_slug, r.project_name, r.preview]
61
+ .filter(Boolean)
62
+ .some((v) => String(v).toLowerCase().includes(needle)));
63
+ }, [rows, q]);
64
+
65
+ return (
66
+ <aside className="flex w-full shrink-0 flex-col border-r border-border sm:w-72" data-testid="inbox-list">
67
+ <div className="flex shrink-0 items-center gap-2 border-b border-border p-2">
68
+ <div className="relative flex-1">
69
+ <Search size={13} className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-fg" />
70
+ <input
71
+ value={q}
72
+ onChange={(e) => setQ(e.target.value)}
73
+ placeholder={t("inbox.search")}
74
+ aria-label={t("inbox.search")}
75
+ className="w-full rounded-lg border border-border bg-muted/30 py-1.5 pl-7 pr-2 text-sm outline-none placeholder:text-muted-fg focus:border-primary/60"
76
+ />
77
+ </div>
78
+ {action}
79
+ </div>
80
+
81
+ <div className="min-h-0 flex-1 overflow-y-auto p-1.5">
82
+ {!filtered.length ? (
83
+ <p className="px-3 py-6 text-center text-xs text-muted-fg">
84
+ {q ? t("inbox.no_match") : t("inbox.empty")}
85
+ </p>
86
+ ) : null}
87
+
88
+ {filtered.map((row) => {
89
+ const key = rowKey(row);
90
+ const isActive = key === selectedKey;
91
+ const label = row.agent_name || row.agent_slug;
92
+ return (
93
+ <button
94
+ key={key}
95
+ type="button"
96
+ data-testid={`inbox-row-${row.agent_slug}`}
97
+ onClick={() => onSelect(row)}
98
+ className={cn(
99
+ "flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors",
100
+ isActive ? "bg-accent text-accent-fg" : "hover:bg-muted/50",
101
+ )}
102
+ >
103
+ {/* A coloured disc, not an emoji on plain background: at this size
104
+ the colour is what the eye picks the row out by. */}
105
+ <span
106
+ className={cn(
107
+ "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full text-sm",
108
+ colourFor(row.agent_slug),
109
+ )}
110
+ aria-hidden
111
+ >
112
+ {row.agent_emoji || label.slice(0, 1).toUpperCase()}
113
+ </span>
114
+
115
+ <span className="min-w-0 flex-1">
116
+ <span className="flex items-baseline gap-2">
117
+ <span className="truncate text-sm font-medium">{label}</span>
118
+ {row.pinned ? (
119
+ <span className="shrink-0 rounded bg-primary/15 px-1 text-[9px] font-semibold uppercase tracking-wide text-primary">
120
+ {t("inbox.pinned")}
121
+ </span>
122
+ ) : null}
123
+ <span className="ml-auto shrink-0 text-[10px] text-muted-fg">
124
+ {shortTime(row.last_activity_at)}
125
+ </span>
126
+ </span>
127
+
128
+ <span className="mt-0.5 flex items-center gap-1.5 text-[10px] text-muted-fg">
129
+ {row.project_name ? <span className="truncate">{row.project_name}</span> : null}
130
+ {row.channel ? <span className="opacity-70">· {row.channel}</span> : null}
131
+ </span>
132
+
133
+ {/* The agent's own last line. Echoing the user's prompt back
134
+ tells them nothing they do not already know. */}
135
+ <span className="mt-0.5 block truncate text-xs text-muted-fg">
136
+ {row.preview || t("inbox.no_reply_yet")}
137
+ </span>
138
+ </span>
139
+ </button>
140
+ );
141
+ })}
142
+ </div>
143
+ </aside>
144
+ );
145
+ }
@@ -1,34 +1,43 @@
1
1
  import { useMemo, useState } from "react";
2
2
  import useSWR from "swr";
3
- import { Brain, Bot, Crown, RefreshCw } from "lucide-react";
3
+ import { Brain, Bot, Crown, NotebookPen, RefreshCw } from "lucide-react";
4
4
  import { Agents, Projects } from "../../lib/api";
5
+ import { Notebook } from "../../lib/api/notebook";
5
6
  import type { AgentEntry, FileContent } from "../../types/daemon";
6
7
  import { cn } from "../../lib/cn";
7
8
  import { Spinner, Empty } from "../ui";
8
9
  import { useToast } from "../Toast";
9
10
  import { t } from "../../i18n";
11
+ import { usePersonaName } from "../../hooks/usePersonaName";
10
12
  import { FileViewer } from "../files/FileViewer";
11
13
 
12
14
  // Which memory is open in the right pane.
13
- type Sel = { kind: "project" } | { kind: "agent"; slug: string };
15
+ type Sel = { kind: "notebook" } | { kind: "project" } | { kind: "agent"; slug: string };
14
16
 
15
17
  function selId(s: Sel): string {
18
+ if (s.kind === "notebook") return "notebook";
16
19
  return s.kind === "project" ? "project" : `agent:${s.slug}`;
17
20
  }
18
21
 
19
22
  // On-disk-ish path shown in the viewer header (mirrors the real memory.md
20
23
  // locations so it reads familiarly, like the docs surface).
21
24
  function selPath(s: Sel): string {
25
+ // The notebook is NOT under the project — it is global to the super-agent.
26
+ // Showing its real absolute-ish path is the whole point: the confusion this
27
+ // entry fixes was not knowing where it lived.
28
+ if (s.kind === "notebook") return "~/.apx/memory.md";
22
29
  return s.kind === "project" ? ".apc/memory.md" : `agents/${s.slug}/memory.md`;
23
30
  }
24
31
 
25
32
  function loadBody(pid: string, s: Sel): Promise<string> {
33
+ if (s.kind === "notebook") return Notebook.get().then((r) => r.body);
26
34
  return s.kind === "project"
27
35
  ? Projects.memory.get(pid).then((r) => r.body)
28
36
  : Agents.memory.get(pid, s.slug).then((r) => r.body);
29
37
  }
30
38
 
31
39
  function saveBody(pid: string, s: Sel, body: string): Promise<void> {
40
+ if (s.kind === "notebook") return Notebook.put(body).then(() => {});
32
41
  return s.kind === "project"
33
42
  ? Projects.memory.put(pid, body).then(() => {})
34
43
  : Agents.memory.put(pid, s.slug, body).then(() => {});
@@ -65,11 +74,16 @@ function SidebarItem({
65
74
  // right for markdown edit / split-preview / save — the same surface as /docs.
66
75
  export function MemoryBrowser({ pid }: { pid: string }) {
67
76
  const toast = useToast();
68
- const [sel, setSel] = useState<Sel>({ kind: "project" });
77
+ // Opens on the notebook. It is the memory that ships in every prompt on
78
+ // every channel, so it is the one most worth seeing first.
79
+ const [sel, setSel] = useState<Sel>({ kind: "notebook" });
80
+ const notebook = useSWR("/api/notebook", () => Notebook.get());
81
+ // Never hardcode the agent's name (AGENTS.md rule 13) — it is the user's to set.
82
+ const persona = usePersonaName();
69
83
 
70
- const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
84
+ const agents = useSWR(`/api/projects/${pid}/agents`, () => Agents.list(pid));
71
85
 
72
- const bodyKey = `/memory/${pid}/${selId(sel)}`;
86
+ const bodyKey = `/api/memory/${pid}/${selId(sel)}`;
73
87
  const body = useSWR(bodyKey, () => loadBody(pid, sel));
74
88
 
75
89
  // Adapt the raw memory body into the FileContent shape FileViewer expects.
@@ -115,8 +129,24 @@ export function MemoryBrowser({ pid }: { pid: string }) {
115
129
  </div>
116
130
 
117
131
  <div className="min-h-0 flex-1 overflow-y-auto p-1.5">
118
- {/* General / project memory */}
132
+ {/* The super-agent's own notebook. First, and in its own group,
133
+ because it is global — not a property of this project — and its
134
+ absence from this list is what made "where is Roby's memory?" an
135
+ unanswerable question. */}
119
136
  <p className="px-1.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
137
+ {t("project.memories.super_agent_group")}
138
+ </p>
139
+ <SidebarItem
140
+ active={sel.kind === "notebook"}
141
+ onClick={() => setSel({ kind: "notebook" })}
142
+ icon={NotebookPen}
143
+ iconClass="text-emerald-400"
144
+ label={t("project.memories.notebook_item", { persona })}
145
+ sub={notebook.data ? t("project.memories.tokens", { n: notebook.data.approx_tokens }) : undefined}
146
+ />
147
+
148
+ {/* General / project memory */}
149
+ <p className="px-1.5 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
120
150
  {t("project.memories.general_group")}
121
151
  </p>
122
152
  <SidebarItem
@@ -120,7 +120,7 @@ function RunDetailPanel({ m, onClose }: { m: MessageEntry; onClose: () => void }
120
120
  * opens a side grid column with that run's details. */
121
121
  export function ExecutionsList({ pid, name, running }: { pid: string; name: string; running?: boolean }) {
122
122
  const runs = useSWR(
123
- `/projects/${pid}/routines/${name}/runs`,
123
+ `/api/projects/${pid}/routines/${name}/runs`,
124
124
  async () => {
125
125
  const msgs = await Messages.project(pid, { channel: "routine", limit: 200 });
126
126
  // Keep one row per run: the runner's end-of-run system summary.
@@ -4,6 +4,7 @@ import { Badge, Button, Switch, Tip } from "../ui";
4
4
  import { cn } from "../../lib/cn";
5
5
  import { t } from "../../i18n";
6
6
  import { kindMeta, scheduleHuman } from "./shared";
7
+ import { relativeWhen } from "../../lib/when";
7
8
  import { ReadOnlyBlock } from "./ReadOnlyBlock";
8
9
  import { ExecutionsList } from "./ExecutionsList";
9
10
 
@@ -30,7 +31,7 @@ export function RoutineDetail({
30
31
  // Read-only content blocks, in pipeline order (pre → action → post).
31
32
  const blocks: { title: string; body: string; mono?: boolean }[] = [];
32
33
  if (pre.length) blocks.push({ title: t("project.routines.block_pre"), body: pre.join("\n"), mono: true });
33
- if (routine.kind === "exec_agent" || routine.kind === "super_agent") {
34
+ if (routine.kind === "exec_agent" || routine.kind === "super_agent" || routine.kind === "watch") {
34
35
  blocks.push({ title: t("project.routines.block_prompt"), body: String(spec.prompt || "") });
35
36
  } else if (routine.kind === "telegram") {
36
37
  blocks.push({ title: t("project.routines.block_text"), body: String(spec.text || "") });
@@ -64,9 +65,20 @@ export function RoutineDetail({
64
65
 
65
66
  {/* compact meta: schedule / next / last */}
66
67
  <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-fg">
67
- <span>⏱ {scheduleHuman(routine.schedule)}</span>
68
- {routine.next_run_at && <span>{t("project.routines.next_run")} {new Date(routine.next_run_at).toLocaleString()}</span>}
69
- {routine.last_run_at && <span>{t("project.routines.last_run")} {new Date(routine.last_run_at).toLocaleString()}</span>}
68
+ <span title={routine.schedule}>⏱ {scheduleHuman(routine.schedule)}</span>
69
+ {/* "in 8 hours" answers the question; the absolute time is one hover
70
+ away for when the exact minute matters. Two full timestamps side
71
+ by side made the row unreadable and still needed mental arithmetic. */}
72
+ {routine.next_run_at && (
73
+ <span title={new Date(routine.next_run_at).toLocaleString()}>
74
+ {t("project.routines.next_run")} {relativeWhen(routine.next_run_at, t as never)}
75
+ </span>
76
+ )}
77
+ {routine.last_run_at && (
78
+ <span title={new Date(routine.last_run_at).toLocaleString()}>
79
+ {t("project.routines.last_run")} {relativeWhen(routine.last_run_at, t as never)}
80
+ </span>
81
+ )}
70
82
  <span className={cn(routine.last_status === "ok" && "text-emerald-500", routine.last_status === "error" && "text-destructive")}>
71
83
  {t("agents_ui.last_label")} {routine.last_status || "—"}
72
84
  </span>
@@ -8,6 +8,8 @@ import { useToast } from "../Toast";
8
8
  import { cn } from "../../lib/cn";
9
9
  import { t } from "../../i18n";
10
10
  import { type Kind, kindMeta, kindOptions, schedPresets, scheduleHuman, splitLines, varsFor } from "./shared";
11
+ import { CronPicker } from "../cron/CronPicker";
12
+ import { parseCron } from "../../lib/cron";
11
13
  import { VarTextarea } from "./VarTextarea";
12
14
  import { AvailableVarsCard } from "./AvailableVarsCard";
13
15
 
@@ -15,7 +17,7 @@ export function RoutineEditor({
15
17
  draft, onClose, onSaved, pid,
16
18
  }: { draft: Partial<RoutineEntry> | null; onClose: () => void; onSaved: () => void; pid: string }) {
17
19
  const toast = useToast();
18
- const agentsList = useSWR(draft ? `/projects/${pid}/agents` : null, () => Agents.list(pid));
20
+ const agentsList = useSWR(draft ? `/api/projects/${pid}/agents` : null, () => Agents.list(pid));
19
21
  const [busy, setBusy] = useState(false);
20
22
 
21
23
  const [name, setName] = useState("");
@@ -35,7 +37,7 @@ export function RoutineEditor({
35
37
  const [post, setPost] = useState("");
36
38
 
37
39
  // Telegram channels for the selector (default + any project/global channels).
38
- const tgChannels = useSWR(draft && kind === "telegram" ? "/telegram/channels" : null, () => Telegram.channels.list());
40
+ const tgChannels = useSWR(draft && kind === "telegram" ? "/api/telegram/channels" : null, () => Telegram.channels.list());
39
41
 
40
42
  // Load draft → fields.
41
43
  useEffect(() => {
@@ -59,12 +61,13 @@ export function RoutineEditor({
59
61
 
60
62
  // Pre/post shell wrap the LLM kinds AND telegram (pre can fetch data, the text
61
63
  // can use {{pre_output}}, post can react to the result).
62
- const usesPrePost = kind === "exec_agent" || kind === "super_agent" || kind === "telegram";
64
+ const usesPrePost = kind === "exec_agent" || kind === "super_agent" || kind === "telegram" || kind === "watch";
63
65
 
64
66
  const buildSpec = (): Record<string, unknown> => {
65
67
  switch (kind) {
66
68
  case "exec_agent": return { agent, prompt };
67
69
  case "super_agent": return { prompt };
70
+ case "watch": return { prompt };
68
71
  case "telegram": return { channel: tgChannel, ...(tgChatId ? { chat_id: tgChatId } : {}), text: tgText };
69
72
  case "shell": return { command };
70
73
  case "heartbeat": return { channel: hbChannel, message: hbMessage };
@@ -109,6 +112,7 @@ export function RoutineEditor({
109
112
  switch (kind) {
110
113
  case "exec_agent": return agent ? t("agents_ui.action_agent_answers", { agent }) : t("agents_ui.action_agent_pick_answers");
111
114
  case "super_agent": return t("agents_ui.action_super_answers");
115
+ case "watch": return t("agents_ui.action_watch");
112
116
  case "telegram": return t("agents_ui.action_telegram_channel", { channel: tgChannel });
113
117
  case "shell": return command ? t("agents_ui.summary_runs_cmd", { cmd: command.slice(0, 48) }) : t("agents_ui.action_runs_shell");
114
118
  case "heartbeat": return t("agents_ui.summary_heartbeat");
@@ -176,7 +180,13 @@ export function RoutineEditor({
176
180
  {t("agents_ui.preset_manual")}
177
181
  </button>
178
182
  </div>
179
- <Input value={schedule} onChange={(e) => setSchedule(e.target.value)} placeholder="every:10m · cron 0 9 * * 1-5 · once:ISO · manual" />
183
+ {/* A cron schedule gets the picker; every:/once:/manual stay a
184
+ text field, because those forms are already readable. */}
185
+ {parseCron(schedule.replace(/^cron\s+/i, "")) ? (
186
+ <CronPicker value={schedule.replace(/^cron\s+/i, "")} onChange={setSchedule} />
187
+ ) : (
188
+ <Input value={schedule} onChange={(e) => setSchedule(e.target.value)} placeholder="every:10m · 0 9 * * 1-5 · once:ISO · manual" />
189
+ )}
180
190
  </div>
181
191
  </Field>
182
192
  <AvailableVarsCard />