groove-dev 0.27.144 → 0.27.146

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 (135) hide show
  1. package/CLAUDE.md +7 -0
  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 +12 -6
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +59 -58
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +20 -0
  7. package/node_modules/@groove-dev/daemon/src/process.js +262 -15
  8. package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -3
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +15 -3
  10. package/node_modules/@groove-dev/daemon/src/routes/agents.js +49 -83
  11. package/node_modules/@groove-dev/daemon/templates/lab-general.json +12 -0
  12. package/node_modules/@groove-dev/daemon/templates/llama-cpp-setup.json +12 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-BKbsE_hn.js +1011 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +80 -95
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  19. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +132 -4
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -8
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +199 -75
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +21 -4
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +10 -13
  24. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +3 -3
  25. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +42 -34
  26. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +9 -3
  27. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +13 -3
  28. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +66 -65
  29. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  30. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +124 -127
  31. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  32. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
  33. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +24 -1
  34. package/node_modules/@groove-dev/gui/src/components/ui/question-modal.jsx +107 -0
  35. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +2 -2
  36. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  37. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  38. package/node_modules/@groove-dev/gui/src/stores/groove.js +49 -2
  39. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +18 -2
  40. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +14 -14
  41. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +68 -32
  42. package/node_modules/@groove-dev/gui/src/views/models.jsx +57 -36
  43. package/node_modules/axios/CHANGELOG.md +260 -0
  44. package/node_modules/axios/README.md +595 -223
  45. package/node_modules/axios/dist/axios.js +1460 -1090
  46. package/node_modules/axios/dist/axios.js.map +1 -1
  47. package/node_modules/axios/dist/axios.min.js +3 -3
  48. package/node_modules/axios/dist/axios.min.js.map +1 -1
  49. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  50. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  51. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  52. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  53. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  54. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  55. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  56. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  57. package/node_modules/axios/index.d.cts +40 -41
  58. package/node_modules/axios/index.d.ts +151 -227
  59. package/node_modules/axios/index.js +2 -0
  60. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  61. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  62. package/node_modules/axios/lib/adapters/http.js +306 -58
  63. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  64. package/node_modules/axios/lib/core/Axios.js +7 -3
  65. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  66. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  67. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  68. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  69. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  70. package/node_modules/axios/lib/core/settle.js +7 -11
  71. package/node_modules/axios/lib/defaults/index.js +14 -9
  72. package/node_modules/axios/lib/env/data.js +1 -1
  73. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  74. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  75. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  76. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  77. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  78. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  79. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  80. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  81. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  82. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  83. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  84. package/node_modules/axios/lib/helpers/validator.js +3 -1
  85. package/node_modules/axios/lib/utils.js +33 -21
  86. package/node_modules/axios/package.json +17 -24
  87. package/node_modules/follow-redirects/README.md +7 -5
  88. package/node_modules/follow-redirects/index.js +24 -1
  89. package/node_modules/follow-redirects/package.json +1 -1
  90. package/package.json +1 -1
  91. package/packages/cli/package.json +1 -1
  92. package/packages/daemon/package.json +1 -1
  93. package/packages/daemon/src/api.js +12 -6
  94. package/packages/daemon/src/conversations.js +59 -58
  95. package/packages/daemon/src/introducer.js +20 -0
  96. package/packages/daemon/src/process.js +262 -15
  97. package/packages/daemon/src/providers/groove-network.js +1 -3
  98. package/packages/daemon/src/rotator.js +15 -3
  99. package/packages/daemon/src/routes/agents.js +49 -83
  100. package/packages/daemon/templates/lab-general.json +12 -0
  101. package/packages/daemon/templates/llama-cpp-setup.json +12 -0
  102. package/packages/gui/dist/assets/index-BKbsE_hn.js +1011 -0
  103. package/packages/gui/dist/assets/index-CEkPsSAm.css +1 -0
  104. package/packages/gui/dist/index.html +2 -2
  105. package/packages/gui/package.json +1 -1
  106. package/packages/gui/src/components/agents/agent-feed.jsx +80 -95
  107. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  108. package/packages/gui/src/components/agents/spawn-wizard.jsx +132 -4
  109. package/packages/gui/src/components/chat/chat-header.jsx +3 -8
  110. package/packages/gui/src/components/chat/chat-input.jsx +199 -75
  111. package/packages/gui/src/components/chat/chat-messages.jsx +21 -4
  112. package/packages/gui/src/components/chat/chat-view.jsx +10 -13
  113. package/packages/gui/src/components/chat/model-picker.jsx +3 -3
  114. package/packages/gui/src/components/lab/chat-playground.jsx +42 -34
  115. package/packages/gui/src/components/lab/lab-assistant.jsx +9 -3
  116. package/packages/gui/src/components/lab/metrics-panel.jsx +13 -3
  117. package/packages/gui/src/components/lab/parameter-panel.jsx +66 -65
  118. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  119. package/packages/gui/src/components/lab/runtime-config.jsx +124 -127
  120. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  121. package/packages/gui/src/components/layout/app-shell.jsx +2 -0
  122. package/packages/gui/src/components/layout/status-bar.jsx +24 -1
  123. package/packages/gui/src/components/ui/question-modal.jsx +107 -0
  124. package/packages/gui/src/components/ui/sheet.jsx +2 -2
  125. package/packages/gui/src/components/ui/slider.jsx +8 -8
  126. package/packages/gui/src/lib/status.js +1 -0
  127. package/packages/gui/src/stores/groove.js +49 -2
  128. package/packages/gui/src/stores/slices/agents-slice.js +18 -2
  129. package/packages/gui/src/stores/slices/chat-slice.js +14 -14
  130. package/packages/gui/src/views/model-lab.jsx +68 -32
  131. package/packages/gui/src/views/models.jsx +57 -36
  132. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +0 -1012
  133. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +0 -1
  134. package/packages/gui/dist/assets/index-BcoF6_eF.js +0 -1012
  135. package/packages/gui/dist/assets/index-Dd7qhiEd.css +0 -1
