groove-dev 0.27.91 → 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.
Files changed (68) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +228 -3
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
  5. package/node_modules/@groove-dev/daemon/src/process.js +5 -1
  6. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +8 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/codex.js +33 -4
  9. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -1
  10. package/node_modules/@groove-dev/daemon/src/providers/grok.js +8 -1
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
  13. package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
  20. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +15 -5
  24. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
  25. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
  26. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
  27. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
  28. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
  29. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
  30. package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
  31. package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
  32. package/package.json +1 -1
  33. package/packages/cli/package.json +1 -1
  34. package/packages/daemon/package.json +1 -1
  35. package/packages/daemon/src/api.js +228 -3
  36. package/packages/daemon/src/introducer.js +42 -0
  37. package/packages/daemon/src/process.js +5 -1
  38. package/packages/daemon/src/providers/base.js +4 -0
  39. package/packages/daemon/src/providers/claude-code.js +8 -0
  40. package/packages/daemon/src/providers/codex.js +33 -4
  41. package/packages/daemon/src/providers/gemini.js +14 -1
  42. package/packages/daemon/src/providers/grok.js +8 -1
  43. package/packages/daemon/src/providers/local.js +8 -1
  44. package/packages/daemon/src/tunnel-manager.js +74 -5
  45. package/packages/daemon/src/validate.js +22 -1
  46. package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  47. package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  48. package/packages/gui/dist/index.html +2 -2
  49. package/packages/gui/package.json +1 -1
  50. package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
  51. package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
  52. package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
  53. package/packages/gui/src/components/chat/chat-header.jsx +3 -30
  54. package/packages/gui/src/components/chat/chat-input.jsx +163 -153
  55. package/packages/gui/src/components/chat/chat-view.jsx +15 -5
  56. package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
  57. package/packages/gui/src/components/editor/code-editor.jsx +29 -23
  58. package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
  59. package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
  60. package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
  61. package/packages/gui/src/components/ui/slider.jsx +50 -0
  62. package/packages/gui/src/stores/groove.js +145 -9
  63. package/packages/gui/src/views/agents.jsx +707 -14
  64. package/workspace.png +0 -0
  65. package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  66. package/node_modules/@groove-dev/gui/dist/assets/index-MLIZRMj1.js +0 -8642
  67. package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  68. package/packages/gui/dist/assets/index-MLIZRMj1.js +0 -8642
@@ -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
- if (textareaRef.current) {
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 px-4 py-3 bg-surface-1">
79
- {replyContext && (
80
- <div className="flex items-center gap-2 mb-2 px-3 py-2 rounded-lg bg-accent/5 border border-accent/15">
81
- <ImageIcon size={12} className="text-accent flex-shrink-0" />
82
- <span className="flex-1 text-2xs text-text-2 font-sans truncate">Iterating: &quot;{replyContext.prompt}&quot;</span>
83
- <button onClick={onClearReply} className="text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0">
84
- <Square size={10} />
85
- </button>
86
- </div>
87
- )}
88
-
89
- <textarea
90
- ref={textareaRef}
91
- value={input}
92
- onChange={(e) => setInput(e.target.value)}
93
- onKeyDown={onKeyDown}
94
- placeholder={placeholder}
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: &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>
105
96
  )}
106
- />
107
-
108
- <div className="flex items-center gap-2 mt-2">
109
- <input
110
- ref={fileInputRef}
111
- type="file"
112
- multiple
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
- 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"
121
- title="Attach file"
122
- >
123
- <Paperclip size={14} />
124
- </button>
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 h-7 rounded-lg bg-surface-3 border border-border-subtle p-0.5">
127
- <button
128
- onClick={() => onModeChange?.('api')}
129
- className={cn(
130
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
131
- currentMode === 'api' ? 'bg-accent/15 text-accent border border-accent/25' : 'text-text-3 hover:text-text-1',
132
- )}
133
- title="Lightweight — fast and cheap, no tools"
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={() => onModeChange?.('agent')}
139
- 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
- currentMode === 'agent' ? 'bg-purple/15 text-purple border border-purple/25' : 'text-text-3 hover:text-text-1',
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
- <Bot size={11} /> Agent
132
+ <Paperclip size={14} />
146
133
  </button>
147
- </div>
148
134
 
