@agentprojectcontext/apx 1.81.0 → 1.89.1

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 (838) hide show
  1. package/package.json +9 -8
  2. package/skills/apc-context/SKILL.md +14 -7
  3. package/skills/apx/SKILL.md +62 -2
  4. package/skills/apx-mcp/SKILL.md +42 -15
  5. package/src/core/agent/a2a/peers.js +161 -0
  6. package/src/core/agent/a2a/reply.js +379 -27
  7. package/src/core/agent/abort.js +25 -0
  8. package/src/core/agent/agent-model.js +58 -0
  9. package/src/core/agent/agent-tools.js +101 -0
  10. package/src/core/agent/author-line.js +110 -0
  11. package/src/core/agent/build-agent-system.js +52 -18
  12. package/src/core/agent/channels/voice-context.js +17 -2
  13. package/src/core/agent/closing-floor.js +117 -0
  14. package/src/core/agent/constants.js +119 -11
  15. package/src/core/agent/engine-call.js +56 -0
  16. package/src/core/agent/group/run-group-turn.js +218 -0
  17. package/src/core/agent/group/turn-resolver.js +165 -0
  18. package/src/core/agent/judge.js +138 -11
  19. package/src/core/agent/loop/side-effects.js +106 -3
  20. package/src/core/agent/memory.js +31 -18
  21. package/src/core/agent/model-capabilities.js +47 -0
  22. package/src/core/agent/model-router.js +11 -40
  23. package/src/core/agent/prompt-builder.js +47 -0
  24. package/src/core/agent/prompts/channels/a2a.md +12 -0
  25. package/src/core/agent/prompts/channels/code.md +3 -1
  26. package/src/core/agent/prompts/channels/telegram.md +2 -0
  27. package/src/core/agent/prompts/channels/web_code.md +1 -1
  28. package/src/core/agent/prompts/channels/whatsapp.md +37 -0
  29. package/src/core/agent/prompts/core/agent-base.md +2 -0
  30. package/src/core/agent/prompts/discipline/action.md +1 -0
  31. package/src/core/agent/prompts/discipline/two-segment.md +2 -1
  32. package/src/core/agent/prompts/modes/mobility.md +36 -0
  33. package/src/core/agent/retry.js +42 -0
  34. package/src/core/agent/run-agent.js +322 -25
  35. package/src/core/agent/run-turn.js +253 -0
  36. package/src/core/agent/self-memory.js +18 -52
  37. package/src/core/agent/skills/agent-skills.js +118 -0
  38. package/src/core/agent/skills/catalog.js +1 -1
  39. package/src/core/agent/skills/declared.js +13 -0
  40. package/src/core/agent/skills/index-store.js +8 -1
  41. package/src/core/agent/skills/inspector.js +20 -3
  42. package/src/core/agent/skills/loader.js +18 -3
  43. package/src/core/agent/skills/media.js +121 -0
  44. package/src/core/agent/skills/origin.js +61 -0
  45. package/src/core/agent/skills/sections.js +136 -0
  46. package/src/core/agent/super-agent.js +54 -14
  47. package/src/core/agent/tools/handlers/_calendar.js +50 -0
  48. package/src/core/agent/tools/handlers/add-mcp.js +103 -0
  49. package/src/core/agent/tools/handlers/asana-create-task.js +3 -1
  50. package/src/core/agent/tools/handlers/attach-media.js +56 -0
  51. package/src/core/agent/tools/handlers/calendar-create-event.js +38 -0
  52. package/src/core/agent/tools/handlers/calendar-find-slot.js +39 -0
  53. package/src/core/agent/tools/handlers/calendar-list-events.js +39 -0
  54. package/src/core/agent/tools/handlers/calendar-update-event.js +35 -0
  55. package/src/core/agent/tools/handlers/call-agent.js +14 -6
  56. package/src/core/agent/tools/handlers/call-runtime.js +27 -49
  57. package/src/core/agent/tools/handlers/comment-task.js +78 -0
  58. package/src/core/agent/tools/handlers/complete-task.js +60 -0
  59. package/src/core/agent/tools/handlers/configure-agent.js +70 -0
  60. package/src/core/agent/tools/handlers/create-agent.js +68 -0
  61. package/src/core/agent/tools/handlers/create-task.js +41 -3
  62. package/src/core/agent/tools/handlers/import-agent.js +5 -5
  63. package/src/core/agent/tools/handlers/list-mcp-tools.js +83 -0
  64. package/src/core/agent/tools/handlers/list-routines.js +46 -0
  65. package/src/core/agent/tools/handlers/list-tasks.js +8 -1
  66. package/src/core/agent/tools/handlers/mark-commitment.js +57 -0
  67. package/src/core/agent/tools/handlers/read-skill.js +64 -0
  68. package/src/core/agent/tools/handlers/remember-routine.js +2 -8
  69. package/src/core/agent/tools/handlers/remember.js +82 -6
  70. package/src/core/agent/tools/handlers/remove-agent.js +42 -0
  71. package/src/core/agent/tools/handlers/run-routine.js +94 -0
  72. package/src/core/agent/tools/handlers/run-shell.js +44 -2
  73. package/src/core/agent/tools/handlers/send-telegram.js +41 -19
  74. package/src/core/agent/tools/handlers/set-agent-prompt.js +44 -0
  75. package/src/core/agent/tools/handlers/view-media.js +69 -0
  76. package/src/core/agent/tools/handlers/write-agent-memory.js +52 -0
  77. package/src/core/agent/tools/helpers.js +36 -4
  78. package/src/core/agent/tools/names.js +64 -0
  79. package/src/core/agent/tools/registry-bridge.js +24 -1
  80. package/src/core/agent/tools/registry.js +79 -0
  81. package/src/core/agent/tools/tool-call-parser.js +136 -13
  82. package/src/core/agent/vision-bridge.js +69 -0
  83. package/src/core/apc/agent-identity.js +74 -0
  84. package/src/core/apc/agent-rename-refs.js +187 -0
  85. package/src/core/apc/agent-write.js +312 -0
  86. package/src/core/apc/agents-vault.js +1 -1
  87. package/src/core/apc/blob-keys.js +42 -0
  88. package/src/core/apc/frontmatter.js +166 -10
  89. package/src/core/apc/parser.js +33 -83
  90. package/src/core/apc/paths.js +4 -6
  91. package/src/core/apc/project-memory.js +44 -0
  92. package/src/core/apc/projects-helpers.js +33 -0
  93. package/src/core/apc/scaffold.js +36 -16
  94. package/src/core/channels/telegram/ask-callbacks.js +103 -0
  95. package/src/core/channels/telegram/dispatch.js +154 -30
  96. package/src/core/channels/telegram/helpers.js +59 -7
  97. package/src/core/channels/telegram/inbound/audio.js +26 -30
  98. package/src/core/channels/telegram/inbound/file.js +22 -32
  99. package/src/core/channels/telegram/inbound/photo.js +24 -28
  100. package/src/core/channels/telegram/progress-gate.js +76 -0
  101. package/src/core/channels/telegram/reply.js +151 -43
  102. package/src/core/channels/telegram/voice-note.js +150 -0
  103. package/src/core/config/index.js +53 -54
  104. package/src/core/config/paths.js +117 -31
  105. package/src/core/config/redact.js +89 -4
  106. package/src/core/config/secret-values.js +2 -4
  107. package/src/core/constants/actors.js +5 -0
  108. package/src/core/constants/channels.js +3 -1
  109. package/src/core/constants/task-categories.js +73 -0
  110. package/src/core/constants/task-fields.js +28 -0
  111. package/src/core/engines/_globs.js +33 -0
  112. package/src/core/engines/anthropic.js +19 -1
  113. package/src/core/engines/catalog.js +20 -0
  114. package/src/core/engines/gemini.js +84 -16
  115. package/src/core/engines/index.js +19 -3
  116. package/src/core/engines/mock.js +145 -1
  117. package/src/core/engines/openai-compatible.js +150 -15
  118. package/src/core/engines/presets.js +36 -9
  119. package/src/core/engines/zen.js +131 -0
  120. package/src/core/events/bus.js +140 -0
  121. package/src/core/events/mascot-notify.js +156 -0
  122. package/src/core/http-tools/browser.js +366 -36
  123. package/src/core/http-tools/catalog.js +206 -22
  124. package/src/core/http-tools/grep.js +62 -7
  125. package/src/core/http-tools/registry.js +3 -1
  126. package/src/core/i18n/en.js +34 -3
  127. package/src/core/i18n/es.js +36 -4
  128. package/src/core/i18n/index.js +5 -5
  129. package/src/core/i18n/pt.js +32 -3
  130. package/src/core/identity/self.js +14 -1
  131. package/src/core/images/engines/a1111.js +219 -0
  132. package/src/core/images/engines/index.js +187 -0
  133. package/src/core/images/engines/mock.js +74 -0
  134. package/src/core/images/engines/openai.js +130 -0
  135. package/src/core/images/engines/sdcpp.js +204 -0
  136. package/src/core/images/engines/shared.js +218 -0
  137. package/src/core/images/generate.js +260 -0
  138. package/src/core/integrations/catalog.js +8 -3
  139. package/src/core/integrations/plugins/_google-oauth.js +179 -0
  140. package/src/core/integrations/plugins/asana.js +7 -3
  141. package/src/core/integrations/plugins/calendar.js +404 -0
  142. package/src/core/integrations/store.js +2 -0
  143. package/src/core/memory/broker.js +16 -1
  144. package/src/core/memory/compactor.js +19 -3
  145. package/src/core/memory/consolidate.js +2 -2
  146. package/src/core/memory/dated-log.js +66 -0
  147. package/src/core/memory/embed-engines/custom.js +59 -0
  148. package/src/core/memory/embed-engines/gemini.js +9 -3
  149. package/src/core/memory/embed-engines/index.js +87 -15
  150. package/src/core/memory/embeddings.js +52 -24
  151. package/src/core/memory/index.js +13 -3
  152. package/src/core/memory/indexer.js +83 -27
  153. package/src/core/memory/prune.js +182 -0
  154. package/src/core/memory/store.js +22 -0
  155. package/src/core/memory/summarizer.js +72 -8
  156. package/src/core/mobility/answer.js +80 -0
  157. package/src/core/mobility/geofence.js +667 -0
  158. package/src/core/mobility/osm-route.js +282 -0
  159. package/src/core/mobility/preferences.js +91 -0
  160. package/src/core/mobility/state.js +245 -0
  161. package/src/core/mobility/trip-event.js +384 -0
  162. package/src/core/net/tailscale.js +168 -0
  163. package/src/core/profiles/bundled/secretary/PROFILE.md +32 -42
  164. package/src/core/profiles/bundled/secretary/config.schema.es.json +68 -0
  165. package/src/core/profiles/bundled/secretary/config.schema.json +30 -2
  166. package/src/core/profiles/bundled/secretary/profile.json +3 -1
  167. package/src/core/profiles/bundled/secretary/routines/a2a-sweep.json +20 -0
  168. package/src/core/profiles/bundled/secretary/routines/day-close.json +4 -1
  169. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -1
  170. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -2
  171. package/src/core/profiles/lifecycle.js +243 -19
  172. package/src/core/profiles/paths.js +12 -0
  173. package/src/core/profiles/store.js +47 -0
  174. package/src/core/routines/active-runs.js +213 -0
  175. package/src/core/routines/delivery.js +612 -0
  176. package/src/core/routines/header.js +88 -0
  177. package/src/core/routines/index.js +26 -0
  178. package/src/core/routines/run-log.js +78 -0
  179. package/src/core/routines/runner.js +629 -66
  180. package/src/core/routines/signals.js +75 -0
  181. package/src/core/runtime-skills/apc-context/SKILL.md +14 -7
  182. package/src/core/runtime-skills/apx/SKILL.md +93 -1
  183. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
  184. package/src/core/runtime-skills/apx-agent/SKILL.md +134 -19
  185. package/src/core/runtime-skills/apx-commitment/SKILL.md +21 -11
  186. package/src/core/runtime-skills/apx-image/SKILL.md +60 -0
  187. package/src/core/runtime-skills/apx-integrations/SKILL.md +82 -0
  188. package/src/core/runtime-skills/apx-mcp/SKILL.md +45 -14
  189. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +4 -4
  190. package/src/core/runtime-skills/apx-profile/SKILL.md +27 -2
  191. package/src/core/runtime-skills/apx-project/SKILL.md +23 -2
  192. package/src/core/runtime-skills/apx-routine/SKILL.md +40 -17
  193. package/src/core/runtime-skills/apx-runtime/SKILL.md +124 -14
  194. package/src/core/runtime-skills/apx-sessions/SKILL.md +7 -6
  195. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +15 -13
  196. package/src/core/runtime-skills/apx-task/SKILL.md +79 -26
  197. package/src/core/runtime-skills/apx-telegram/SKILL.md +12 -12
  198. package/src/core/runtime-skills/apx-transcribe/SKILL.md +56 -0
  199. package/src/core/runtime-skills/apx-voice/SKILL.md +27 -8
  200. package/src/core/runtime-skills/claude-code/SKILL.md +8 -0
  201. package/src/core/runtime-skills/openrouter/SKILL.md +8 -2
  202. package/src/core/runtimes/_spawn.js +9 -1
  203. package/src/core/runtimes/claude-code.js +11 -2
  204. package/src/core/runtimes/codex.js +44 -10
  205. package/src/core/runtimes/index.js +17 -2
  206. package/src/core/runtimes/opencode.js +88 -9
  207. package/src/core/runtimes/outcome.js +119 -0
  208. package/src/core/sessions/index.js +193 -1
  209. package/src/core/stores/agent-inbox.js +109 -23
  210. package/src/core/stores/code-sessions.js +21 -0
  211. package/src/core/stores/commitments.js +27 -3
  212. package/src/core/stores/conversations.js +220 -20
  213. package/src/core/stores/deliveries.js +177 -0
  214. package/src/core/stores/media-archive.js +228 -0
  215. package/src/core/stores/messages.js +1096 -112
  216. package/src/core/stores/organization.js +34 -0
  217. package/src/core/stores/project-memory.js +71 -0
  218. package/src/core/stores/routine-memory.js +16 -8
  219. package/src/core/stores/routines.js +27 -49
  220. package/src/core/stores/sessions.js +4 -8
  221. package/src/core/stores/tasks.js +212 -18
  222. package/src/core/stores/turn-record.js +209 -0
  223. package/src/core/tasks/columns.js +136 -0
  224. package/src/core/tasks/comment-turn.js +258 -0
  225. package/src/core/util/time.js +13 -0
  226. package/src/core/voice/engines/index.js +54 -1
  227. package/src/core/voice/engines/openai.js +50 -4
  228. package/src/core/voice/opus.js +67 -0
  229. package/src/core/voice/pronounceable.js +43 -0
  230. package/src/core/voice/transcription.js +4 -0
  231. package/src/core/voice/tts.js +114 -24
  232. package/src/host/daemon/active-turns.js +188 -0
  233. package/src/host/daemon/api/admin-config.js +9 -0
  234. package/src/host/daemon/api/agents.js +99 -90
  235. package/src/host/daemon/api/artifact-preview.js +7 -6
  236. package/src/host/daemon/api/artifacts.js +3 -2
  237. package/src/host/daemon/api/code.js +55 -9
  238. package/src/host/daemon/api/commitments.js +11 -0
  239. package/src/host/daemon/api/confirm.js +3 -2
  240. package/src/host/daemon/api/conversations.js +543 -35
  241. package/src/host/daemon/api/deck.js +9 -8
  242. package/src/host/daemon/api/deliveries.js +33 -0
  243. package/src/host/daemon/api/desktop.js +5 -4
  244. package/src/host/daemon/api/embeddings.js +15 -6
  245. package/src/host/daemon/api/engines.js +59 -8
  246. package/src/host/daemon/api/exec.js +437 -121
  247. package/src/host/daemon/api/groups.js +215 -0
  248. package/src/host/daemon/api/images.js +107 -0
  249. package/src/host/daemon/api/inbox.js +123 -2
  250. package/src/host/daemon/api/integrations.js +64 -2
  251. package/src/host/daemon/api/mcps.js +7 -6
  252. package/src/host/daemon/api/media.js +283 -0
  253. package/src/host/daemon/api/mobility.js +177 -0
  254. package/src/host/daemon/api/net.js +182 -0
  255. package/src/host/daemon/api/pairing.js +38 -5
  256. package/src/host/daemon/api/profiles.js +30 -2
  257. package/src/host/daemon/api/routines.js +36 -4
  258. package/src/host/daemon/api/runtimes.js +74 -39
  259. package/src/host/daemon/api/sessions-search.js +7 -2
  260. package/src/host/daemon/api/sessions.js +44 -2
  261. package/src/host/daemon/api/shared.js +127 -20
  262. package/src/host/daemon/api/skills.js +11 -4
  263. package/src/host/daemon/api/super-agent.js +384 -53
  264. package/src/host/daemon/api/tasks.js +180 -10
  265. package/src/host/daemon/api/telegram.js +11 -10
  266. package/src/host/daemon/api/thread-faces.js +159 -0
  267. package/src/host/daemon/api/top-level.js +3 -3
  268. package/src/host/daemon/api/transcribe.js +11 -10
  269. package/src/host/daemon/api/tts.js +97 -4
  270. package/src/host/daemon/api/turn-abort.js +23 -0
  271. package/src/host/daemon/api/turns.js +49 -0
  272. package/src/host/daemon/api/voice.js +5 -4
  273. package/src/host/daemon/api/web.js +98 -0
  274. package/src/host/daemon/api.js +39 -0
  275. package/src/host/daemon/delivery-sweep.js +58 -0
  276. package/src/host/daemon/desktop-ws.js +5 -23
  277. package/src/host/daemon/events-ws.js +273 -0
  278. package/src/host/daemon/index.js +59 -15
  279. package/src/host/daemon/plugins/desktop/index.js +17 -4
  280. package/src/host/daemon/plugins/telegram/index.js +78 -40
  281. package/src/host/daemon/pty-bridge.py +99 -0
  282. package/src/host/daemon/routines-scheduler.js +1 -0
  283. package/src/host/daemon/smoke.js +1 -2
  284. package/src/host/daemon/terminal-ws.js +132 -0
  285. package/src/host/daemon/tts-keepwarm.js +87 -0
  286. package/src/host/daemon/wakeup.js +11 -6
  287. package/src/host/daemon/whisper-server.js +44 -0
  288. package/src/host/daemon/whisper-server.py +70 -13
  289. package/src/host/daemon/ws-auth.js +28 -0
  290. package/src/interfaces/android/app/build.gradle +42 -0
  291. package/src/interfaces/android/app/proguard-rules.pro +4 -0
  292. package/src/interfaces/android/app/src/main/AndroidManifest.xml +139 -0
  293. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AlertScreen.java +139 -0
  294. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AndroidAutoDetector.java +106 -0
  295. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxCarAppService.java +57 -0
  296. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxPreferences.java +165 -0
  297. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarAlertStore.java +84 -0
  298. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageActionReceiver.java +47 -0
  299. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageNotification.java +152 -0
  300. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonAddress.java +35 -0
  301. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonClient.java +339 -0
  302. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DeviceLocation.java +38 -0
  303. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ErrandScreen.java +127 -0
  304. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MainActivity.java +872 -0
  305. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationDetector.java +63 -0
  306. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationListenerService.java +453 -0
  307. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsShareIntentParser.java +93 -0
  308. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotBlobCatalog.java +66 -0
  309. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotOverlayService.java +448 -0
  310. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotView.java +249 -0
  311. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MessageFrameParser.java +240 -0
  312. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/NotifyChannels.java +69 -0
  313. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityActionReceiver.java +78 -0
  314. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityNotification.java +161 -0
  315. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/StartupReceiver.java +57 -0
  316. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelEventGate.java +62 -0
  317. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelStatusBanner.java +116 -0
  318. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripLocationService.java +272 -0
  319. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripPlacesSheet.java +203 -0
  320. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripScreen.java +182 -0
  321. package/src/interfaces/android/app/src/main/res/drawable/app_background.xml +12 -0
  322. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_app.xml +10 -0
  323. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_launcher_foreground.xml +12 -0
  324. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_notification.xml +22 -0
  325. package/src/interfaces/android/app/src/main/res/drawable/ic_trip_car.xml +37 -0
  326. package/src/interfaces/android/app/src/main/res/drawable-nodpi/apx_logo.png +0 -0
  327. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_amatista.png +0 -0
  328. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_aqua.png +0 -0
  329. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_brasa.png +0 -0
  330. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_campana.png +0 -0
  331. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_caracol.png +0 -0
  332. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_carbon.png +0 -0
  333. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cobalto.png +0 -0
  334. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cometa.png +0 -0
  335. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_coral.png +0 -0
  336. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cubi.png +0 -0
  337. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_eclipse.png +0 -0
  338. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_faro.png +0 -0
  339. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_gajo.png +0 -0
  340. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_iris.png +0 -0
  341. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_kiwi.png +0 -0
  342. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_laguna.png +0 -0
  343. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_lila.png +0 -0
  344. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_menta.png +0 -0
  345. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_nimbo.png +0 -0
  346. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_noche.png +0 -0
  347. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_onyx.png +0 -0
  348. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_orbita.png +0 -0
  349. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_ovni.png +0 -0
  350. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_papa.png +0 -0
  351. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_parche.png +0 -0
  352. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_peon.png +0 -0
  353. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_perla.png +0 -0
  354. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_pino.png +0 -0
  355. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_quad.png +0 -0
  356. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_rubi.png +0 -0
  357. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_saturno.png +0 -0
  358. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trebol.png +0 -0
  359. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trino.png +0 -0
  360. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_volcan.png +0 -0
  361. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_zafiro.png +0 -0
  362. package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  363. package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  364. package/src/interfaces/android/app/src/main/res/raw/apx_notification.mp3 +0 -0
  365. package/src/interfaces/android/app/src/main/res/values/colors.xml +4 -0
  366. package/src/interfaces/android/app/src/main/res/values/strings.xml +25 -0
  367. package/src/interfaces/android/app/src/main/res/values/styles.xml +10 -0
  368. package/src/interfaces/android/app/src/main/res/values-v31/styles.xml +14 -0
  369. package/src/interfaces/android/app/src/main/res/xml/automotive_app_desc.xml +9 -0
  370. package/src/interfaces/android/app/src/main/res/xml/network_security_config.xml +10 -0
  371. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/AndroidAutoDetectorTest.java +85 -0
  372. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/CarAlertStoreTest.java +104 -0
  373. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/DaemonAddressTest.java +23 -0
  374. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsNavigationDetectorTest.java +69 -0
  375. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsShareIntentParserTest.java +31 -0
  376. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MessageFrameParserTest.java +180 -0
  377. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TravelEventGateTest.java +64 -0
  378. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TripPlacesSheetTest.java +66 -0
  379. package/src/interfaces/android/build.gradle +3 -0
  380. package/src/interfaces/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  381. package/src/interfaces/android/gradle/wrapper/gradle-wrapper.properties +9 -0
  382. package/src/interfaces/android/gradle.properties +3 -0
  383. package/src/interfaces/android/gradlew +248 -0
  384. package/src/interfaces/android/gradlew.bat +82 -0
  385. package/src/interfaces/android/settings.gradle +18 -0
  386. package/src/interfaces/cli/commands/a2a.js +145 -4
  387. package/src/interfaces/cli/commands/agent.js +247 -22
  388. package/src/interfaces/cli/commands/code.js +1 -1
  389. package/src/interfaces/cli/commands/commitment.js +5 -0
  390. package/src/interfaces/cli/commands/config.js +85 -6
  391. package/src/interfaces/cli/commands/exec.js +90 -1
  392. package/src/interfaces/cli/commands/image.js +314 -0
  393. package/src/interfaces/cli/commands/mcp.js +87 -23
  394. package/src/interfaces/cli/commands/memory.js +35 -22
  395. package/src/interfaces/cli/commands/panel.js +94 -0
  396. package/src/interfaces/cli/commands/profile.js +14 -0
  397. package/src/interfaces/cli/commands/project.js +29 -0
  398. package/src/interfaces/cli/commands/routine.js +45 -10
  399. package/src/interfaces/cli/commands/runtime.js +30 -7
  400. package/src/interfaces/cli/commands/session.js +90 -38
  401. package/src/interfaces/cli/commands/sessions.js +35 -2
  402. package/src/interfaces/cli/commands/skills.js +15 -9
  403. package/src/interfaces/cli/commands/status.js +2 -3
  404. package/src/interfaces/cli/commands/task.js +54 -5
  405. package/src/interfaces/cli/commands/telegram.js +12 -4
  406. package/src/interfaces/cli/commands/transcribe.js +177 -0
  407. package/src/interfaces/cli/help/index.js +311 -72
  408. package/src/interfaces/cli/http.js +61 -13
  409. package/src/interfaces/cli/index.js +1 -1
  410. package/src/interfaces/cli/routes/agent.js +5 -4
  411. package/src/interfaces/cli/routes/commitment.js +3 -2
  412. package/src/interfaces/cli/routes/config.js +7 -2
  413. package/src/interfaces/cli/routes/image.js +11 -0
  414. package/src/interfaces/cli/routes/index.js +2 -0
  415. package/src/interfaces/cli/routes/log.js +1 -1
  416. package/src/interfaces/cli/routes/memory.js +3 -2
  417. package/src/interfaces/cli/routes/panel.js +15 -2
  418. package/src/interfaces/cli/routes/profile.js +3 -2
  419. package/src/interfaces/cli/routes/transcribe.js +10 -0
  420. package/src/interfaces/cli/stdin.js +29 -0
  421. package/src/interfaces/desktop/assets/blobs/amatista.png +0 -0
  422. package/src/interfaces/desktop/assets/blobs/aqua.png +0 -0
  423. package/src/interfaces/desktop/assets/blobs/brasa.png +0 -0
  424. package/src/interfaces/desktop/assets/blobs/campana.png +0 -0
  425. package/src/interfaces/desktop/assets/blobs/caracol.png +0 -0
  426. package/src/interfaces/desktop/assets/blobs/carbon.png +0 -0
  427. package/src/interfaces/desktop/assets/blobs/cobalto.png +0 -0
  428. package/src/interfaces/desktop/assets/blobs/cometa.png +0 -0
  429. package/src/interfaces/desktop/assets/blobs/coral.png +0 -0
  430. package/src/interfaces/desktop/assets/blobs/cubi.png +0 -0
  431. package/src/interfaces/desktop/assets/blobs/eclipse.png +0 -0
  432. package/src/interfaces/desktop/assets/blobs/faro.png +0 -0
  433. package/src/interfaces/desktop/assets/blobs/gajo.png +0 -0
  434. package/src/interfaces/desktop/assets/blobs/iris.png +0 -0
  435. package/src/interfaces/desktop/assets/blobs/kiwi.png +0 -0
  436. package/src/interfaces/desktop/assets/blobs/laguna.png +0 -0
  437. package/src/interfaces/desktop/assets/blobs/lila.png +0 -0
  438. package/src/interfaces/desktop/assets/blobs/menta.png +0 -0
  439. package/src/interfaces/desktop/assets/blobs/nimbo.png +0 -0
  440. package/src/interfaces/desktop/assets/blobs/noche.png +0 -0
  441. package/src/interfaces/desktop/assets/blobs/onyx.png +0 -0
  442. package/src/interfaces/desktop/assets/blobs/orbita.png +0 -0
  443. package/src/interfaces/desktop/assets/blobs/ovni.png +0 -0
  444. package/src/interfaces/desktop/assets/blobs/papa.png +0 -0
  445. package/src/interfaces/desktop/assets/blobs/parche.png +0 -0
  446. package/src/interfaces/desktop/assets/blobs/peon.png +0 -0
  447. package/src/interfaces/desktop/assets/blobs/perla.png +0 -0
  448. package/src/interfaces/desktop/assets/blobs/pino.png +0 -0
  449. package/src/interfaces/desktop/assets/blobs/quad.png +0 -0
  450. package/src/interfaces/desktop/assets/blobs/rubi.png +0 -0
  451. package/src/interfaces/desktop/assets/blobs/saturno.png +0 -0
  452. package/src/interfaces/desktop/assets/blobs/trebol.png +0 -0
  453. package/src/interfaces/desktop/assets/blobs/trino.png +0 -0
  454. package/src/interfaces/desktop/assets/blobs/volcan.png +0 -0
  455. package/src/interfaces/desktop/assets/blobs/zafiro.png +0 -0
  456. package/src/interfaces/desktop/assets/notification.mp3 +0 -0
  457. package/src/interfaces/desktop/main.js +473 -29
  458. package/src/interfaces/desktop/mascot-blobs.js +41 -0
  459. package/src/interfaces/desktop/mascot-preload.js +32 -0
  460. package/src/interfaces/desktop/mascot.css +186 -0
  461. package/src/interfaces/desktop/mascot.html +40 -0
  462. package/src/interfaces/desktop/mascot.js +163 -0
  463. package/src/interfaces/desktop/preload.js +10 -0
  464. package/src/interfaces/desktop/renderer.js +308 -48
  465. package/src/interfaces/desktop/style.css +34 -0
  466. package/src/interfaces/mcp-server/index.js +87 -0
  467. package/src/interfaces/tui/_shims/util-locale.ts +0 -2
  468. package/src/interfaces/tui/app.tsx +0 -6
  469. package/src/interfaces/tui/attach.ts +0 -1
  470. package/src/interfaces/tui/context/sdk-apx.tsx +0 -4
  471. package/src/interfaces/tui/plugin/api.tsx +0 -3
  472. package/src/interfaces/tui/plugin/runtime.ts +0 -3
  473. package/src/interfaces/tui/thread.ts +0 -16
  474. package/src/interfaces/web/README.md +22 -14
  475. package/src/interfaces/web/dist/assets/claude-XtpLmGtF.webp +0 -0
  476. package/src/interfaces/web/dist/assets/codex-DExPUgvh.webp +0 -0
  477. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css +32 -0
  478. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.br +0 -0
  479. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.gz +0 -0
  480. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js +1074 -0
  481. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.br +0 -0
  482. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.gz +0 -0
  483. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map +1 -0
  484. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.br +0 -0
  485. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.gz +0 -0
  486. package/src/interfaces/web/dist/favicon/dark/site.webmanifest +12 -6
  487. package/src/interfaces/web/dist/favicon/white/site.webmanifest +12 -6
  488. package/src/interfaces/web/dist/index.html +22 -6
  489. package/src/interfaces/web/dist/index.html.br +0 -0
  490. package/src/interfaces/web/dist/index.html.gz +0 -0
  491. package/src/interfaces/web/dist/manifest.webmanifest +25 -0
  492. package/src/interfaces/web/dist/modules/blobs/amatista.png +0 -0
  493. package/src/interfaces/web/dist/modules/blobs/aqua.png +0 -0
  494. package/src/interfaces/web/dist/modules/blobs/brasa.png +0 -0
  495. package/src/interfaces/web/dist/modules/blobs/campana.png +0 -0
  496. package/src/interfaces/web/dist/modules/blobs/caracol.png +0 -0
  497. package/src/interfaces/web/dist/modules/blobs/carbon.png +0 -0
  498. package/src/interfaces/web/dist/modules/blobs/cobalto.png +0 -0
  499. package/src/interfaces/web/dist/modules/blobs/cometa.png +0 -0
  500. package/src/interfaces/web/dist/modules/blobs/coral.png +0 -0
  501. package/src/interfaces/web/dist/modules/blobs/cubi.png +0 -0
  502. package/src/interfaces/web/dist/modules/blobs/eclipse.png +0 -0
  503. package/src/interfaces/web/dist/modules/blobs/faro.png +0 -0
  504. package/src/interfaces/web/dist/modules/blobs/gajo.png +0 -0
  505. package/src/interfaces/web/dist/modules/blobs/iris.png +0 -0
  506. package/src/interfaces/web/dist/modules/blobs/kiwi.png +0 -0
  507. package/src/interfaces/web/dist/modules/blobs/laguna.png +0 -0
  508. package/src/interfaces/web/dist/modules/blobs/lila.png +0 -0
  509. package/src/interfaces/web/dist/modules/blobs/menta.png +0 -0
  510. package/src/interfaces/web/dist/modules/blobs/nimbo.png +0 -0
  511. package/src/interfaces/web/dist/modules/blobs/noche.png +0 -0
  512. package/src/interfaces/web/dist/modules/blobs/onyx.png +0 -0
  513. package/src/interfaces/web/dist/modules/blobs/orbita.png +0 -0
  514. package/src/interfaces/web/dist/modules/blobs/ovni.png +0 -0
  515. package/src/interfaces/web/dist/modules/blobs/papa.png +0 -0
  516. package/src/interfaces/web/dist/modules/blobs/parche.png +0 -0
  517. package/src/interfaces/web/dist/modules/blobs/peon.png +0 -0
  518. package/src/interfaces/web/dist/modules/blobs/perla.png +0 -0
  519. package/src/interfaces/web/dist/modules/blobs/pino.png +0 -0
  520. package/src/interfaces/web/dist/modules/blobs/quad.png +0 -0
  521. package/src/interfaces/web/dist/modules/blobs/rubi.png +0 -0
  522. package/src/interfaces/web/dist/modules/blobs/saturno.png +0 -0
  523. package/src/interfaces/web/dist/modules/blobs/trebol.png +0 -0
  524. package/src/interfaces/web/dist/modules/blobs/trino.png +0 -0
  525. package/src/interfaces/web/dist/modules/blobs/volcan.png +0 -0
  526. package/src/interfaces/web/dist/modules/blobs/zafiro.png +0 -0
  527. package/src/interfaces/web/dist/sw.js +106 -0
  528. package/src/interfaces/web/dist/sw.js.br +0 -0
  529. package/src/interfaces/web/dist/sw.js.gz +0 -0
  530. package/src/interfaces/web/eslint.config.js +83 -0
  531. package/src/interfaces/web/index.html +20 -4
  532. package/src/interfaces/web/package-lock.json +1651 -1280
  533. package/src/interfaces/web/package.json +9 -10
  534. package/src/interfaces/web/pnpm-lock.yaml +867 -869
  535. package/src/interfaces/web/public/favicon/dark/site.webmanifest +12 -6
  536. package/src/interfaces/web/public/favicon/white/site.webmanifest +12 -6
  537. package/src/interfaces/web/public/manifest.webmanifest +25 -0
  538. package/src/interfaces/web/public/modules/blobs/amatista.png +0 -0
  539. package/src/interfaces/web/public/modules/blobs/aqua.png +0 -0
  540. package/src/interfaces/web/public/modules/blobs/brasa.png +0 -0
  541. package/src/interfaces/web/public/modules/blobs/campana.png +0 -0
  542. package/src/interfaces/web/public/modules/blobs/caracol.png +0 -0
  543. package/src/interfaces/web/public/modules/blobs/carbon.png +0 -0
  544. package/src/interfaces/web/public/modules/blobs/cobalto.png +0 -0
  545. package/src/interfaces/web/public/modules/blobs/cometa.png +0 -0
  546. package/src/interfaces/web/public/modules/blobs/coral.png +0 -0
  547. package/src/interfaces/web/public/modules/blobs/cubi.png +0 -0
  548. package/src/interfaces/web/public/modules/blobs/eclipse.png +0 -0
  549. package/src/interfaces/web/public/modules/blobs/faro.png +0 -0
  550. package/src/interfaces/web/public/modules/blobs/gajo.png +0 -0
  551. package/src/interfaces/web/public/modules/blobs/iris.png +0 -0
  552. package/src/interfaces/web/public/modules/blobs/kiwi.png +0 -0
  553. package/src/interfaces/web/public/modules/blobs/laguna.png +0 -0
  554. package/src/interfaces/web/public/modules/blobs/lila.png +0 -0
  555. package/src/interfaces/web/public/modules/blobs/menta.png +0 -0
  556. package/src/interfaces/web/public/modules/blobs/nimbo.png +0 -0
  557. package/src/interfaces/web/public/modules/blobs/noche.png +0 -0
  558. package/src/interfaces/web/public/modules/blobs/onyx.png +0 -0
  559. package/src/interfaces/web/public/modules/blobs/orbita.png +0 -0
  560. package/src/interfaces/web/public/modules/blobs/ovni.png +0 -0
  561. package/src/interfaces/web/public/modules/blobs/papa.png +0 -0
  562. package/src/interfaces/web/public/modules/blobs/parche.png +0 -0
  563. package/src/interfaces/web/public/modules/blobs/peon.png +0 -0
  564. package/src/interfaces/web/public/modules/blobs/perla.png +0 -0
  565. package/src/interfaces/web/public/modules/blobs/pino.png +0 -0
  566. package/src/interfaces/web/public/modules/blobs/quad.png +0 -0
  567. package/src/interfaces/web/public/modules/blobs/rubi.png +0 -0
  568. package/src/interfaces/web/public/modules/blobs/saturno.png +0 -0
  569. package/src/interfaces/web/public/modules/blobs/trebol.png +0 -0
  570. package/src/interfaces/web/public/modules/blobs/trino.png +0 -0
  571. package/src/interfaces/web/public/modules/blobs/volcan.png +0 -0
  572. package/src/interfaces/web/public/modules/blobs/zafiro.png +0 -0
  573. package/src/interfaces/web/public/sw.js +106 -0
  574. package/src/interfaces/web/src/App.tsx +106 -10
  575. package/src/interfaces/web/src/assets/cli/claude.webp +0 -0
  576. package/src/interfaces/web/src/assets/cli/codex.webp +0 -0
  577. package/src/interfaces/web/src/assets/cli/cursor.svg +1 -0
  578. package/src/interfaces/web/src/assets/cli/opencode.png +0 -0
  579. package/src/interfaces/web/src/components/Combobox.tsx +201 -0
  580. package/src/interfaces/web/src/components/MobileHint.tsx +95 -0
  581. package/src/interfaces/web/src/components/MobileLinkDialog.tsx +156 -0
  582. package/src/interfaces/web/src/components/ModelCombobox.tsx +21 -113
  583. package/src/interfaces/web/src/components/{routines/ReadOnlyBlock.tsx → ReadOnlyBlock.tsx} +13 -4
  584. package/src/interfaces/web/src/components/Roby.tsx +5 -3
  585. package/src/interfaces/web/src/components/RobyBubble.tsx +23 -5
  586. package/src/interfaces/web/src/components/RowMenu.tsx +46 -0
  587. package/src/interfaces/web/src/components/TimezoneSelect.tsx +4 -1
  588. package/src/interfaces/web/src/components/Toast.tsx +50 -14
  589. package/src/interfaces/web/src/components/UiSelect.tsx +12 -5
  590. package/src/interfaces/web/src/components/agents/AgentAvatar.tsx +171 -0
  591. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +42 -11
  592. package/src/interfaces/web/src/components/agents/AgentModelBadge.tsx +157 -0
  593. package/src/interfaces/web/src/components/agents/AgentModelSelect.tsx +57 -0
  594. package/src/interfaces/web/src/components/agents/AgentSkillsPicker.tsx +115 -0
  595. package/src/interfaces/web/src/components/agents/BlobAvatar.tsx +50 -0
  596. package/src/interfaces/web/src/components/agents/blobPresets.ts +50 -0
  597. package/src/interfaces/web/src/components/agents/modelCatalog.ts +66 -0
  598. package/src/interfaces/web/src/components/chat/ActionGroup.tsx +162 -0
  599. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +4 -3
  600. package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +1 -1
  601. package/src/interfaces/web/src/components/chat/Attachment.tsx +220 -0
  602. package/src/interfaces/web/src/components/chat/ChatList.tsx +226 -51
  603. package/src/interfaces/web/src/components/chat/ChatRowActivity.tsx +45 -0
  604. package/src/interfaces/web/src/components/chat/Composer.tsx +672 -23
  605. package/src/interfaces/web/src/components/chat/ComposerCommands.tsx +175 -0
  606. package/src/interfaces/web/src/components/chat/ComposerSuggest.tsx +215 -0
  607. package/src/interfaces/web/src/components/chat/ContextBar.tsx +133 -65
  608. package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +107 -47
  609. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +460 -119
  610. package/src/interfaces/web/src/components/chat/MessageList.tsx +165 -18
  611. package/src/interfaces/web/src/components/chat/ReasoningBlock.tsx +58 -0
  612. package/src/interfaces/web/src/components/chat/SessionPicker.tsx +182 -0
  613. package/src/interfaces/web/src/components/chat/SkillTrace.tsx +183 -0
  614. package/src/interfaces/web/src/components/chat/ToolCall.tsx +11 -11
  615. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +87 -53
  616. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +11 -52
  617. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +5 -4
  618. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +31 -7
  619. package/src/interfaces/web/src/components/code/CodeSessionList.tsx +32 -11
  620. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +33 -3
  621. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +5 -4
  622. package/src/interfaces/web/src/components/code/NewCodeSessionDialog.tsx +164 -0
  623. package/src/interfaces/web/src/components/commitments/CommitmentDetail.tsx +162 -0
  624. package/src/interfaces/web/src/components/commitments/CommitmentFormDialog.tsx +158 -0
  625. package/src/interfaces/web/src/components/commitments/CommitmentList.tsx +194 -0
  626. package/src/interfaces/web/src/components/commitments/commitmentState.tsx +46 -0
  627. package/src/interfaces/web/src/components/common/BulkActionBar.tsx +60 -0
  628. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +4 -1
  629. package/src/interfaces/web/src/components/common/SelectCheckbox.tsx +40 -0
  630. package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
  631. package/src/interfaces/web/src/components/cron/CronPicker.tsx +1 -1
  632. package/src/interfaces/web/src/components/files/FileBrowser.tsx +1 -1
  633. package/src/interfaces/web/src/components/files/FileTree.tsx +6 -4
  634. package/src/interfaces/web/src/components/files/FileViewer.tsx +9 -5
  635. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +45 -9
  636. package/src/interfaces/web/src/components/images/ImageDefaultsCard.tsx +93 -0
  637. package/src/interfaces/web/src/components/images/ImageProviderList.tsx +142 -0
  638. package/src/interfaces/web/src/components/images/ImageProviderModal.tsx +263 -0
  639. package/src/interfaces/web/src/components/images/ImageTestCard.tsx +142 -0
  640. package/src/interfaces/web/src/components/inbox/ChannelFilter.tsx +178 -0
  641. package/src/interfaces/web/src/components/inbox/InboxList.tsx +77 -81
  642. package/src/interfaces/web/src/components/inbox/InboxRowItem.tsx +140 -0
  643. package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +2 -2
  644. package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +208 -30
  645. package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +1 -1
  646. package/src/interfaces/web/src/components/layout/ProjectAvatar.tsx +39 -24
  647. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +253 -58
  648. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +94 -47
  649. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +202 -90
  650. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +28 -6
  651. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +16 -5
  652. package/src/interfaces/web/src/components/routines/RoutineList.tsx +49 -9
  653. package/src/interfaces/web/src/components/routines/VarTextarea.tsx +1 -1
  654. package/src/interfaces/web/src/components/routines/runChat.ts +121 -0
  655. package/src/interfaces/web/src/components/sessions/SessionDetail.tsx +148 -0
  656. package/src/interfaces/web/src/components/sessions/SessionTerminal.tsx +98 -0
  657. package/src/interfaces/web/src/components/settings/AccessPanel.tsx +205 -0
  658. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +246 -101
  659. package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +36 -2
  660. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +19 -6
  661. package/src/interfaces/web/src/components/settings/EmbedProviderList.tsx +274 -0
  662. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +26 -6
  663. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +118 -119
  664. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +4 -3
  665. package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +15 -4
  666. package/src/interfaces/web/src/components/settings/PanelPrefs.tsx +374 -0
  667. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +70 -2
  668. package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +2 -1
  669. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +102 -43
  670. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +76 -21
  671. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +10 -3
  672. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +8 -0
  673. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +11 -2
  674. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +19 -8
  675. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +12 -2
  676. package/src/interfaces/web/src/components/settings/WebPanel.tsx +22 -31
  677. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +15 -3
  678. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +97 -66
  679. package/src/interfaces/web/src/components/settings/providers/ProviderTestDialog.tsx +150 -0
  680. package/src/interfaces/web/src/components/settings/providers/typeStyles.ts +67 -25
  681. package/src/interfaces/web/src/components/settings/providers/types.ts +3 -0
  682. package/src/interfaces/web/src/components/tasks/TaskBoard.tsx +201 -0
  683. package/src/interfaces/web/src/components/tasks/TaskColumnsDialog.tsx +243 -0
  684. package/src/interfaces/web/src/components/tasks/TaskComments.tsx +167 -0
  685. package/src/interfaces/web/src/components/tasks/TaskDetail.tsx +335 -0
  686. package/src/interfaces/web/src/components/tasks/TaskFormDialog.tsx +406 -0
  687. package/src/interfaces/web/src/components/tasks/TaskList.tsx +258 -0
  688. package/src/interfaces/web/src/components/tasks/TaskSubtasks.tsx +126 -0
  689. package/src/interfaces/web/src/components/tasks/columns.ts +51 -0
  690. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +159 -34
  691. package/src/interfaces/web/src/components/tasks/taskViewPrefs.ts +73 -0
  692. package/src/interfaces/web/src/components/tasks/useTaskColumns.ts +31 -0
  693. package/src/interfaces/web/src/components/ui/chat-input.tsx +136 -12
  694. package/src/interfaces/web/src/components/ui/context-menu.tsx +15 -198
  695. package/src/interfaces/web/src/components/ui/dialog.tsx +3 -2
  696. package/src/interfaces/web/src/components/ui/filter-chips.tsx +4 -0
  697. package/src/interfaces/web/src/components/ui/popover.tsx +14 -49
  698. package/src/interfaces/web/src/components/ui/sheet.tsx +2 -1
  699. package/src/interfaces/web/src/components/ui/spinner.tsx +2 -1
  700. package/src/interfaces/web/src/components/ui.tsx +62 -23
  701. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +2 -1
  702. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +66 -8
  703. package/src/interfaces/web/src/components/voice/VoiceQvoxInstallCard.tsx +63 -0
  704. package/src/interfaces/web/src/components/voice/VoiceQvoxStatusCard.tsx +51 -0
  705. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +4 -3
  706. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +38 -4
  707. package/src/interfaces/web/src/constants/index.ts +1 -0
  708. package/src/interfaces/web/src/hooks/useChannelPrefs.ts +39 -0
  709. package/src/interfaces/web/src/hooks/useChat.ts +1092 -85
  710. package/src/interfaces/web/src/hooks/useChatActivity.ts +24 -0
  711. package/src/interfaces/web/src/hooks/useInbox.ts +26 -4
  712. package/src/interfaces/web/src/hooks/useLiveMessages.ts +16 -0
  713. package/src/interfaces/web/src/hooks/useOllamaModels.ts +79 -0
  714. package/src/interfaces/web/src/hooks/useRecorder.ts +198 -0
  715. package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +23 -6
  716. package/src/interfaces/web/src/i18n/en.ts +832 -175
  717. package/src/interfaces/web/src/i18n/es.ts +841 -188
  718. package/src/interfaces/web/src/lib/api/agents.ts +23 -4
  719. package/src/interfaces/web/src/lib/api/code.ts +17 -0
  720. package/src/interfaces/web/src/lib/api/commitments.ts +7 -1
  721. package/src/interfaces/web/src/lib/api/conversations.ts +38 -11
  722. package/src/interfaces/web/src/lib/api/embeddings.ts +3 -0
  723. package/src/interfaces/web/src/lib/api/engines.ts +14 -0
  724. package/src/interfaces/web/src/lib/api/groups.ts +93 -0
  725. package/src/interfaces/web/src/lib/api/images.ts +193 -0
  726. package/src/interfaces/web/src/lib/api/inbox.ts +29 -5
  727. package/src/interfaces/web/src/lib/api/integrations.ts +9 -0
  728. package/src/interfaces/web/src/lib/api/media.ts +126 -0
  729. package/src/interfaces/web/src/lib/api/net.ts +44 -0
  730. package/src/interfaces/web/src/lib/api/profiles.ts +12 -0
  731. package/src/interfaces/web/src/lib/api/projects.ts +9 -1
  732. package/src/interfaces/web/src/lib/api/routines.ts +9 -1
  733. package/src/interfaces/web/src/lib/api/sessions.ts +18 -0
  734. package/src/interfaces/web/src/lib/api/skills.ts +8 -0
  735. package/src/interfaces/web/src/lib/api/super_agent.ts +3 -0
  736. package/src/interfaces/web/src/lib/api/tasks.ts +46 -6
  737. package/src/interfaces/web/src/lib/api/tools.ts +2 -0
  738. package/src/interfaces/web/src/lib/api/transcribe.ts +31 -0
  739. package/src/interfaces/web/src/lib/api/turns.ts +20 -0
  740. package/src/interfaces/web/src/lib/api/voice.ts +72 -2
  741. package/src/interfaces/web/src/lib/api.ts +3 -0
  742. package/src/interfaces/web/src/lib/channels.ts +214 -0
  743. package/src/interfaces/web/src/lib/chat-activity.ts +175 -0
  744. package/src/interfaces/web/src/lib/chat-prefs.ts +48 -0
  745. package/src/interfaces/web/src/lib/code-context.ts +0 -2
  746. package/src/interfaces/web/src/lib/http.ts +51 -17
  747. package/src/interfaces/web/src/lib/inbox-selection.ts +29 -0
  748. package/src/interfaces/web/src/lib/live.ts +252 -0
  749. package/src/interfaces/web/src/lib/net.ts +211 -0
  750. package/src/interfaces/web/src/lib/notify.ts +331 -0
  751. package/src/interfaces/web/src/lib/projectNav.ts +40 -0
  752. package/src/interfaces/web/src/lib/pwa.ts +115 -0
  753. package/src/interfaces/web/src/lib/qvox.ts +24 -0
  754. package/src/interfaces/web/src/lib/slug.ts +3 -0
  755. package/src/interfaces/web/src/lib/tone.ts +150 -0
  756. package/src/interfaces/web/src/main.tsx +15 -4
  757. package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +33 -11
  758. package/src/interfaces/web/src/screens/InboxScreen.tsx +126 -71
  759. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -4
  760. package/src/interfaces/web/src/screens/SettingsScreen.tsx +7 -4
  761. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +30 -9
  762. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +150 -168
  763. package/src/interfaces/web/src/screens/base/LogsTab.tsx +8 -7
  764. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +99 -37
  765. package/src/interfaces/web/src/screens/base/WorkspacesTab.tsx +1 -1
  766. package/src/interfaces/web/src/screens/mobile/MobileChat.tsx +85 -0
  767. package/src/interfaces/web/src/screens/mobile/MobileChatList.tsx +244 -0
  768. package/src/interfaces/web/src/screens/mobile/MobileCommitments.tsx +350 -0
  769. package/src/interfaces/web/src/screens/mobile/MobileScreen.tsx +138 -0
  770. package/src/interfaces/web/src/screens/mobile/MobileTabBar.tsx +105 -0
  771. package/src/interfaces/web/src/screens/mobile/MobileTasks.tsx +407 -0
  772. package/src/interfaces/web/src/screens/mobile/NewChatSheet.tsx +313 -0
  773. package/src/interfaces/web/src/screens/mobile/mobileList.tsx +194 -0
  774. package/src/interfaces/web/src/screens/mobile/routes.ts +239 -0
  775. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +135 -64
  776. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +0 -1
  777. package/src/interfaces/web/src/screens/modules/ImagesScreen.tsx +172 -0
  778. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +36 -4
  779. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +117 -30
  780. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +537 -92
  781. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +344 -93
  782. package/src/interfaces/web/src/screens/project/ArtifactsTab.tsx +1 -1
  783. package/src/interfaces/web/src/screens/project/ChatTab.tsx +1160 -94
  784. package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +3 -2
  785. package/src/interfaces/web/src/screens/project/McpsTab.tsx +62 -14
  786. package/src/interfaces/web/src/screens/project/Overview.tsx +36 -15
  787. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +93 -8
  788. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +4 -1
  789. package/src/interfaces/web/src/screens/project/StructureTab.tsx +5 -4
  790. package/src/interfaces/web/src/screens/project/TasksTab.tsx +351 -129
  791. package/src/interfaces/web/src/screens/project/VarsTab.tsx +17 -5
  792. package/src/interfaces/web/src/styles.css +106 -31
  793. package/src/interfaces/web/src/types/daemon.ts +246 -2
  794. package/src/interfaces/web/src/vite-env.d.ts +20 -0
  795. package/src/interfaces/tui/plugin/command-shim.ts +0 -109
  796. package/src/interfaces/web/components.json +0 -25
  797. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +0 -849
  798. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +0 -1
  799. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +0 -1
  800. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +0 -142
  801. package/src/interfaces/web/src/components/ui/accordion.tsx +0 -72
  802. package/src/interfaces/web/src/components/ui/alert-dialog.tsx +0 -187
  803. package/src/interfaces/web/src/components/ui/alert.tsx +0 -76
  804. package/src/interfaces/web/src/components/ui/aspect-ratio.tsx +0 -22
  805. package/src/interfaces/web/src/components/ui/avatar.tsx +0 -107
  806. package/src/interfaces/web/src/components/ui/breadcrumb.tsx +0 -125
  807. package/src/interfaces/web/src/components/ui/button-group.tsx +0 -87
  808. package/src/interfaces/web/src/components/ui/calendar.tsx +0 -221
  809. package/src/interfaces/web/src/components/ui/card.tsx +0 -103
  810. package/src/interfaces/web/src/components/ui/carousel.tsx +0 -242
  811. package/src/interfaces/web/src/components/ui/chart.tsx +0 -371
  812. package/src/interfaces/web/src/components/ui/checkbox.tsx +0 -29
  813. package/src/interfaces/web/src/components/ui/collapsible.tsx +0 -19
  814. package/src/interfaces/web/src/components/ui/combobox.tsx +0 -295
  815. package/src/interfaces/web/src/components/ui/command.tsx +0 -196
  816. package/src/interfaces/web/src/components/ui/direction.tsx +0 -4
  817. package/src/interfaces/web/src/components/ui/drawer.tsx +0 -134
  818. package/src/interfaces/web/src/components/ui/empty.tsx +0 -104
  819. package/src/interfaces/web/src/components/ui/field.tsx +0 -236
  820. package/src/interfaces/web/src/components/ui/hover-card.tsx +0 -51
  821. package/src/interfaces/web/src/components/ui/input-otp.tsx +0 -85
  822. package/src/interfaces/web/src/components/ui/item.tsx +0 -201
  823. package/src/interfaces/web/src/components/ui/menubar.tsx +0 -280
  824. package/src/interfaces/web/src/components/ui/native-select.tsx +0 -61
  825. package/src/interfaces/web/src/components/ui/navigation-menu.tsx +0 -168
  826. package/src/interfaces/web/src/components/ui/pagination.tsx +0 -130
  827. package/src/interfaces/web/src/components/ui/progress.tsx +0 -83
  828. package/src/interfaces/web/src/components/ui/radio-group.tsx +0 -36
  829. package/src/interfaces/web/src/components/ui/resizable.tsx +0 -50
  830. package/src/interfaces/web/src/components/ui/scroll-area.tsx +0 -53
  831. package/src/interfaces/web/src/components/ui/sidebar.tsx +0 -726
  832. package/src/interfaces/web/src/components/ui/slider.tsx +0 -52
  833. package/src/interfaces/web/src/components/ui/sonner.tsx +0 -49
  834. package/src/interfaces/web/src/components/ui/table.tsx +0 -116
  835. package/src/interfaces/web/src/components/ui/toggle-group.tsx +0 -87
  836. package/src/interfaces/web/src/components/ui/toggle.tsx +0 -45
  837. package/src/interfaces/web/src/hooks/use-mobile.ts +0 -19
  838. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +0 -161
