groove-dev 0.27.144 → 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 (100) 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/conversations.js +18 -48
  4. package/node_modules/@groove-dev/daemon/src/routes/agents.js +6 -83
  5. package/node_modules/@groove-dev/gui/dist/assets/{index-BcoF6_eF.js → index-Bxc0gU06.js} +232 -238
  6. package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
  7. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  8. package/node_modules/@groove-dev/gui/package.json +1 -1
  9. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +80 -95
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  11. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
  14. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
  15. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +66 -65
  16. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  17. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +126 -127
  18. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  19. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  20. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +17 -0
  22. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +8 -1
  23. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +13 -14
  24. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +41 -10
  25. package/node_modules/@groove-dev/gui/src/views/models.jsx +57 -36
  26. package/node_modules/axios/CHANGELOG.md +260 -0
  27. package/node_modules/axios/README.md +595 -223
  28. package/node_modules/axios/dist/axios.js +1460 -1090
  29. package/node_modules/axios/dist/axios.js.map +1 -1
  30. package/node_modules/axios/dist/axios.min.js +3 -3
  31. package/node_modules/axios/dist/axios.min.js.map +1 -1
  32. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  33. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  34. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  35. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  36. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  37. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  38. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  39. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  40. package/node_modules/axios/index.d.cts +40 -41
  41. package/node_modules/axios/index.d.ts +151 -227
  42. package/node_modules/axios/index.js +2 -0
  43. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  44. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  45. package/node_modules/axios/lib/adapters/http.js +306 -58
  46. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  47. package/node_modules/axios/lib/core/Axios.js +7 -3
  48. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  49. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  50. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  51. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  52. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  53. package/node_modules/axios/lib/core/settle.js +7 -11
  54. package/node_modules/axios/lib/defaults/index.js +14 -9
  55. package/node_modules/axios/lib/env/data.js +1 -1
  56. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  57. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  58. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  59. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  60. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  61. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  62. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  63. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  64. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  65. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  66. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  67. package/node_modules/axios/lib/helpers/validator.js +3 -1
  68. package/node_modules/axios/lib/utils.js +33 -21
  69. package/node_modules/axios/package.json +17 -24
  70. package/node_modules/follow-redirects/README.md +7 -5
  71. package/node_modules/follow-redirects/index.js +24 -1
  72. package/node_modules/follow-redirects/package.json +1 -1
  73. package/package.json +1 -1
  74. package/packages/cli/package.json +1 -1
  75. package/packages/daemon/package.json +1 -1
  76. package/packages/daemon/src/conversations.js +18 -48
  77. package/packages/daemon/src/routes/agents.js +6 -83
  78. package/packages/gui/dist/assets/{index-BcoF6_eF.js → index-Bxc0gU06.js} +232 -238
  79. package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
  80. package/packages/gui/dist/index.html +2 -2
  81. package/packages/gui/package.json +1 -1
  82. package/packages/gui/src/components/agents/agent-feed.jsx +80 -95
  83. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  84. package/packages/gui/src/components/chat/chat-header.jsx +2 -0
  85. package/packages/gui/src/components/chat/chat-input.jsx +68 -66
  86. package/packages/gui/src/components/chat/chat-view.jsx +4 -8
  87. package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
  88. package/packages/gui/src/components/lab/parameter-panel.jsx +66 -65
  89. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  90. package/packages/gui/src/components/lab/runtime-config.jsx +126 -127
  91. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  92. package/packages/gui/src/components/ui/slider.jsx +8 -8
  93. package/packages/gui/src/lib/status.js +1 -0
  94. package/packages/gui/src/stores/groove.js +17 -0
  95. package/packages/gui/src/stores/slices/agents-slice.js +8 -1
  96. package/packages/gui/src/stores/slices/chat-slice.js +13 -14
  97. package/packages/gui/src/views/model-lab.jsx +41 -10
  98. package/packages/gui/src/views/models.jsx +57 -36
  99. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +0 -1
  100. package/packages/gui/dist/assets/index-Dd7qhiEd.css +0 -1
@@ -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
 
@@ -8,6 +8,8 @@ import { cn } from '../../lib/cn';
8
8
  import { roleColor } from '../../lib/status';
9
9
 