@@ -0,0 +1,107 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { AnimatePresence, motion } from 'framer-motion';
4
+ import { MessageCircleQuestion, Send } from 'lucide-react';
5
+ import { Button } from '../ui/button';
6
+ import { useGrooveStore } from '../../stores/groove';
7
+
8
+ export function QuestionModal() {
9
+ const pendingQuestions = useGrooveStore((s) => s.pendingQuestions);
10
+ const answerQuestion = useGrooveStore((s) => s.answerQuestion);
11
+ const [answers, setAnswers] = useState({});
12
+
13
+ if (!pendingQuestions?.length) return null;
14
+
15
+ function handleSubmit(q) {
16
+ const questionAnswers = {};
17
+ for (const qItem of q.questions) {
18
+ const key = qItem.question || qItem.header || `q${q.questions.indexOf(qItem)}`;
19
+ questionAnswers[key] = answers[`${q.agentId}:${key}`] || '';
20
+ }
21
+ answerQuestion(q.agentId, questionAnswers);
22
+ setAnswers((prev) => {
23
+ const next = { ...prev };
24
+ for (const key of Object.keys(next)) {
25
+ if (key.startsWith(q.agentId + ':')) delete next[key];
26
+ }
27
+ return next;
28
+ });
29
+ }
30
+
31
+ return (
32
+ <div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-lg flex flex-col gap-2 px-4">
33
+ <AnimatePresence>
34
+ {pendingQuestions.map((q) => (
35
+ <motion.div
36
+ key={q.id}
37
+ initial={{ y: 20, opacity: 0 }}
38
+ animate={{ y: 0, opacity: 1 }}
39
+ exit={{ y: 20, opacity: 0 }}
40
+ transition={{ duration: 0.2 }}
41
+ className="rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden"
42
+ >
43
+ <div className="px-4 py-3 flex items-start gap-3">
44
+ <MessageCircleQuestion size={16} className="text-accent shrink-0 mt-0.5" />
45
+ <div className="flex-1 min-w-0">
46
+ <p className="text-sm font-semibold text-text-0 font-sans">
47
+ {q.agentName || 'Agent'} has a question
48
+ </p>
49
+ </div>
50
+ </div>
51
+ <div className="px-4 pb-3 flex flex-col gap-2">
52
+ {q.questions.map((qItem, i) => {
53
+ const key = qItem.question || qItem.header || `q${i}`;
54
+ const inputKey = `${q.agentId}:${key}`;
55
+ return (
56
+ <div key={i}>
57
+ <p className="text-2xs text-text-2 font-sans mb-1">{qItem.question || key}</p>
58
+ {qItem.options?.length > 0 ? (
59
+ <div className="flex flex-wrap gap-1">
60
+ {qItem.options.map((opt) => {
61
+ const label = typeof opt === 'string' ? opt : opt.label;
62
+ const selected = answers[inputKey] === label;
63
+ return (
64
+ <button
65
+ key={label}
66
+ onClick={() => setAnswers((p) => ({ ...p, [inputKey]: label }))}
67
+ className={`px-2 py-1 text-2xs rounded border font-sans transition-colors ${
68
+ selected
69
+ ? 'border-accent bg-accent/20 text-text-0'
70
+ : 'border-border-subtle bg-surface-1 text-text-2 hover:border-accent/50'
71
+ }`}
72
+ >
73
+ {label}
74
+ </button>
75
+ );
76
+ })}
77
+ </div>
78
+ ) : (
79
+ <input
80
+ type="text"
81
+ className="w-full px-2 py-1.5 text-xs rounded border border-border-subtle bg-surface-1 text-text-0 font-sans placeholder:text-text-3 focus:outline-none focus:border-accent"
82
+ placeholder="Type your answer..."
83
+ value={answers[inputKey] || ''}
84
+ onChange={(e) => setAnswers((p) => ({ ...p, [inputKey]: e.target.value }))}
85
+ onKeyDown={(e) => e.key === 'Enter' && handleSubmit(q)}
86
+ />
87
+ )}
88
+ </div>
89
+ );
90
+ })}
91
+ </div>
92
+ <div className="px-4 py-2.5 border-t border-border-subtle flex items-center justify-end">
93
+ <Button
94
+ size="sm"
95
+ variant="accent"
96
+ onClick={() => handleSubmit(q)}
97
+ >
98
+ <Send size={14} className="mr-1" />
99
+ Answer
100
+ </Button>
101
+ </div>
102
+ </motion.div>
103
+ ))}
104
+ </AnimatePresence>
105
+ </div>
106
+ );
107
+ }
@@ -30,12 +30,12 @@ export function SheetContent({ children, className, title, side = 'right', width
30
30
  <DialogPrimitive.Title className="text-base font-semibold text-text-0 font-sans">
31
31
  {title}
32
32
  </DialogPrimitive.Title>
33
- <DialogPrimitive.Close
33
+ <button
34
34
  onClick={onClose}
35
35
  className="p-1 rounded-md text-text-3 hover:text-text-0 hover:bg-surface-5 transition-colors cursor-pointer"
36
36
  >
37
37
  <X size={16} />
38
- </DialogPrimitive.Close>
38
+ </button>
39
39
  </div>
40
40
  )}