@@ -1,19 +1,45 @@
1
- import { useEffect, useMemo, useState } from "react";
2
- import { useSearchParams } from "react-router-dom";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useNavigate, useSearchParams } from "react-router-dom";
3
3
  import useSWR, { mutate } from "swr";
4
- import { Plus, RotateCcw, Trash2 } from "lucide-react";
5
- import { Agents, Conversations } from "../../lib/api";
6
- import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch } from "../../components/ui";
4
+ import { Archive, ArchiveRestore, ArrowDown, ArrowUpRight, ChevronLeft, Eye, MessageSquareDashed, MoreVertical, Pencil, Plus, RotateCcw, Trash2, UserPlus, Wrench, X } from "lucide-react";
5
+ import { Agents, Conversations, Groups } from "../../lib/api";
6
+ import { Button, Dialog, Empty, Field, Input, Loading, Switch, Tip } from "../../components/ui";
7
7
  import { Composer } from "../../components/chat/Composer";
8
8
  import { MessageList } from "../../components/chat/MessageList";
9
9
  import { ContextBar } from "../../components/chat/ContextBar";
10
10
  import { InlineAskPanel, pendingAskQuestions } from "../../components/chat/InlineAskPanel";
11
- import { ChatList, type ChatKey, type ChatSelectionMeta } from "../../components/chat/ChatList";
12
- import { useChat } from "../../hooks/useChat";
11
+ import { ChatList, chatKeyToString, type ChatKey, type ChatSelectionMeta } from "../../components/chat/ChatList";
12
+ import { queryForChat } from "../mobile/routes";
13
+ import { SessionPicker } from "../../components/chat/SessionPicker";
14
+ import { messageForInvite, parseInviteCommand } from "../../components/chat/ComposerCommands";
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuGroup,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from "../../components/ui/dropdown-menu";
24
+ import { attachmentsOf, useChat, type ChatMsg } from "../../hooks/useChat";
25
+ import { useLiveMessages } from "../../hooks/useLiveMessages";
26
+ import { concernsConversation, concernsThread, type LiveEvent } from "../../lib/live";
27
+ import type { UploadedMedia } from "../../lib/api/media";
13
28
  import { useToast } from "../../components/Toast";