10
10
  const CHAT_ROLES = [
11
+ { id: 'chat', label: 'Chat', desc: 'General conversation' },
12
+ { id: 'research', label: 'Research Assistant', desc: 'Explore ideas, web search' },
11
13
  { id: 'fullstack', label: 'Fullstack', desc: 'End-to-end engineering' },
12
14
  { id: 'backend', label: 'Backend', desc: 'APIs, services, databases' },
13
15
  { id: 'frontend', label: 'Frontend', desc: 'UI, components, styling' },
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useEffect, useCallback } from 'react';
3
- import { Send, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal } from 'lucide-react';
3
+ import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal } from 'lucide-react';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { formatModelName } from './model-picker';
6
6
 
@@ -19,7 +19,7 @@ const VERBOSITY_OPTIONS = [
19
19
 
20
20
  export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
21
21
  const [input, setInput] = useState('');
22
- const [inputHeight, setInputHeight] = useState(40);
22
+ const [inputHeight, setInputHeight] = useState(88);
23
23
  const textareaRef = useRef(null);
24
24
  const fileInputRef = useRef(null);
25
25
 
@@ -32,7 +32,6 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
32
32
  if (!text || sending || disabled) return;
33
33
  onSend(text);
34
34
  setInput('');
35
- setInputHeight(40);
36
35
  }
37
36
 
