groove-dev 0.27.143 → 0.27.145

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 (251) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +18 -48
  6. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  7. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  9. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  11. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  13. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  14. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  15. package/node_modules/@groove-dev/daemon/src/routes/agents.js +812 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  22. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  23. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  24. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  25. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  26. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  27. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  28. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  29. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  30. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  31. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  32. package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  33. package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
  34. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  35. package/node_modules/@groove-dev/gui/package.json +1 -1
  36. package/node_modules/@groove-dev/gui/src/{app.jsx → App.jsx} +0 -2
  37. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  39. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +210 -112
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  42. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  43. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  44. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  46. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  47. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  48. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
  49. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
  50. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  54. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  55. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  56. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  57. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  58. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  59. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  60. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
  61. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  62. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  63. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +200 -18
  64. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  65. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +335 -152
  66. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  67. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  68. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  69. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  70. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  71. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  73. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  74. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  75. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  76. package/node_modules/@groove-dev/gui/src/lib/status.js +25 -24
  77. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  78. package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -3144
  79. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +459 -0
  81. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  82. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +226 -0
  83. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  84. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  85. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  86. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  87. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  88. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  89. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  90. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  91. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  92. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  93. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +54 -12
  94. package/node_modules/@groove-dev/gui/src/views/models.jsx +419 -496
  95. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  96. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  97. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  98. package/node_modules/axios/CHANGELOG.md +260 -0
  99. package/node_modules/axios/README.md +595 -223
  100. package/node_modules/axios/dist/axios.js +1460 -1090
  101. package/node_modules/axios/dist/axios.js.map +1 -1
  102. package/node_modules/axios/dist/axios.min.js +3 -3
  103. package/node_modules/axios/dist/axios.min.js.map +1 -1
  104. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  105. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  106. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  107. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  108. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  109. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  110. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  111. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  112. package/node_modules/axios/index.d.cts +40 -41
  113. package/node_modules/axios/index.d.ts +151 -227
  114. package/node_modules/axios/index.js +2 -0
  115. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  116. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  117. package/node_modules/axios/lib/adapters/http.js +306 -58
  118. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  119. package/node_modules/axios/lib/core/Axios.js +7 -3
  120. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  121. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  122. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  123. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  124. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  125. package/node_modules/axios/lib/core/settle.js +7 -11
  126. package/node_modules/axios/lib/defaults/index.js +14 -9
  127. package/node_modules/axios/lib/env/data.js +1 -1
  128. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  129. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  130. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  131. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  132. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  133. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  134. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  135. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  136. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  137. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  138. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  139. package/node_modules/axios/lib/helpers/validator.js +3 -1
  140. package/node_modules/axios/lib/utils.js +33 -21
  141. package/node_modules/axios/package.json +17 -24
  142. package/node_modules/follow-redirects/README.md +7 -5
  143. package/node_modules/follow-redirects/index.js +24 -1
  144. package/node_modules/follow-redirects/package.json +1 -1
  145. package/package.json +1 -1
  146. package/packages/cli/package.json +1 -1
  147. package/packages/daemon/package.json +1 -1
  148. package/packages/daemon/src/api.js +1086 -6532
  149. package/packages/daemon/src/conversations.js +18 -48
  150. package/packages/daemon/src/gateways/manager.js +35 -1
  151. package/packages/daemon/src/index.js +3 -0
  152. package/packages/daemon/src/journalist.js +23 -13
  153. package/packages/daemon/src/mlx-server.js +365 -0
  154. package/packages/daemon/src/model-lab.js +308 -12
  155. package/packages/daemon/src/pm.js +1 -1
  156. package/packages/daemon/src/process.js +2 -2
  157. package/packages/daemon/src/providers/local.js +36 -8
  158. package/packages/daemon/src/registry.js +21 -5
  159. package/packages/daemon/src/routes/agents.js +812 -0
  160. package/packages/daemon/src/routes/coordination.js +318 -0
  161. package/packages/daemon/src/routes/files.js +751 -0
  162. package/packages/daemon/src/routes/integrations.js +485 -0
  163. package/packages/daemon/src/routes/network.js +1784 -0
  164. package/packages/daemon/src/routes/providers.js +755 -0
  165. package/packages/daemon/src/routes/schedules.js +110 -0
  166. package/packages/daemon/src/routes/teams.js +650 -0
  167. package/packages/daemon/src/scheduler.js +456 -24
  168. package/packages/daemon/src/teams.js +1 -1
  169. package/packages/daemon/src/validate.js +38 -1
  170. package/packages/daemon/templates/mlx-setup.json +12 -0
  171. package/packages/daemon/templates/tgi-setup.json +1 -1
  172. package/packages/daemon/templates/vllm-setup.json +1 -1
  173. package/packages/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  174. package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
  175. package/packages/gui/dist/index.html +2 -2
  176. package/packages/gui/package.json +1 -1
  177. package/packages/gui/src/{app.jsx → App.jsx} +0 -2
  178. package/packages/gui/src/app.css +35 -0
  179. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  180. package/packages/gui/src/components/agents/agent-feed.jsx +210 -112
  181. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  182. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  183. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  184. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  185. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  186. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  187. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  188. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  189. package/packages/gui/src/components/chat/chat-header.jsx +2 -0
  190. package/packages/gui/src/components/chat/chat-input.jsx +68 -66
  191. package/packages/gui/src/components/chat/chat-view.jsx +4 -8
  192. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  193. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  194. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  195. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  196. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  197. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  198. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  199. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  200. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  201. package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
  202. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  203. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  204. package/packages/gui/src/components/lab/parameter-panel.jsx +200 -18
  205. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  206. package/packages/gui/src/components/lab/runtime-config.jsx +335 -152
  207. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  208. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  209. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  210. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  211. package/packages/gui/src/components/network/network-health.jsx +2 -2
  212. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  213. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  214. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  215. package/packages/gui/src/components/ui/slider.jsx +8 -8
  216. package/packages/gui/src/lib/cron.js +64 -0
  217. package/packages/gui/src/lib/status.js +25 -24
  218. package/packages/gui/src/lib/theme-hex.js +1 -0
  219. package/packages/gui/src/stores/groove.js +51 -3144
  220. package/packages/gui/src/stores/helpers.js +10 -0
  221. package/packages/gui/src/stores/slices/agents-slice.js +459 -0
  222. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  223. package/packages/gui/src/stores/slices/chat-slice.js +226 -0
  224. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  225. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  226. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  227. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  228. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  229. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  230. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  231. package/packages/gui/src/views/agents.jsx +5 -5
  232. package/packages/gui/src/views/dashboard.jsx +12 -13
  233. package/packages/gui/src/views/marketplace.jsx +191 -3
  234. package/packages/gui/src/views/model-lab.jsx +54 -12
  235. package/packages/gui/src/views/models.jsx +419 -496
  236. package/packages/gui/src/views/network.jsx +3 -3
  237. package/packages/gui/src/views/settings.jsx +81 -94
  238. package/packages/gui/src/views/teams.jsx +40 -483
  239. package/SECURITY_SWEEP.md +0 -228
  240. package/TRAINING_DATA_v4.md +0 -6
  241. package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +0 -1
  242. package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +0 -984
  243. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -379
  244. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  245. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  246. package/packages/gui/dist/assets/index-CCVvAoQn.css +0 -1
  247. package/packages/gui/dist/assets/index-DGIv_TRm.js +0 -984
  248. package/packages/gui/src/components/agents/agent-chat.jsx +0 -379
  249. package/packages/gui/src/views/preview.jsx +0 -6
  250. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  251. package/test.py +0 -571