29
+ import { cn } from "../../lib/cn";
30
+ import { isNativeShell } from "../../lib/net";
14
31
  import { t } from "../../i18n";
32
+ import { toneChip } from "../../lib/tone";
15
33
  import { usePersonaName } from "../../hooks/usePersonaName";
16
- import type { AgentEntry } from "../../types/daemon";
34
+ import { useSuperAgentConfig } from "../../hooks/useGlobalConfig";
35
+ import { AgentAvatar, AgentAvatarGroup, SUPER_AGENT_ICON, type AgentFace } from "../../components/agents/AgentAvatar";
36
+ import type { AgentEntry, ConversationListEntry } from "../../types/daemon";
37
+ import { useChatVisibility } from "../../hooks/useChatActivity";
38
+ import {
39
+ conversationActivityKey,
40
+ liveActivityKey,
41
+ threadActivityKey,
42
+ } from "../../lib/chat-activity";
17
43
 
18
44
  // Virtual entry slug used in the agent dropdown to address the daemon-level
19
45
  // super-agent (persona "Roby" for the owner). Picked so it can't collide
@@ -28,22 +54,110 @@ const ROBY_SLUG = "__super_agent__";
28
54
  export function ChatTab({
29
55
  pid,
30
56
  hideSidebar = false,
57
+ hideHeader = false,
31
58
  initialSelection,
59
+ bare = false,
60
+ onOpenInProject,
61
+ compact = false,
62
+ onBack,
63
+ onSelectionChange,
64
+ channelScope,
32
65
  }: {
33
66
  pid: string;
34
67
  hideSidebar?: boolean;
68
+ /** Drop the thread header. The phone surface draws its own — with the back
69
+ * button and the session switcher on it — and two stacked headers naming the
70
+ * same agent is a rendering bug, not a layout. */
71
+ hideHeader?: boolean;
35
72
  /** Open a specific conversation on mount instead of a fresh live session. */
36
73
  initialSelection?: ChatKey;
74
+ /** Drop the card chrome. An embedding screen already draws a panel around
75
+ * this, and a card inside a card reads as a rendering bug. */
76
+ bare?: boolean;
77
+ /** Structural way out from the inbox axis (leave inbox → project / agent card).
78
+ * When omitted, a 1:1 project-agent chat defaults to opening that agent's
79
+ * ficha (`/p/:pid/agents/:slug`) via the face and the ⋯ menu. */
80
+ onOpenInProject?: () => void;
81
+ /** Phone shaping: no avatar column, wider bubbles, and a header sized for a
82
+ * thumb with its actions folded behind one ⋯. The phone surface sets it; the
83
+ * desktop pane never does. */
84
+ compact?: boolean;
85
+ /** Renders the back arrow in the header. Only the phone has anywhere to go
86
+ * back TO — inside a project the chat is a tab, not a screen. */
87
+ onBack?: () => void;
88
+ /** The host owns the URL. Passed by the phone surface, where a session is a
89
+ * path segment rather than a query string, so picking one has to navigate
90
+ * instead of writing `?conv=` that a reload would not read back. */
91
+ onSelectionChange?: (key: ChatKey) => void;
92
+ /** Limit the session switcher to one channel. The inbox and the phone pass
93
+ * "web" so their switcher never offers a Telegram thread; project-first
94
+ * navigation omits it and keeps every channel. */
95
+ channelScope?: string;
37
96
  }) {
38
97
  const toast = useToast();
98
+ const navigate = useNavigate();
39
99
  const [params, setSearchParams] = useSearchParams();
40
100
  const agents = useSWR(`/api/projects/${pid}/agents`, () => Agents.list(pid));
41
101
  const [creating, setCreating] = useState(false);
42
102
  const [model, setModel] = useState("");
43
103
  const [dismissedAskKey, setDismissedAskKey] = useState<string | null>(null);
44
- const { msgs, send: sendChat, stop, clear, load, loadThread, streaming } =
104
+ const { msgs, send: sendChat, sendGroup, regenerate, editAndResend, stop, clear, load, loadThread, streaming, queued, unqueue, conversationId, conversationMeta } =
45
105
  useChat(pid, (m) => toast.error(m));
46
106
  const persona = usePersonaName();
107
+ const { superAgent } = useSuperAgentConfig();
108
+ const superAgentIcon = superAgent?.icon || SUPER_AGENT_ICON;
109
+
110
+ // How tall the floating dock currently is. It moves constantly — a draft
111
+ // wrapping to a third line, an attachment strip appearing, the context panel
112
+ // opening — and the thread needs the live number, not a guess, or the last
113
+ // message ends up behind the field exactly when you are reading it.
114
+ //
115
+ // A callback ref, not an effect on a ref object: this component renders
116
+ // <Loading/> until the agent list arrives, so a mount effect would look for a
117
+ // dock that does not exist yet, find null, and never run again — leaving the
118
+ // inset at zero and the field parked on top of the last three lines.
119
+ const [dockH, setDockH] = useState(0);
120
+ // While the context detail is open the dock is temporarily much taller, and
121
+ // the thread must NOT reserve that: opening a panel for two seconds should
122
+ // cover the conversation, not shove it up and drop it back down. So the inset
123
+ // is the last height measured with the panel shut.
124
+ const [ctxOpen, setCtxOpen] = useState(false);
125
+ const restingDock = useRef(0);
126
+ if (!ctxOpen) restingDock.current = dockH;
127
+ const bottomInset = ctxOpen ? restingDock.current : dockH;
128
+
129
+ // Whether the reader is following the end of the thread, and what they have
130
+ // missed while they were not. Nothing scrolls on its own any more, so the
131
+ // count is the only way to know something arrived while you were reading
132
+ // further up — an arrow that appears silently is easy to look straight past.
133
+ const scrollerRef = useRef<HTMLDivElement | null>(null);
134
+ const [atBottom, setAtBottom] = useState(true);
135
+ const [missed, setMissed] = useState(0);
136
+ const seenCount = useRef(0);
137
+ useEffect(() => {
138
+ if (atBottom) {
139
+ seenCount.current = msgs.length;
140
+ setMissed(0);
141
+ return;
142
+ }
143
+ setMissed(Math.max(0, msgs.length - seenCount.current));
144
+ }, [msgs.length, atBottom]);
145
+
146
+ const jumpToLatest = useCallback(() => {
147
+ const el = scrollerRef.current;
148
+ if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
149
+ }, []);
150
+ const dockWatch = useRef<ResizeObserver | null>(null);
151
+ const dockRef = useCallback((el: HTMLDivElement | null) => {
152
+ dockWatch.current?.disconnect();
153
+ if (!el) {
154
+ dockWatch.current = null;
155
+ return;
156
+ }
157
+ const ro = new ResizeObserver(([entry]) => setDockH(entry.contentRect.height));
158
+ ro.observe(el);
159
+ dockWatch.current = ro;
160
+ }, []);
47
161
 
48
162
  // Selection state — drives both the sidebar highlight and the right-pane
49
163
  // header. Restored from the URL query on mount (so a chat is deep-linkable),
@@ -63,30 +177,55 @@ export function ChatTab({
63
177
  if (agent) return { kind: "live", agentSlug: agent };
64
178
  return { kind: "live", agentSlug: ROBY_SLUG };
65
179
  });
180
+ // Nobody chose for us — no deep link in the URL, no host-supplied selection —
181
+ // so let the sidebar open this project's most recent chat once its lists land.
182
+ // Read once at mount: selectChat() writes those same params, and re-reading
183
+ // them later would turn every switch back to a live session into an auto-pick.
184
+ const [autoSelectLatest] = useState(
185
+ () =>
186
+ !initialSelection &&
187
+ !params.get("conv") &&
188
+ !params.get("thread") &&
189
+ !params.get("agent"),
190
+ );
66
191
  // Display metadata for the current selection (channel/created date/title),
67
192
  // carried from the sidebar so the header can show it without a second fetch.
68
193
  const [selectedMeta, setSelectedMeta] = useState<ChatSelectionMeta | undefined>(undefined);
69
194
  const [confirmDelete, setConfirmDelete] = useState(false);
195
+ const [confirmNew, setConfirmNew] = useState(false);
196
+ const [renaming, setRenaming] = useState<string | null>(null);
70
197
  const [deleting, setDeleting] = useState(false);
198
+ const [addOpen, setAddOpen] = useState(false);
199
+ // A pending group rewind awaiting confirmation (there are messages after the
200
+ // target that the regenerate/edit would overwrite).
201
+ const [groupRewind, setGroupRewind] = useState<
202
+ { kind: "edit" | "regen"; keepVisible: number; drop: number; text?: string; media?: UploadedMedia[]; from?: string; reason?: string | null } | null
203
+ >(null);
71
204
 
72
205
  // Select a chat and mirror its id into the URL query so the current chat is
73
206
  // shareable/deep-linkable. `replace` keeps navigation history clean.
74
207
  const selectChat = (key: ChatKey, meta?: ChatSelectionMeta) => {
75
208
  setSelected(key);
76
209
  setSelectedMeta(meta);
77
- const next = new URLSearchParams();
78
- if (key.kind === "conv") {
79
- next.set("agent", key.agentSlug);
80
- next.set("conv", key.convId);
81
- } else if (key.kind === "thread") {
82
- next.set("channel", key.channel);
83
- next.set("thread", key.threadId);
84
- } else {
85
- next.set("agent", key.agentSlug);
210
+ // A host that owns the URL writes it in its own shape; writing both would
211
+ // leave two spellings of the same thing to disagree on the next reload.
212
+ if (onSelectionChange) {
213
+ onSelectionChange(key);
214
+ return;
86
215
  }
87
- setSearchParams(next, { replace: true });
216
+ setSearchParams(queryForChat(key), { replace: true });
88
217
  };
89
218
 
219
+ // An embedding host (the inbox) picks the thread via initialSelection and
220
+ // never calls selectChat. Without this write, `/inbox` has no query, and
221
+ // agent notifications cannot tell you are already reading the row that just
222
+ // moved — they fire for a message already on screen.
223
+ const initialAddr = initialSelection && !onSelectionChange ? queryForChat(initialSelection).toString() : "";
224
+ useEffect(() => {
225
+ if (!initialAddr) return;
226
+ setSearchParams(new URLSearchParams(initialAddr), { replace: true });
227
+ }, [initialAddr, setSearchParams]);
228
+
90
229
  const agentList = agents.data || [];
91
230
  const isRoby = (slug: string | null | undefined) => slug === ROBY_SLUG;
92
231
 
@@ -101,6 +240,38 @@ export function ChatTab({
101
240
  );
102
241
  const activeIsRoby = selected.kind === "thread" || isRoby(selected.agentSlug);
103
242
 
243
+ const visibleActivityKey = useMemo(() => {
244
+ if (selected.kind === "conv") return conversationActivityKey(pid, selected.convId);
245
+ if (selected.kind === "thread") {
246
+ return threadActivityKey(pid, selected.channel, selected.threadId);
247
+ }
248
+ if (conversationId) return conversationActivityKey(pid, conversationId);
249
+ return activeIsRoby
250
+ ? threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10))
251
+ : liveActivityKey(pid, selected.agentSlug);
252
+ }, [pid, selected, conversationId, activeIsRoby]);
253
+ useChatVisibility(visibleActivityKey);
254
+
255
+ const isA2A = selected.kind === "thread" && selected.channel === "a2a";
256
+ const isGroup = selected.kind === "thread" && selected.channel === "group";
257
+ // a2a and group are both multi-agent threads: many faces, no super-agent badge,
258
+ // no rewind. Their name comes from the thread, not an agent.
259
+ const isMultiThread = isA2A || isGroup;
260
+
261
+ // Transcript layout: tools visible (ActionGroup) vs pelado (text only).
262
+ // Persisted per chat so the header switch and the create-group checkbox stick,
263
+ // falling back to this device's last header flip for chats never set.
264
+ const showToolsKey = `${SHOW_TOOLS_PREF}.${pid}.${chatKeyToString(selected)}`;
265
+ const [showTools, setShowToolsState] = useState(() => readShowTools(showToolsKey));
266
+ useEffect(() => {
267
+ setShowToolsState(readShowTools(showToolsKey));
268
+ }, [showToolsKey]);
269
+ const setShowTools = (v: boolean) => {
270
+ setShowToolsState(v);
271
+ writeShowTools(showToolsKey, v);
272
+ writeShowToolsDefault(v);
273
+ };
274
+
104
275
  // Whenever the user picks a stored conversation or a channel thread, reload
105
276
  // the in-memory chat with its persisted history. Conversations bind the
106
277
  // conversation_id (sends append to the file); threads stay unbound —
@@ -115,7 +286,11 @@ export function ChatTab({
115
286
  // conversationId undefined, so an `if (conversationId)` guard would skip
116
287
  // clearing and the previous chat's messages would linger under the new
117
288
  // header — the "title changes but content stays" bug.)
118
- clear();
289
+ clear(
290
+ isRoby(selected.agentSlug)
291
+ ? threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10))
292
+ : liveActivityKey(pid, selected.agentSlug),
293
+ );
119
294
  }