41
41
  <DialogPrimitive.Description className="sr-only">Panel</DialogPrimitive.Description>
@@ -10,13 +10,13 @@ export function TuningSlider({
10
10
  const display = typeof fmt === 'function' ? fmt(value) : (typeof fmt === 'string' ? fmt : value);
11
11
 
12
12
  return (
13
- <div className={cn('group flex items-center gap-2 py-1.5', disabled && 'opacity-40 pointer-events-none', className)}>
14
- <span className="text-2xs text-text-2 font-sans w-20 shrink-0 truncate">{label}</span>
13
+ <div className={cn('group flex items-center gap-2.5 h-8', disabled && 'opacity-40 pointer-events-none', className)}>
14
+ <span className="text-[11px] text-text-2 font-sans w-[76px] shrink-0 truncate">{label}</span>
15
15
  <div className="relative flex-1 flex items-center h-5">
16
16
  <div className="absolute inset-y-0 flex items-center w-full pointer-events-none">
17
- <div className="w-full h-1 rounded-full bg-surface-5">
17
+ <div className="w-full h-[3px] rounded-full bg-surface-5">
18
18
  <div
19
- className="h-full rounded-full bg-accent/70 group-hover:bg-accent transition-colors"
19
+ className="h-full rounded-full bg-accent/60 group-hover:bg-accent transition-colors"
20
20
  style={{ width: `${pct}%` }}
21
21
  />
22
22
  </div>
@@ -30,20 +30,20 @@ export function TuningSlider({
30
30
  disabled={disabled}
31
31
  onChange={(e) => onChange(Number(e.target.value))}
32
32
  className="relative w-full h-5 appearance-none bg-transparent cursor-pointer
33
- [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
33
+ [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5
34
34
  [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent
35
35
  [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_var(--color-surface-1)]
36
- [&::-webkit-slider-thumb]:hover:bg-accent [&::-webkit-slider-thumb]:hover:shadow-[0_0_0_2px_var(--color-surface-1),0_0_8px_rgba(51,175,188,0.4)]
36
+ [&::-webkit-slider-thumb]:hover:shadow-[0_0_0_2px_var(--color-surface-1),0_0_6px_rgba(51,175,188,0.35)]
37
37
  [&::-webkit-slider-thumb]:active:scale-110
38
38
  [&::-webkit-slider-thumb]:transition-all
39
- [&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
39
+ [&::-moz-range-thumb]:w-2.5 [&::-moz-range-thumb]:h-2.5 [&::-moz-range-thumb]:rounded-full
40
40
  [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-none
41
41
  [&::-moz-range-thumb]:shadow-[0_0_0_2px_var(--color-surface-1)]
42
42
  [&::-moz-range-track]:bg-transparent
43
43
  disabled:cursor-not-allowed"
44
44
  />
45
45
  </div>
46
- <span className="text-2xs text-accent font-mono font-medium w-10 text-right shrink-0 tabular-nums">{display}</span>
46
+ <span className="text-[11px] text-accent font-mono font-medium w-9 text-right shrink-0 tabular-nums">{display}</span>
47
47
  </div>
48
48
  );
49
49
  }
@@ -45,6 +45,7 @@ export const ROLE_COLORS = {
45
45
  creative: NEUTRAL_ROLE,
46
46
  slides: NEUTRAL_ROLE,
47
47
  chat: NEUTRAL_ROLE,
48
+ research: NEUTRAL_ROLE,
48
49
  };
49
50
 
50
51
  export function roleColor(role) {
@@ -91,6 +91,11 @@ export const useGrooveStore = create((set, get) => ({
91
91
  if (data) set({ subscription: { ...get().subscription, ...data } });
92
92
  });
93
93
  }
94
+ if (window.groove?.update?.onUpdateAvailable) {
95
+ window.groove.update.onUpdateAvailable((data) => {
96
+ set({ updateProgress: { percent: 0, version: data.version } });
97
+ });
98
+ }
94
99
  if (window.groove?.update?.onUpdateProgress) {
95
100
  window.groove.update.onUpdateProgress((data) => {
96
101
  set({ updateProgress: data });
@@ -101,6 +106,18 @@ export const useGrooveStore = create((set, get) => ({
101
106
  set({ updateReady: data.version, updateModalOpen: true, updateProgress: null });
102
107
  });
103
108
  }
109
+ if (window.groove?.update?.getUpdateStatus) {
110
+ window.groove.update.getUpdateStatus().then((state) => {
111
+ if (!state) return;
112
+ if (state.downloaded) {
113
+ set({ updateReady: state.downloaded.version, updateProgress: null });
114
+ } else if (state.progress) {
115
+ set({ updateProgress: state.progress });
116
+ } else if (state.available) {
117
+ set({ updateProgress: { percent: 0, version: state.available.version } });
118
+ }
119
+ }).catch(() => {});
120
+ }
104
121
  };
105
122
 
106
123
  ws.onmessage = (event) => {
@@ -297,6 +314,11 @@ export const useGrooveStore = create((set, get) => ({
297
314
  }
298
315
 
299
316
  case 'agent:exit': {
317
+ if (msg.status === 'waiting_for_input') {
318
+ const waitAgent = get().agents.find((a) => a.id === msg.agentId);
319
+ get().addToast('info', `${waitAgent?.name || msg.agentId.slice(0, 8)} needs your input`, 'Check the question popup below');
320
+ break;
321
+ }
300
322
  const agent = get().agents.find((a) => a.id === msg.agentId);
301
323
  const name = agent?.name || msg.agentId;
302
324
  const isKill = msg.status === 'killed' || msg.code === 143 || msg.code === 137;
@@ -398,6 +420,14 @@ export const useGrooveStore = create((set, get) => ({
398
420
  get().addChatMessage(msg.agentId, 'system', 'Agent is working — message will be delivered when it finishes.');
399
421
  break;
400
422
 
423
+ case 'agent:question':
424
+ set((s) => ({ pendingQuestions: [...s.pendingQuestions, msg.data] }));
425
+ break;
426
+
427
+ case 'agent:question:resolved':
428
+ set((s) => ({ pendingQuestions: s.pendingQuestions.filter((q) => q.agentId !== msg.agentId) }));
429
+ break;
430
+
401
431
  case 'ollama:pull:progress':
402
432
  set({ ollamaPullProgress: { ...get().ollamaPullProgress, [msg.model]: { status: 'pulling', progress: msg.progress } } });
403
433
  break;
@@ -433,6 +463,8 @@ export const useGrooveStore = create((set, get) => ({
433
463
  case 'lab:runtime:added':
434
464
  case 'lab:runtime:updated':
435
465
  case 'lab:runtime:removed':
466
+ case 'lab:runtime:started':
467
+ case 'lab:runtime:stopped':
436
468
  case 'llama:server:stopped':
437
469
  get().fetchLabRuntimes();
438
470
  break;
@@ -871,6 +903,17 @@ export const useGrooveStore = create((set, get) => ({
871
903
  break;
872
904
  }
873
905
 
906
+ case 'conversation:tool': {
907
+ const { conversationId, name, summary } = msg.data || msg;
908
+ if (!conversationId) break;
909
+ set((s) => {
910
+ const tools = { ...s.conversationActiveTools };
911
+ tools[conversationId] = { name: name || 'Tool', summary: summary || null, timestamp: Date.now() };
912
+ return { conversationActiveTools: tools };
913
+ });
914
+ break;
915
+ }
916
+
874
917
  case 'conversation:chunk': {
875
918
  const { conversationId, text } = msg.data || msg;
876
919
  if (!conversationId || !text) break;
@@ -885,7 +928,9 @@ export const useGrooveStore = create((set, get) => ({
885
928
  arr.push({ from: 'assistant', text, timestamp: Date.now() });
886
929
  }
887
930
  msgs[conversationId] = arr.slice(-200);
888
- return { conversationMessages: msgs, streamingConversationId: conversationId };
931
+ const tools = { ...s.conversationActiveTools };
932
+ delete tools[conversationId];
933
+ return { conversationMessages: msgs, streamingConversationId: conversationId, conversationActiveTools: tools };
889
934
  });
890
935
  break;
891
936
  }
@@ -893,7 +938,9 @@ export const useGrooveStore = create((set, get) => ({
893
938
  case 'conversation:complete': {
894
939
  const { conversationId } = msg.data || msg;
895
940
  if (conversationId && get().streamingConversationId === conversationId) {
896
- set({ sendingMessage: false, streamingConversationId: null });
941
+ const tools = { ...get().conversationActiveTools };
942
+ delete tools[conversationId];
943
+ set({ sendingMessage: false, streamingConversationId: null, conversationActiveTools: tools });
897
944
  }
898
945
  if (conversationId) persistJSON('groove:conversationMessages', get().conversationMessages);
899
946
  break;
@@ -14,6 +14,15 @@ export const createAgentsSlice = (set, get) => ({
14
14
  // Track which agents are thinking (sent a message, waiting for response)
15
15
  thinkingAgents: new Set(),
16
16
 
17
+ // Pending questions from agents awaiting user input
18
+ pendingQuestions: [],
19
+
20
+ answerQuestion(agentId, answers) {
21
+ return api.post(`/agents/${agentId}/answer`, { answers }).then(() => {
22
+ set((s) => ({ pendingQuestions: s.pendingQuestions.filter((q) => q.agentId !== agentId) }));
23
+ });
24
+ },
25
+
17
26
  // ── Workspace Mode ────────────────────────────────────────
18
27
  workspaceMode: localStorage.getItem('groove:workspaceMode') === 'true',
19
28
  workspaceAgentId: null,
@@ -38,7 +47,7 @@ export const createAgentsSlice = (set, get) => ({
38
47
 
39
48
  async spawnAgent(config) {
40
49
  try {
41
- const teamId = get().activeTeamId;
50
+ const teamId = config.teamId || get().activeTeamId;
42
51
  const agent = await api.post('/agents', { ...config, teamId });
43
52
  get().addToast('success', `Spawned ${agent.name}`);
44
53
  return agent;
@@ -407,15 +416,22 @@ export const createAgentsSlice = (set, get) => ({
407
416
 
408
417
  case 'read': {
409
418
  if (tags.length === 0) { addSystemMsg('Usage: [read] #tag1 #tag2 ...'); return true; }
419
+ const userText = rest.replace(/#[\w/.-]+/g, '').trim();
410
420
  get().addChatMessage(agentId, 'user', message, false);
411
421
  const readBrief = await api.post('/keeper/pull', { tags });
412
422
  if (readBrief?.brief) {
423
+ const memoryBlock = `\n\n---\nContext from memories (${tags.map(t => '#' + t).join(', ')}):\n\n${readBrief.brief}`;
424
+ set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, agentId]) }));
413
425
  await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, {
414
- message: `Here is context from my tagged memories:\n\n${readBrief.brief}`,
426
+ message: userText ? `${userText}${memoryBlock}` : `Here is context from my tagged memories:\n\n${readBrief.brief}`,
415
427
  });
416
428
  addSystemMsg(`Sent ${tags.map(t => '#' + t).join(', ')} to agent`);
417
429
  } else {
418
430
  addSystemMsg(`No memories found for ${tags.map(t => '#' + t).join(', ')}`);
431
+ if (userText) {
432
+ set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, agentId]) }));
433
+ await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, { message: userText });
434
+ }
419
435
  }
420
436
  return true;
421
437
  }
@@ -10,6 +10,7 @@ export const createChatSlice = (set, get) => ({
10
10
  conversations: [],
11
11
  activeConversationId: localStorage.getItem('groove:activeConversationId') || null,
12
12
  conversationMessages: loadJSON('groove:conversationMessages'),
13
+ conversationActiveTools: {},
13
14
  sendingMessage: false,
14
15
  streamingConversationId: null,
15
16
  conversationRoles: loadJSON('groove:conversationRoles'),
@@ -166,20 +167,19 @@ export const createChatSlice = (set, get) => ({
166
167
 
167
168
  try {
168
169
  const body = { message };
169
- if (conv.mode === 'api' || !conv.mode) {
170
- const history = get().conversationMessages[conversationId] || [];
171
- body.history = history.slice(0, -1);
172
-
173
- const role = get().conversationRoles?.[conversationId];
174
- const rules = ['Never use emojis in your responses.', 'Be professional, concise, and direct.'];
175
- if (role) rules.unshift(`You are a professional ${role}. Respond with deep expertise in that domain.`);
176
- const systemCtx = rules.join(' ');
177
- body.history = [
178
- { from: 'user', text: `Instructions: ${systemCtx}` },
179
- { from: 'assistant', text: 'Understood.' },
180
- ...body.history,
181
- ];
182
- }
170
+ const history = get().conversationMessages[conversationId] || [];
171
+ body.history = history.slice(0, -1);
172
+
173
+ const role = get().conversationRoles?.[conversationId];
174
+ const rules = ['Never use emojis in your responses.', 'Be professional, concise, and direct.'];
175
+ if (role && role !== 'chat') rules.unshift(`You are a professional ${role}. Respond with deep expertise in that domain.`);
176
+ if (role === 'research') rules.unshift('You are a research assistant. Help explore ideas, synthesize information, and provide thorough analysis with sources when possible.');
177
+ const systemCtx = rules.join(' ');
178
+ body.history = [
179
+ { from: 'user', text: `Instructions: ${systemCtx}` },
180
+ { from: 'assistant', text: 'Understood.' },
181
+ ...body.history,
182
+ ];
183
183
  const effort = get().conversationReasoningEffort?.[conversationId] || 'medium';
184
184
  const verbosity = get().conversationVerbosity?.[conversationId] || 'medium';
185
185
  if (conv.provider === 'codex') {
@@ -12,7 +12,7 @@ import { LabAssistant } from '../components/lab/lab-assistant';
12
12
  import { MetricsPanel } from '../components/lab/metrics-panel';
13
13
  import { PresetManager } from '../components/lab/preset-manager';
14
14
  import { cn } from '../lib/cn';
15
- import { FlaskConical, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Box } from 'lucide-react';
15
+ import { FlaskConical, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Box, ChevronRight } from 'lucide-react';
16
16
 
17
17
  const LEFT_DEFAULT = 280;
18
18
  const LEFT_MIN = 220;
@@ -21,6 +21,39 @@ const RIGHT_DEFAULT = 240;
21
21
  const RIGHT_MIN = 200;
22
22
  const RIGHT_MAX = 360;
23
23
 
24
+ export function SidebarSection({ label, action, children, className, collapsible = false, defaultOpen = true }) {
25
+ const [open, setOpen] = useState(defaultOpen);
26
+ const isOpen = collapsible ? open : true;
27
+
28
+ return (
29
+ <div className={cn('space-y-3', className)}>
30
+ {label && (
31
+ <div className="flex items-center justify-between h-6">
32
+ {collapsible ? (
33
+ <button
34
+ onClick={() => setOpen(!open)}
35
+ className="flex items-center gap-1.5 cursor-pointer group"
36
+ >
37
+ <ChevronRight
38
+ size={10}
39
+ className={cn(
40
+ 'text-text-4 transition-transform duration-150 flex-shrink-0',
41
+ isOpen && 'rotate-90',
42
+ )}
43
+ />
44
+ <span className="text-[10px] font-semibold font-sans text-text-3 uppercase tracking-widest group-hover:text-text-2 transition-colors">{label}</span>
45
+ </button>
46
+ ) : (
47
+ <span className="text-[10px] font-semibold font-sans text-text-3 uppercase tracking-widest">{label}</span>
48
+ )}
49
+ {isOpen && action}
50
+ </div>
51
+ )}
52
+ {isOpen && children}
53
+ </div>
54
+ );
55
+ }
56
+
24
57
  function ModelSelector() {
25
58
  const models = useGrooveStore((s) => s.labModels);
26
59
  const runtimes = useGrooveStore((s) => s.labRuntimes);
@@ -35,18 +68,16 @@ function ModelSelector() {
35
68
 
36
69
  if (!online) {
37
70
  return (
38
- <div className="space-y-1.5">
39
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider">Model</span>
40
- <p className="text-2xs text-text-4 font-sans px-1">Start the runtime to select a model</p>
41
- </div>
71
+ <SidebarSection label="Model">
72
+ <p className="text-2xs text-text-4 font-sans">Start the runtime to select a model</p>
73
+ </SidebarSection>
42
74
  );
43
75
  }
44
76
 
45
77
  if (models.length <= 1 && activeModel) return null;
46
78
 
47
79
  return (
48
- <div className="space-y-1.5">
49
- <span className="text-2xs font-semibold font-sans text-text-3 uppercase tracking-wider">Model</span>
80
+ <SidebarSection label="Model">
50
81
  <Combobox
51
82
  value={activeModel || ''}
52
83
  onChange={setActiveModel}
@@ -60,7 +91,7 @@ function ModelSelector() {
60
91
  </div>
61
92
  )}
62
93
  />
63
- </div>
94
+ </SidebarSection>
64
95
  );
65
96
  }
66
97
 
@@ -104,6 +135,7 @@ export default function ModelLabView() {
104
135
  const labAssistantAgentId = useGrooveStore((s) => s.labAssistantAgentId);
105
136
  const labAssistantMode = useGrooveStore((s) => s.labAssistantMode);
106
137
  const setLabAssistantMode = useGrooveStore((s) => s.setLabAssistantMode);
138
+ const launchLabAssistant = useGrooveStore((s) => s.launchLabAssistant);
107
139
 
108
140
  useEffect(() => { fetchLabRuntimes(); }, [fetchLabRuntimes]);
109
141
 
@@ -175,7 +207,7 @@ export default function ModelLabView() {
175
207
  style={leftCollapsed ? undefined : { width: leftWidth }}
176
208
  >
177
209
  <div className="h-full flex flex-col">
178
- <div className="flex-shrink-0 flex items-center justify-between px-4 h-10">
210
+ <div className="flex-shrink-0 flex items-center justify-between px-4 h-10 border-b border-border-subtle">
179
211
  <div className="flex items-center gap-2">
180
212
  <FlaskConical size={13} className="text-accent" />
181
213
  <span className="text-xs font-semibold font-sans text-text-1">Model Lab</span>
@@ -183,7 +215,7 @@ export default function ModelLabView() {
183
215
  <PanelToggle collapsed={false} onClick={() => setLeftCollapsed(true)} side="left" />
184
216
  </div>
185
217
  <ScrollArea className="flex-1 min-h-0">
186
- <div className="px-4 pb-4 space-y-5 divide-y divide-border-subtle [&>*]:pt-5 [&>*:first-child]:pt-0">
218
+ <div className="px-4 py-4 space-y-6">
187
219
  <RuntimeSection />
188
220
  <ModelSelector />
189
221
  <ParameterPanel />
@@ -203,28 +235,32 @@ export default function ModelLabView() {
203
235
  {leftCollapsed && (
204
236
  <PanelToggle collapsed onClick={() => setLeftCollapsed(false)} side="left" />
205
237
  )}
206
- {labAssistantAgentId && (
207
- <div className="flex items-center gap-px bg-surface-2 rounded p-px">
208
- <button
209
- onClick={() => setLabAssistantMode(false)}
210
- className={cn(
211
- 'px-3 py-1 text-2xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
212
- !labAssistantMode ? 'text-text-0 bg-surface-4' : 'text-text-3 hover:text-text-1',
213
- )}
214
- >
215
- Playground
216
- </button>
217
- <button
218
- onClick={() => setLabAssistantMode(true)}
219
- className={cn(
220
- 'px-3 py-1 text-2xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
221
- labAssistantMode ? 'text-text-0 bg-surface-4' : 'text-text-3 hover:text-text-1',
222
- )}
223
- >
224
- Assistant
225
- </button>
226
- </div>
227
- )}
238
+ <div className="flex items-center gap-px bg-surface-2 rounded p-px">
239
+ <button
240
+ onClick={() => setLabAssistantMode(false)}
241
+ className={cn(
242
+ 'px-3 py-1 text-2xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
243
+ !labAssistantMode ? 'text-text-0 bg-surface-4' : 'text-text-3 hover:text-text-1',
244
+ )}
245
+ >
246
+ Playground
247
+ </button>
248
+ <button
249
+ onClick={() => {
250
+ if (labAssistantAgentId) {
251
+ setLabAssistantMode(true);
252
+ } else {
253
+ launchLabAssistant('lab-general');
254
+ }
255
+ }}
256
+ className={cn(
257
+ 'px-3 py-1 text-2xs font-sans font-medium rounded-sm transition-colors cursor-pointer',
258
+ labAssistantMode ? 'text-text-0 bg-surface-4' : 'text-text-3 hover:text-text-1',
259
+ )}
260
+ >
261
+ Assistant
262
+ </button>
263
+ </div>
228
264
  <div className="flex-1" />
229
265
  {rightCollapsed && (
230
266
  <PanelToggle collapsed onClick={() => setRightCollapsed(false)} side="right" />