149
- {currentModel && (
150
- <div className={cn(
151
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-mono border',
152
- isImageModel
153
- ? 'bg-purple/8 border-purple/20 text-purple'
154
- : 'bg-surface-3 border-border-subtle text-text-3',
155
- )}>
156
- {isImageModel && <ImageIcon size={9} />}
157
- <span className="max-w-[80px] truncate">{formatModelName(currentModel)}</span>
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
- <div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
182
- {VERBOSITY_OPTIONS.map((opt) => (
183
- <button
184
- key={opt.value}
185
- onClick={() => onVerbosityChange?.(opt.value)}
186
- className={cn(
187
- 'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
188
- verbosity === opt.value
189
- ? 'bg-accent/15 text-accent'
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
- <div className="flex-1" />
202
-
203
- {isActive ? (
204
- <button
205
- onClick={onStop}
206
- 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"
207
- title="Stop generation"
208
- >
209
- <Square size={14} fill="currentColor" />
210
- </button>
211
- ) : (
212
- <button
213
- onClick={handleSend}
214
- disabled={!canSend}
215
- className={cn(
216
- 'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
217
- 'disabled:opacity-20 disabled:cursor-not-allowed',
218
- canSend
219
- ? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
220
- : 'bg-surface-4 text-text-4',
221
- )}
222
- >
223
- {sending ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
224
- </button>
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';
@@ -132,17 +132,27 @@ export function ChatView() {
132
132
  <div className="flex h-full bg-surface-0">
133
133
  {/* Conversation sidebar */}
134
134
  <div className={cn(
135
- 'flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
136
- 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',
137
137
  )}>
138
- <ConversationList onNewChat={() => handleNewChat()} />
138
+ <ConversationList onNewChat={() => handleNewChat()} onCollapse={() => setSidebarCollapsed(true)} />
139
139
  </div>
140
140
 
141
141
  {/* Main chat area */}
142
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
+
143
153
  {activeConversation ? (
144
154
  <>
145
- <ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange} onModeChange={handleModeChange} role={activeRole} onRoleChange={handleRoleChange} />
155
+ <ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange} role={activeRole} onRoleChange={handleRoleChange} sidebarCollapsed={sidebarCollapsed} />
146
156
  <ChatMessages
147
157
  messages={messages}
148
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 { Plus, MessageCircle, Pin, Pencil, PinOff, Trash2, Zap, Bot } from 'lucide-react';
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 rounded-md transition-colors cursor-pointer group',
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
- ? 'bg-accent/10 text-text-0'
49
- : 'text-text-2 hover:bg-surface-4 hover:text-text-1',
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-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
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: '#1a1e25', color: '#bcc2cd', fontFamily: 'var(--font-mono)', fontSize: '13px', height: '100%' },
30
- '.cm-scroller': { overflow: 'auto' },
31
- '.cm-content': { caretColor: '#33afbc' },
32
- '.cm-cursor': { borderLeftColor: '#33afbc' },
33
- '.cm-gutters': { backgroundColor: '#1a1e25', borderRight: '1px solid #22272e', color: '#505862' },
34
- '.cm-activeLineGutter': { backgroundColor: '#22272e' },
35
- '.cm-activeLine': { backgroundColor: 'rgba(34, 39, 46, 0.5)' },
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: '#1a1e25', borderBottom: '1px solid #3e4451' },
39
- '.cm-panels.cm-panels-top': { borderBottom: '1px solid #3e4451' },
40
- '.cm-panels.cm-panels-bottom': { borderTop: '1px solid #3e4451' },
41
- '.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '12px', display: 'flex', flexWrap: 'wrap', alignItems: 'center' },
42
- '.cm-search label': { display: 'flex', alignItems: 'center', gap: '4px', color: '#8b929e', fontSize: '11px' },
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: '#e6e6e6',
45
- padding: '2px 6px', fontSize: '12px', fontFamily: 'var(--font-mono)', outline: 'none',
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: '#2c313a', border: '1px solid #3e4451', borderRadius: '4px', color: '#bcc2cd',
50
- padding: '2px 8px', fontSize: '11px', fontFamily: 'var(--font-sans)', cursor: 'pointer',
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: '#333842', color: '#e6e6e6' },
54
- '.cm-search .cm-button:active': { backgroundColor: '#3a3f4b' },
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: '#6e7681', cursor: 'pointer', padding: '0 4px' },
57
- '.cm-panel.cm-search [name=close]:hover': { color: '#e6e6e6' },
58
- '.cm-searchMatch': { backgroundColor: 'rgba(51, 175, 188, 0.2)', outline: '1px solid rgba(51, 175, 188, 0.4)' },
59
- '.cm-searchMatch-selected': { backgroundColor: 'rgba(51, 175, 188, 0.35)' },
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
- addToast('error', 'Connection failed', err?.message || 'Unknown error');
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 === 'starting'
83
- ? 'Starting daemon...'
84
- : connecting
85
- ? 'Connecting...'
86
- : 'Connect';
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
- setTestResult({ error: err?.body?.error || err?.message || 'Connection failed' });
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
  }