groove-dev 0.27.142 → 0.27.144

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 (187) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
@@ -5,7 +5,7 @@ import { HEX } from '../../lib/theme-hex';
5
5
  import { Tooltip } from '../ui/tooltip';
6
6
  import { HelpCircle } from 'lucide-react';
7
7
 
8
- function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
8
+ function MiniSparkline({ data, color = HEX.text3, width = 72, height = 22 }) {
9
9
  if (!data || data.length < 2) return <div style={{ width, height }} />;
10
10
  const vals = data.map((d) => d.v);
11
11
  const min = Math.min(...vals);
@@ -34,7 +34,7 @@ function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
34
34
  );
35
35
  }
36
36
 
37
- const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.accent, hint, className }) {
37
+ const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.text3, hint, className }) {
38
38
  return (
39
39
  <div className={cn(
40
40
  'flex items-center gap-2.5 px-3 py-2.5 min-w-0',
@@ -59,7 +59,7 @@ const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.acc
59
59
 
60
60
  export function KpiStrip({ kpis }) {
61
61
  return (
62
- <div className="flex flex-wrap border-b border-border" style={{ background: 'var(--color-surface-0)' }}>
62
+ <div className="flex flex-wrap border-b border-border bg-surface-0">
63
63
  {kpis.map((kpi) => (
64
64
  <KpiCard
65
65
  key={kpi.label}
@@ -1,9 +1,9 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { memo } from 'react';
3
- import { HEX } from '../../lib/theme-hex';
3
+ import { HEX, hexAlpha } from '../../lib/theme-hex';
4
4
  import { fmtNum, fmtPct } from '../../lib/format';
5
5
 
6
- const TIER_COLORS = { heavy: HEX.danger, medium: HEX.warning, light: HEX.success };
6
+ const TIER_COLORS = { heavy: HEX.text0, medium: HEX.text2, light: HEX.text4 };
7
7
  const TIER_LABELS = { heavy: 'Heavy', medium: 'Medium', light: 'Light' };
8
8
 
9
9
  const DONUT_SIZE = 80;
@@ -126,7 +126,7 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
126
126
  <div className="h-0.5 bg-surface-4 rounded-sm overflow-hidden">
127
127
  <div
128
128
  className="h-full rounded-sm transition-all duration-500"
129
- style={{ width: `${Math.max(barPct, 2)}%`, background: HEX.accent }}
129
+ style={{ width: `${Math.max(barPct, 2)}%`, background: HEX.text2 }}
130
130
  />
131
131
  </div>
132
132
  </div>
@@ -41,7 +41,7 @@ export const TeamBurnPanel = memo(function TeamBurnPanel({ teams = [] }) {
41
41
  <div className="h-0.5 bg-surface-2 rounded-sm overflow-hidden">
42
42
  <div
43
43
  className="h-full rounded-sm transition-all"
44
- style={{ width: `${pct}%`, background: HEX.accent }}
44
+ style={{ width: `${pct}%`, background: HEX.text3 }}
45
45
  />
46
46
  </div>
47
47
  </div>
@@ -144,7 +144,7 @@ const TokenChart = memo(function TokenChart({ data }) {
144
144
  const hasCacheData = caches.some((c) => c > 0);
145
145
  if (hasCacheData) {
146
146
  ctx.beginPath();
147
- ctx.strokeStyle = hexAlpha(HEX.info, 0.45);
147
+ ctx.strokeStyle = hexAlpha(HEX.text2, 0.5);
148
148
  ctx.lineWidth = 1;
149
149
  ctx.lineJoin = 'round';
150
150
  ctx.setLineDash([2, 3]);
@@ -164,7 +164,7 @@ const TokenChart = memo(function TokenChart({ data }) {
164
164
  const ly = 14;
165
165
 
166
166
  if (hasCacheData) {
167
- ctx.fillStyle = hexAlpha(HEX.info, 0.5);
167
+ ctx.fillStyle = hexAlpha(HEX.text2, 0.5);
168
168
  ctx.fillText('Cache %', rx, ly);
169
169
  rx -= ctx.measureText('Cache %').width + 4;
170
170
  ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
@@ -203,8 +203,8 @@ const TokenChart = memo(function TokenChart({ data }) {
203
203
  // Tooltip
204
204
  const lines = [
205
205
  { label: 'Burn', value: `${fmtNum(d.burnRate)}/m`, color: HEX.accent },
206
- { label: 'Cache', value: fmtPct(d.cacheHitRate * 100), color: HEX.info },
207
- { label: 'Agents', value: `${d.running}/${d.agents}`, color: HEX.text2 },
206
+ { label: 'Cache', value: fmtPct(d.cacheHitRate * 100), color: HEX.text2 },
207
+ { label: 'Agents', value: `${d.running}/${d.agents}`, color: HEX.text3 },
208
208
  ];
209
209
  const tooltipW = 104;
210
210
  const tooltipH = lines.length * 16 + 12;
@@ -18,6 +18,7 @@ export function SelectionMenu({ x, y, filePath, lineStart, lineEnd, selectedCode
18
18
  const sendCodeToAgent = useGrooveStore((s) => s.sendCodeToAgent);
19
19
  const toggleAiPanel = useGrooveStore((s) => s.toggleAiPanel);
20
20
  const aiPanelOpen = useGrooveStore((s) => s.editorAiPanelOpen);
21
+ const selectAgent = useGrooveStore((s) => s.selectAgent);
21
22
 
22
23
  useEffect(() => {
23
24
  function handleClick(e) {
@@ -51,6 +52,7 @@ export function SelectionMenu({ x, y, filePath, lineStart, lineEnd, selectedCode
51
52
  if (!agentId) return;
52
53
  sendCodeToAgent(agentId, action.instruction, filePath, lineStart, lineEnd, selectedCode);
53
54
  if (!aiPanelOpen) toggleAiPanel();
55
+ selectAgent(agentId);
54
56
  onClose();
55
57
  }
56
58
 
@@ -4,41 +4,237 @@ import { useGrooveStore } from '../../stores/groove';
4
4
  import { ScrollArea } from '../ui/scroll-area';
5
5
  import { Badge } from '../ui/badge';
6
6
  import { cn } from '../../lib/cn';
7
- import { Send, X, Bot, ArrowRight } from 'lucide-react';
7
+ import { Send, X, ArrowRight, ChevronDown, AlertCircle, FlaskConical } from 'lucide-react';
8
8
 
9
+ // ── Inline formatting (bold, code) ──────────────────────────
10
+ function InlineFormat({ text }) {
11
+ if (!text) return null;
12
+ return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).map((part, i) => {
13
+ if (part.startsWith('**') && part.endsWith('**'))
14
+ return <strong key={i} className="font-semibold text-text-0">{part.slice(2, -2)}</strong>;
15
+ if (part.startsWith('`') && part.endsWith('`'))
16
+ return <code key={i} className="px-1 py-px rounded bg-accent/8 text-[11px] font-mono text-accent">{part.slice(1, -1)}</code>;
17
+ return <span key={i}>{part}</span>;
18
+ });
19
+ }
20
+
21
+ // ── Structured message renderer ─────────────────────────────
22
+ function StructuredMessage({ text }) {
23
+ if (!text) return null;
24
+
25
+ const blocks = [];
26
+ const lines = text.split('\n');
27
+ let i = 0;
28
+
29
+ while (i < lines.length) {
30
+ const line = lines[i];
31
+
32
+ if (line.trimStart().startsWith('```')) {
33
+ const codeLines = [];
34
+ i++;
35
+ while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
36
+ codeLines.push(lines[i]);
37
+ i++;
38
+ }
39
+ i++;
40
+ blocks.push({ type: 'code', content: codeLines.join('\n') });
41
+ continue;
42
+ }
43
+
44
+ if (/^#{1,3}\s/.test(line) || /^\*\*[^*]+:\*\*\s*$/.test(line.trim())) {
45
+ const heading = line.replace(/^#+\s*/, '').replace(/^\*\*/, '').replace(/:\*\*\s*$/, ':').trim();
46
+ blocks.push({ type: 'heading', content: heading });
47
+ i++;
48
+ continue;
49
+ }
50
+
51
+ if (/^\s*[-*]\s/.test(line)) {
52
+ const items = [];
53
+ while (i < lines.length && /^\s*[-*]\s/.test(lines[i])) {
54
+ items.push(lines[i].replace(/^\s*[-*]\s+/, '').trim());
55
+ i++;
56
+ }
57
+ blocks.push({ type: 'list', items });
58
+ continue;
59
+ }
60
+
61
+ if (/^\s*\d+[\.)]\s/.test(line)) {
62
+ const items = [];
63
+ while (i < lines.length && /^\s*\d+[\.)]\s/.test(lines[i])) {
64
+ items.push(lines[i].replace(/^\s*\d+[\.)]\s+/, '').trim());
65
+ i++;
66
+ }
67
+ blocks.push({ type: 'numbered', items });
68
+ continue;
69
+ }
70
+
71
+ if (!line.trim()) { i++; continue; }
72
+
73
+ if (/^(Note|Warning|Important|IMPORTANT|TODO):/i.test(line.trim())) {
74
+ blocks.push({ type: 'note', content: line.trim() });
75
+ i++;
76
+ continue;
77
+ }
78
+
79
+ const paraLines = [];
80
+ while (i < lines.length && lines[i].trim() && !/^#{1,3}\s/.test(lines[i]) && !/^\s*[-*]\s/.test(lines[i]) && !/^\s*\d+[\.)]\s/.test(lines[i]) && !lines[i].trimStart().startsWith('```')) {
81
+ paraLines.push(lines[i].trim());
82
+ i++;
83
+ }
84
+ if (paraLines.length > 0) {
85
+ blocks.push({ type: 'para', content: paraLines.join(' ') });
86
+ }
87
+ }
88
+
89
+ return (
90
+ <div className="space-y-2">
91
+ {blocks.map((block, idx) => {
92
+ switch (block.type) {
93
+ case 'heading':
94
+ return (
95
+ <div key={idx} className="flex items-center gap-1.5 pt-1.5 first:pt-0">
96
+ <div className="w-1 h-3.5 rounded-full bg-accent/40 flex-shrink-0" />
97
+ <span className="text-[13px] font-semibold text-text-0 font-sans"><InlineFormat text={block.content} /></span>
98
+ </div>
99
+ );
100
+ case 'list':
101
+ return (
102
+ <div key={idx} className="space-y-1 pl-2">
103
+ {block.items.map((item, j) => (
104
+ <div key={j} className="flex gap-2 text-[13px] text-text-1 font-sans leading-relaxed">
105
+ <span className="text-accent/50 mt-0.5 flex-shrink-0">-</span>
106
+ <span className="min-w-0"><InlineFormat text={item} /></span>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ );
111
+ case 'numbered':
112
+ return (
113
+ <div key={idx} className="space-y-1 pl-2">
114
+ {block.items.map((item, j) => (
115
+ <div key={j} className="flex gap-2 text-[13px] text-text-1 font-sans leading-relaxed">
116
+ <span className="text-text-4 font-mono w-4 text-right flex-shrink-0">{j + 1}.</span>
117
+ <span className="min-w-0"><InlineFormat text={item} /></span>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ );
122
+ case 'code':
123
+ return (
124
+ <pre key={idx} className="p-2.5 rounded-md bg-[#0d1117] text-[11px] font-mono text-[#c9d1d9] overflow-x-auto whitespace-pre-wrap border border-white/[0.06] leading-relaxed">
125
+ {block.content}
126
+ </pre>
127
+ );
128
+ case 'note':
129
+ return (
130
+ <div key={idx} className="flex items-start gap-1.5 px-2.5 py-1.5 rounded-md bg-warning/6 border border-warning/12">
131
+ <AlertCircle size={10} className="text-warning mt-0.5 flex-shrink-0" />
132
+ <span className="text-[12px] text-warning/80 font-sans"><InlineFormat text={block.content} /></span>
133
+ </div>
134
+ );
135
+ case 'para':
136
+ default:
137
+ return <p key={idx} className="text-[13px] text-text-1 font-sans leading-relaxed"><InlineFormat text={block.content} /></p>;
138
+ }
139
+ })}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ // ── Thinking indicator ──────────────────────────────────────
145
+ const THINKING_MESSAGES = [
146
+ 'Checking your system...',
147
+ 'Running setup commands...',
148
+ 'Working through installation...',
149
+ 'Configuring the server...',
150
+ 'Making progress...',
151
+ 'Almost there...',
152
+ ];
153
+
154
+ function LabThinkingIndicator() {
155
+ const [idx, setIdx] = useState(0);
156
+ const [fade, setFade] = useState(true);
157
+
158
+ useEffect(() => {
159
+ const t = setInterval(() => {
160
+ setFade(false);
161
+ setTimeout(() => {
162
+ setIdx((i) => (i + 1) % THINKING_MESSAGES.length);
163
+ setFade(true);
164
+ }, 250);
165
+ }, 2800);
166
+ return () => clearInterval(t);
167
+ }, []);
168
+
169
+ return (
170
+ <div>
171
+ <div className="flex items-center gap-2 mb-1">
172
+ <span className="text-2xs font-semibold text-text-1 font-sans">Lab Assistant</span>
173
+ <span className="text-2xs text-accent font-mono">working</span>
174
+ </div>
175
+ <div className="border-l border-accent/40 pl-3.5 py-1 flex items-center gap-2.5">
176
+ <div className="relative w-3.5 h-3.5 flex-shrink-0">
177
+ <span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
178
+ </div>
179
+ <span
180
+ className="text-[13px] font-sans text-text-3 transition-opacity duration-[250ms]"
181
+ style={{ opacity: fade ? 1 : 0 }}
182
+ >
183
+ {THINKING_MESSAGES[idx]}
184
+ </span>
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ // ── Message components ──────────────────────────────────────
9
191
  function AssistantMessage({ msg }) {
192
+ const [collapsed, setCollapsed] = useState(msg.text?.length > 800);
193
+ const isLong = msg.text?.length > 800;
194
+
10
195
  return (
11
196
  <div className="animate-chat-fade-in">
12
- <div className="flex items-center gap-1.5 mb-1">
13
- <div className="w-4 h-4 rounded-sm bg-surface-4 flex items-center justify-center">
14
- <Bot size={10} className="text-text-3" />
15
- </div>
16
- <span className="text-2xs text-text-3 font-sans font-medium">Lab Assistant</span>
197
+ <div className="flex items-center gap-2 mb-1">
198
+ <span className="text-2xs font-semibold text-text-1 font-sans">Lab Assistant</span>
17
199
  </div>
18
- <div className="ml-5">
19
- <div className={cn(
20
- 'text-xs font-sans whitespace-pre-wrap break-words leading-relaxed',
21
- msg.error ? 'text-danger' : 'text-text-1',
22
- )}>
23
- {msg.text}
24
- </div>
200
+ <div className="border-l border-accent pl-3.5 py-1">
201
+ <StructuredMessage text={collapsed ? msg.text.slice(0, 800) + '...' : msg.text} />
25
202
  </div>
203
+ {collapsed && (
204
+ <button
205
+ onClick={() => setCollapsed(false)}
206
+ className="ml-3.5 mt-1.5 flex items-center gap-1.5 text-[11px] text-accent/70 hover:text-accent font-sans font-medium cursor-pointer transition-colors"
207
+ >
208
+ <ChevronDown size={11} />
209
+ Show full response
210
+ </button>
211
+ )}
212
+ {isLong && !collapsed && (
213
+ <button
214
+ onClick={() => setCollapsed(true)}
215
+ className="ml-3.5 mt-1.5 flex items-center gap-1.5 text-[11px] text-accent/70 hover:text-accent font-sans font-medium cursor-pointer transition-colors"
216
+ >
217
+ <ChevronDown size={11} className="rotate-180" />
218
+ Collapse
219
+ </button>
220
+ )}
26
221
  </div>
27
222
  );
28
223
  }
29
224
 
30
225
  function UserMessage({ msg }) {
31
226
  return (
32
- <div className="flex justify-end animate-chat-fade-in">
33
- <div className="max-w-[80%]">
34
- <div className="px-3.5 py-2 bg-accent/8 rounded rounded-br-none">
35
- <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
227
+ <div className="flex justify-end pl-8 animate-chat-fade-in">
228
+ <div className="max-w-[90%]">
229
+ <div className="px-3.5 py-2.5 rounded-lg border bg-info/10 border-info/25">
230
+ <p className="text-[13px] text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
36
231
  </div>
37
232
  </div>
38
233
  </div>
39
234
  );
40
235
  }
41
236
 
237
+ // ── Main component ──────────────────────────────────────────
42
238
  export function LabAssistant() {
43
239
  const agentId = useGrooveStore((s) => s.labAssistantAgentId);
44
240
  const backend = useGrooveStore((s) => s.labAssistantBackend);
@@ -48,10 +244,15 @@ export function LabAssistant() {
48
244
  const instructAgent = useGrooveStore((s) => s.instructAgent);
49
245
  const dismissLabAssistant = useGrooveStore((s) => s.dismissLabAssistant);
50
246
  const setLabAssistantMode = useGrooveStore((s) => s.setLabAssistantMode);
247
+ const onLabAssistantComplete = useGrooveStore((s) => s.onLabAssistantComplete);
248
+ const fetchLabRuntimes = useGrooveStore((s) => s.fetchLabRuntimes);
249
+ const activeRuntime = useGrooveStore((s) => s.labActiveRuntime);
250
+ const activeModel = useGrooveStore((s) => s.labActiveModel);
51
251
 
52
252
  const [input, setInput] = useState('');
53
253
  const scrollRef = useRef(null);
54
254
  const inputRef = useRef(null);
255
+ const completionHandled = useRef(false);
55
256
 
56
257
  const agent = agents.find((a) => a.id === agentId);
57
258
  const messages = chatHistory[agentId] || [];
@@ -59,19 +260,33 @@ export function LabAssistant() {
59
260
  const isRunning = agent?.status === 'running';
60
261
  const isComplete = agent && agent.status !== 'running';
61
262
 
263
+ useEffect(() => {
264
+ if (isComplete && !completionHandled.current) {
265
+ completionHandled.current = true;
266
+ onLabAssistantComplete();
267
+ }
268
+ if (isRunning) completionHandled.current = false;
269
+ }, [isComplete, isRunning, onLabAssistantComplete]);
270
+
271
+ useEffect(() => {
272
+ if (!isRunning || activeRuntime) return;
273
+ const interval = setInterval(fetchLabRuntimes, 5000);
274
+ return () => clearInterval(interval);
275
+ }, [isRunning, activeRuntime, fetchLabRuntimes]);
276
+
62
277
  useEffect(() => {
63
278
  if (scrollRef.current) {
64
279
  const el = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
65
280
  if (el) el.scrollTop = el.scrollHeight;
66
281
  }
67
- }, [messages.length, messages[messages.length - 1]?.text]);
282
+ }, [messages.length, messages[messages.length - 1]?.text, isThinking]);
68
283
 
69
284
  const handleSend = useCallback(() => {
70
285
  const text = input.trim();
71
- if (!text || !agentId || !isRunning) return;
286
+ if (!text || !agentId) return;
72
287
  setInput('');
73
288
  instructAgent(agentId, text);
74
- }, [input, agentId, isRunning, instructAgent]);
289
+ }, [input, agentId, instructAgent]);
75
290
 
76
291
  function handleKeyDown(e) {
77
292
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -84,39 +299,31 @@ export function LabAssistant() {
84
299
 
85
300
  return (
86
301
  <div className="h-full flex flex-col">
87
- {/* Header */}
88
- <div className="flex-shrink-0 flex items-center justify-between px-4 pb-2">
89
- <div className="flex items-center gap-2">
90
- {backend && (
91
- <span className="text-2xs font-mono text-text-3">{backend}</span>
92
- )}
93
- {agent && (
94
- <Badge
95
- variant={isRunning ? 'success' : isComplete ? 'default' : 'warning'}
96
- className="text-2xs"
97
- >
98
- {agent.status}
99
- </Badge>
100
- )}
302
+ {/* Streaming status bar */}
303
+ {isRunning && (
304
+ <div className="flex-shrink-0 flex items-center gap-3 px-4 h-8 border-b border-border-subtle bg-surface-1/80">
305
+ <div className="relative flex items-center justify-center w-4 h-4">
306
+ <span className="absolute inset-0 rounded-full bg-accent/15 animate-ping [animation-duration:2s]" />
307
+ <span className="relative w-1.5 h-1.5 rounded-full bg-accent" />
308
+ </div>
309
+ <span className="text-2xs font-sans text-text-2">
310
+ Lab Assistant is setting up <span className="font-medium text-text-1">{backend?.toUpperCase()}</span>
311
+ </span>
312
+ <div className="flex-1" />
313
+ <Badge variant="success" className="text-2xs">running</Badge>
101
314
  </div>
102
- <button
103
- onClick={dismissLabAssistant}
104
- className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
105
- >
106
- <X size={14} />
107
- </button>
108
- </div>
315
+ )}
109
316
 
110
317
  {/* Messages */}
111
318
  <ScrollArea ref={scrollRef} className="flex-1 min-h-0">
112
- <div className="px-4 py-3 space-y-5">
319
+ <div className="px-5 py-4 space-y-6">
113
320
  {messages.length === 0 && !isThinking ? (
114
321
  <div className="flex flex-col items-center justify-center py-20 text-center">
115
- <div className="w-10 h-10 rounded bg-surface-2 flex items-center justify-center mb-3">
116
- <Bot size={20} className="text-text-4" />
322
+ <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-3">
323
+ <FlaskConical size={22} className="text-accent" />
117
324
  </div>
118
- <p className="text-sm text-text-2 font-sans font-medium">Setting up {backend}</p>
119
- <p className="text-xs text-text-4 font-sans mt-1">The assistant is starting up...</p>
325
+ <p className="text-sm text-text-1 font-sans font-medium">Setting up {backend?.toUpperCase()}</p>
326
+ <p className="text-[13px] text-text-3 font-sans mt-1.5">The assistant is starting up and will begin configuring your runtime...</p>
120
327
  </div>
121
328
  ) : (
122
329
  messages.map((msg, i) =>
@@ -128,69 +335,96 @@ export function LabAssistant() {
128
335
  )
129
336
  )}
130
337
 
131
- {isThinking && (
132
- <div className="animate-chat-fade-in">
133
- <div className="flex items-center gap-1.5 mb-1">
134
- <div className="w-4 h-4 rounded-sm bg-surface-4 flex items-center justify-center">
135
- <Bot size={10} className="text-text-3" />
136
- </div>
137
- <span className="text-2xs text-text-3 font-sans font-medium">Lab Assistant</span>
138
- </div>
139
- <div className="ml-5 flex items-center gap-1.5 py-1">
140
- <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" />
141
- <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '150ms' }} />
142
- <span className="w-1 h-1 rounded-full bg-text-4 animate-pulse" style={{ animationDelay: '300ms' }} />
143
- </div>
144
- </div>
145
- )}
338
+ {isThinking && <LabThinkingIndicator />}
146
339
  </div>
147
340
  </ScrollArea>
148
341
 
149
342
  {/* Completion banner */}
150
343
  {isComplete && messages.length > 0 && (
151
- <div className="flex-shrink-0 px-4 py-2 bg-success/5 border-t border-success/10">
152
- <div className="flex items-center justify-between">
153
- <span className="text-xs font-sans text-success font-medium">Setup complete</span>
154
- <button
155
- onClick={() => setLabAssistantMode(false)}
156
- className="flex items-center gap-1 px-3 py-1.5 text-xs font-sans font-medium text-surface-0 bg-accent hover:bg-accent/90 rounded-sm transition-colors cursor-pointer"
157
- >
158
- <ArrowRight size={12} /> Playground
159
- </button>
160
- </div>
344
+ <div className={cn(
345
+ 'flex-shrink-0 px-4 py-3 border-t',
346
+ activeRuntime && activeModel
347
+ ? 'bg-success/10 border-success/20'
348
+ : activeRuntime
349
+ ? 'bg-warning/10 border-warning/20'
350
+ : 'bg-surface-2 border-border',
351
+ )}>
352
+ {activeRuntime && activeModel ? (
353
+ <div className="flex items-center justify-between">
354
+ <div>
355
+ <span className="text-[13px] font-sans text-success font-semibold">Runtime ready</span>
356
+ <p className="text-xs text-text-2 font-sans mt-0.5">Your model is loaded and ready to chat</p>
357
+ </div>
358
+ <button
359
+ onClick={() => setLabAssistantMode(false)}
360
+ className="flex items-center gap-1.5 px-4 py-2 text-[13px] font-sans font-medium text-surface-0 bg-accent hover:bg-accent/90 rounded-md transition-colors cursor-pointer"
361
+ >
362
+ <ArrowRight size={14} /> Open Playground
363
+ </button>
364
+ </div>
365
+ ) : activeRuntime ? (
366
+ <div className="flex items-center justify-between">
367
+ <div>
368
+ <span className="text-[13px] font-sans text-warning font-semibold">Runtime registered</span>
369
+ <p className="text-xs text-text-2 font-sans mt-0.5">Model may still be loading — test the runtime in the sidebar</p>
370
+ </div>
371
+ <button
372
+ onClick={() => setLabAssistantMode(false)}
373
+ className="flex items-center gap-1.5 px-4 py-2 text-[13px] font-sans font-medium text-text-0 bg-surface-3 hover:bg-surface-4 rounded-md transition-colors cursor-pointer"
374
+ >
375
+ <ArrowRight size={14} /> View Playground
376
+ </button>
377
+ </div>
378
+ ) : (
379
+ <div className="flex items-center justify-between">
380
+ <div>
381
+ <span className="text-[13px] font-sans text-text-1 font-semibold">Setup finished</span>
382
+ <p className="text-xs text-text-2 font-sans mt-0.5">
383
+ If the model is still downloading, the runtime will appear once the server starts.
384
+ </p>
385
+ </div>
386
+ <button
387
+ onClick={fetchLabRuntimes}
388
+ className="flex items-center gap-1.5 px-4 py-2 text-[13px] font-sans font-medium text-text-0 bg-surface-3 hover:bg-surface-4 rounded-md transition-colors cursor-pointer"
389
+ >
390
+ Check Runtimes
391
+ </button>
392
+ </div>
393
+ )}
161
394
  </div>
162
395
  )}
163
396
 
164
397
  {/* Input */}
165
398
  <div className="flex-shrink-0 px-4 py-3">
166
- <div className="flex items-end gap-2 bg-surface-1 border border-border-subtle rounded-md p-1.5 focus-within:border-accent/30 transition-colors">
399
+ <div className="flex items-end gap-2 border rounded-xl bg-surface-0 p-1.5 transition-colors border-border-subtle focus-within:border-accent/30">
167
400
  <textarea
168
401
  ref={inputRef}
169
402
  value={input}
170
403
  onChange={(e) => setInput(e.target.value)}
171
404
  onKeyDown={handleKeyDown}
172
- placeholder={isRunning ? 'Type a message...' : 'Assistant is not running'}
173
- disabled={!isRunning}
405
+ placeholder={isRunning ? 'Ask the assistant anything...' : 'Send a message to continue...'}
174
406
  rows={1}
175
407
  className={cn(
176
- 'flex-1 resize-none bg-transparent px-2 py-1.5',
177
- 'text-xs text-text-0 font-sans placeholder:text-text-4',
408
+ 'flex-1 resize-none bg-transparent px-2.5 py-1.5',
409
+ 'text-[13px] text-text-0 font-sans placeholder:text-text-4',
178
410
  'focus:outline-none',
179
- 'disabled:opacity-40 disabled:cursor-not-allowed',
180
- 'min-h-[28px] max-h-32',
411
+ 'min-h-[32px] max-h-32',
181
412
  )}
182
413
  style={{ height: 'auto', overflowY: input.split('\n').length > 4 ? 'auto' : 'hidden' }}
183
414
  onInput={(e) => { e.target.style.height = 'auto'; e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`; }}
184
415
  />
185
416
  <button
186
- disabled={!input.trim() || !isRunning}
417
+ disabled={!input.trim()}
187
418
  onClick={handleSend}
188
419
  className={cn(
189
- 'flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm transition-colors cursor-pointer',
190
- input.trim() && isRunning ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
420
+ 'flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer',
421
+ 'disabled:opacity-15 disabled:cursor-not-allowed',
422
+ input.trim()
423
+ ? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
424
+ : 'bg-transparent text-text-4',
191
425
  )}
192
426
  >
193
- <Send size={12} />
427
+ <Send size={13} />
194
428
  </button>
195
429
  </div>
196
430
  </div>