120
295
  // eslint-disable-next-line react-hooks/exhaustive-deps
121
296
  }, [
@@ -127,13 +302,95 @@ export function ChatTab({
127
302
  : selected.agentSlug,
128
303
  ]);
129
304
 
130
- const send = async (text: string) => {
305
+ // Who is in this room, straight off the loaded thread. The daemon resolves the
306
+ // roster and the faces with the messages (api/thread-faces.js), so there is
307
+ // nothing left to fetch a second time and nothing to re-derive: the header
308
+ // shows every member and "add someone" knows who is already in.
309
+ const groupParticipants = conversationMeta?.participants ?? [];
310
+
311
+ // The conversation on screen moved somewhere ELSE — a message on Telegram,
312
+ // another device on the same thread, a routine writing into the file. The
313
+ // daemon says which thread moved and we re-read it; nothing arrives over the
314
+ // socket except the fact that something did (see lib/live.ts).
315
+ //
316
+ // Skipped while this tab is streaming its own answer: the turn being painted
317
+ // token by token is not in storage yet, and re-reading mid-stream would
318
+ // replace it with a version that stops at the last thing written.
319
+ useLiveMessages(
320
+ useCallback(
321
+ (events: LiveEvent[]) => {
322
+ if (streaming) return;
323
+ if (selected.kind === "thread") {
324
+ if (events.some((e) => concernsThread(e, selected.channel, selected.threadId))) {
325
+ void loadThread(selected.channel, selected.threadId, { silent: true });
326
+ }
327
+ } else if (selected.kind === "conv") {
328
+ if (events.some((e) => concernsConversation(e, selected.agentSlug, selected.convId))) {
329
+ void load(selected.agentSlug, selected.convId, { silent: true });
330
+ }
331
+ }
332
+ // A live session has no stored thread to catch up with: everything it
333
+ // shows was produced in this tab.
334
+ },
335
+ [selected, streaming, load, loadThread],
336
+ ),
337
+ );
338
+
339
+ // A group room streams through useChat's own pending-bubble machinery — the
340
+ // owner's line fans out to the members and each speaker's tokens land live,
341
+ // exactly like a 1:1 turn. Refresh the sidebar/inbox once it settles.
342
+ const nameOfSlug = (slug: string) => agentList.find((a) => a.slug === slug)?.name || slug;
343
+ const groupSend = async (gid: string, text: string, media?: UploadedMedia[]) => {
344
+ await sendGroup(gid, text, nameOfSlug, media?.length ? { media } : undefined);
345
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
346
+ void mutate((key) => typeof key === "string" && key.startsWith(`/api/inbox`));
347
+ };
348
+
349
+ const send = async (text: string, media?: UploadedMedia[]) => {
350
+ // `/invite andy hola…` — pull them in, then send the message (with @ so
351
+ // the cascade addresses them). The composer only inserts the command; the
352
+ // invite itself happens here on send.
353
+ const inv = parseInviteCommand(text);
354
+ if (inv && canAddPeople) {
355
+ let gid: string | null = null;
356
+ if (isGroup && selected.kind === "thread") {
357
+ if (groupParticipants.includes(inv.slug)) {
358
+ gid = selected.threadId;
359
+ } else {
360
+ gid = await addToGroup(inv.slug);
361
+ if (!gid) return;
362
+ }
363
+ } else if (inv.slug !== activeAgent?.slug) {
364
+ gid = await escalateToGroup(inv.slug);
365
+ if (!gid) return;
366
+ }
367
+ if (gid) {
368
+ await groupSend(gid, messageForInvite(inv.slug, inv.message), media);
369
+ return;
370
+ }
371
+ }
372
+ if (selected.kind === "thread" && selected.channel === "group") {
373
+ await groupSend(selected.threadId, text, media);
374
+ return;
375
+ }
131
376
  if (activeIsRoby) {
132
- await sendChat(text, { model: model || undefined });
377
+ await sendChat(text, {
378
+ model: model || undefined,
379
+ ...(media?.length ? { attachments: media } : {}),
380
+ });
381
+ // The turn just wrote itself into the channel ledger. Revalidate so the
382
+ // new chat shows up in the sidebar now, instead of only after a reload —
383
+ // which read as "the conversation was lost".
384
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
133
385
  return;
134
386
  }
135
387
  if (!activeAgent) return;
136
- await sendChat(text, { model: model || undefined, agentSlug: activeAgent.slug });
388
+ await sendChat(text, {
389
+ model: model || undefined,
390
+ agentSlug: activeAgent.slug,
391
+ ...(media?.length ? { attachments: media } : {}),
392
+ });
393
+ void mutate(`/api/projects/${pid}/agents/${activeAgent.slug}/conversations`);
137
394
  };
138
395
 
139
396
  const copyToClipboard = async (text: string) => {
@@ -149,6 +406,77 @@ export function ChatTab({
149
406
  clear();
150
407
  };
151
408
 
409
+ // "New group" from the sidebar: create the room, then open it as a group
410
+ // thread. It shows in the Groups section (and the inbox) from here on.
411
+ // `showTools` seeds the header switch for that room (pelado by default).
412
+ const onNewGroup = async (agentSlugs: string[], opts?: { showTools?: boolean }) => {
413
+ try {
414
+ const g = await Groups.create(pid, { participants: agentSlugs });
415
+ const key = { kind: "thread" as const, channel: "group", threadId: g.id };
416
+ writeShowTools(`${SHOW_TOOLS_PREF}.${pid}.${chatKeyToString(key)}`, opts?.showTools === true);
417
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
418
+ selectChat(key, { channel: "group", title: g.title });
419
+ } catch (e) {
420
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
421
+ }
422
+ };
423
+
424
+ // Add a member to the CURRENT group room.
425
+ const addToGroup = async (slug: string): Promise<string | null> => {
426
+ if (selected.kind !== "thread") return null;
427
+ const gid = selected.threadId;
428
+ try {
429
+ await Groups.addParticipant(pid, gid, slug);
430
+ // The silent re-read brings the new roster back with the messages.
431
+ await loadThread("group", gid, { silent: true });
432
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
433
+ return gid;
434
+ } catch (e) {
435
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
436
+ return null;
437
+ }
438
+ };
439
+
440
+ // Remove a member from the current group (records a "… salió del chat" notice;
441
+ // agents stop citing them). Keep at least one agent in the room.
442
+ const removeFromGroup = async (slug: string) => {
443
+ if (selected.kind !== "thread") return;
444
+ const gid = selected.threadId;
445
+ try {
446
+ await Groups.removeParticipant(pid, gid, slug);
447
+ await loadThread("group", gid, { silent: true });
448
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
449
+ } catch (e) {
450
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
451
+ }
452
+ };
453
+
454
+ // Escalate a 1:1 with a project agent into a group by pulling someone in.
455
+ const escalateToGroup = async (slug: string): Promise<string | null> => {
456
+ const base = activeAgent?.slug;
457
+ if (!base) return null;
458
+ try {
459
+ const g = await Groups.create(pid, { participants: [base, slug] });
460
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
461
+ selectChat({ kind: "thread", channel: "group", threadId: g.id }, { channel: "group", title: g.title });
462
+ return g.id;
463
+ } catch (e) {
464
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
465
+ return null;
466
+ }
467
+ };
468
+
469
+ // Who "add someone" offers: in a group, every agent not already in it; in a
470
+ // 1:1 with a project agent, every other project agent (adding one makes it a
471
+ // group). Not offered for the super-agent or a2a threads.
472
+ // activeAgent is only set for a 1:1 with a project agent (undefined for threads
473
+ // and the super-agent), so it alone marks the escalate-able case.
474
+ const canAddPeople = isGroup || (!activeIsRoby && !!activeAgent);
475
+ const addCandidates = isGroup
476
+ ? agentList.filter((a) => !groupParticipants.includes(a.slug))
477
+ : agentList.filter((a) => a.slug !== activeAgent?.slug);
478
+ const onPickAdd = (slug: string) => (isGroup ? addToGroup(slug) : escalateToGroup(slug));
479
+
152
480
  // "New session" header button: reset the pane but stay with the current
153
481
  // agent (Roby for channel threads / the super-agent, else the project agent).
154
482
  const newSession = () => {
@@ -172,7 +500,7 @@ export function ChatTab({
172
500
  }
173
501
  toast.success(t("project.chat.deleted"));
174
502
  setConfirmDelete(false);
175
- newSession();
503
+ await openNeighbour();
176
504
  } catch (e) {
177
505
  toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
178
506
  } finally {
@@ -180,105 +508,723 @@ export function ChatTab({
180
508
  }
181
509
  };
182
510
 
183
- // Header shows "Created {date} · {channel} · {agent}" (or "New chat · …" for a
184
- // fresh session with no persisted date yet), per the sidebar redesign.
185
- const agentLabel = activeIsRoby ? persona : activeAgent?.slug ?? selected.agentSlug;
511
+ /** Both lists that could be showing this session. Cheaper to revalidate the
512
+ * pair than to work out which frame we are in. */
513
+ const refreshSessions = () => {
514
+ if (selected.kind === "thread") void mutate(`/api/projects/${pid}/super-agent/threads`);
515
+ else if (selected.kind === "conv") void mutate(`/api/projects/${pid}/agents/${selected.agentSlug}/conversations`);
516
+ void mutate(`sessions:${pid}:${activeAgent?.slug || ""}:${activeIsRoby}`);
517
+ };
518
+
519
+ /** Name it something you will recognise. An empty name is not an error — it
520
+ * drops the override, and the derived one (the first thing said) comes back. */
521
+ const doRename = async (title: string) => {
522
+ try {
523
+ if (selected.kind === "conv") {
524
+ await Conversations.update(pid, selected.agentSlug, selected.convId, { title });
525
+ } else if (selected.kind === "thread") {
526
+ await Conversations.updateThread(pid, selected.channel, selected.threadId, { title });
527
+ }
528
+ setSelectedMeta((m) => ({ ...(m || {}), title: title.trim() || undefined }) as ChatSelectionMeta);
529
+ refreshSessions();
530
+ setRenaming(null);
531
+ toast.success(t("project.chat.renamed"));
532
+ } catch (e) {
533
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
534
+ }
535
+ };
536
+
537
+ /** Put away, or take back out. The smaller decision than deleting: the record
538
+ * stays exactly where it is and only leaves the lists that offer chats to
539
+ * resume, so archiving is never the end of anything. */
540
+ const doArchive = async (archived: boolean) => {
541
+ try {
542
+ if (selected.kind === "conv") {
543
+ await Conversations.update(pid, selected.agentSlug, selected.convId, { archived });
544
+ } else if (selected.kind === "thread") {
545
+ await Conversations.updateThread(pid, selected.channel, selected.threadId, { archived });
546
+ }
547
+ refreshSessions();
548
+ toast.success(t(archived ? "project.chat.archived_ok" : "project.chat.unarchived_ok"));
549
+ // Archiving takes this thread out of the list you are reading it from, so
550
+ // stay somewhere that still exists. Un-archiving leaves you put.
551
+ if (archived) await openNeighbour();
552
+ } catch (e) {
553
+ toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
554
+ }
555
+ };
556
+
557
+ /** Where to land once a conversation is gone: the most recent one still
558
+ * there, for the same agent or the same channel wall. Dropping the reader on
559
+ * an empty new session instead is the app answering "deleted" with "and now
560
+ * you are nowhere" — the thread next to it is what they were reading around.
561
+ * An empty list, or a list we cannot fetch, still falls back to a fresh one. */
562
+ const openNeighbour = async () => {
563
+ try {
564
+ if (selected.kind === "conv") {
565
+ const gone = selected.convId;
566
+ const slug = selected.agentSlug;
567
+ const rest = (await Conversations.list(pid, slug)).filter((c) => c.id !== gone);
568
+ const next = rest.sort(byRecency)[0];
569
+ if (next) return selectChat({ kind: "conv", agentSlug: slug, convId: next.id });
570
+ } else if (selected.kind === "thread") {
571
+ const { channel, threadId } = selected;
572
+ const rest = (await Conversations.threads(pid)).filter(
573
+ (th) => !(th.channel === channel && th.id === threadId),
574
+ );
575
+ const next = [...rest].sort((a, b) => (b.last_ts || b.id).localeCompare(a.last_ts || a.id))[0];
576
+ if (next) return selectChat({ kind: "thread", channel: next.channel, threadId: next.id });
577
+ }
578
+ } catch {
579
+ /* the list is unreachable: a fresh session is still a place to be */
580
+ }
581
+ newSession();
582
+ };
583
+
584
+ // The header answers WHO first and WHICH CONVERSATION second: the agent's
585
+ // face and name on the title line, the thread's own identity (title, date,
586
+ // channel) demoted to the meta line under it. It used to lead with the
587
+ // conversation id — a bare date string — while the agent was a chip off to
588
+ // the right, so a thread looked like it belonged to nobody.
589
+ // An a2a thread is a conversation BETWEEN two agents — not the super-agent's,
590
+ // even though (like every stored thread) selected.kind is "thread". It gets
591
+ // both faces and their "A · B" title, and none of the super-agent chrome.
592
+ // Faces for the multi-agent header — read, not derived. They arrive resolved
593
+ // on the thread, which is what makes this identical in every frame around the
594
+ // chat: opened from the inbox, from the phone, or from the project sidebar,
595
+ // an a2a thread wears the same two avatars and the same "Andy · Claude". The
596
+ // panel used to resolve them here against `agentList`, which knows neither the
597
+ // super-agent nor a coding CLI, so the surface that had no inbox row to copy
598
+ // from showed a faceless header titled with the raw pair id.
599
+ const a2aFaces = useMemo<AgentFace[]>(
600
+ () => (isMultiThread ? conversationMeta?.faces ?? [] : []),
601
+ [isMultiThread, conversationMeta],
602
+ );
603
+ // What a multi-agent thread is CALLED — "Andy · Claude", not `andy~claude-code`.
604
+ // The loaded thread first (it is the only source on a deep link), then the row
605
+ // that opened it, so clicking one in the sidebar does not flash the pair id for
606
+ // the length of a fetch. Both already carry the daemon's resolved name.
607
+ const multiThreadLabel = isMultiThread
608
+ ? conversationMeta?.title ||
609
+ selectedMeta?.title ||
610
+ (selected.kind === "thread" ? selected.threadId : "")
611
+ : "";
612
+ const agentLabel = isMultiThread
613
+ ? multiThreadLabel
614
+ : activeIsRoby ? persona : activeAgent?.name || activeAgent?.slug || selected.agentSlug;
186
615
  const channelLabel =
187
616
  selected.kind === "thread" ? selected.channel : selectedMeta?.channel || "web";
188
617
  const createdIso =
189
618
  selected.kind === "thread" ? selected.threadId : selectedMeta?.createdAt;
190
619
 
191
- const headerTitle =
620
+ // What this session is CALLED. The loaded file (or thread) knows its own
621
+ // name, including the one the reader gave it; the list row is only what
622
+ // happened to be carried in from wherever you clicked, and on a deep link
623
+ // there is no row at all — which is how the header ended up showing a bare
624
+ // date where the name should be.
625
+ const convLabel =
192
626
  selected.kind === "live"
193
- ? t("project.chat.live_title", { agent: agentLabel })
194
- : selectedMeta?.title ||
195
- (selected.kind === "thread" ? selected.threadId : selected.convId);
196
- const headerSubtitle = createdIso
197
- ? t("project.chat.meta_created", { date: formatDate(createdIso), channel: channelLabel })
198
- : t("project.chat.meta_new", { channel: channelLabel });
627
+ ? ""
628
+ : isMultiThread
629
+ ? multiThreadLabel
630
+ : conversationMeta?.title ||
631
+ selectedMeta?.title ||
632
+ (selected.kind === "thread" ? selected.threadId : selected.convId);
633
+ // Kept as the delete-dialog subject: there the conversation, not the agent,
634
+ // is the thing being destroyed.
635
+ const headerTitle = convLabel || t("project.chat.live_title", { agent: agentLabel });
636
+ // The loaded file knows its own channel and engine; the selection metadata is
637
+ // only what the list row happened to carry. Prefer the file — a routine
638
+ // conversation was reading as "new chat · web" with no model named anywhere.
639
+ const shownChannel = conversationMeta?.channel || channelLabel;
640
+ // One face per speaker, resolved from the same data the inbox uses. Turns a
641
+ // delegated agent produced inside a super-agent thread keep THEIR face, so a
642
+ // multi-agent thread is readable without expanding anything.
643
+ const headerFace: AgentFace = activeIsRoby
644
+ ? { icon: superAgentIcon, name: persona }
645
+ : { icon: activeAgent?.icon, emoji: activeAgent?.emoji, name: agentLabel };
646
+
647
+ const faceFor = (msg: ChatMsg): AgentFace => {
648
+ const id = msg.agentId || msg.agent;
649
+ if (!id) return headerFace;
650
+ if (id === "super_agent") return { icon: superAgentIcon, name: msg.agent || persona };
651
+ const hit = agentList.find((a) => a.slug === id || a.name === id);
652
+ if (hit) return { icon: hit.icon, emoji: hit.emoji, name: hit.name || hit.slug };
653
+ const participant = a2aFaces.find((face) => face.slug === id || face.name === id);
654
+ if (participant) return participant;
655
+ if (["roby", "apx", "default", "superagent", "super-agent", "__super_agent__"].includes(String(id).toLowerCase())) {
656
+ return { icon: superAgentIcon, name: msg.agent || persona };
657
+ }
658
+ return { ...headerFace, name: msg.agent || headerFace.name };
659
+ };
660
+
661
+ // A stored session is the only kind you can name, put away or destroy: a live
662
+ // one is not anywhere yet.
663
+ const storedSession = selected.kind === "conv" || selected.kind === "thread";
664
+ const isArchived = !!selectedMeta?.archived;
665
+ const newSessionAction = {
666
+ key: "new",
667
+ icon: RotateCcw,
668
+ label: t("project.chat.new_session"),
669
+ onClick: () => setConfirmNew(true),
670
+ disabled: streaming || msgs.length === 0,
671
+ };
672
+
673
+ // Go to the agent's card / project. Inbox passes `onOpenInProject` (leave the
674
+ // inbox axis). Inside a project chat we default to the agent's own ficha —
675
+ // tapping the face used to do nothing there because the prop was absent.
676
+ // `!activeIsRoby` already means this is not a thread (a2a/group/super-agent),
677
+ // so `selected` is a live/conv here and carries an agentSlug.
678
+ const agentViewSlug = !activeIsRoby && !isMultiThread
679
+ ? (activeAgent?.slug || (selected as { agentSlug?: string }).agentSlug)
680
+ : undefined;
681
+ const openAgentView = agentViewSlug
682
+ ? () => navigate(`/p/${pid}/agents/${encodeURIComponent(agentViewSlug)}`)
683
+ : undefined;
684
+ const openAgentAction = onOpenInProject || openAgentView;
685
+ const openAgentActionMeta = openAgentAction
686
+ ? {
687
+ key: "open-project",
688
+ icon: onOpenInProject ? ArrowUpRight : Eye,
689
+ label: t(onOpenInProject ? "inbox.open_in_project" : "project.chat.view_agent"),
690
+ onClick: openAgentAction,
691
+ disabled: false,
692
+ }
693
+ : null;
694
+
695
+ // Resolve a group/a2a face to a project-agent slug we can open. Coding CLIs
696
+ // and the super-agent have no ficha here — tip still names them, click no-ops.
697
+ const resolveFaceSlug = (face: AgentFace): string | undefined => {
698
+ if (face.slug && agentList.some((a) => a.slug === face.slug)) return face.slug;
699
+ return agentList.find((a) => a.name === face.name || a.slug === face.name)?.slug;
700
+ };
701
+ const openFace = (face: AgentFace) => {
702
+ const slug = resolveFaceSlug(face);
703
+ if (!slug) return;
704
+ navigate(`/p/${pid}/agents/${encodeURIComponent(slug)}`);
705
+ };
706
+ const faceOpenable = (face: AgentFace) => !!resolveFaceSlug(face);
707
+
708
+ // What the ⋯ holds: everything that edits THIS session. Described once, so
709
+ // the phone and the desktop cannot drift into offering different things.
710
+ const menuActions = [
711
+ // Face + ⋯ both offer the same escape hatch (inbox → project, or chat →
712
+ // agent ficha). Always in the menu so it is not only a discoverable click
713
+ // on the blob.
714
+ ...(openAgentActionMeta ? [openAgentActionMeta] : []),
715
+ ...(compact ? [newSessionAction] : []),
716
+ ...(storedSession
717
+ ? [
718
+ {
719
+ key: "rename",
720
+ icon: Pencil,
721
+ label: t("project.chat.rename"),
722
+ onClick: () => setRenaming(convLabel || ""),
723
+ disabled: false,
724
+ },
725
+ {
726
+ key: "archive",
727
+ icon: isArchived ? ArchiveRestore : Archive,
728
+ label: t(isArchived ? "project.chat.unarchive" : "project.chat.archive"),
729
+ onClick: () => void doArchive(!isArchived),
730
+ disabled: streaming,
731
+ },
732
+ ]
733
+ : []),
734
+ ];
735
+ // Below the line, on its own: the one that cannot be taken back.
736
+ const deleteAction = storedSession
737
+ ? { key: "delete", icon: Trash2, label: t("project.chat.delete"), onClick: () => setConfirmDelete(true), disabled: streaming }
738
+ : null;
739
+
740
+ // Regenerate / edit-and-resend are wired only for a project agent's own chat —
741
+ // the case whose history the daemon rebuilds from a file we can rewind. The
742
+ // super-agent's channel threads and a2a share a day-ledger, not a per-chat
743
+ // file, so they're left out for now.
744
+ const canRewind = !activeIsRoby && !!activeAgent && !isMultiThread && !streaming;
745
+ const afterRewind = () =>
746
+ void mutate(`/api/projects/${pid}/agents/${activeAgent?.slug}/conversations`);
747
+ const onRegenerate = canRewind
748
+ ? (index: number) => { void regenerate(index, { model: model || undefined, agentSlug: activeAgent!.slug }); afterRewind(); }
749
+ : undefined;
750
+ const onEditResend = canRewind
751
+ ? (index: number, text: string) => { void editAndResend(index, text, { model: model || undefined, agentSlug: activeAgent!.slug }); afterRewind(); }
752
+ : undefined;
753
+
754
+ // ── Group rewind (regenerate / edit & resend) ─────────────────────────────
755
+ // A group is a ledger thread, so rewinding truncates the ledger (not a file)
756
+ // and then resumes the cascade from the target speaker. Regenerating the last
757
+ // bubble keeps earlier replies this turn; regenerating an earlier one drops
758
+ // everything after it (those speakers may be pulled back in by a new @mention).
759
+ //
760
+ // keepVisible = pane bubbles before the target. truncateGroupThread collapses
761
+ // the ledger the same way (tools ride with their agent; consecutive same-speaker
762
+ // agent rows = one bubble). A raw row count used to cut too early and delete
763
+ // the owner's message when Candela had spoken twice in a row.
764
+ const gidOf = () => (selected.kind === "thread" ? selected.threadId : "");
765
+ const groupKeepVisible = (index: number) =>
766
+ msgs.slice(0, index).filter((m) => m.role === "user" || (m.role === "assistant" && !m.event)).length;
767
+ const runGroupEdit = async (keepVisible: number, text: string, media?: UploadedMedia[]) => {
768
+ const gid = gidOf();
769
+ try { await Groups.truncate(pid, gid, keepVisible); } catch (e) { toast.error((e as Error)?.message); return; }
770
+ await loadThread("group", gid, { silent: true });
771
+ await groupSend(gid, text, media);
772
+ };
773
+ const runGroupRegen = async (keepVisible: number, from?: string, reason?: string | null) => {
774
+ const gid = gidOf();
775
+ if (streaming) return;
776
+ try { await Groups.truncate(pid, gid, keepVisible); } catch (e) { toast.error((e as Error)?.message); return; }
777
+ await loadThread("group", gid, { silent: true });
778
+ await sendGroup(gid, "", nameOfSlug, { rerun: true, from, reason });
779
+ void mutate(`/api/projects/${pid}/super-agent/threads`);
780
+ };
781
+ const groupRegenerate = (index: number) => {
782
+ const target = msgs[index];
783
+ if (target?.role !== "assistant" || target.event || !target.agentId) return;
784
+ // Keep owner/agent turns BEFORE this bubble; drop it and anything after.
785
+ const keepVisible = groupKeepVisible(index);
786
+ const from = target.agentId;
787
+ const reason = target.reason || null;
788
+ if (index < msgs.length - 1) setGroupRewind({ kind: "regen", keepVisible, drop: msgs.length - 1 - index, from, reason });
789
+ else void runGroupRegen(keepVisible, from, reason);
790
+ };
791
+ const groupEdit = (index: number, text: string) => {
792
+ const target = msgs[index];
793
+ if (!target || target.role !== "user") return;
794
+ const keepVisible = groupKeepVisible(index); // drop the edited owner line + everything after
795
+ // Keep the photo/file on the edited turn — only the caption changes.
796
+ const media = attachmentsOf(target);
797
+ if (index < msgs.length - 1) setGroupRewind({ kind: "edit", keepVisible, text, media, drop: msgs.length - 1 - index });
798
+ else void runGroupEdit(keepVisible, text, media);
799
+ };
800
+ // In a group, the same affordances rewind the ledger instead of a file.
801
+ const regenerateHandler = isGroup ? (streaming ? undefined : groupRegenerate) : onRegenerate;
802
+ const editHandler = isGroup ? (streaming ? undefined : groupEdit) : onEditResend;
199
803
 
200
804
  if (agents.isLoading) return <Loading />;
201
805
 
202
806
  return (
203
- <div className="flex h-full overflow-hidden rounded-xl border border-border bg-card/40">
807
+ <div className={cn("flex h-full min-h-0 overflow-hidden", !bare && "rounded-xl border border-border bg-card/40")}>
204
808
  {hideSidebar ? null : <ChatList
205
809
  pid={pid}
206
810
  agents={agentList}
207
811
  superAgentSlug={ROBY_SLUG}
208
812
  superAgentLabel={t("agents_ui.super_agent_label", { persona })}
813
+ superAgentIcon={superAgentIcon}
209
814
  selected={selected}
210
815
  onSelect={selectChat}
211
816
  onNewChat={onNewChat}
817
+ onNewGroup={onNewGroup}
818
+ autoSelectLatest={autoSelectLatest}
212
819
  />}
213
820
 
214
- <section className="flex min-w-0 flex-1 flex-col">
215
- <header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
216
- <div className="min-w-0">
217
- <h2 className="truncate text-sm font-semibold">{headerTitle}</h2>
218
- <p className="truncate text-[11px] text-muted-fg">{headerSubtitle}</p>
821
+ {/* min-h-0 is what makes the message list the only scroller: without it a
822
+ flex child refuses to shrink below its content, the column grows past
823
+ the viewport, and the composer sits under the fold with no way to
824
+ reach it. Invisible on a tall desktop pane, fatal on a phone. */}
825
+ <section className="flex min-h-0 min-w-0 flex-1 flex-col">
826
+ {hideHeader ? null : <header
827
+ className={cn(
828
+ "flex shrink-0 items-center justify-between gap-3 border-b border-border",
829
+ // The phone pays for the notch here rather than in a second header
830
+ // of its own: one implementation, two frames around it.
831
+ //
832
+ // Except inside the Android app, which already lays the WebView out
833
+ // below the system bars — there `env(safe-area-inset-top)` is paid
834
+ // a SECOND time and the chat opens with a band of empty space the
835
+ // list it came from does not have. Same condition the phone list
836
+ // uses for its own header.
837
+ compact
838
+ ? cn("gap-2 px-2 pb-2", isNativeShell() ? "pt-1.5" : "pt-[max(0.5rem,env(safe-area-inset-top))]")
839
+ : "px-3 py-2",
840
+ )}
841
+ >
842
+ {onBack && (
843
+ <button
844
+ type="button"
845
+ onClick={onBack}
846
+ aria-label={t("mobile.back")}
847
+ className="flex size-10 shrink-0 items-center justify-center rounded-full text-muted-fg active:bg-accent/60"
848
+ >
849
+ <ChevronLeft size={22} />
850
+ </button>
851
+ )}
852
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
853
+ {(() => {
854
+ // Group / a2a: each face is its own control (tip + open that agent).
855
+ // Wrapping the whole cluster in one button made every blob do the
856
+ // same thing — or nothing, when openAgentAction was absent.
857
+ if (isMultiThread && a2aFaces.length) {
858
+ return (
859
+ <AgentAvatarGroup
860
+ faces={a2aFaces}
861
+ size={compact ? 30 : 26}
862
+ max={3}
863
+ onFaceClick={openFace}
864
+ faceOpenable={faceOpenable}
865
+ />
866
+ );
867
+ }
868
+ const avatar = <AgentAvatar {...headerFace} size={compact ? 36 : 30} />;
869
+ // Tapping the face is the quick way to the agent's card/project —
870
+ // more discoverable than the button in the row of actions.
871
+ return openAgentAction ? (
872
+ <button
873
+ type="button"
874
+ onClick={openAgentAction}
875
+ aria-label={t(onOpenInProject ? "inbox.open_in_project" : "project.chat.view_agent")}
876
+ className="shrink-0 rounded-full transition-opacity hover:opacity-80 active:opacity-70"
877
+ >
878
+ {avatar}
879
+ </button>
880
+ ) : avatar;
881
+ })()}
882
+ {/* The session on top, WHO on the line under it. The header used to
883
+ lead with the agent and demote the thread to a date in the meta
884
+ line — but the agent's face is already sitting right there, and
885
+ the date was standing in the one place the name should be. */}
886
+ <div className="min-w-0 flex-1">
887
+ {/* A group is one room, not an agent with many sessions — so it
888
+ gets a plain title, not the session switcher. */}
889
+ {isGroup ? (
890
+ <div className={cn(
891
+ "max-w-full truncate font-semibold text-foreground",
892
+ compact ? "text-[15px] leading-tight" : "text-sm",
893
+ )}>
894
+ {convLabel || t("project.groups.title")}
895
+ </div>
896
+ ) : (
897
+ <SessionPicker
898
+ pid={pid}
899
+ agentSlug={activeAgent?.slug || (selected.kind === "thread" ? "" : selected.agentSlug)}
900
+ isSuper={activeIsRoby}
901
+ selected={selected}
902
+ label={convLabel || t("mobile.live_session")}
903
+ onPick={selectChat}
904
+ channelScope={channelScope}
905
+ className={cn(
906
+ "max-w-full font-semibold text-foreground",
907
+ compact ? "text-[15px] leading-tight" : "text-sm",
908
+ )}
909
+ />
910
+ )}
911
+ {/* Who answered, where, and when — three facts, each short. Kept
912
+ on one line and truncated as a whole, so a long agent name
913
+ cannot push the channel and the date onto a second row. */}
914
+ <p className="flex min-w-0 items-center gap-1.5 truncate text-[11px] text-muted-fg">
915
+ {/* For a2a the picker above already names both agents, so the
916
+ meta line skips the "who" and leads with the channel. */}
917
+ {!isMultiThread && <span className="truncate">{agentLabel}</span>}
918
+ {/* Not on a phone: at 375px the badge and the two facts after it
919
+ squeezed "Roby" down to "Ro…", and the face two inches to
920
+ the left already says which agent this is. */}
921
+ {activeIsRoby && !isMultiThread && !compact && (
922
+ <span className={cn("shrink-0 rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wide", toneChip.emerald)}>
923
+ {t("agents_ui.super_agent_badge")}
924
+ </span>
925
+ )}
926
+ <span className="shrink-0">· {shownChannel}</span>
927
+ {createdIso && <span className="shrink-0">· {formatDate(createdIso)}</span>}
928
+ {conversationMeta?.engine && !compact && (
929
+ <span className="truncate">· {conversationMeta.engine}</span>
930
+ )}
931
+ </p>
932
+ </div>
219
933
  </div>
220
- <div className="flex items-center gap-2">
221
- {activeIsRoby ? (
222
- <Badge tone="success">{t("agents_ui.super_agent_badge")}</Badge>
223
- ) : (
224
- <Badge tone="info">{agentLabel}</Badge>
225
- )}
226
- {!agentList.length && !activeIsRoby && (
934
+
935
+ {/* Same actions on both surfaces, presented for the room available:
936
+ spelled out where there is width, folded behind one ⋯ where there
937
+ is not. On the phone they used to be absent altogether. */}
938
+ <div className="flex shrink-0 items-center gap-1">
939
+ {/* Same switch as "create group": on = tools/ActionGroup, off = pelado
940
+ (narration as bubbles, tools hidden). Lives next to add-person. */}
941
+ <Tip content={showTools ? t("chat_ui.show_tools_on") : t("chat_ui.show_tools_off")}>
942
+ <div
943
+ className={cn(
944
+ "flex items-center gap-1.5 rounded-full text-muted-fg",
945
+ compact ? "px-1.5" : "px-1",
946
+ )}
947
+ aria-label={t("chat_ui.show_tools")}
948
+ >
949
+ {!compact && <Wrench size={13} className="shrink-0 opacity-70" aria-hidden />}
950
+ <Switch checked={showTools} onChange={setShowTools} />
951
+ </div>
952
+ </Tip>
953
+ {!agentList.length && !activeIsRoby && !compact && (
227
954
  <Button variant="primary" size="sm" onClick={() => setCreating(true)}>
228
955
  <Plus size={14} /> {t("project.chat.create_agent")}
229
956
  </Button>
230
957
  )}
231
- <Button
232
- variant="ghost"
233
- size="sm"
234
- disabled={streaming || msgs.length === 0}
235
- onClick={newSession}
236
- >
237
- <RotateCcw size={13} /> {t("project.chat.new_session")}
238
- </Button>
239
- {(selected.kind === "conv" || selected.kind === "thread") && (
240
- <Button
241
- variant="destructive"
242
- size="sm"
243
- disabled={streaming}
244
- onClick={() => setConfirmDelete(true)}
245
- >
246
- <Trash2 size={13} /> {t("project.chat.delete")}
958
+ {/* Add someone: turns a 1:1 into a group, or grows an existing one.
959
+ Same control, two meanings — the menu names which. */}
960
+ {canAddPeople && (isGroup || addCandidates.length > 0) && (
961
+ <div className="relative">
962
+ <Tip content={isGroup ? t("project.groups.members_label") : t("project.groups.make_group")}>
963
+ <button
964
+ type="button"
965
+ aria-label={isGroup ? t("project.groups.members_label") : t("project.groups.make_group")}
966
+ onClick={() => setAddOpen((o) => !o)}
967
+ className={cn(
968
+ "flex items-center justify-center rounded-full text-muted-fg",
969
+ compact ? "size-10 active:bg-accent/60" : "size-8 hover:bg-accent/60",
970
+ addOpen && "bg-accent/60",
971
+ )}
972
+ >
973
+ <UserPlus size={compact ? 20 : 16} />
974
+ </button>
975
+ </Tip>
976
+ {addOpen && (
977
+ <>
978
+ <button type="button" aria-hidden tabIndex={-1}
979
+ className="fixed inset-0 z-10 cursor-default" onClick={() => setAddOpen(false)} />
980
+ <div className="absolute right-0 top-full z-20 mt-1 w-60 rounded-md border border-border bg-card p-1 shadow-lg">
981
+ {/* In a group: the current roster (each removable) then who
982
+ to add. In a 1:1: only who to pull in to make a group. */}
983
+ {isGroup && groupParticipants.length > 0 && (
984
+ <>
985
+ <p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
986
+ {t("project.groups.members_label")}
987
+ </p>
988
+ <div className="max-h-40 overflow-y-auto">
989
+ {groupParticipants.map((slug) => {
990
+ const a = agentList.find((x) => x.slug === slug);
991
+ return (
992
+ <div key={slug} className="group/mem flex items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-accent/50">
993
+ <AgentAvatar icon={a?.icon} emoji={a?.emoji} name={a?.name || slug} size={18} />
994
+ <span className="min-w-0 flex-1 truncate">{a?.name || slug}</span>
995
+ {groupParticipants.length > 1 && (
996
+ <button
997
+ type="button"
998
+ aria-label={t("project.groups.remove_member")}
999
+ title={t("project.groups.remove_member")}
1000
+ onClick={() => { setAddOpen(false); void removeFromGroup(slug); }}
1001
+ className="shrink-0 rounded p-0.5 text-muted-fg opacity-0 hover:text-destructive group-hover/mem:opacity-100"
1002
+ >
1003
+ <X size={13} />
1004
+ </button>
1005
+ )}
1006
+ </div>
1007
+ );
1008
+ })}
1009
+ </div>
1010
+ <div className="my-1 border-t border-border" />
1011
+ </>
1012
+ )}
1013
+ <p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
1014
+ {isGroup ? t("project.groups.add_member") : t("project.groups.make_group")}
1015
+ </p>
1016
+ <div className="max-h-40 overflow-y-auto">
1017
+ {addCandidates.map((a) => (
1018
+ <button
1019
+ key={a.slug}
1020
+ type="button"
1021
+ onClick={() => { setAddOpen(false); void onPickAdd(a.slug); }}
1022
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-accent/50"
1023
+ >
1024
+ <AgentAvatar icon={a.icon} emoji={a.emoji} name={a.name || a.slug} size={18} />
1025
+ <span className="truncate">{a.name || a.slug}</span>
1026
+ </button>
1027
+ ))}
1028
+ {isGroup && addCandidates.length === 0 && (
1029
+ <p className="px-2 py-1.5 text-[11px] text-muted-fg">{t("project.groups.all_in")}</p>
1030
+ )}
1031
+ </div>
1032
+ </div>
1033
+ </>
1034
+ )}
1035
+ </div>
1036
+ )}
1037
+ {/* Inbox only: leave the inbox axis in words. Inside a project the
1038
+ face + ⋯ already open the agent ficha — a second labeled button
1039
+ just repeats "you are already here". */}
1040
+ {onOpenInProject && !compact && (
1041
+ <Button variant="ghost" size="sm" onClick={onOpenInProject}>
1042
+ {t("inbox.open_in_project")} <ArrowUpRight size={13} />
247
1043
  </Button>
248
1044
  )}
1045
+ {!compact && (
1046
+ <Tip content={newSessionAction.label}>
1047
+ <Button
1048
+ variant="ghost"
1049
+ size="sm"
1050
+ aria-label={newSessionAction.label}
1051
+ disabled={newSessionAction.disabled}
1052
+ onClick={newSessionAction.onClick}
1053
+ >
1054
+ <RotateCcw size={13} />
1055
+ </Button>
1056
+ </Tip>
1057
+ )}
1058
+ {(menuActions.length > 0 || deleteAction) && (
1059
+ <DropdownMenu>
1060
+ <DropdownMenuTrigger
1061
+ aria-label={t("common.more")}
1062
+ className={cn(
1063
+ "flex items-center justify-center rounded-full text-muted-fg data-[popup-open]:bg-accent/60",
1064
+ compact ? "size-10 active:bg-accent/60" : "size-8 hover:bg-accent/60",
1065
+ )}
1066
+ >
1067
+ <MoreVertical size={compact ? 20 : 16} />
1068
+ </DropdownMenuTrigger>
1069
+ <DropdownMenuContent align="end" sideOffset={6} className="w-60">
1070
+ {/* Which session these act on, named at the top — the same
1071
+ thing the project's right-click menu does. Without it the
1072
+ menu is four verbs with no subject. */}
1073
+ <DropdownMenuGroup>
1074
+ <DropdownMenuLabel className="truncate text-[11px] font-normal text-muted-fg">
1075
+ {convLabel || t("mobile.live_session")}
1076
+ </DropdownMenuLabel>
1077
+ </DropdownMenuGroup>
1078
+ {menuActions.map((a) => (
1079
+ <DropdownMenuItem key={a.key} onClick={a.onClick} disabled={a.disabled}>
1080
+ <a.icon className="size-4" /> {a.label}
1081
+ </DropdownMenuItem>
1082
+ ))}
1083
+ {deleteAction && (
1084
+ <>
1085
+ <DropdownMenuSeparator />
1086
+ <DropdownMenuItem
1087
+ onClick={deleteAction.onClick}
1088
+ disabled={deleteAction.disabled}
1089
+ className="text-destructive"
1090
+ >
1091
+ <Trash2 className="size-4 text-destructive" /> {deleteAction.label}
1092
+ </DropdownMenuItem>
1093
+ </>
1094
+ )}
1095
+ </DropdownMenuContent>
1096
+ </DropdownMenu>
1097
+ )}
249
1098
  </div>
250
- </header>
251
-
252
- <div className="flex-1 overflow-y-auto">
253
- {msgs.length ? (
254
- <MessageList msgs={msgs} onCopy={copyToClipboard} />
255
- ) : (
256
- <div className="flex h-full items-center justify-center p-8">
257
- <p className="text-sm text-muted-fg">{t("project.chat.empty")}</p>
258
- </div>
1099
+ </header>}
1100
+
1101
+ {/* The thread and the dock share the same box: the composer HOVERS over
1102
+ the conversation instead of taking a slice of the column away from
1103
+ it. The list is given the dock's exact height as trailing space, so
1104
+ the last line can always be scrolled clear of the field — floating
1105
+ over the text is only an improvement while you can still read the
1106
+ line you are answering. */}
1107
+ <div className="relative min-h-0 flex-1">
1108
+ <div ref={scrollerRef} className="h-full overflow-y-auto overflow-x-hidden">
1109
+ {msgs.length || queued.length ? (
1110
+ <MessageList
1111
+ msgs={msgs}
1112
+ queued={queued}
1113
+ onUnqueue={unqueue}
1114
+ onCopy={copyToClipboard}
1115
+ onRegenerate={regenerateHandler}
1116
+ onEdit={editHandler}
1117
+ faceFor={faceFor}
1118
+ showSpeaker={isGroup}
1119
+ nameOf={(slug) => agentList.find((a) => a.slug === slug)?.name || slug}
1120
+ showTools={showTools}
1121
+ compact={compact}
1122
+ bottomInset={bottomInset}
1123
+ onAtBottomChange={setAtBottom}
1124
+ />
1125
+ ) : (
1126
+ <Empty fill icon={MessageSquareDashed}>{t("project.chat.empty")}</Empty>
1127
+ )}
1128
+ </div>
1129
+
1130
+ {/* The way back to the end, once leaving it is something you can do
1131
+ without being dragged straight back. Positioned against the same
1132
+ box as the dock and sized by it, so it clears the field without
1133
+ being part of it — inside the dock it would grow the space the
1134
+ thread reserves, and the conversation would jump every time this
1135
+ appeared. */}
1136
+ {!atBottom && msgs.length > 0 && (
1137
+ <button
1138
+ type="button"
1139
+ onClick={jumpToLatest}
1140
+ aria-label={missed ? t("chat_ui.jump_latest_new") : t("chat_ui.jump_latest")}
1141
+ // Against the dock's REAL height, not the frozen inset: it has to
1142
+ // clear whatever is actually on screen, panel included.
1143
+ style={{ bottom: dockH + 8 }}
1144
+ className={cn(
1145
+ "absolute left-1/2 z-10 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-border",
1146
+ "bg-card px-3 py-1.5 text-xs shadow-lg transition-colors hover:bg-accent",
1147
+ missed > 0 && "border-primary/50 text-primary",
1148
+ )}
1149
+ >
1150
+ <ArrowDown size={14} />
1151
+ {missed > 0 && <span className="font-medium tabular-nums">{missed}</span>}
1152
+ </button>
259
1153
  )}
260
- </div>
261
- <ContextBar msgs={msgs} />
262
- {(() => {
263
- const pending = !streaming ? pendingAskQuestions(msgs) : null;
264
- if (!pending || pending.turnKey === dismissedAskKey) return null;
265
- return (
266
- <InlineAskPanel
267
- turnKey={pending.turnKey}
268
- questions={pending.questions}
269
- onSubmit={(compiled) => void send(compiled)}
270
- onDismiss={() => setDismissedAskKey(pending.turnKey)}
271
- disabled={streaming}
1154
+
1155
+ <div ref={dockRef} className="absolute inset-x-0 bottom-0">
1156
+ <Composer
1157
+ onSend={send}
1158
+ onStop={stop}
1159
+ streaming={streaming}
1160
+ model={model}
1161
+ onModelChange={setModel}
1162
+ // Every chat carries files now — Roby and project agents alike.
1163
+ // A project agent's turn goes through /agents/:slug/chat, which
1164
+ // resolves the same media dir and folds a marker into the prompt,
1165
+ // so an engine with vision renders the image and one without is
1166
+ // still told a file arrived and where it lives.
1167
+ allowFiles
1168
+ floating
1169
+ placeholder={
1170
+ isGroup
1171
+ ? t("project.groups.composer_ph")
1172
+ : canAddPeople
1173
+ ? t("project.groups.invite_ph")
1174
+ : undefined
1175
+ }
1176
+ mentionAgents={
1177
+ isGroup
1178
+ ? groupParticipants.map((slug) => {
1179
+ const a = agentList.find((x) => x.slug === slug);
1180
+ return {
1181
+ slug,
1182
+ name: a?.name || slug,
1183
+ icon: a?.icon,
1184
+ emoji: a?.emoji,
1185
+ };
1186
+ })
1187
+ : undefined
1188
+ }
1189
+ inviteAgents={
1190
+ canAddPeople
1191
+ ? addCandidates.map((a) => ({
1192
+ slug: a.slug,
1193
+ name: a.name || a.slug,
1194
+ icon: a.icon,
1195
+ emoji: a.emoji,
1196
+ }))
1197
+ : undefined
1198
+ }
1199
+ inviteKind={isGroup ? "add" : "escalate"}
1200
+ onNewSession={() => setConfirmNew(true)}
1201
+ onToggleTools={() => setShowTools(!showTools)}
1202
+ // The strip and the questions are both part of the field: what the
1203
+ // turn cost, and what it is waiting on. The panel used to hover as
1204
+ // a separate card above the composer, which put the thing you have
1205
+ // to answer somewhere other than the thing you answer with.
1206
+ context={
1207
+ <>
1208
+ <ContextBar msgs={msgs} docked onOpenChange={setCtxOpen} />
1209
+ {(() => {
1210
+ const pending = !streaming ? pendingAskQuestions(msgs) : null;
1211
+ if (!pending || pending.turnKey === dismissedAskKey) return null;
1212
+ return (
1213
+ <InlineAskPanel
1214
+ docked
1215
+ turnKey={pending.turnKey}
1216
+ questions={pending.questions}
1217
+ onSubmit={(compiled) => void send(compiled)}
1218
+ onDismiss={() => setDismissedAskKey(pending.turnKey)}
1219
+ disabled={streaming}
1220
+ />
1221
+ );
1222
+ })()}
1223
+ </>
1224
+ }
272
1225
  />
273
- );
274
- })()}
275
- <Composer
276
- onSend={send}
277
- onStop={stop}
278
- streaming={streaming}
279
- model={model}
280
- onModelChange={setModel}
281
- />
1226
+ </div>
1227
+ </div>
282
1228
  </section>
283
1229
 
284
1230
  <CreateAgentDialog
@@ -288,6 +1234,54 @@ export function ChatTab({
288
1234
  onCreated={() => { setCreating(false); agents.mutate(); }}
289
1235
  />
290
1236
 
1237
+ <Dialog
1238
+ open={renaming !== null}
1239
+ onClose={() => setRenaming(null)}
1240
+ title={t("project.chat.rename_title")}
1241
+ size="sm"
1242
+ footer={
1243
+ <>
1244
+ <Button variant="ghost" onClick={() => setRenaming(null)}>
1245
+ {t("common.cancel")}
1246
+ </Button>
1247
+ <Button variant="primary" onClick={() => void doRename(renaming || "")}>
1248
+ {t("common.save")}
1249
+ </Button>
1250
+ </>
1251
+ }
1252
+ >
1253
+ <Field label={t("project.chat.rename_label")} hint={t("project.chat.rename_hint")}>
1254
+ <Input
1255
+ autoFocus
1256
+ value={renaming ?? ""}
1257
+ onChange={(e) => setRenaming(e.target.value)}
1258
+ onKeyDown={(e) => {
1259
+ if (e.key === "Enter") void doRename(renaming || "");
1260
+ }}
1261
+ />
1262
+ </Field>
1263
+ </Dialog>
1264
+
1265
+ <Dialog
1266
+ open={confirmNew}
1267
+ onClose={() => setConfirmNew(false)}
1268
+ title={t("project.chat.new_session_confirm_title")}
1269
+ description={t("project.chat.new_session_confirm_desc")}
1270
+ size="sm"
1271
+ footer={
1272
+ <>
1273
+ <Button variant="ghost" onClick={() => setConfirmNew(false)}>
1274
+ {t("common.cancel")}
1275
+ </Button>
1276
+ <Button variant="primary" onClick={() => { setConfirmNew(false); newSession(); }}>
1277
+ <RotateCcw size={14} /> {t("project.chat.new_session")}
1278
+ </Button>
1279
+ </>
1280
+ }
1281
+ >
1282
+ <p className="text-sm text-muted-fg">{headerTitle}</p>
1283
+ </Dialog>
1284
+
291
1285
  <Dialog
292
1286
  open={confirmDelete}
293
1287
  onClose={() => setConfirmDelete(false)}
@@ -307,10 +1301,46 @@ export function ChatTab({
307
1301
  >
308
1302
  <p className="text-sm text-muted-fg">{headerTitle}</p>
309
1303
  </Dialog>
1304
+
1305
+ {/* Group regenerate / edit overwrites the messages after the target. */}
1306
+ <Dialog
1307
+ open={!!groupRewind}
1308
+ onClose={() => setGroupRewind(null)}
1309
+ title={groupRewind?.kind === "edit" ? t("project.chat.group_edit_title") : t("project.chat.group_regen_title")}
1310
+ description={t("project.chat.group_rewind_desc", { n: groupRewind?.drop ?? 0 })}
1311
+ size="sm"
1312
+ footer={
1313
+ <>
1314
+ <Button variant="ghost" onClick={() => setGroupRewind(null)}>{t("common.cancel")}</Button>
1315
+ <Button
1316
+ variant="primary"
1317
+ onClick={() => {
1318
+ const r = groupRewind;
1319
+ setGroupRewind(null);
1320
+ if (!r) return;
1321
+ if (r.kind === "edit") void runGroupEdit(r.keepVisible, r.text || "", r.media);
1322
+ else void runGroupRegen(r.keepVisible, r.from, r.reason);
1323
+ }}
1324
+ >
1325
+ {groupRewind?.kind === "edit" ? t("project.chat.group_edit_confirm") : t("project.chat.group_regen_confirm")}
1326
+ </Button>
1327
+ </>
1328
+ }
1329
+ >
1330
+ <p className="text-sm text-muted-fg">{convLabel}</p>
1331
+ </Dialog>
310
1332
  </div>
311
1333
  );
312
1334
  }
313
1335
 
1336
+ /** Most recent first. `ended_at` is when it was last written to; a conversation
1337
+ * that has only ever been opened has just `started_at`, and the id is the last
1338
+ * resort so the order is never arbitrary. */
1339
+ function byRecency(a: ConversationListEntry, b: ConversationListEntry): number {
1340
+ const when = (c: ConversationListEntry) => c.ended_at || c.started_at || c.id;
1341
+ return when(b).localeCompare(when(a));
1342
+ }
1343
+
314
1344
  // Localised short date for the header "Created {date}" line. Falls back to the
315
1345
  // raw string for anything Date can't parse.
316
1346
  function formatDate(iso?: string): string {
@@ -320,6 +1350,42 @@ function formatDate(iso?: string): string {
320
1350
  return d.toLocaleDateString();
321
1351
  }
322
1352
 
1353
+ // Per-chat preference: show tool ActionGroups vs pelado transcript.
1354
+ // A chat you have already set keeps its own value; one you have never opened
1355
+ // starts from the last flip of the header switch, so you do not re-flip it in
1356
+ // every new conversation. Pelado the very first time. Both keys live in
1357
+ // localStorage, so each device remembers what you read on that device.
1358
+ const SHOW_TOOLS_PREF = "apx.chat.showTools";
1359
+ const SHOW_TOOLS_LAST = "apx.chat.showTools.last";
1360
+
1361
+ function readFlag(key: string): boolean | null {
1362
+ try {
1363
+ const raw = localStorage.getItem(key);
1364
+ if (raw === null) return null;
1365
+ return raw === "1" || raw === "true";
1366
+ } catch {
1367
+ return null;
1368
+ }
1369
+ }
1370
+
1371
+ function readShowTools(key: string): boolean {
1372
+ return readFlag(key) ?? readFlag(SHOW_TOOLS_LAST) ?? false;
1373
+ }
1374
+
1375
+ function writeShowTools(key: string, v: boolean): void {
1376
+ try {
1377
+ localStorage.setItem(key, v ? "1" : "0");
1378
+ } catch { /* quota / private mode */ }
1379
+ }
1380
+
1381
+ /** The header switch also moves the device default. The create-group checkbox
1382
+ * does not: that one is a choice about that one room. */
1383
+ function writeShowToolsDefault(v: boolean): void {
1384
+ try {
1385
+ localStorage.setItem(SHOW_TOOLS_LAST, v ? "1" : "0");
1386
+ } catch { /* quota / private mode */ }
1387
+ }
1388
+
323
1389
  function CreateAgentDialog({
324
1390
  open,
325
1391
  onClose,
@@ -368,7 +1434,7 @@ function CreateAgentDialog({
368
1434
  }
369
1435
  >
370
1436
  <div className="space-y-3">
371
- <Field label="slug">
1437
+ <Field label={t("project.chat.slug_label")}>
372
1438
  <Input autoFocus value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="master" />
373
1439
  </Field>
374
1440
  <Field label={t("project.chat.role_label")}>