38
37
  function onKeyDown(e) {
@@ -54,7 +53,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
54
53
  e.preventDefault();
55
54
  const startY = e.clientY;
56
55
  const startH = inputHeight;
57
- const onMove = (ev) => setInputHeight(Math.min(Math.max(40, startH - (ev.clientY - startY)), 400));
56
+ const onMove = (ev) => setInputHeight(Math.min(Math.max(56, startH - (ev.clientY - startY)), 400));
58
57
  const onUp = () => {
59
58
  window.removeEventListener('mousemove', onMove);
60
59
  window.removeEventListener('mouseup', onUp);
@@ -76,95 +75,97 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
76
75
  : 'Send a message...';
77
76
 
78
77
  return (
79
- <div className="border-t border-border-subtle bg-surface-1">
78
+ <div className="px-4 pb-3">
80
79
  <div
81
80
  onMouseDown={onDragStart}
82
- className="flex items-center justify-center h-3 cursor-row-resize hover:bg-surface-3/50 transition-colors group"
81
+ className="flex items-center justify-center h-5 cursor-row-resize group"
83
82
  >
84
- <GripHorizontal size={10} className="text-text-4 group-hover:text-text-2 transition-colors" />
83
+ <GripHorizontal size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
85
84
  </div>
86
85
 
87
- <div className="px-4 pb-3">
88
- {replyContext && (
89
- <div className="flex items-center gap-2 mb-2 px-3 py-2 rounded-lg bg-accent/5 border border-accent/15">
90
- <ImageIcon size={12} className="text-accent flex-shrink-0" />
91
- <span className="flex-1 text-2xs text-text-2 font-sans truncate">Iterating: &quot;{replyContext.prompt}&quot;</span>
92
- <button onClick={onClearReply} className="text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0">
93
- <Square size={10} />
94
- </button>
95
- </div>
96
- )}
97
-
98
- <textarea
99
- ref={textareaRef}
100
- value={input}
101
- onChange={(e) => setInput(e.target.value)}
102
- onKeyDown={onKeyDown}
103
- placeholder={placeholder}
104
- disabled={disabled}
105
- rows={1}
106
- style={{ height: inputHeight }}
107
- className={cn(
108
- 'w-full resize-none rounded-xl px-4 py-2.5 text-sm',
109
- 'bg-surface-0 border text-text-0 font-sans',
110
- 'placeholder:text-text-4',
111
- 'focus:outline-none focus:ring-1',
112
- 'border-border focus:ring-accent/40',
113
- 'disabled:opacity-50 disabled:cursor-not-allowed',
114
- )}
86
+ {replyContext && (
87
+ <div className="flex items-center gap-2 mb-2 px-3 py-2 rounded-lg bg-accent/5 border border-accent/15">
88
+ <ImageIcon size={12} className="text-accent flex-shrink-0" />
89
+ <span className="flex-1 text-2xs text-text-2 font-sans truncate">Iterating: &quot;{replyContext.prompt}&quot;</span>
90
+ <button onClick={onClearReply} className="text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0">
91
+ <Square size={10} />
92
+ </button>
93
+ </div>
94
+ )}
95
+
96
+ <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-0 transition-colors overflow-hidden focus-within:border-text-4/40">
97
+ <input
98
+ ref={fileInputRef}
99
+ type="file"
100
+ multiple
101
+ accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
102
+ onChange={handleFileSelect}
103
+ className="hidden"
115
104
  />
116
105
 
117
- <div className="flex items-center gap-2 mt-2">
118
- <input
119
- ref={fileInputRef}
120
- type="file"
121
- multiple
122
- accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
123
- onChange={handleFileSelect}
124
- className="hidden"
106
+ <div className="px-1">
107
+ <textarea
108
+ ref={textareaRef}
109
+ value={input}
110
+ onChange={(e) => setInput(e.target.value)}
111
+ onKeyDown={onKeyDown}
112
+ placeholder={placeholder}
113
+ disabled={disabled}
114
+ rows={1}
115
+ className={cn(
116
+ 'w-full resize-none px-3 py-2.5 text-[13px]',
117
+ 'bg-transparent font-sans text-text-0',
118
+ 'placeholder:text-text-4',
119
+ 'focus:outline-none',
120
+ 'disabled:opacity-40 disabled:cursor-not-allowed',
121
+ )}
122
+ style={{ height: inputHeight }}
125
123
  />
124
+ </div>
125
+
126
+ <div className="flex items-center gap-1.5 px-1.5 pb-1.5 pt-0.5">
126
127
  <button
127
128
  onClick={() => fileInputRef.current?.click()}
128
129
  disabled={disabled}
129
- className="w-8 h-8 flex items-center justify-center rounded-lg text-text-4 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed flex-shrink-0"
130
+ className="w-7 h-7 flex items-center justify-center rounded-md text-text-4 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
130
131
  title="Attach file"
131
132
  >
132
133
  <Paperclip size={14} />
133
134
  </button>
134
135
 
135
- <div className="flex items-center h-7 rounded-lg bg-surface-3 border border-border-subtle p-0.5">
136
+ <div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
136
137
  <button
137
138
  onClick={() => onModeChange?.('api')}
138
139
  disabled={modeChanging}
139
140
  className={cn(
140
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
141
+ 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
141
142
  'disabled:opacity-50 disabled:cursor-not-allowed',
142
- currentMode === 'api' ? 'bg-accent/15 text-accent border border-accent/25' : 'text-text-3 hover:text-text-1',
143
+ currentMode === 'api' ? 'bg-accent/15 text-accent' : 'text-text-3 hover:text-text-1',
143
144
  )}
144
145
  title="Lightweight — fast and cheap, no tools"
145
146
  >
146
- <Zap size={11} /> Chat
147
+ <Zap size={10} /> Chat
147
148
  </button>
148
149
  <button
149
150
  onClick={() => onModeChange?.('agent')}
150
151
  disabled={modeChanging}
151
152
  className={cn(
152
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
153
+ 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
153
154
  'disabled:opacity-50 disabled:cursor-not-allowed',
154
- currentMode === 'agent' ? 'bg-purple/15 text-purple border border-purple/25' : 'text-text-3 hover:text-text-1',
155
+ currentMode === 'agent' ? 'bg-purple/15 text-purple' : 'text-text-3 hover:text-text-1',
155
156
  )}
156
157
  title="Full agent — tools, files, session resume"
157
158
  >
158
- <Bot size={11} /> Agent
159
+ <Bot size={10} /> Agent
159
160
  </button>
160
161
  </div>
161
162
 
162
163
  {currentModel && (
163
164
  <div className={cn(
164
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-mono border',
165
+ 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-mono',
165
166
  isImageModel
166
- ? 'bg-purple/8 border-purple/20 text-purple'
167
- : 'bg-surface-3 border-border-subtle text-text-3',
167
+ ? 'bg-purple/8 text-purple'
168
+ : 'text-text-3',
168
169
  )}>
169
170
  {isImageModel && <ImageIcon size={9} />}
170
171
  <span className="max-w-[80px] truncate">{formatModelName(currentModel)}</span>
@@ -173,13 +174,13 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
173
174
 
174
175
  {isCodex && (
175
176
  <>
176
- <div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
177
+ <div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
177
178
  {EFFORT_OPTIONS.map((opt) => (
178
179
  <button
179
180
  key={opt.value}
180
181
  onClick={() => onReasoningEffortChange?.(opt.value)}
181
182
  className={cn(
182
- 'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
183
+ 'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
183
184
  reasoningEffort === opt.value
184
185
  ? 'bg-accent/15 text-accent'
185
186
  : 'text-text-4 hover:text-text-1',
@@ -191,13 +192,13 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
191
192
  ))}
192
193
  </div>
193
194
 
194
- <div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
195
+ <div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
195
196
  {VERBOSITY_OPTIONS.map((opt) => (
196
197
  <button
197
198
  key={opt.value}
198
199
  onClick={() => onVerbosityChange?.(opt.value)}
199
200
  className={cn(
200
- 'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
201
+ 'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
201
202
  verbosity === opt.value
202
203
  ? 'bg-accent/15 text-accent'
203
204
  : 'text-text-4 hover:text-text-1',
@@ -216,24 +217,25 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
216
217
  {isActive ? (
217
218
  <button
218
219
  onClick={onStop}
219
- className="w-8 h-8 flex items-center justify-center rounded-lg bg-danger/80 text-white hover:bg-danger transition-all cursor-pointer shadow-lg shadow-danger/20 flex-shrink-0"
220
220
  title="Stop generation"
221
+ className="group w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer"
221
222
  >
222
- <Square size={14} fill="currentColor" />
223
+ <span className="relative flex items-center justify-center w-3.5 h-3.5">
224
+ <span className="absolute inset-0 rounded-full bg-accent/30 group-hover:bg-red-500/30 animate-ping [animation-duration:2s] transition-colors" />
225
+ <span className="relative w-2.5 h-2.5 rounded-full bg-accent group-hover:bg-red-500 transition-colors" />
226
+ </span>
223
227
  </button>
224
228
  ) : (
225
229
  <button
226
230
  onClick={handleSend}
227
231
  disabled={!canSend}
228
232
  className={cn(
229
- 'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
230
- 'disabled:opacity-20 disabled:cursor-not-allowed',
231
- canSend
232
- ? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
233
- : 'bg-surface-4 text-text-4',
233
+ 'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
234
+ 'disabled:opacity-15 disabled:cursor-not-allowed',
235
+ canSend ? 'text-text-0 hover:text-text-1' : 'text-text-4',
234
236
  )}
235
237
  >
236
- {sending ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
238
+ {sending ? <Loader2 size={15} className="animate-spin" /> : <SendHorizontal size={15} />}
237
239
  </button>
238
240
  )}
239
241
  </div>
@@ -37,7 +37,7 @@ export function ChatView() {
37
37
  const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
38
38
  const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
39
39
  const sendImageMessage = useGrooveStore((s) => s.sendImageMessage);
40
- const stopAgent = useGrooveStore((s) => s.stopAgent);
40
+
41
41
  const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
42
42
  const setConversationMode = useGrooveStore((s) => s.setConversationMode);
43
43
  const setConversationModel = useGrooveStore((s) => s.setConversationModel);
@@ -54,7 +54,7 @@ export function ChatView() {
54
54
  const [replyContext, setReplyContext] = useState(null);
55
55
  const [modeChanging, setModeChanging] = useState(false);
56
56
 
57
- const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || null) : null;
57
+ const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || 'chat') : 'chat';
58
58
  const activeReasoningEffort = activeConversationId ? (conversationReasoningEffort?.[activeConversationId] || 'medium') : 'medium';
59
59
  const activeVerbosity = activeConversationId ? (conversationVerbosity?.[activeConversationId] || 'medium') : 'medium';
60
60
 
@@ -101,12 +101,8 @@ export function ChatView() {
101
101
 
102
102
  const handleStop = useCallback(() => {
103
103
  if (!activeConversation) return;
104
- if (activeConversation.mode === 'agent' && activeConversation.agentId) {
105
- stopAgent(activeConversation.agentId);
106
- } else {
107
- stopChatStreaming(activeConversationId);
108
- }
109
- }, [activeConversation, activeConversationId, stopAgent, stopChatStreaming]);
104
+ stopChatStreaming(activeConversationId);
105
+ }, [activeConversation, activeConversationId, stopChatStreaming]);
110
106
 
111
107
  const handleModelChange = useCallback(async (selection) => {
112
108
  if (activeConversationId) {
@@ -5,7 +5,7 @@ import { ScrollArea } from '../ui/scroll-area';
5
5
  import { Button } from '../ui/button';
6
6
  import { Tooltip } from '../ui/tooltip';
7
7
  import { cn } from '../../lib/cn';
8
- import { Send, Plus, ChevronDown, Square, Clock, Zap, Bot } from 'lucide-react';
8
+ import { SendHorizontal, Plus, ChevronDown, Clock, Zap, Bot } from 'lucide-react';
9
9
 
10
10
  function MessageMetrics({ metrics }) {
11
11
  if (!metrics) return null;
@@ -217,44 +217,52 @@ export function ChatPlayground() {
217
217
 
218
218
  {/* Input */}
219
219
  <div className="flex-shrink-0 px-4 py-3">
220
- <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">
221
- <textarea
222
- ref={inputRef}
223
- value={input}
224
- onChange={(e) => setInput(e.target.value)}
225
- onKeyDown={handleKeyDown}
226
- placeholder={!activeRuntime ? 'Select a runtime first' : !activeModel ? 'Select a model first' : 'Type a message...'}
227
- disabled={!activeRuntime || !activeModel}
228
- rows={1}
229
- className={cn(
230
- 'flex-1 resize-none bg-transparent px-2 py-1.5',
231
- 'text-xs text-text-0 font-sans placeholder:text-text-4',
232
- 'focus:outline-none',
233
- 'disabled:opacity-40 disabled:cursor-not-allowed',
234
- 'min-h-[28px] max-h-32',
220
+ <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-0 transition-colors overflow-hidden focus-within:border-text-4/40">
221
+ <div className="px-1">
222
+ <textarea
223
+ ref={inputRef}
224
+ value={input}
225
+ onChange={(e) => setInput(e.target.value)}
226
+ onKeyDown={handleKeyDown}
227
+ placeholder={!activeRuntime ? 'Select a runtime first' : !activeModel ? 'Select a model first' : 'Type a message...'}
228
+ disabled={!activeRuntime || !activeModel}
229
+ rows={1}
230
+ className={cn(
231
+ 'w-full resize-none px-3 py-2.5 text-[13px]',
232
+ 'bg-transparent font-sans text-text-0',
233
+ 'placeholder:text-text-4',
234
+ 'focus:outline-none',
235
+ 'disabled:opacity-40 disabled:cursor-not-allowed',
236
+ )}
237
+ style={{ height: 88 }}
238
+ />
239
+ </div>
240
+ <div className="flex items-center gap-1 px-1.5 pb-1.5 pt-0.5">
241
+ <div className="flex-1" />
242
+ {streaming && (
243
+ <button
244
+ onClick={() => useGrooveStore.getState().stopLabInference()}
245
+ title="Stop generation"
246
+ className="group w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer"
247
+ >
248
+ <span className="relative flex items-center justify-center w-3.5 h-3.5">
249
+ <span className="absolute inset-0 rounded-full bg-accent/30 group-hover:bg-red-500/30 animate-ping [animation-duration:2s] transition-colors" />
250
+ <span className="relative w-2.5 h-2.5 rounded-full bg-accent group-hover:bg-red-500 transition-colors" />
251
+ </span>
252
+ </button>
235
253
  )}
236
- style={{ height: 'auto', overflowY: input.split('\n').length > 4 ? 'auto' : 'hidden' }}
237
- onInput={(e) => { e.target.style.height = 'auto'; e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`; }}
238
- />
239
- {streaming ? (
240
- <button
241
- onClick={() => useGrooveStore.getState().stopLabInference()}
242
- className="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm bg-danger/15 text-danger hover:bg-danger/25 transition-colors cursor-pointer"
243
- >
244
- <Square size={12} />
245
- </button>
246
- ) : (
247
254
  <button
248
255
  disabled={!canSend}
249
256
  onClick={handleSend}
250
257
  className={cn(
251
- 'flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-sm transition-colors cursor-pointer',
252
- canSend ? 'bg-accent text-surface-0 hover:bg-accent/90' : 'bg-surface-3 text-text-4 cursor-not-allowed',
258
+ 'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
259
+ 'disabled:opacity-15 disabled:cursor-not-allowed',
260
+ canSend ? 'text-text-0 hover:text-text-1' : 'text-text-4',
253
261
  )}
254
262
  >
255
- <Send size={12} />
263
+ <SendHorizontal size={15} />
256
264
  </button>
257
- )}
265
+ </div>
258
266
  </div>
259
267
  </div>
260
268
  </div>