@@ -1,10 +1,11 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import {
4
- Send, Loader2, MessageSquare, ArrowRight, Square,
4
+ Loader2, MessageSquare, SendHorizontal, Pause,
5
5
  FileEdit, Search, Terminal, CheckCircle2, AlertCircle,
6
6
  RotateCw, Zap, Wrench, Eye, Code2, Bug,
7
- ChevronDown, HelpCircle, Pencil, Paperclip, GripHorizontal,
7
+ ChevronDown, Paperclip, GripHorizontal,
8
+ FileCode, X,
8
9
  } from 'lucide-react';
9
10
  import { AnimatePresence, motion } from 'framer-motion';
10
11
  import { useGrooveStore } from '../../stores/groove';
@@ -15,6 +16,18 @@ import { ThinkingIndicator } from '../ui/thinking-indicator';
15
16
  import { TableTree } from '../ui/table-tree';
16
17
 
17
18
  const EMPTY = [];
19
+ const KEEPER_RE = /(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|#[\w/.-]+)/gi;
20
+ const KEEPER_CMD_RE = /^\[(?:save|append|update|delete|view|doc|link|read|instruct)\]$/i;
21
+ const KEEPER_TAG_RE = /^#[\w/.-]+$/;
22
+ const KEEPER_DETECT_RE = /\[(?:save|append|update|delete|view|doc|link|read|instruct)\]/i;
23
+
24
+ function highlightKeeperInput(text) {
25
+ return text.split(KEEPER_RE).map((part, i) => {
26
+ if (KEEPER_CMD_RE.test(part)) return <span key={i} className="text-accent">{part}</span>;
27
+ if (KEEPER_TAG_RE.test(part)) return <span key={i} className="text-accent">{part}</span>;
28
+ return <span key={i} className="text-text-0">{part}</span>;
29
+ });
30
+ }
18
31
 
19
32
  // ── Activity metadata ────────────────────────────────────────
20
33
  function activityMeta(text) {
@@ -221,22 +234,10 @@ function FormattedText({ text }) {
221
234
  // ── Message components ───────────────────────────────────────
222
235
 
223
236
  function UserMessage({ msg }) {
224
- const isQuery = msg.isQuery;
225
237
  return (
226
238
  <div className="flex justify-end pl-8">
227
239
  <div className="max-w-[90%]">
228
- {isQuery && (
229
- <div className="flex items-center justify-end gap-1 mb-1">
230
- <HelpCircle size={9} className="text-info" />
231
- <span className="text-2xs text-info font-sans font-medium">Query</span>
232
- </div>
233
- )}
234
- <div className={cn(
235
- 'px-3.5 py-2.5 rounded-lg border',
236
- isQuery
237
- ? 'bg-info/10 border-info/25'
238
- : 'bg-info/10 border-info/25',
239
- )}>
240
+ <div className="px-3.5 py-2.5 rounded-lg border bg-info/10 border-info/25">
240
241
  <div className="text-[12px] font-sans whitespace-pre-wrap break-words leading-relaxed text-text-0">
241
242
  <FormattedText text={msg.text} />
242
243
  </div>
@@ -369,7 +370,7 @@ function StreamingBar({ agent }) {
369
370
  <div className="flex items-center gap-3 px-4 h-8 border-b border-border-subtle bg-surface-1/80 flex-shrink-0">
370
371
  <div className="flex items-center gap-2 flex-1 min-w-0">
371
372
  <div className="relative flex items-center justify-center w-4 h-4">
372
- <span className="absolute inset-0 rounded-full bg-accent/15 animate-ping" style={{ animationDuration: '2s' }} />
373
+ <span className="absolute inset-0 rounded-full bg-accent/15 animate-ping [animation-duration:2s]" />
373
374
  <span className="relative w-1.5 h-1.5 rounded-full bg-accent" />
374
375
  </div>
375
376
  {isRecent ? (
@@ -432,8 +433,8 @@ function BootSequence({ agent }) {
432
433
  {/* Agent identity */}
433
434
  <div className="flex items-center gap-3 mb-5">
434
435
  <div className="relative w-9 h-9">
435
- <span className="absolute inset-0 rounded-full border-2 border-accent/20 animate-ping" style={{ animationDuration: '2s' }} />
436
- <span className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin" style={{ animationDuration: '1s' }} />
436
+ <span className="absolute inset-0 rounded-full border-2 border-accent/20 animate-ping [animation-duration:2s]" />
437
+ <span className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin [animation-duration:1s]" />
437
438
  <span className="absolute inset-[5px] rounded-full bg-accent/8" />
438
439
  </div>
439
440
  <div>
@@ -467,9 +468,9 @@ function BootSequence({ agent }) {
467
468
  </span>
468
469
  {isLast && visible && (
469
470
  <span className="flex gap-0.5 ml-1">
470
- <span className="w-1 h-1 rounded-full bg-accent animate-pulse" style={{ animationDelay: '0ms' }} />
471
- <span className="w-1 h-1 rounded-full bg-accent animate-pulse" style={{ animationDelay: '200ms' }} />
472
- <span className="w-1 h-1 rounded-full bg-accent animate-pulse" style={{ animationDelay: '400ms' }} />
471
+ <span className="w-1 h-1 rounded-full bg-accent animate-pulse [animation-delay:0ms]" />
472
+ <span className="w-1 h-1 rounded-full bg-accent animate-pulse [animation-delay:200ms]" />
473
+ <span className="w-1 h-1 rounded-full bg-accent animate-pulse [animation-delay:400ms]" />
473
474
  </span>
474
475
  )}
475
476
  </div>
@@ -480,13 +481,39 @@ function BootSequence({ agent }) {
480
481
  );
481
482
  }
482
483
 
484
+ // ── Snippet Tag ─────────────────────────────────────────────
485
+
486
+ function SnippetTag({ snippet, onRemove }) {
487
+ const isCode = snippet.type === 'code';
488
+ const Icon = isCode ? FileCode : Terminal;
489
+ const lines = snippet.code.split('\n').length;
490
+ let label;
491
+ if (isCode && snippet.filePath) {
492
+ const fileName = snippet.filePath.split('/').pop();
493
+ label = `${fileName}:${snippet.lineStart}-${snippet.lineEnd}`;
494
+ } else {
495
+ label = `${isCode ? '' : 'Terminal · '}${lines} line${lines !== 1 ? 's' : ''}`;
496
+ }
497
+ return (
498
+ <div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-accent/10 border border-accent/20 text-accent">
499
+ <Icon size={11} className="flex-shrink-0" />
500
+ <span className="text-2xs font-sans font-medium truncate max-w-[160px]">{label}</span>
501
+ {snippet.instruction && (
502
+ <span className="text-2xs text-accent/60 truncate max-w-[100px]">· {snippet.instruction}</span>
503
+ )}
504
+ <button onClick={onRemove} className="p-0.5 rounded hover:bg-accent/20 cursor-pointer flex-shrink-0">
505
+ <X size={9} />
506
+ </button>
507
+ </div>
508
+ );
509
+ }
510
+
483
511
  // ── Main Feed ────────────────────────────────────────────────
484
512
 
485
513
  export function AgentFeed({ agent }) {
486
514
  const rawChatHistory = useGrooveStore((s) => s.chatHistory[agent.id]) || EMPTY;
487
515
  const rawActivityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
488
516
  const instructAgent = useGrooveStore((s) => s.instructAgent);
489
- const queryAgent = useGrooveStore((s) => s.queryAgent);
490
517
  const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
491
518
  const cachedChatRef = useRef(EMPTY);
492
519
  const cachedActivityRef = useRef(EMPTY);
@@ -495,19 +522,39 @@ export function AgentFeed({ agent }) {
495
522
  const chatHistory = rawChatHistory.length > 0 ? rawChatHistory : cachedChatRef.current;
496
523
  const activityLog = rawActivityLog.length > 0 ? rawActivityLog : cachedActivityRef.current;
497
524
 
525
+ const pendingSnippet = useGrooveStore((s) => s.editorPendingSnippet);
526
+ const clearSnippet = useGrooveStore((s) => s.clearSnippet);
527
+
498
528
  const storeInput = useGrooveStore((s) => s.chatInputs[agent.id] || '');
499
- const setStoreInput = (val) => useGrooveStore.setState((s) => ({ chatInputs: { ...s.chatInputs, [agent.id]: val } }));
529
+ const setStoreInput = (val) => useGrooveStore.setState((s) => {
530
+ const current = s.chatInputs[agent.id] || '';
531
+ const next = typeof val === 'function' ? val(current) : val;
532
+ return { chatInputs: { ...s.chatInputs, [agent.id]: next } };
533
+ });
500
534
  const input = storeInput;
501
535
  const setInput = setStoreInput;
502
- const [mode, setMode] = useState('instruct'); // instruct | query
503
536
  const [sending, setSending] = useState(false);
504
- const [inputHeight, setInputHeight] = useState(36);
537
+ const [inputHeight, setInputHeight] = useState(88);
538
+ const [providerModels, setProviderModels] = useState([]);
505
539
  const dragRef = useRef(null);
506
540
  const scrollRef = useRef(null);
507
541
  const inputRef = useRef(null);
508
542
  const fileInputRef = useRef(null);
543
+ const highlightRef = useRef(null);
509
544
  const isAtBottomRef = useRef(true);
510
545
 
546
+ useEffect(() => {
547
+ if (pendingSnippet) inputRef.current?.focus();
548
+ }, [pendingSnippet]);
549
+
550
+ useEffect(() => {
551
+ if (!agent.provider) return;
552
+ api.get('/providers').then((data) => {
553
+ const p = (Array.isArray(data) ? data : []).find((pr) => pr.id === agent.provider);
554
+ setProviderModels((p?.models || []).filter((m) => !m.disabled));
555
+ }).catch(() => {});
556
+ }, [agent.provider]);
557
+
511
558
  useEffect(() => {
512
559
  const el = scrollRef.current;
513
560
  if (!el) return;
@@ -522,7 +569,7 @@ export function AgentFeed({ agent }) {
522
569
  e.preventDefault();
523
570
  const startY = e.clientY;
524
571
  const startH = inputHeight;
525
- const onMove = (ev) => setInputHeight(Math.min(Math.max(36, startH - (ev.clientY - startY)), 280));
572
+ const onMove = (ev) => setInputHeight(Math.min(Math.max(56, startH - (ev.clientY - startY)), 280));
526
573
  const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
527
574
  window.addEventListener('mousemove', onMove);
528
575
  window.addEventListener('mouseup', onUp);
@@ -616,7 +663,7 @@ export function AgentFeed({ agent }) {
616
663
 
617
664
  async function handleSend() {
618
665
  const text = input.trim();
619
- if (!text || sending) return;
666
+ if ((!text && !pendingSnippet) || sending) return;
620
667
 
621
668
  if (text === '/rotate') {
622
669
  const rotateAgent = useGrooveStore.getState().rotateAgent;
@@ -625,18 +672,29 @@ export function AgentFeed({ agent }) {
625
672
  return;
626
673
  }
627
674
 
675
+ const parts = [];
676
+ if (text) parts.push(text);
677
+ if (pendingSnippet) {
678
+ const s = pendingSnippet;
679
+ if (s.type === 'code' && s.filePath) {
680
+ if (s.instruction && !text) parts.push(s.instruction);
681
+ parts.push(`File: ${s.filePath} (lines ${s.lineStart}-${s.lineEnd})`);
682
+ parts.push('```\n' + s.code + '\n```');
683
+ } else if (s.code) {
684
+ parts.push('```\n' + s.code + '\n```');
685
+ }
686
+ }
687
+ const message = parts.join('\n\n');
688
+
628
689
  setInput('');
690
+ clearSnippet();
629
691
  setSending(true);
630
692
  isAtBottomRef.current = true;
631
693
  requestAnimationFrame(() => {
632
694
  if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
633
695
  });
634
696
  try {
635
- if (mode === 'query') {
636
- await queryAgent(agent.id, text);
637
- } else {
638
- await instructAgent(agent.id, text);
639
- }
697
+ await instructAgent(agent.id, message);
640
698
  } catch { /* toast handles */ }
641
699
  setSending(false);
642
700
  inputRef.current?.focus();
@@ -707,41 +765,27 @@ export function AgentFeed({ agent }) {
707
765
  </div>
708
766
 
709
767
  <div className="px-4 pb-3">
710
- {/* Mode pills */}
711
- <div className="flex items-center gap-1 mb-2">
712
- <button
713
- onClick={() => setMode('instruct')}
714
- className={cn(
715
- 'flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-sans font-medium transition-colors cursor-pointer',
716
- mode === 'instruct'
717
- ? 'bg-accent/12 text-accent border border-accent/20'
718
- : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
719
- )}
720
- >
721
- <Pencil size={10} />
722
- Instruct
723
- </button>
724
- <button
725
- onClick={() => setMode('query')}
726
- className={cn(
727
- 'flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-sans font-medium transition-colors cursor-pointer',
728
- mode === 'query'
729
- ? 'bg-info/12 text-info border border-info/20'
730
- : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
731
- )}
732
- >
733
- <HelpCircle size={10} />
734
- Query
735
- </button>
736
- <span className="text-[10px] text-text-4 font-sans ml-auto">
737
- {mode === 'query' ? 'Read-only — agent keeps working' : isAlive ? 'Directs the agent' : 'Continues the session'}
738
- </span>
739
- </div>
768
+ {/* Snippet tag */}
769
+ {pendingSnippet && (
770
+ <div className="mb-2">
771
+ <SnippetTag snippet={pendingSnippet} onRemove={clearSnippet} />
772
+ </div>
773
+ )}
740
774
 
741
- <div className={cn(
742
- 'flex items-end gap-1 rounded-xl border bg-surface-0 p-1 transition-colors',
743
- mode === 'query' ? 'border-info/20 focus-within:border-info/40' : 'border-border-subtle focus-within:border-accent/30',
744
- )}>
775
+ {/* Keeper command indicator */}
776
+ {input && /\[(?:save|append|update|delete|view|doc|link|read|instruct)\]/i.test(input) && (() => {
777
+ const cmdMatch = input.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
778
+ const tags = (input.match(/#[\w/.-]+/g) || []);
779
+ return (
780
+ <div className="flex items-center gap-1.5 px-3 py-1 mb-2 rounded-lg bg-accent/5 border border-accent/10">
781
+ <span className="px-1.5 py-0.5 rounded bg-accent/15 text-accent font-semibold font-mono text-[10px]">{cmdMatch[0]}</span>
782
+ {tags.map((tag, i) => <span key={i} className="text-accent font-medium text-[10px]">{tag}</span>)}
783
+ <span className="text-[10px] text-text-4 ml-auto">memory command</span>
784
+ </div>
785
+ );
786
+ })()}
787
+
788
+ <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-0 transition-colors overflow-hidden focus-within:border-text-4/40">
745
789
  <input
746
790
  ref={fileInputRef}
747
791
  type="file"
@@ -750,57 +794,111 @@ export function AgentFeed({ agent }) {
750
794
  onChange={handleFileSelect}
751
795
  className="hidden"
752
796
  />
753
- <button
754
- onClick={() => fileInputRef.current?.click()}
755
- className="w-9 h-9 flex items-center justify-center rounded-lg text-text-4 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer flex-shrink-0 mb-px"
756
- title="Attach file"
757
- >
758
- <Paperclip size={14} />
759
- </button>
760
- <textarea
761
- ref={inputRef}
762
- value={input}
763
- onChange={(e) => setInput(e.target.value)}
764
- onKeyDown={onKeyDown}
765
- placeholder={mode === 'query'
766
- ? 'Ask about this agent\'s work...'
767
- : isAlive ? 'Send an instruction...' : 'Continue this session...'}
768
- rows={1}
769
- className={cn(
770
- 'flex-1 resize-none px-3 py-2 text-[13px]',
771
- 'bg-transparent text-text-0 font-sans',
772
- 'placeholder:text-text-4',
773
- 'focus:outline-none',
797
+ {/* Textarea — full width */}
798
+ <div className="relative px-1">
799
+ {input && KEEPER_DETECT_RE.test(input) && (
800
+ <div
801
+ ref={highlightRef}
802
+ aria-hidden
803
+ className="absolute inset-0 px-3 py-2.5 text-[13px] leading-[20px] font-sans pointer-events-none whitespace-pre-wrap break-words overflow-y-hidden"
804
+ style={{ height: inputHeight }}
805
+ >
806
+ {highlightKeeperInput(input)}
807
+ </div>
774
808
  )}
775
- style={{ height: inputHeight }}
776
- />
777
- {isAlive && (
809
+ <textarea
810
+ ref={inputRef}
811
+ value={input}
812
+ onChange={(e) => setInput(e.target.value)}
813
+ onKeyDown={onKeyDown}
814
+ onScroll={(e) => { if (highlightRef.current) highlightRef.current.scrollTop = e.target.scrollTop; }}
815
+ onDragOver={(e) => e.preventDefault()}
816
+ onDrop={(e) => {
817
+ e.preventDefault();
818
+ if (e.dataTransfer?.files?.length) {
819
+ const dt = new DataTransfer();
820
+ for (const f of e.dataTransfer.files) dt.items.add(f);
821
+ fileInputRef.current.files = dt.files;
822
+ fileInputRef.current.dispatchEvent(new Event('change', { bubbles: true }));
823
+ }
824
+ }}
825
+ placeholder={pendingSnippet ? 'Add a message (optional)...'
826
+ : isAlive ? 'Send an instruction...' : 'Continue this session...'}
827
+ rows={1}
828
+ className={cn(
829
+ 'w-full resize-none px-3 py-2.5 text-[13px] leading-[20px]',
830
+ 'bg-transparent font-sans relative z-10',
831
+ 'placeholder:text-text-4',
832
+ 'focus:outline-none',
833
+ input && KEEPER_DETECT_RE.test(input)
834
+ ? 'text-transparent caret-text-0'
835
+ : 'text-text-0',
836
+ )}
837
+ style={{ height: inputHeight }}
838
+ />
839
+ </div>
840
+ {/* Bottom toolbar */}
841
+ <div className="flex items-center gap-1 px-1.5 pb-1.5 pt-0.5">
842
+ {/* Left: attach */}
778
843
  <button
779
- onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
780
- title="Stop agent"
781
- className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-danger/10 transition-colors cursor-pointer flex-shrink-0 mb-px"
844
+ onClick={() => fileInputRef.current?.click()}
845
+ className="w-7 h-7 flex items-center justify-center rounded-md text-text-4 hover:text-text-1 transition-colors cursor-pointer"
846
+ title="Attach file"
782
847
  >
783
- <span className="relative flex items-center justify-center w-3.5 h-3.5">
784
- <span className="absolute inset-0 rounded-full bg-danger/30 animate-ping" style={{ animationDuration: '2s' }} />
785
- <span className="relative w-2.5 h-2.5 rounded-full bg-danger" />
786
- </span>
848
+ <Paperclip size={14} />
787
849
  </button>
788
- )}
789
- <button
790
- onClick={handleSend}
791
- disabled={!input.trim() || sending}
792
- className={cn(
793
- 'w-9 h-9 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0 mb-px',
794
- 'disabled:opacity-15 disabled:cursor-not-allowed',
795
- input.trim()
796
- ? mode === 'query'
797
- ? 'bg-info/15 text-info hover:bg-info/25 border border-info/25'
798
- : 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
799
- : 'bg-transparent text-text-4',
850
+ {/* Model selector */}
851
+ {providerModels.length > 1 && (
852
+ <div className="relative flex items-center">
853
+ <select
854
+ value={agent.model || ''}
855
+ onChange={(e) => {
856
+ if (!e.target.value) return;
857
+ api.patch(`/agents/${agent.id}`, { model: e.target.value }).catch(() => {});
858
+ }}
859
+ className="h-7 pl-2 pr-5 text-[11px] font-mono bg-transparent text-text-3 hover:text-text-1 rounded-md cursor-pointer focus:outline-none appearance-none border-none"
860
+ title="Switch model"
861
+ >
862
+ {providerModels.map((m) => (
863
+ <option key={m.id} value={m.id}>{m.name || m.id}</option>
864
+ ))}
865
+ </select>
866
+ <ChevronDown size={10} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
867
+ </div>
868
+ )}
869
+ {/* Pulsating activity indicator */}
870
+ {isAlive && (
871
+ <span className="relative flex items-center justify-center w-3 h-3 mr-auto">
872
+ <span className="absolute inset-0 rounded-full bg-accent/30 animate-ping [animation-duration:2s]" />
873
+ <span className="relative w-2 h-2 rounded-full bg-accent" />
874
+ </span>
800
875
  )}
801
- >
802
- {sending ? <Loader2 size={15} className="animate-spin" /> : <Send size={15} />}
803
- </button>
876
+ <div className="flex-1" />
877
+ {/* Right: pause (when alive) or send (when idle) */}
878
+ {isAlive ? (
879
+ <button
880
+ onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
881
+ className="flex items-center gap-1.5 h-7 px-2 rounded-md text-text-0 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer"
882
+ >
883
+ <Pause size={13} />
884
+ <span className="text-[11px] font-sans font-medium">Pause</span>
885
+ </button>
886
+ ) : (
887
+ <button
888
+ onClick={handleSend}
889
+ disabled={(!input.trim() && !pendingSnippet) || sending}
890
+ className={cn(
891
+ 'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
892
+ 'disabled:opacity-15 disabled:cursor-not-allowed',
893
+ (input.trim() || pendingSnippet)
894
+ ? 'text-text-0 hover:text-text-1'
895
+ : 'text-text-4',
896
+ )}
897
+ >
898
+ {sending ? <Loader2 size={15} className="animate-spin" /> : <SendHorizontal size={15} />}
899
+ </button>
900
+ )}
901
+ </div>
804
902
  </div>
805
903
  </div>
806
904
  </div>
@@ -8,7 +8,6 @@ import { fmtNum, fmtDollar, fmtUptime } from '../../lib/format';
8
8
 
9
9
  const EMPTY = [];
10
10
  const ERROR_RE = /error|crash|fail/i;
11
- const BAR_BG = 'rgba(51, 175, 188, 0.15)';
12
11
  const BAR_H = 'h-[2px]';
13
12
 
14
13
  function shortModel(id) {
@@ -98,13 +97,9 @@ const AgentNode = memo(({ data, selected }) => {
98
97
 
99
98
  {/* Scan line — running only */}
100
99
  {isAlive && (
101
- <div className="absolute inset-0 overflow-hidden pointer-events-none" style={{ borderRadius: 3 }}>
100
+ <div className="absolute inset-0 overflow-hidden pointer-events-none rounded-[3px]">
102
101
  <div
103
- className="absolute left-0 right-0 h-px"
104
- style={{
105
- background: 'linear-gradient(90deg, transparent 0%, rgba(97,175,239,0.25) 50%, transparent 100%)',
106
- animation: 'node-scan 3s ease-in-out infinite',
107
- }}
102
+ className="absolute left-0 right-0 h-px [background:linear-gradient(90deg,transparent_0%,rgba(97,175,239,0.25)_50%,transparent_100%)] [animation:node-scan_3s_ease-in-out_infinite]"
108
103
  />
109
104
  </div>
110
105
  )}
@@ -116,8 +111,8 @@ const AgentNode = memo(({ data, selected }) => {
116
111
  <span className="absolute inset-0 rounded-sm" style={{ background: sColor }} />
117
112
  {isAlive && (
118
113
  <span
119
- className="absolute inset-[-2px] rounded-sm"
120
- style={{ background: sColor, opacity: 0.15, animation: 'node-pulse-bar 2s ease-in-out infinite' }}
114
+ className="absolute inset-[-2px] rounded-sm opacity-[0.15] [animation:node-pulse-bar_2s_ease-in-out_infinite]"
115
+ style={{ background: sColor }}
121
116
  />
122
117
  )}
123
118
  </span>
@@ -155,7 +150,7 @@ const AgentNode = memo(({ data, selected }) => {
155
150
  </div>
156
151
 
157
152
  {/* Context bar */}
158
- <div className={`mt-1.5 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
153
+ <div className={`mt-1.5 ${BAR_H} rounded-sm overflow-hidden bg-[rgba(51,175,188,0.15)]`}>
159
154
  <div
160
155
  className="h-full rounded-sm transition-all duration-700"
161
156
  style={{
@@ -185,7 +180,7 @@ const AgentNode = memo(({ data, selected }) => {
185
180
  )}
186
181
  </div>
187
182
  <div className="flex items-center gap-2">
188
- <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
183
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden bg-[rgba(51,175,188,0.15)]`}>
189
184
  <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${Math.max(contextPct, 1)}%`, background: ctxColor }} />
190
185
  </div>
191
186
  <span className="text-[9px] font-mono font-medium" style={{ color: ctxColor }}>{contextPct}%</span>
@@ -196,7 +191,7 @@ const AgentNode = memo(({ data, selected }) => {
196
191
  <div className="px-3 pt-1 pb-1">
197
192
  <span className="text-[9px] font-mono text-[#505862] uppercase tracking-wider">Quality</span>
198
193
  <div className="flex items-center gap-2 mt-1">
199
- <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
194
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden bg-[rgba(51,175,188,0.15)]`}>
200
195
  <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${qScore != null ? Math.max(qScore, 1) : 0}%`, background: qColor || '#505862' }} />
201
196
  </div>
202
197
  <span className="text-[9px] font-mono font-medium" style={{ color: qColor || '#505862' }}>{qScore != null ? qScore : '—'}</span>
@@ -207,7 +202,7 @@ const AgentNode = memo(({ data, selected }) => {
207
202
  <div className="px-3 pt-1 pb-1">
208
203
  <span className="text-[9px] font-mono text-[#505862] uppercase tracking-wider">Efficiency</span>
209
204
  <div className="flex items-center gap-2 mt-1">
210
- <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
205
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden bg-[rgba(51,175,188,0.15)]`}>
211
206
  <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${effPct != null ? Math.max(effPct, 1) : 0}%`, background: effColor || '#505862' }} />
212
207
  </div>
213
208
  <span className="text-[9px] font-mono font-medium" style={{ color: effColor || '#505862' }}>{effPct != null ? `${effPct}%` : '—'}</span>
@@ -1,12 +1,12 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { useState, useRef } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { Badge } from '../ui/badge';
5
5
  import { AgentFeed } from './agent-feed';
6
6
  import { AgentConfig } from './agent-config';
7
7
  import { AgentTelemetry } from './agent-telemetry';
8
8
  import { AgentMdFiles } from './agent-mdfiles';
9
- import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X, TrendingDown } from 'lucide-react';
9
+ import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X } from 'lucide-react';
10
10
  import { fmtNum, fmtUptime } from '../../lib/format';
11
11
  import { cn } from '../../lib/cn';
12
12
  import { roleColor } from '../../lib/status';
@@ -77,56 +77,6 @@ function InlineName({ agent }) {
77
77
  );
78
78
  }
79
79
 
80
- function useRoutingSuggestion(agentId, isAlive) {
81
- const [suggestion, setSuggestion] = useState(null);
82
- const [dismissed, setDismissed] = useState(false);
83
-
84
- useEffect(() => {
85
- if (!agentId || !isAlive || dismissed) { setSuggestion(null); return; }
86
- let cancelled = false;
87
- async function poll() {
88
- try {
89
- const res = await fetch(`/api/agents/${agentId}/routing/suggestion`);
90
- if (cancelled) return;
91
- if (res.status === 204 || !res.ok) { setSuggestion(null); return; }
92
- const data = await res.json();
93
- setSuggestion(data);
94
- } catch { setSuggestion(null); }
95
- }
96
- poll();
97
- const id = setInterval(poll, 30000);
98
- return () => { cancelled = true; clearInterval(id); };
99
- }, [agentId, isAlive, dismissed]);
100
-
101
- const dismiss = useCallback(() => setDismissed(true), []);
102
- const reset = useCallback(() => setDismissed(false), []);
103
-
104
- return { suggestion: dismissed ? null : suggestion, dismiss, reset };
105
- }
106
-
107
- function DownshiftPill({ suggestion, onAccept, onDismiss }) {
108
- if (!suggestion) return null;
109
- const { suggestedModel } = suggestion;
110
- return (
111
- <div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-success/10 border border-success/20 text-2xs font-mono animate-in fade-in slide-in-from-left-1 duration-200">
112
- <TrendingDown size={10} className="text-success flex-shrink-0" />
113
- <span className="text-success/90 truncate max-w-[80px]">{suggestedModel.name}</span>
114
- <button
115
- onClick={onAccept}
116
- className="px-1 py-px rounded bg-success/20 text-success font-semibold hover:bg-success/30 transition-colors cursor-pointer"
117
- >
118
- Switch
119
- </button>
120
- <button
121
- onClick={onDismiss}
122
- className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"
123
- >
124
- <X size={8} />
125
- </button>
126
- </div>
127
- );
128
- }
129
-
130
80
  export function AgentPanel() {
131
81
  const detailPanel = useGrooveStore((s) => s.detailPanel);
132
82
  const agents = useGrooveStore((s) => s.agents);
@@ -141,7 +91,6 @@ export function AgentPanel() {
141
91
  else if (cachedAgentRef.current && cachedAgentRef.current.id !== agentId) cachedAgentRef.current = null;
142
92
  const agent = liveAgent || cachedAgentRef.current;
143
93
  const isAlive = liveAgent?.status === 'running' || liveAgent?.status === 'starting';
144
- const { suggestion, dismiss: dismissSuggestion } = useRoutingSuggestion(agentId, isAlive);
145
94
 
146
95
  if (!agent) return null;
147
96
  if (activeTeamId && agent.teamId && agent.teamId !== activeTeamId) return null;
@@ -151,17 +100,6 @@ export function AgentPanel() {
151
100
  const uptime = spawned ? Math.floor((Date.now() - new Date(spawned).getTime()) / 1000) : 0;
152
101
  const colors = roleColor(agent.role);
153
102
 
154
- async function acceptSuggestion() {
155
- if (!suggestion) return;
156
- try {
157
- await api.patch(`/agents/${agent.id}`, { model: suggestion.suggestedModel.id });
158
- addToast('success', `Model → ${suggestion.suggestedModel.name}`);
159
- dismissSuggestion();
160
- } catch (err) {
161
- addToast('error', 'Model switch failed', err.message);
162
- }
163
- }
164
-
165
103
  return (
166
104
  <div className="flex flex-col h-full">
167
105
  {/* ── Header ─────────────────────────────────────────── */}
@@ -194,12 +132,6 @@ export function AgentPanel() {
194
132
  )}
195
133
  <span className="text-text-4">·</span>
196
134
  <span>{fmtUptime(uptime)}</span>
197
- {suggestion && (
198
- <>
199
- <span className="text-text-4">·</span>
200
- <DownshiftPill suggestion={suggestion} onAccept={acceptSuggestion} onDismiss={dismissSuggestion} />
201
- </>
202
- )}
203
135
  </div>
204
136
  </div>
205
137