groove-dev 0.27.89 → 0.27.92
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.
- package/moe-training/client/parsers/claude-code.js +0 -2
- package/moe-training/client/session-attestation.js +2 -1
- package/moe-training/client/trajectory-capture.js +6 -0
- package/moe-training/test/client/parsers/claude-code.test.js +2 -2
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +244 -12
- package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
- package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
- package/node_modules/@groove-dev/daemon/src/process.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +9 -1
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -5
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +15 -2
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +10 -3
- package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +16 -8
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +244 -12
- package/packages/daemon/src/conversations.js +32 -6
- package/packages/daemon/src/introducer.js +42 -0
- package/packages/daemon/src/process.js +5 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +9 -1
- package/packages/daemon/src/providers/codex.js +34 -5
- package/packages/daemon/src/providers/gemini.js +15 -2
- package/packages/daemon/src/providers/grok.js +10 -3
- package/packages/daemon/src/providers/local.js +8 -1
- package/packages/daemon/src/tunnel-manager.js +74 -5
- package/packages/daemon/src/validate.js +22 -1
- package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/packages/gui/src/components/chat/chat-header.jsx +3 -30
- package/packages/gui/src/components/chat/chat-input.jsx +163 -153
- package/packages/gui/src/components/chat/chat-view.jsx +16 -8
- package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
- package/packages/gui/src/components/editor/code-editor.jsx +29 -23
- package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
- package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/packages/gui/src/components/ui/slider.jsx +50 -0
- package/packages/gui/src/stores/groove.js +145 -9
- package/packages/gui/src/views/agents.jsx +707 -14
- package/workspace.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/packages/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
|
@@ -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 } from 'lucide-react';
|
|
3
|
+
import { Send, 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,20 +19,10 @@ 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 }) {
|
|
21
21
|
const [input, setInput] = useState('');
|
|
22
|
+
const [inputHeight, setInputHeight] = useState(40);
|
|
22
23
|
const textareaRef = useRef(null);
|
|
23
24
|
const fileInputRef = useRef(null);
|
|
24
25
|
|
|
25
|
-
const adjustHeight = useCallback(() => {
|
|
26
|
-
const el = textareaRef.current;
|
|
27
|
-
if (!el) return;
|
|
28
|
-
el.style.height = 'auto';
|
|
29
|
-
el.style.height = Math.min(el.scrollHeight, 400) + 'px';
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
adjustHeight();
|
|
34
|
-
}, [input, adjustHeight]);
|
|
35
|
-
|
|
36
26
|
useEffect(() => {
|
|
37
27
|
if (!disabled && textareaRef.current) textareaRef.current.focus();
|
|
38
28
|
}, [disabled]);
|
|
@@ -42,9 +32,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
42
32
|
if (!text || sending || disabled) return;
|
|
43
33
|
onSend(text);
|
|
44
34
|
setInput('');
|
|
45
|
-
|
|
46
|
-
textareaRef.current.style.height = 'auto';
|
|
47
|
-
}
|
|
35
|
+
setInputHeight(40);
|
|
48
36
|
}
|
|
49
37
|
|
|
50
38
|
function onKeyDown(e) {
|
|
@@ -62,6 +50,19 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
62
50
|
e.target.value = '';
|
|
63
51
|
}
|
|
64
52
|
|
|
53
|
+
const onDragStart = useCallback((e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
const startY = e.clientY;
|
|
56
|
+
const startH = inputHeight;
|
|
57
|
+
const onMove = (ev) => setInputHeight(Math.min(Math.max(40, startH - (ev.clientY - startY)), 400));
|
|
58
|
+
const onUp = () => {
|
|
59
|
+
window.removeEventListener('mousemove', onMove);
|
|
60
|
+
window.removeEventListener('mouseup', onUp);
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('mousemove', onMove);
|
|
63
|
+
window.addEventListener('mouseup', onUp);
|
|
64
|
+
}, [inputHeight]);
|
|
65
|
+
|
|
65
66
|
const isActive = streaming || sending;
|
|
66
67
|
const canSend = input.trim() && !sending && !disabled;
|
|
67
68
|
const currentMode = mode || 'api';
|
|
@@ -75,154 +76,163 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
75
76
|
: 'Send a message...';
|
|
76
77
|
|
|
77
78
|
return (
|
|
78
|
-
<div className="border-t border-border-subtle
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
disabled={disabled}
|
|
96
|
-
rows={1}
|
|
97
|
-
className={cn(
|
|
98
|
-
'w-full resize-y rounded-xl px-4 py-2.5 text-sm',
|
|
99
|
-
'bg-surface-0 border text-text-0 font-sans',
|
|
100
|
-
'placeholder:text-text-4',
|
|
101
|
-
'focus:outline-none focus:ring-1',
|
|
102
|
-
'min-h-[40px]',
|
|
103
|
-
'border-border focus:ring-accent/40',
|
|
104
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
79
|
+
<div className="border-t border-border-subtle bg-surface-1">
|
|
80
|
+
<div
|
|
81
|
+
onMouseDown={onDragStart}
|
|
82
|
+
className="flex items-center justify-center h-3 cursor-row-resize hover:bg-surface-3/50 transition-colors group"
|
|
83
|
+
>
|
|
84
|
+
<GripHorizontal size={10} className="text-text-4 group-hover:text-text-2 transition-colors" />
|
|
85
|
+
</div>
|
|
86
|
+
|
|
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: "{replyContext.prompt}"</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>
|
|
105
96
|
)}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
|
|
114
|
-
onChange={handleFileSelect}
|
|
115
|
-
className="hidden"
|
|
116
|
-
/>
|
|
117
|
-
<button
|
|
118
|
-
onClick={() => fileInputRef.current?.click()}
|
|
97
|
+
|
|
98
|
+
<textarea
|
|
99
|
+
ref={textareaRef}
|
|
100
|
+
value={input}
|
|
101
|
+
onChange={(e) => setInput(e.target.value)}
|
|
102
|
+
onKeyDown={onKeyDown}
|
|
103
|
+
placeholder={placeholder}
|
|
119
104
|
disabled={disabled}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
)}
|
|
115
|
+
/>
|
|
125
116
|
|
|
126
|
-
<div className="flex items-center
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<Zap size={11} /> Chat
|
|
136
|
-
</button>
|
|
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"
|
|
125
|
+
/>
|
|
137
126
|
<button
|
|
138
|
-
onClick={() =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)}
|
|
143
|
-
title="Full agent — tools, files, session resume"
|
|
127
|
+
onClick={() => fileInputRef.current?.click()}
|
|
128
|
+
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
|
+
title="Attach file"
|
|
144
131
|
>
|
|
145
|
-
<
|
|
132
|
+
<Paperclip size={14} />
|
|
146
133
|
</button>
|
|
147
|
-
</div>
|
|
148
134
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
135
|
+
<div className="flex items-center h-7 rounded-lg bg-surface-3 border border-border-subtle p-0.5">
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => onModeChange?.('api')}
|
|
138
|
+
className={cn(
|
|
139
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
140
|
+
currentMode === 'api' ? 'bg-accent/15 text-accent border border-accent/25' : 'text-text-3 hover:text-text-1',
|
|
141
|
+
)}
|
|
142
|
+
title="Lightweight — fast and cheap, no tools"
|
|
143
|
+
>
|
|
144
|
+
<Zap size={11} /> Chat
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => onModeChange?.('agent')}
|
|
148
|
+
className={cn(
|
|
149
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
150
|
+
currentMode === 'agent' ? 'bg-purple/15 text-purple border border-purple/25' : 'text-text-3 hover:text-text-1',
|
|
151
|
+
)}
|
|
152
|
+
title="Full agent — tools, files, session resume"
|
|
153
|
+
>
|
|
154
|
+
<Bot size={11} /> Agent
|
|
155
|
+
</button>
|
|
158
156
|
</div>
|
|
159
|
-
)}
|
|
160
|
-
|
|
161
|
-
{isCodex && (
|
|
162
|
-
<>
|
|
163
|
-
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
164
|
-
{EFFORT_OPTIONS.map((opt) => (
|
|
165
|
-
<button
|
|
166
|
-
key={opt.value}
|
|
167
|
-
onClick={() => onReasoningEffortChange?.(opt.value)}
|
|
168
|
-
className={cn(
|
|
169
|
-
'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
170
|
-
reasoningEffort === opt.value
|
|
171
|
-
? 'bg-accent/15 text-accent'
|
|
172
|
-
: 'text-text-4 hover:text-text-1',
|
|
173
|
-
)}
|
|
174
|
-
title={`Reasoning: ${opt.label}`}
|
|
175
|
-
>
|
|
176
|
-
{opt.label}
|
|
177
|
-
</button>
|
|
178
|
-
))}
|
|
179
|
-
</div>
|
|
180
157
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
: 'text-text-4 hover:text-text-1',
|
|
191
|
-
)}
|
|
192
|
-
title={`Verbosity: ${opt.label}`}
|
|
193
|
-
>
|
|
194
|
-
{opt.label}
|
|
195
|
-
</button>
|
|
196
|
-
))}
|
|
158
|
+
{currentModel && (
|
|
159
|
+
<div className={cn(
|
|
160
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-mono border',
|
|
161
|
+
isImageModel
|
|
162
|
+
? 'bg-purple/8 border-purple/20 text-purple'
|
|
163
|
+
: 'bg-surface-3 border-border-subtle text-text-3',
|
|
164
|
+
)}>
|
|
165
|
+
{isImageModel && <ImageIcon size={9} />}
|
|
166
|
+
<span className="max-w-[80px] truncate">{formatModelName(currentModel)}</span>
|
|
197
167
|
</div>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{isCodex && (
|
|
171
|
+
<>
|
|
172
|
+
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
173
|
+
{EFFORT_OPTIONS.map((opt) => (
|
|
174
|
+
<button
|
|
175
|
+
key={opt.value}
|
|
176
|
+
onClick={() => onReasoningEffortChange?.(opt.value)}
|
|
177
|
+
className={cn(
|
|
178
|
+
'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
179
|
+
reasoningEffort === opt.value
|
|
180
|
+
? 'bg-accent/15 text-accent'
|
|
181
|
+
: 'text-text-4 hover:text-text-1',
|
|
182
|
+
)}
|
|
183
|
+
title={`Reasoning: ${opt.label}`}
|
|
184
|
+
>
|
|
185
|
+
{opt.label}
|
|
186
|
+
</button>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
191
|
+
{VERBOSITY_OPTIONS.map((opt) => (
|
|
192
|
+
<button
|
|
193
|
+
key={opt.value}
|
|
194
|
+
onClick={() => onVerbosityChange?.(opt.value)}
|
|
195
|
+
className={cn(
|
|
196
|
+
'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
197
|
+
verbosity === opt.value
|
|
198
|
+
? 'bg-accent/15 text-accent'
|
|
199
|
+
: 'text-text-4 hover:text-text-1',
|
|
200
|
+
)}
|
|
201
|
+
title={`Verbosity: ${opt.label}`}
|
|
202
|
+
>
|
|
203
|
+
{opt.label}
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
<div className="flex-1" />
|
|
211
|
+
|
|
212
|
+
{isActive ? (
|
|
213
|
+
<button
|
|
214
|
+
onClick={onStop}
|
|
215
|
+
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"
|
|
216
|
+
title="Stop generation"
|
|
217
|
+
>
|
|
218
|
+
<Square size={14} fill="currentColor" />
|
|
219
|
+
</button>
|
|
220
|
+
) : (
|
|
221
|
+
<button
|
|
222
|
+
onClick={handleSend}
|
|
223
|
+
disabled={!canSend}
|
|
224
|
+
className={cn(
|
|
225
|
+
'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
|
|
226
|
+
'disabled:opacity-20 disabled:cursor-not-allowed',
|
|
227
|
+
canSend
|
|
228
|
+
? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
|
|
229
|
+
: 'bg-surface-4 text-text-4',
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{sending ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
|
233
|
+
</button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
226
236
|
</div>
|
|
227
237
|
</div>
|
|
228
238
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useCallback } from 'react';
|
|
3
|
-
import { Plus } from 'lucide-react';
|
|
3
|
+
import { Plus, PanelLeftOpen } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { ConversationList } from './conversation-list';
|
|
@@ -64,10 +64,8 @@ export function ChatView() {
|
|
|
64
64
|
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
65
65
|
|
|
66
66
|
const handleNewChat = useCallback(async (provider, model) => {
|
|
67
|
-
const p = provider || 'claude-code';
|
|
68
|
-
const m = model || 'claude-sonnet-4-6';
|
|
69
67
|
try {
|
|
70
|
-
await createConversation(
|
|
68
|
+
await createConversation(provider || null, model || null, 'api');
|
|
71
69
|
} catch { /* toast handles */ }
|
|
72
70
|
}, [createConversation]);
|
|
73
71
|
|
|
@@ -134,17 +132,27 @@ export function ChatView() {
|
|
|
134
132
|
<div className="flex h-full bg-surface-0">
|
|
135
133
|
{/* Conversation sidebar */}
|
|
136
134
|
<div className={cn(
|
|
137
|
-
'flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
|
|
138
|
-
sidebarCollapsed ? 'w-0' : 'w-64',
|
|
135
|
+
'relative flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
|
|
136
|
+
sidebarCollapsed ? 'w-0 border-r-0' : 'w-64',
|
|
139
137
|
)}>
|
|
140
|
-
<ConversationList onNewChat={() => handleNewChat()} />
|
|
138
|
+
<ConversationList onNewChat={() => handleNewChat()} onCollapse={() => setSidebarCollapsed(true)} />
|
|
141
139
|
</div>
|
|
142
140
|
|
|
143
141
|
{/* Main chat area */}
|
|
144
142
|
<div className="flex-1 flex flex-col min-w-0">
|
|
143
|
+
{sidebarCollapsed && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setSidebarCollapsed(false)}
|
|
146
|
+
className="absolute top-3 left-2 z-10 w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
|
|
147
|
+
title="Show sidebar"
|
|
148
|
+
>
|
|
149
|
+
<PanelLeftOpen size={15} />
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
|
|
145
153
|
{activeConversation ? (
|
|
146
154
|
<>
|
|
147
|
-
<ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange}
|
|
155
|
+
<ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange} role={activeRole} onRoleChange={handleRoleChange} sidebarCollapsed={sidebarCollapsed} />
|
|
148
156
|
<ChatMessages
|
|
149
157
|
messages={messages}
|
|
150
158
|
isStreaming={isStreaming}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { SquarePen, MessageCircle, Pin, Pencil, PinOff, Trash2, Zap, Bot, PanelLeftClose } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { Badge } from '../ui/badge';
|
|
@@ -43,13 +43,12 @@ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete
|
|
|
43
43
|
<button
|
|
44
44
|
onClick={() => onSelect(conv.id)}
|
|
45
45
|
className={cn(
|
|
46
|
-
'w-full flex items-center gap-2 px-3 py-2 text-left
|
|
46
|
+
'w-full flex items-center gap-2 px-3 py-2.5 text-left transition-colors cursor-pointer group border-b border-border-subtle/50',
|
|
47
47
|
isActive
|
|
48
|
-
? '
|
|
49
|
-
: 'text-text-2 hover:bg-surface-
|
|
48
|
+
? 'text-text-0'
|
|
49
|
+
: 'text-text-2 hover:bg-surface-3/40 hover:text-text-1',
|
|
50
50
|
)}
|
|
51
51
|
>
|
|
52
|
-
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-accent' : 'text-text-4 group-hover:text-text-3')} />
|
|
53
52
|
<div className="flex-1 min-w-0">
|
|
54
53
|
<div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
|
|
55
54
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
@@ -81,7 +80,7 @@ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete
|
|
|
81
80
|
);
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
export function ConversationList({ onNewChat }) {
|
|
83
|
+
export function ConversationList({ onNewChat, onCollapse }) {
|
|
85
84
|
const conversations = useGrooveStore((s) => s.conversations);
|
|
86
85
|
const activeConversationId = useGrooveStore((s) => s.activeConversationId);
|
|
87
86
|
const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
|
|
@@ -122,7 +121,27 @@ export function ConversationList({ onNewChat }) {
|
|
|
122
121
|
|
|
123
122
|
return (
|
|
124
123
|
<div className="flex flex-col h-full">
|
|
125
|
-
<div className="flex-
|
|
124
|
+
<div className="flex items-center justify-between px-3 pt-3 pb-2">
|
|
125
|
+
<span className="text-xs font-semibold text-text-2 font-sans">Chats</span>
|
|
126
|
+
<div className="flex items-center gap-0.5">
|
|
127
|
+
<button
|
|
128
|
+
onClick={onNewChat}
|
|
129
|
+
className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
|
|
130
|
+
title="New chat"
|
|
131
|
+
>
|
|
132
|
+
<SquarePen size={15} />
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
onClick={onCollapse}
|
|
136
|
+
className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
|
|
137
|
+
title="Collapse sidebar"
|
|
138
|
+
>
|
|
139
|
+
<PanelLeftClose size={15} />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="flex-1 overflow-y-auto pt-1 pb-3">
|
|
126
145
|
{conversations.length === 0 ? (
|
|
127
146
|
<div className="flex flex-col items-center justify-center py-16 text-center px-4">
|
|
128
147
|
<MessageCircle size={24} className="text-text-4 mb-3" />
|
|
@@ -139,16 +158,6 @@ export function ConversationList({ onNewChat }) {
|
|
|
139
158
|
</>
|
|
140
159
|
)}
|
|
141
160
|
</div>
|
|
142
|
-
|
|
143
|
-
<div className="p-3 border-t border-border-subtle">
|
|
144
|
-
<button
|
|
145
|
-
onClick={onNewChat}
|
|
146
|
-
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
|
|
147
|
-
>
|
|
148
|
-
<Plus size={14} />
|
|
149
|
-
New Chat
|
|
150
|
-
</button>
|
|
151
|
-
</div>
|
|
152
161
|
</div>
|
|
153
162
|
);
|
|
154
163
|
}
|
|
@@ -26,37 +26,43 @@ const LANGS = {
|
|
|
26
26
|
|
|
27
27
|
// Custom theme overrides to match our design tokens
|
|
28
28
|
const grooveTheme = EditorView.theme({
|
|
29
|
-
'&': { backgroundColor: '#
|
|
30
|
-
'.cm-scroller': { overflow: 'auto' },
|
|
31
|
-
'.cm-content': { caretColor: '#33afbc' },
|
|
32
|
-
'.cm-cursor': { borderLeftColor: '#33afbc' },
|
|
33
|
-
'.cm-gutters': { backgroundColor: '#
|
|
34
|
-
'.cm-activeLineGutter': { backgroundColor: '#
|
|
35
|
-
'.cm-activeLine': { backgroundColor: 'rgba(
|
|
29
|
+
'&': { backgroundColor: '#13161b', color: '#d4d8e0', fontFamily: 'var(--font-mono)', fontSize: '12px', height: '100%', lineHeight: '1.6' },
|
|
30
|
+
'.cm-scroller': { overflow: 'auto', padding: '4px 0' },
|
|
31
|
+
'.cm-content': { caretColor: '#33afbc', fontWeight: '400' },
|
|
32
|
+
'.cm-cursor': { borderLeftColor: '#33afbc', borderLeftWidth: '1.5px' },
|
|
33
|
+
'.cm-gutters': { backgroundColor: '#13161b', borderRight: '1px solid #1e2229', color: '#404852', minWidth: '40px' },
|
|
34
|
+
'.cm-activeLineGutter': { backgroundColor: '#1a1e25' },
|
|
35
|
+
'.cm-activeLine': { backgroundColor: 'rgba(255, 255, 255, 0.03)' },
|
|
36
36
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'rgba(51, 175, 188, 0.15)' },
|
|
37
|
+
'.cm-line': { fontWeight: '400' },
|
|
38
|
+
// Markdown heading overrides — prevent bold/large rendering
|
|
39
|
+
'.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6': { fontWeight: '400', fontSize: '12px' },
|
|
40
|
+
'.ͼ1 .cm-line .tok-heading': { fontWeight: '400', fontSize: '12px' },
|
|
41
|
+
'.tok-heading': { fontWeight: '400' },
|
|
42
|
+
'.tok-heading1, .tok-heading2, .tok-heading3': { fontWeight: '400', fontSize: '12px' },
|
|
37
43
|
// Search panel styling
|
|
38
|
-
'.cm-panels': { backgroundColor: '#
|
|
39
|
-
'.cm-panels.cm-panels-top': { borderBottom: '1px solid #
|
|
40
|
-
'.cm-panels.cm-panels-bottom': { borderTop: '1px solid #
|
|
41
|
-
'.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '
|
|
42
|
-
'.cm-search label': { display: 'flex', alignItems: 'center', gap: '4px', color: '#
|
|
44
|
+
'.cm-panels': { backgroundColor: '#13161b', borderBottom: '1px solid #1e2229' },
|
|
45
|
+
'.cm-panels.cm-panels-top': { borderBottom: '1px solid #1e2229' },
|
|
46
|
+
'.cm-panels.cm-panels-bottom': { borderTop: '1px solid #1e2229' },
|
|
47
|
+
'.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '11px', display: 'flex', flexWrap: 'wrap', alignItems: 'center' },
|
|
48
|
+
'.cm-search label': { display: 'flex', alignItems: 'center', gap: '4px', color: '#6e7681', fontSize: '10px' },
|
|
43
49
|
'.cm-search input, .cm-search .cm-textfield': {
|
|
44
|
-
backgroundColor: '#1a1e25', border: '1px solid #2c313a', borderRadius: '4px', color: '#
|
|
45
|
-
padding: '2px 6px', fontSize: '
|
|
50
|
+
backgroundColor: '#1a1e25', border: '1px solid #2c313a', borderRadius: '4px', color: '#d4d8e0',
|
|
51
|
+
padding: '2px 6px', fontSize: '11px', fontFamily: 'var(--font-mono)', outline: 'none',
|
|
46
52
|
},
|
|
47
53
|
'.cm-search input:focus, .cm-search .cm-textfield:focus': { borderColor: '#33afbc' },
|
|
48
54
|
'.cm-search .cm-button, .cm-button': {
|
|
49
|
-
backgroundColor: '#
|
|
50
|
-
padding: '2px 8px', fontSize: '
|
|
55
|
+
backgroundColor: '#1e2229', border: '1px solid #2c313a', borderRadius: '4px', color: '#a0a8b4',
|
|
56
|
+
padding: '2px 8px', fontSize: '10px', fontFamily: 'var(--font-sans)', cursor: 'pointer',
|
|
51
57
|
backgroundImage: 'none',
|
|
52
58
|
},
|
|
53
|
-
'.cm-search .cm-button:hover, .cm-button:hover': { backgroundColor: '#
|
|
54
|
-
'.cm-search .cm-button:active': { backgroundColor: '#
|
|
59
|
+
'.cm-search .cm-button:hover, .cm-button:hover': { backgroundColor: '#2c313a', color: '#d4d8e0' },
|
|
60
|
+
'.cm-search .cm-button:active': { backgroundColor: '#333842' },
|
|
55
61
|
'.cm-search br': { display: 'none' },
|
|
56
|
-
'.cm-panel.cm-search [name=close]': { color: '#
|
|
57
|
-
'.cm-panel.cm-search [name=close]:hover': { color: '#
|
|
58
|
-
'.cm-searchMatch': { backgroundColor: 'rgba(51, 175, 188, 0.
|
|
59
|
-
'.cm-searchMatch-selected': { backgroundColor: 'rgba(51, 175, 188, 0.
|
|
62
|
+
'.cm-panel.cm-search [name=close]': { color: '#505862', cursor: 'pointer', padding: '0 4px' },
|
|
63
|
+
'.cm-panel.cm-search [name=close]:hover': { color: '#d4d8e0' },
|
|
64
|
+
'.cm-searchMatch': { backgroundColor: 'rgba(51, 175, 188, 0.15)', outline: '1px solid rgba(51, 175, 188, 0.3)' },
|
|
65
|
+
'.cm-searchMatch-selected': { backgroundColor: 'rgba(51, 175, 188, 0.3)' },
|
|
60
66
|
}, { dark: true });
|
|
61
67
|
|
|
62
68
|
export function CodeEditor({ content, language, onChange, onSave, onCursorChange, viewRef: externalViewRef }) {
|
|
@@ -135,5 +141,5 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
|
|
|
135
141
|
view.dispatch({ effects: langCompartment.current.reconfigure(langExt()) });
|
|
136
142
|
}, [language]);
|
|
137
143
|
|
|
138
|
-
return <div ref={containerRef} className="w-full h-full overflow-hidden" />;
|
|
144
|
+
return <div ref={containerRef} className="w-full h-full overflow-hidden bg-[#13161b]" />;
|
|
139
145
|
}
|
|
@@ -27,7 +27,11 @@ export function QuickConnect() {
|
|
|
27
27
|
await useGrooveStore.getState().connectTunnel(id);
|
|
28
28
|
toggle();
|
|
29
29
|
} catch (err) {
|
|
30
|
-
|
|
30
|
+
let detail = err?.message || 'Unknown error';
|
|
31
|
+
if (detail.toLowerCase().includes('port forward')) {
|
|
32
|
+
detail += ' — Try testing the connection first, or check your SSH key configuration.';
|
|
33
|
+
}
|
|
34
|
+
addToast('error', 'Connection failed', detail);
|
|
31
35
|
}
|
|
32
36
|
setConnectingId(null);
|
|
33
37
|
}
|
|
@@ -79,11 +79,15 @@ export function RemoteServerCard({ server, onEdit, onDelete, onConnect, onDiscon
|
|
|
79
79
|
|
|
80
80
|
const connectLabel = connectStep === 'installing'
|
|
81
81
|
? 'Installing Groove...'
|
|
82
|
-
: connectStep === '
|
|
83
|
-
? '
|
|
84
|
-
:
|
|
85
|
-
? '
|
|
86
|
-
: '
|
|
82
|
+
: connectStep === 'upgrading'
|
|
83
|
+
? 'Updating Groove...'
|
|
84
|
+
: connectStep === 'starting'
|
|
85
|
+
? 'Starting daemon...'
|
|
86
|
+
: connectStep === 'forwarding'
|
|
87
|
+
? 'Establishing tunnel...'
|
|
88
|
+
: connecting
|
|
89
|
+
? 'Connecting...'
|
|
90
|
+
: 'Connect';
|
|
87
91
|
|
|
88
92
|
const uptimeSeconds = server.active && server.startedAt
|
|
89
93
|
? Math.floor((Date.now() - new Date(server.startedAt).getTime()) / 1000)
|
|
@@ -207,7 +207,11 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
|
|
|
207
207
|
setCompletedSteps((prev) => [...new Set([...prev, 2])]);
|
|
208
208
|
setStep(3);
|
|
209
209
|
} catch (err) {
|
|
210
|
-
|
|
210
|
+
let msg = err?.body?.error || err?.message || 'Connection failed';
|
|
211
|
+
if (msg.toLowerCase().includes('port forward')) {
|
|
212
|
+
msg += ' — Check that the remote server is reachable and SSH port forwarding is allowed.';
|
|
213
|
+
}
|
|
214
|
+
setTestResult({ error: msg });
|
|
211
215
|
}
|
|
212
216
|
setConnecting(false);
|
|
213
217
|
}
|