groove-dev 0.27.87 → 0.27.88
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/CLAUDE.md +3 -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 +115 -7
- package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
- package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -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-file-tree.jsx +254 -0
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
- 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 +115 -7
- package/packages/daemon/src/conversations.js +29 -3
- package/packages/daemon/src/providers/codex.js +28 -10
- package/packages/daemon/src/registry.js +30 -0
- package/packages/daemon/src/validate.js +23 -0
- package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +254 -0
- package/packages/gui/src/components/agents/code-review.jsx +177 -0
- package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/packages/gui/src/components/chat/chat-input.jsx +117 -40
- package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
- package/packages/gui/src/components/chat/chat-view.jsx +26 -1
- package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
- package/packages/gui/src/components/chat/model-picker.jsx +5 -0
- package/packages/gui/src/stores/groove.js +132 -1
- package/packages/gui/src/views/agents.jsx +22 -3
- package/test/doomsday-clock/index.html +55 -0
- package/test/doomsday-clock/script.js +66 -0
- package/test/doomsday-clock/style.css +315 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
- package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/packages/gui/dist/assets/index-C5e7KVGN.js +0 -8637
|
@@ -45,12 +45,20 @@ export function ChatView() {
|
|
|
45
45
|
const conversationRoles = useGrooveStore((s) => s.conversationRoles);
|
|
46
46
|
const setConversationRole = useGrooveStore((s) => s.setConversationRole);
|
|
47
47
|
|
|
48
|
+
const conversationReasoningEffort = useGrooveStore((s) => s.conversationReasoningEffort);
|
|
49
|
+
const setConversationReasoningEffort = useGrooveStore((s) => s.setConversationReasoningEffort);
|
|
50
|
+
const conversationVerbosity = useGrooveStore((s) => s.conversationVerbosity);
|
|
51
|
+
const setConversationVerbosity = useGrooveStore((s) => s.setConversationVerbosity);
|
|
52
|
+
|
|
48
53
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
49
54
|
const [replyContext, setReplyContext] = useState(null);
|
|
50
55
|
|
|
51
56
|
const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || null) : null;
|
|
57
|
+
const activeReasoningEffort = activeConversationId ? (conversationReasoningEffort?.[activeConversationId] || 'medium') : 'medium';
|
|
58
|
+
const activeVerbosity = activeConversationId ? (conversationVerbosity?.[activeConversationId] || 'medium') : 'medium';
|
|
52
59
|
|
|
53
60
|
const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
|
|
61
|
+
const isCodexProvider = activeConversation?.provider === 'codex';
|
|
54
62
|
const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
|
|
55
63
|
const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
|
|
56
64
|
const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
|
|
@@ -108,6 +116,16 @@ export function ChatView() {
|
|
|
108
116
|
setReplyContext(msg);
|
|
109
117
|
}, []);
|
|
110
118
|
|
|
119
|
+
const handleReasoningEffortChange = useCallback((effort) => {
|
|
120
|
+
if (!activeConversationId) return;
|
|
121
|
+
setConversationReasoningEffort(activeConversationId, effort);
|
|
122
|
+
}, [activeConversationId, setConversationReasoningEffort]);
|
|
123
|
+
|
|
124
|
+
const handleVerbosityChange = useCallback((verbosity) => {
|
|
125
|
+
if (!activeConversationId) return;
|
|
126
|
+
setConversationVerbosity(activeConversationId, verbosity);
|
|
127
|
+
}, [activeConversationId, setConversationVerbosity]);
|
|
128
|
+
|
|
111
129
|
const currentModel = activeConversation
|
|
112
130
|
? { provider: activeConversation.provider, model: activeConversation.model }
|
|
113
131
|
: null;
|
|
@@ -116,7 +134,7 @@ export function ChatView() {
|
|
|
116
134
|
<div className="flex h-full bg-surface-0">
|
|
117
135
|
{/* Conversation sidebar */}
|
|
118
136
|
<div className={cn(
|
|
119
|
-
'flex-shrink-0 border-r border-accent/12 bg-
|
|
137
|
+
'flex-shrink-0 border-r border-accent/12 bg-accent transition-all duration-200 overflow-hidden',
|
|
120
138
|
sidebarCollapsed ? 'w-0' : 'w-64',
|
|
121
139
|
)}>
|
|
122
140
|
<ConversationList onNewChat={() => handleNewChat()} />
|
|
@@ -146,6 +164,13 @@ export function ChatView() {
|
|
|
146
164
|
replyContext={replyContext}
|
|
147
165
|
onClearReply={() => setReplyContext(null)}
|
|
148
166
|
role={activeRole}
|
|
167
|
+
isCodex={isCodexProvider}
|
|
168
|
+
reasoningEffort={activeReasoningEffort}
|
|
169
|
+
onReasoningEffortChange={handleReasoningEffortChange}
|
|
170
|
+
verbosity={activeVerbosity}
|
|
171
|
+
onVerbosityChange={handleVerbosityChange}
|
|
172
|
+
mode={activeConversation?.mode || 'api'}
|
|
173
|
+
onModeChange={handleModeChange}
|
|
149
174
|
/>
|
|
150
175
|
</>
|
|
151
176
|
) : (
|
|
@@ -31,7 +31,7 @@ function groupByDate(conversations) {
|
|
|
31
31
|
function GroupLabel({ label }) {
|
|
32
32
|
return (
|
|
33
33
|
<div className="px-3 pt-4 pb-1.5">
|
|
34
|
-
<span className="text-2xs font-semibold text-
|
|
34
|
+
<span className="text-2xs font-semibold text-white/60 uppercase tracking-wider font-sans">{label}</span>
|
|
35
35
|
</div>
|
|
36
36
|
);
|
|
37
37
|
}
|
|
@@ -45,23 +45,23 @@ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete
|
|
|
45
45
|
className={cn(
|
|
46
46
|
'w-full flex items-center gap-2 px-3 py-2 text-left rounded-md transition-colors cursor-pointer group',
|
|
47
47
|
isActive
|
|
48
|
-
? 'bg-
|
|
49
|
-
: 'text-
|
|
48
|
+
? 'bg-white/15 text-white'
|
|
49
|
+
: 'text-white/80 hover:bg-white/10 hover:text-white',
|
|
50
50
|
)}
|
|
51
51
|
>
|
|
52
|
-
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-
|
|
52
|
+
<MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-white' : 'text-white/50 group-hover:text-white/70')} />
|
|
53
53
|
<div className="flex-1 min-w-0">
|
|
54
|
-
<div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
|
|
54
|
+
<div className="text-xs font-medium font-sans truncate text-white">{conv.title || 'New Chat'}</div>
|
|
55
55
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
56
56
|
{conv.mode === 'agent'
|
|
57
|
-
? <Bot size={9} className="text-
|
|
58
|
-
: <Zap size={9} className="text-
|
|
57
|
+
? <Bot size={9} className="text-white/70 flex-shrink-0" />
|
|
58
|
+
: <Zap size={9} className="text-white/70 flex-shrink-0" />
|
|
59
59
|
}
|
|
60
60
|
{conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
|
|
61
|
-
<span className="text-2xs text-
|
|
61
|
+
<span className="text-2xs text-white/50 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
|
-
{conv.pinned && <Pin size={10} className="text-
|
|
64
|
+
{conv.pinned && <Pin size={10} className="text-white flex-shrink-0" />}
|
|
65
65
|
</button>
|
|
66
66
|
</ContextMenuTrigger>
|
|
67
67
|
<ContextMenuContent>
|
|
@@ -125,9 +125,9 @@ export function ConversationList({ onNewChat }) {
|
|
|
125
125
|
<div className="flex-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
|
|
126
126
|
{conversations.length === 0 ? (
|
|
127
127
|
<div className="flex flex-col items-center justify-center py-16 text-center px-4">
|
|
128
|
-
<MessageCircle size={24} className="text-
|
|
129
|
-
<p className="text-xs text-
|
|
130
|
-
<p className="text-2xs text-
|
|
128
|
+
<MessageCircle size={24} className="text-white/50 mb-3" />
|
|
129
|
+
<p className="text-xs text-white/80 font-sans">No conversations yet</p>
|
|
130
|
+
<p className="text-2xs text-white/60 font-sans mt-1">Start a new chat to begin</p>
|
|
131
131
|
</div>
|
|
132
132
|
) : (
|
|
133
133
|
<>
|
|
@@ -140,10 +140,10 @@ export function ConversationList({ onNewChat }) {
|
|
|
140
140
|
)}
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
|
-
<div className="p-3 border-t border-
|
|
143
|
+
<div className="p-3 border-t border-white/15">
|
|
144
144
|
<button
|
|
145
145
|
onClick={onNewChat}
|
|
146
|
-
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-
|
|
146
|
+
className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-white/15 text-white text-xs font-semibold font-sans hover:bg-white/25 transition-colors cursor-pointer border border-white/20"
|
|
147
147
|
>
|
|
148
148
|
<Plus size={14} />
|
|
149
149
|
New Chat
|
|
@@ -23,6 +23,10 @@ const TIER_CONFIG = {
|
|
|
23
23
|
|
|
24
24
|
function getTier(model) {
|
|
25
25
|
const name = (model || '').toLowerCase();
|
|
26
|
+
if (name.includes('gpt-5.5') || name.includes('gpt-5.4-pro')) return 'frontier';
|
|
27
|
+
if (name.includes('gpt-5.4-mini') || name.includes('gpt-5-mini')) return 'mid';
|
|
28
|
+
if (name.includes('gpt-5.4-nano') || name.includes('gpt-5-nano')) return 'fast';
|
|
29
|
+
if (name.includes('gpt-5.4')) return 'frontier';
|
|
26
30
|
if (name.includes('opus') || name.includes('pro') || name.includes('o3') || name.includes('gpt-4o')) return 'frontier';
|
|
27
31
|
if (name.includes('sonnet') || name.includes('flash') || name.includes('o4-mini')) return 'mid';
|
|
28
32
|
return 'fast';
|
|
@@ -30,6 +34,7 @@ function getTier(model) {
|
|
|
30
34
|
|
|
31
35
|
function getContextSize(model) {
|
|
32
36
|
const name = (model || '').toLowerCase();
|
|
37
|
+
if (name.startsWith('gpt-5')) return '200k';
|
|
33
38
|
if (name.includes('opus') || name.includes('sonnet')) return '200k';
|
|
34
39
|
if (name.includes('haiku')) return '200k';
|
|
35
40
|
if (name.includes('pro')) return '1M';
|
|
@@ -92,6 +92,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
92
92
|
sendingMessage: false,
|
|
93
93
|
streamingConversationId: null,
|
|
94
94
|
conversationRoles: loadJSON('groove:conversationRoles'),
|
|
95
|
+
conversationReasoningEffort: loadJSON('groove:conversationReasoningEffort'),
|
|
96
|
+
conversationVerbosity: loadJSON('groove:conversationVerbosity'),
|
|
95
97
|
|
|
96
98
|
// ── Approvals ─────────────────────────────────────────────
|
|
97
99
|
pendingApprovals: [],
|
|
@@ -173,6 +175,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
173
175
|
editorRecentSaves: {},
|
|
174
176
|
editorSidebarWidth: Number(localStorage.getItem('groove:editorSidebarWidth')) || 240,
|
|
175
177
|
|
|
178
|
+
// ── Workspace Mode ────────────────────────────────────────
|
|
179
|
+
workspaceMode: localStorage.getItem('groove:workspaceMode') === 'true',
|
|
180
|
+
workspaceAgentId: null,
|
|
181
|
+
workspaceSnapshots: {},
|
|
182
|
+
workspaceReviewMode: false,
|
|
183
|
+
workspaceReviewFiles: [],
|
|
184
|
+
|
|
176
185
|
// ── Onboarding ────────────────────────────────────────────
|
|
177
186
|
onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
|
|
178
187
|
|
|
@@ -451,6 +460,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
451
460
|
if (msg.error && msg.agentId) {
|
|
452
461
|
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
453
462
|
}
|
|
463
|
+
// Clear workspace if the exiting agent was the workspace target
|
|
464
|
+
if (get().workspaceAgentId === msg.agentId) {
|
|
465
|
+
const teamAgents = get().agents.filter(
|
|
466
|
+
(a) => a.id !== msg.agentId && a.teamId === get().activeTeamId,
|
|
467
|
+
);
|
|
468
|
+
const next = teamAgents.find((a) => a.status === 'running') || teamAgents[0];
|
|
469
|
+
set({ workspaceAgentId: next?.id || null });
|
|
470
|
+
}
|
|
471
|
+
|
|
454
472
|
// Check for recommended team when planner completes
|
|
455
473
|
if (agent?.role === 'planner' && msg.status === 'completed') {
|
|
456
474
|
setTimeout(() => get().checkRecommendedTeam(), 1000);
|
|
@@ -567,6 +585,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
567
585
|
const savedAt = get().editorRecentSaves[msg.path];
|
|
568
586
|
if (savedAt && Date.now() - savedAt < 2000) break;
|
|
569
587
|
set((s) => ({ editorChangedFiles: { ...s.editorChangedFiles, [msg.path]: msg.timestamp } }));
|
|
588
|
+
// Auto-capture workspace snapshot for diff viewer
|
|
589
|
+
if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
|
|
590
|
+
const existing = get().editorFiles[msg.path];
|
|
591
|
+
if (existing?.content) {
|
|
592
|
+
get().captureSnapshot(msg.path, existing.content);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
570
595
|
break;
|
|
571
596
|
}
|
|
572
597
|
|
|
@@ -2050,7 +2075,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2050
2075
|
const conversationRoles = { ...s.conversationRoles };
|
|
2051
2076
|
delete conversationRoles[id];
|
|
2052
2077
|
persistJSON('groove:conversationRoles', conversationRoles);
|
|
2053
|
-
|
|
2078
|
+
const conversationReasoningEffort = { ...s.conversationReasoningEffort };
|
|
2079
|
+
delete conversationReasoningEffort[id];
|
|
2080
|
+
persistJSON('groove:conversationReasoningEffort', conversationReasoningEffort);
|
|
2081
|
+
const conversationVerbosity = { ...s.conversationVerbosity };
|
|
2082
|
+
delete conversationVerbosity[id];
|
|
2083
|
+
persistJSON('groove:conversationVerbosity', conversationVerbosity);
|
|
2084
|
+
return { conversations, conversationMessages, conversationRoles, conversationReasoningEffort, conversationVerbosity, activeConversationId };
|
|
2054
2085
|
});
|
|
2055
2086
|
} catch (err) {
|
|
2056
2087
|
get().addToast('error', 'Delete failed', err.message);
|
|
@@ -2093,6 +2124,24 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2093
2124
|
});
|
|
2094
2125
|
},
|
|
2095
2126
|
|
|
2127
|
+
setConversationReasoningEffort(id, effort) {
|
|
2128
|
+
set((s) => {
|
|
2129
|
+
const map = { ...s.conversationReasoningEffort };
|
|
2130
|
+
map[id] = effort || 'medium';
|
|
2131
|
+
persistJSON('groove:conversationReasoningEffort', map);
|
|
2132
|
+
return { conversationReasoningEffort: map };
|
|
2133
|
+
});
|
|
2134
|
+
},
|
|
2135
|
+
|
|
2136
|
+
setConversationVerbosity(id, verbosity) {
|
|
2137
|
+
set((s) => {
|
|
2138
|
+
const map = { ...s.conversationVerbosity };
|
|
2139
|
+
map[id] = verbosity || 'medium';
|
|
2140
|
+
persistJSON('groove:conversationVerbosity', map);
|
|
2141
|
+
return { conversationVerbosity: map };
|
|
2142
|
+
});
|
|
2143
|
+
},
|
|
2144
|
+
|
|
2096
2145
|
async sendChatMessage(conversationId, message) {
|
|
2097
2146
|
const conv = get().conversations.find((c) => c.id === conversationId);
|
|
2098
2147
|
if (!conv) return;
|
|
@@ -2122,6 +2171,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2122
2171
|
...body.history,
|
|
2123
2172
|
];
|
|
2124
2173
|
}
|
|
2174
|
+
const effort = get().conversationReasoningEffort?.[conversationId] || 'medium';
|
|
2175
|
+
const verbosity = get().conversationVerbosity?.[conversationId] || 'medium';
|
|
2176
|
+
if (conv.provider === 'codex') {
|
|
2177
|
+
body.reasoning_effort = effort;
|
|
2178
|
+
body.verbosity = verbosity;
|
|
2179
|
+
}
|
|
2125
2180
|
await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
|
|
2126
2181
|
} catch (err) {
|
|
2127
2182
|
set((s) => {
|
|
@@ -2303,6 +2358,82 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2303
2358
|
}
|
|
2304
2359
|
},
|
|
2305
2360
|
|
|
2361
|
+
// ── Workspace Mode ────────────────────────────────────────
|
|
2362
|
+
|
|
2363
|
+
setWorkspaceMode(on) {
|
|
2364
|
+
set({ workspaceMode: on });
|
|
2365
|
+
localStorage.setItem('groove:workspaceMode', String(on));
|
|
2366
|
+
if (on && !get().workspaceAgentId) {
|
|
2367
|
+
const teamAgents = get().agents.filter((a) => a.teamId === get().activeTeamId);
|
|
2368
|
+
const selected = get().detailPanel?.type === 'agent' ? get().detailPanel.agentId : null;
|
|
2369
|
+
const running = teamAgents.find((a) => a.status === 'running');
|
|
2370
|
+
set({ workspaceAgentId: selected || running?.id || teamAgents[0]?.id || null });
|
|
2371
|
+
}
|
|
2372
|
+
},
|
|
2373
|
+
|
|
2374
|
+
setWorkspaceAgent(id) {
|
|
2375
|
+
set({ workspaceAgentId: id });
|
|
2376
|
+
},
|
|
2377
|
+
|
|
2378
|
+
captureSnapshot(path, content) {
|
|
2379
|
+
set((s) => {
|
|
2380
|
+
if (s.workspaceSnapshots[path]) return s;
|
|
2381
|
+
const next = { ...s.workspaceSnapshots, [path]: content };
|
|
2382
|
+
const keys = Object.keys(next);
|
|
2383
|
+
if (keys.length > 200) {
|
|
2384
|
+
delete next[keys[0]];
|
|
2385
|
+
}
|
|
2386
|
+
return { workspaceSnapshots: next };
|
|
2387
|
+
});
|
|
2388
|
+
},
|
|
2389
|
+
|
|
2390
|
+
toggleReviewMode() {
|
|
2391
|
+
const st = get();
|
|
2392
|
+
if (st.workspaceReviewMode) {
|
|
2393
|
+
set({ workspaceReviewMode: false, workspaceReviewFiles: [] });
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const agentId = st.workspaceAgentId;
|
|
2397
|
+
const log = st.activityLog[agentId] || [];
|
|
2398
|
+
const seen = new Set();
|
|
2399
|
+
const files = [];
|
|
2400
|
+
for (const entry of log) {
|
|
2401
|
+
const t = (entry.text || '').toLowerCase();
|
|
2402
|
+
if (!(t.includes('writ') || t.includes('edit') || t.includes('creat'))) continue;
|
|
2403
|
+
const match = entry.text.match(/(?:Write|Edit|Create|wrote|editing|writing)\S*\s+(\S+)/i);
|
|
2404
|
+
if (!match) continue;
|
|
2405
|
+
const path = match[1];
|
|
2406
|
+
if (seen.has(path)) continue;
|
|
2407
|
+
seen.add(path);
|
|
2408
|
+
files.push({ path, status: 'pending', comment: '' });
|
|
2409
|
+
}
|
|
2410
|
+
set({ workspaceReviewMode: true, workspaceReviewFiles: files });
|
|
2411
|
+
},
|
|
2412
|
+
|
|
2413
|
+
approveFile(path) {
|
|
2414
|
+
set((s) => ({
|
|
2415
|
+
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
2416
|
+
f.path === path ? { ...f, status: 'approved' } : f,
|
|
2417
|
+
),
|
|
2418
|
+
}));
|
|
2419
|
+
},
|
|
2420
|
+
|
|
2421
|
+
rejectFile(path) {
|
|
2422
|
+
set((s) => ({
|
|
2423
|
+
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
2424
|
+
f.path === path ? { ...f, status: 'rejected' } : f,
|
|
2425
|
+
),
|
|
2426
|
+
}));
|
|
2427
|
+
},
|
|
2428
|
+
|
|
2429
|
+
commentFile(path, comment) {
|
|
2430
|
+
set((s) => ({
|
|
2431
|
+
workspaceReviewFiles: s.workspaceReviewFiles.map((f) =>
|
|
2432
|
+
f.path === path ? { ...f, comment } : f,
|
|
2433
|
+
),
|
|
2434
|
+
}));
|
|
2435
|
+
},
|
|
2436
|
+
|
|
2306
2437
|
// ── Federation ────────────────────────────────────────────
|
|
2307
2438
|
|
|
2308
2439
|
async fetchFederationStatus() {
|
|
@@ -12,6 +12,7 @@ import { Button } from '../components/ui/button';
|
|
|
12
12
|
import { Badge } from '../components/ui/badge';
|
|
13
13
|
import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Radio, Eye } from 'lucide-react';
|
|
14
14
|
import { PreviewWorkspace } from '../components/preview/preview-workspace';
|
|
15
|
+
import { WorkspaceMode } from '../components/agents/workspace-mode';
|
|
15
16
|
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
|
|
16
17
|
|
|
17
18
|
const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
|
|
@@ -799,6 +800,8 @@ export default function AgentsView() {
|
|
|
799
800
|
const showPreviewInAgents = useGrooveStore((s) => s.showPreviewInAgents);
|
|
800
801
|
const previewState = useGrooveStore((s) => s.previewState);
|
|
801
802
|
const togglePreviewInAgents = useGrooveStore((s) => s.togglePreviewInAgents);
|
|
803
|
+
const workspaceMode = useGrooveStore((s) => s.workspaceMode);
|
|
804
|
+
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
802
805
|
|
|
803
806
|
// Poll for recommended team while a planner is running
|
|
804
807
|
useEffect(() => {
|
|
@@ -849,6 +852,8 @@ export default function AgentsView() {
|
|
|
849
852
|
</div>
|
|
850
853
|
) : teamAgents.length === 0 ? (
|
|
851
854
|
<EmptyState onPlanner={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
|
|
855
|
+
) : workspaceMode ? (
|
|
856
|
+
<WorkspaceMode />
|
|
852
857
|
) : showPreviewInAgents && previewState.url ? (
|
|
853
858
|
<PreviewWorkspace embedded />
|
|
854
859
|
) : (
|
|
@@ -857,8 +862,8 @@ export default function AgentsView() {
|
|
|
857
862
|
</ReactFlowProvider>
|
|
858
863
|
)}
|
|
859
864
|
</div>
|
|
860
|
-
<RecommendedTeamCard />
|
|
861
|
-
{!isLoading && teamAgents.length > 0 && (
|
|
865
|
+
{!workspaceMode && <RecommendedTeamCard />}
|
|
866
|
+
{!isLoading && teamAgents.length > 0 && !workspaceMode && (
|
|
862
867
|
<button
|
|
863
868
|
onClick={() => openDetail({ type: 'spawn' })}
|
|
864
869
|
className="absolute bottom-4 left-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
|
|
@@ -867,7 +872,21 @@ export default function AgentsView() {
|
|
|
867
872
|
Spawn
|
|
868
873
|
</button>
|
|
869
874
|
)}
|
|
870
|
-
{!isLoading && teamAgents.length > 0 &&
|
|
875
|
+
{!isLoading && teamAgents.length > 0 && (
|
|
876
|
+
<button
|
|
877
|
+
onClick={() => setWorkspaceMode(!workspaceMode)}
|
|
878
|
+
className={cn(
|
|
879
|
+
'absolute bottom-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md text-xs font-semibold font-sans transition-colors cursor-pointer select-none shadow-lg shadow-black/10',
|
|
880
|
+
previewState.url && !workspaceMode ? 'right-32' : 'right-4',
|
|
881
|
+
workspaceMode
|
|
882
|
+
? 'bg-accent/15 text-accent hover:bg-accent/25'
|
|
883
|
+
: 'bg-purple/15 text-purple hover:bg-purple/25',
|
|
884
|
+
)}
|
|
885
|
+
>
|
|
886
|
+
{workspaceMode ? <><Users size={14} /> Tree</> : <><Code2 size={14} /> Workspace</>}
|
|
887
|
+
</button>
|
|
888
|
+
)}
|
|
889
|
+
{!isLoading && teamAgents.length > 0 && !workspaceMode && previewState.url && (
|
|
871
890
|
<button
|
|
872
891
|
onClick={togglePreviewInAgents}
|
|
873
892
|
className="absolute bottom-4 right-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-info/15 text-info text-xs font-semibold font-sans hover:bg-info/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.88",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
|
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
18
|
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
19
|
-
import { validateAgentConfig } from './validate.js';
|
|
19
|
+
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
|
|
20
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
21
21
|
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -72,6 +72,9 @@ async function _executeApprovalRetry(daemon, approval) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const FILE_READ_TOOLS = new Set(['Read', 'read_file']);
|
|
76
|
+
const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'write_file', 'edit_file', 'create_file']);
|
|
77
|
+
|
|
75
78
|
export function createApi(app, daemon) {
|
|
76
79
|
_daemon = daemon;
|
|
77
80
|
|
|
@@ -325,6 +328,14 @@ export function createApi(app, daemon) {
|
|
|
325
328
|
}
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
// Track file operations for the files-touched API
|
|
332
|
+
if (targets.length > 0) {
|
|
333
|
+
const op = FILE_WRITE_TOOLS.has(toolName) ? 'write' : FILE_READ_TOOLS.has(toolName) ? 'read' : null;
|
|
334
|
+
if (op) {
|
|
335
|
+
for (const t of targets) daemon.registry.trackFileOp(agentId, t, op);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
328
339
|
daemon.audit.log('knock.allowed', { agentId, toolName, targets });
|
|
329
340
|
res.json({ allow: true });
|
|
330
341
|
});
|
|
@@ -1100,14 +1111,19 @@ export function createApi(app, daemon) {
|
|
|
1100
1111
|
|
|
1101
1112
|
app.post('/api/conversations', async (req, res) => {
|
|
1102
1113
|
try {
|
|
1103
|
-
const { provider, model, title, mode } = req.body;
|
|
1114
|
+
const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
|
|
1104
1115
|
if (!provider || typeof provider !== 'string') {
|
|
1105
1116
|
return res.status(400).json({ error: 'provider is required' });
|
|
1106
1117
|
}
|
|
1107
1118
|
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
1108
1119
|
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
1109
1120
|
}
|
|
1110
|
-
const
|
|
1121
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
1122
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
1123
|
+
const conversation = await daemon.conversations.create(provider, model, title, mode || 'api', {
|
|
1124
|
+
reasoningEffort: validatedEffort,
|
|
1125
|
+
verbosity: validatedVerbosity,
|
|
1126
|
+
});
|
|
1111
1127
|
daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
|
|
1112
1128
|
res.status(201).json(conversation);
|
|
1113
1129
|
} catch (err) {
|
|
@@ -1139,6 +1155,11 @@ export function createApi(app, daemon) {
|
|
|
1139
1155
|
}
|
|
1140
1156
|
await daemon.conversations.setMode(req.params.id, req.body.mode);
|
|
1141
1157
|
}
|
|
1158
|
+
if (req.body.reasoning_effort !== undefined || req.body.verbosity !== undefined) {
|
|
1159
|
+
const validatedEffort = req.body.reasoning_effort !== undefined ? validateReasoningEffort(req.body.reasoning_effort) : undefined;
|
|
1160
|
+
const validatedVerbosity = req.body.verbosity !== undefined ? validateVerbosity(req.body.verbosity) : undefined;
|
|
1161
|
+
daemon.conversations.updateReasoningSettings(req.params.id, validatedEffort, validatedVerbosity);
|
|
1162
|
+
}
|
|
1142
1163
|
daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
|
|
1143
1164
|
res.json(daemon.conversations.get(req.params.id));
|
|
1144
1165
|
} catch (err) {
|
|
@@ -1160,10 +1181,13 @@ export function createApi(app, daemon) {
|
|
|
1160
1181
|
|
|
1161
1182
|
app.post('/api/conversations/:id/message', async (req, res) => {
|
|
1162
1183
|
try {
|
|
1163
|
-
const { message, history } = req.body;
|
|
1184
|
+
const { message, history, reasoning_effort, verbosity } = req.body;
|
|
1164
1185
|
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
1165
1186
|
return res.status(400).json({ error: 'message is required' });
|
|
1166
1187
|
}
|
|
1188
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
1189
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
1190
|
+
|
|
1167
1191
|
const conv = daemon.conversations.get(req.params.id);
|
|
1168
1192
|
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
1169
1193
|
|
|
@@ -1172,7 +1196,10 @@ export function createApi(app, daemon) {
|
|
|
1172
1196
|
|
|
1173
1197
|
// API mode — lightweight headless streaming, no agent spawned
|
|
1174
1198
|
if (conv.mode === 'api' || !conv.agentId) {
|
|
1175
|
-
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || []
|
|
1199
|
+
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || [], {
|
|
1200
|
+
reasoningEffort: validatedEffort,
|
|
1201
|
+
verbosity: validatedVerbosity,
|
|
1202
|
+
});
|
|
1176
1203
|
daemon.audit.log('conversation.message', { id: req.params.id, mode: 'api' });
|
|
1177
1204
|
return res.json({ status: 'streaming', mode: 'api' });
|
|
1178
1205
|
}
|
|
@@ -3064,6 +3091,87 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3064
3091
|
});
|
|
3065
3092
|
});
|
|
3066
3093
|
|
|
3094
|
+
// Files touched by an agent during its session
|
|
3095
|
+
app.get('/api/agents/:id/files-touched', (req, res) => {
|
|
3096
|
+
const agent = daemon.registry.get(req.params.id);
|
|
3097
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
3098
|
+
const files = daemon.registry.getFilesTouched(req.params.id);
|
|
3099
|
+
res.json({ files, total: files.length });
|
|
3100
|
+
});
|
|
3101
|
+
|
|
3102
|
+
// Git diff — structured diff for a file, an agent's touched files, or all uncommitted changes
|
|
3103
|
+
app.get('/api/files/git-diff', (req, res) => {
|
|
3104
|
+
const rootDir = getEditorRoot();
|
|
3105
|
+
if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
|
|
3106
|
+
|
|
3107
|
+
let paths = [];
|
|
3108
|
+
|
|
3109
|
+
if (req.query.path) {
|
|
3110
|
+
const result = validateFilePath(req.query.path, rootDir);
|
|
3111
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
3112
|
+
paths = [req.query.path];
|
|
3113
|
+
} else if (req.query.agentId) {
|
|
3114
|
+
const agent = daemon.registry.get(req.query.agentId);
|
|
3115
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
3116
|
+
paths = daemon.registry.getFilesTouched(req.query.agentId).map(f => f.path);
|
|
3117
|
+
if (paths.length === 0) return res.json({ diffs: [] });
|
|
3118
|
+
// Validate each path
|
|
3119
|
+
for (const p of paths) {
|
|
3120
|
+
if (p.startsWith('/') || p.includes('..') || p.includes('\0')) {
|
|
3121
|
+
return res.status(400).json({ error: 'Invalid path in agent files' });
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const args = ['diff'];
|
|
3127
|
+
const cachedArgs = ['diff', '--cached'];
|
|
3128
|
+
if (paths.length > 0) {
|
|
3129
|
+
args.push('--', ...paths);
|
|
3130
|
+
cachedArgs.push('--', ...paths);
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
try {
|
|
3134
|
+
const unstaged = execFileSync('git', args, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
3135
|
+
const staged = execFileSync('git', cachedArgs, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
3136
|
+
const combined = (staged + '\n' + unstaged).trim();
|
|
3137
|
+
const diffs = parseDiffOutput(combined);
|
|
3138
|
+
res.json({ diffs });
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
if (err.status !== undefined) {
|
|
3141
|
+
return res.json({ diffs: [] });
|
|
3142
|
+
}
|
|
3143
|
+
res.status(500).json({ error: 'Failed to compute diff' });
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
function parseDiffOutput(raw) {
|
|
3148
|
+
if (!raw) return [];
|
|
3149
|
+
const fileDiffs = raw.split(/^diff --git /m).filter(Boolean);
|
|
3150
|
+
return fileDiffs.map(chunk => {
|
|
3151
|
+
const lines = chunk.split('\n');
|
|
3152
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
3153
|
+
const filePath = headerMatch ? headerMatch[2] : 'unknown';
|
|
3154
|
+
let status = 'modified';
|
|
3155
|
+
if (lines.some(l => l.startsWith('new file'))) status = 'added';
|
|
3156
|
+
else if (lines.some(l => l.startsWith('deleted file'))) status = 'deleted';
|
|
3157
|
+
let additions = 0, deletions = 0;
|
|
3158
|
+
const hunks = [];
|
|
3159
|
+
let currentHunk = null;
|
|
3160
|
+
for (const line of lines) {
|
|
3161
|
+
if (line.startsWith('@@')) {
|
|
3162
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
3163
|
+
currentHunk = { header: line, lines: [] };
|
|
3164
|
+
} else if (currentHunk) {
|
|
3165
|
+
currentHunk.lines.push(line);
|
|
3166
|
+
if (line.startsWith('+') && !line.startsWith('+++')) additions++;
|
|
3167
|
+
else if (line.startsWith('-') && !line.startsWith('---')) deletions++;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
3171
|
+
return { path: filePath, status, hunks, additions, deletions, content: 'diff --git ' + chunk };
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3067
3175
|
// File search — fuzzy filename matching for quick-open (Ctrl+P)
|
|
3068
3176
|
app.get('/api/files/search', (req, res) => {
|
|
3069
3177
|
const query = req.query.q;
|
|
@@ -4172,10 +4280,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4172
4280
|
|
|
4173
4281
|
app.post('/api/tunnels', (req, res) => {
|
|
4174
4282
|
try {
|
|
4175
|
-
const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
|
|
4283
|
+
const { name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir } = req.body;
|
|
4176
4284
|
if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
|
|
4177
4285
|
if (!host || typeof host !== 'string') return res.status(400).json({ error: 'host is required (string)' });
|
|
4178
|
-
const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect });
|
|
4286
|
+
const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir });
|
|
4179
4287
|
res.json(result);
|
|
4180
4288
|
} catch (err) {
|
|
4181
4289
|
res.status(400).json({ error: err.message });
|