groove-dev 0.27.75 → 0.27.77

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 (33) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/preview.js +14 -0
  5. package/node_modules/@groove-dev/daemon/src/process.js +4 -1
  6. package/node_modules/@groove-dev/gui/dist/assets/{index-CAT9SCJi.js → index-BbmPDhuW.js} +319 -323
  7. package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +1 -0
  8. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  9. package/node_modules/@groove-dev/gui/package.json +1 -1
  10. package/node_modules/@groove-dev/gui/src/app.css +12 -0
  11. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +32 -27
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +26 -24
  13. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +34 -6
  14. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +19 -4
  15. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  16. package/package.json +1 -1
  17. package/packages/cli/package.json +1 -1
  18. package/packages/daemon/package.json +1 -1
  19. package/packages/daemon/src/preview.js +14 -0
  20. package/packages/daemon/src/process.js +4 -1
  21. package/packages/gui/dist/assets/{index-CAT9SCJi.js → index-BbmPDhuW.js} +319 -323
  22. package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
  23. package/packages/gui/dist/index.html +2 -2
  24. package/packages/gui/package.json +1 -1
  25. package/packages/gui/src/app.css +12 -0
  26. package/packages/gui/src/components/chat/chat-input.jsx +32 -27
  27. package/packages/gui/src/components/chat/chat-messages.jsx +26 -24
  28. package/packages/gui/src/components/preview/preview-toolbar.jsx +34 -6
  29. package/packages/gui/src/components/preview/preview-workspace.jsx +19 -4
  30. package/packages/gui/src/components/preview/screenshot-overlay.jsx +91 -57
  31. package/welcome.png +0 -0
  32. package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +0 -1
  33. package/packages/gui/dist/assets/index-CVzz6zyb.css +0 -1
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
- import { Send, Paperclip, MonitorX, Loader2 } from 'lucide-react';
3
+ import { Send, Paperclip, MonitorX, Loader2, MessageCircle, Camera, RefreshCw } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { timeAgo } from '../../lib/format';
@@ -137,9 +137,24 @@ function PreviewChat() {
137
137
  <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
138
138
  {previewChat.length === 0 && (
139
139
  <div className="flex items-center justify-center h-full">
140
- <p className="text-sm text-text-3 font-sans text-center">
141
- Describe changes and they'll be routed to the team planner
142
- </p>
140
+ <div className="max-w-xs w-full px-5 py-5 bg-surface-1 border border-border-subtle rounded-xl text-center">
141
+ <MessageCircle size={24} className="mx-auto text-accent mb-3" />
142
+ <h3 className="text-sm font-semibold text-text-0 font-sans mb-3">Preview is live!</h3>
143
+ <ul className="text-left space-y-2 text-2xs text-text-2 font-sans">
144
+ <li className="flex gap-2">
145
+ <Send size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
146
+ <span>Type a message to request changes — your feedback goes to the team planner who routes it to the right agent</span>
147
+ </li>
148
+ <li className="flex gap-2">
149
+ <Camera size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
150
+ <span>Use the camera icon to screenshot a specific area and annotate it</span>
151
+ </li>
152
+ <li className="flex gap-2">
153
+ <RefreshCw size={11} className="text-text-3 mt-0.5 flex-shrink-0" />
154
+ <span>Changes auto-refresh via hot module reload</span>
155
+ </li>
156
+ </ul>
157
+ </div>
143
158
  </div>
144
159
  )}
145
160
  {previewChat.map((msg, i) => (
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useCallback, useEffect } from 'react';
3
- import { Send, X } from 'lucide-react';
3
+ import { Send, X, Loader2 } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
 
6
6
  export function ScreenshotOverlay({ iframeRef }) {
@@ -11,8 +11,9 @@ export function ScreenshotOverlay({ iframeRef }) {
11
11
  const [dragging, setDragging] = useState(false);
12
12
  const [start, setStart] = useState(null);
13
13
  const [end, setEnd] = useState(null);
14
- const [captured, setCaptured] = useState(null); // { base64, rect }
14
+ const [captured, setCaptured] = useState(null); // { base64, rect, loading }
15
15
  const [comment, setComment] = useState('');
16
+ const [flashRect, setFlashRect] = useState(null);
16
17
 
17
18
  const handleMouseDown = useCallback((e) => {
18
19
  if (captured) return;
@@ -48,9 +49,37 @@ export function ScreenshotOverlay({ iframeRef }) {
48
49
  return;
49
50
  }
50
51
 
52
+ setCaptured({ base64: null, rect: selRect, loading: true });
53
+
54
+ function finishCapture(base64) {
55
+ setCaptured({ base64, rect: selRect, loading: false });
56
+ setFlashRect(selRect);
57
+ setTimeout(() => setFlashRect(null), 600);
58
+ }
59
+
60
+ function drawPlaceholder() {
61
+ const canvas = document.createElement('canvas');
62
+ const dpr = window.devicePixelRatio || 1;
63
+ canvas.width = selRect.w * dpr;
64
+ canvas.height = selRect.h * dpr;
65
+ const ctx = canvas.getContext('2d');
66
+ ctx.scale(dpr, dpr);
67
+ ctx.fillStyle = '#1e2127';
68
+ ctx.fillRect(0, 0, selRect.w, selRect.h);
69
+ ctx.strokeStyle = '#3e4451';
70
+ ctx.lineWidth = 1;
71
+ ctx.strokeRect(4, 4, selRect.w - 8, selRect.h - 8);
72
+ ctx.fillStyle = '#6e7681';
73
+ ctx.font = '12px Inter, sans-serif';
74
+ ctx.textAlign = 'center';
75
+ ctx.textBaseline = 'middle';
76
+ ctx.fillText(`${Math.round(selRect.w)} × ${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
77
+ return canvas.toDataURL('image/png');
78
+ }
79
+
51
80
  try {
52
81
  const iframe = iframeRef.current;
53
- if (!iframe) { setStart(null); setEnd(null); return; }
82
+ if (!iframe) { finishCapture(drawPlaceholder()); return; }
54
83
 
55
84
  const canvas = document.createElement('canvas');
56
85
  const dpr = window.devicePixelRatio || 1;
@@ -59,53 +88,25 @@ export function ScreenshotOverlay({ iframeRef }) {
59
88
  const ctx = canvas.getContext('2d');
60
89
  ctx.scale(dpr, dpr);
61
90
 
62
- // Same-origin proxy iframe — serialize DOM to SVG foreignObject for capture
63
- const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
64
- if (iframeDoc) {
65
- const serializer = new XMLSerializer();
66
- const html = serializer.serializeToString(iframeDoc.documentElement);
67
- const iframeRect = iframe.getBoundingClientRect();
68
- const overlayRect = overlayRef.current.getBoundingClientRect();
69
- const offsetX = selRect.x - (iframeRect.left - overlayRect.left);
70
- const offsetY = selRect.y - (iframeRect.top - overlayRect.top);
71
-
72
- const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${selRect.w}" height="${selRect.h}">
73
- <foreignObject x="${-offsetX}" y="${-offsetY}" width="${iframeRect.width}" height="${iframeRect.height}">
74
- ${html}
75
- </foreignObject>
76
- </svg>`;
77
- const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
78
- const url = URL.createObjectURL(blob);
79
- const img = new Image();
80
- img.onload = () => {
81
- ctx.drawImage(img, 0, 0, selRect.w, selRect.h);
82
- URL.revokeObjectURL(url);
83
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
84
- };
85
- img.onerror = () => {
86
- URL.revokeObjectURL(url);
87
- // Fallback: region placeholder
88
- ctx.fillStyle = '#1e2127';
89
- ctx.fillRect(0, 0, selRect.w, selRect.h);
90
- ctx.fillStyle = '#6e7681';
91
- ctx.font = '12px Inter, sans-serif';
92
- ctx.textAlign = 'center';
93
- ctx.fillText(`Region ${Math.round(selRect.w)}×${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
94
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
95
- };
96
- img.src = url;
97
- } else {
98
- ctx.fillStyle = '#1e2127';
99
- ctx.fillRect(0, 0, selRect.w, selRect.h);
100
- ctx.fillStyle = '#6e7681';
101
- ctx.font = '12px Inter, sans-serif';
102
- ctx.textAlign = 'center';
103
- ctx.fillText(`Region ${Math.round(selRect.w)}×${Math.round(selRect.h)}`, selRect.w / 2, selRect.h / 2);
104
- setCaptured({ base64: canvas.toDataURL('image/png'), rect: selRect });
91
+ const iframeRect = iframe.getBoundingClientRect();
92
+ const overlayRect = overlayRef.current.getBoundingClientRect();
93
+ const offsetX = selRect.x - (iframeRect.left - overlayRect.left);
94
+ const offsetY = selRect.y - (iframeRect.top - overlayRect.top);
95
+
96
+ try {
97
+ ctx.drawImage(iframe, -offsetX * dpr, -offsetY * dpr, iframeRect.width * dpr, iframeRect.height * dpr, 0, 0, selRect.w, selRect.h);
98
+ const testPixel = ctx.getImageData(0, 0, 1, 1).data;
99
+ if (testPixel[0] === 0 && testPixel[1] === 0 && testPixel[2] === 0 && testPixel[3] === 0) {
100
+ throw new Error('blank');
101
+ }
102
+ finishCapture(canvas.toDataURL('image/png'));
103
+ } catch {
104
+ finishCapture(drawPlaceholder());
105
105
  }
106
106
  } catch {
107
107
  setStart(null);
108
108
  setEnd(null);
109
+ setCaptured(null);
109
110
  }
110
111
  }, [dragging, start, end, iframeRef]);
111
112
 
@@ -118,7 +119,7 @@ export function ScreenshotOverlay({ iframeRef }) {
118
119
  }, [toggleScreenshotMode]);
119
120
 
120
121
  function handleSubmit() {
121
- if (!captured) return;
122
+ if (!captured || captured.loading) return;
122
123
  iteratePreview(comment || 'See screenshot', captured.base64);
123
124
  toggleScreenshotMode();
124
125
  }
@@ -130,6 +131,19 @@ export function ScreenshotOverlay({ iframeRef }) {
130
131
  height: Math.abs(end.y - start.y),
131
132
  } : null;
132
133
 
134
+ const popoverPosition = useCallback((rect) => {
135
+ if (!rect || !overlayRef.current) return {};
136
+ const overlayH = overlayRef.current.clientHeight;
137
+ const overlayW = overlayRef.current.clientWidth;
138
+ const spaceBelow = overlayH - (rect.y + rect.h + 8);
139
+ const popoverH = 200;
140
+ const placeAbove = spaceBelow < popoverH && rect.y > popoverH;
141
+ return {
142
+ left: Math.max(8, Math.min(rect.x, overlayW - 300)),
143
+ top: placeAbove ? rect.y - popoverH - 8 : rect.y + rect.h + 8,
144
+ };
145
+ }, []);
146
+
133
147
  return (
134
148
  <div
135
149
  ref={overlayRef}
@@ -150,21 +164,40 @@ export function ScreenshotOverlay({ iframeRef }) {
150
164
  />
151
165
  )}
152
166
 
167
+ {/* Capture flash */}
168
+ {flashRect && (
169
+ <div
170
+ className="absolute pointer-events-none animate-capture-flash rounded"
171
+ style={{ left: flashRect.x, top: flashRect.y, width: flashRect.w, height: flashRect.h }}
172
+ />
173
+ )}
174
+
175
+ {/* Captured selection outline */}
176
+ {captured && (
177
+ <div
178
+ className="absolute border-2 border-accent rounded pointer-events-none"
179
+ style={{ left: captured.rect.x, top: captured.rect.y, width: captured.rect.w, height: captured.rect.h }}
180
+ />
181
+ )}
182
+
153
183
  {/* Capture popover */}
154
184
  {captured && (
155
185
  <div
156
186
  className="absolute z-40 w-72 bg-surface-2 border border-border rounded-lg shadow-2xl animate-chat-fade-in"
157
- style={{
158
- left: Math.min(captured.rect.x, overlayRef.current?.clientWidth - 300 || 0),
159
- top: captured.rect.y + captured.rect.h + 8,
160
- }}
187
+ style={popoverPosition(captured.rect)}
161
188
  >
162
189
  <div className="p-3 border-b border-border-subtle">
163
- <img
164
- src={captured.base64}
165
- alt="Screenshot"
166
- className="w-full h-auto rounded border border-border-subtle max-h-32 object-contain bg-surface-0"
167
- />
190
+ {captured.loading ? (
191
+ <div className="w-full h-24 rounded border border-border-subtle bg-surface-0 flex items-center justify-center">
192
+ <Loader2 size={20} className="text-accent animate-spin" />
193
+ </div>
194
+ ) : (
195
+ <img
196
+ src={captured.base64}
197
+ alt="Screenshot"
198
+ className="w-full h-auto rounded border border-border-subtle max-h-32 object-contain bg-surface-0"
199
+ />
200
+ )}
168
201
  </div>
169
202
  <div className="p-3 flex items-center gap-2">
170
203
  <input
@@ -178,7 +211,8 @@ export function ScreenshotOverlay({ iframeRef }) {
178
211
  />
179
212
  <button
180
213
  onClick={handleSubmit}
181
- className="w-8 h-8 flex items-center justify-center rounded-md bg-accent/15 text-accent hover:bg-accent/25 transition-colors cursor-pointer"
214
+ disabled={captured.loading}
215
+ className="w-8 h-8 flex items-center justify-center rounded-md bg-accent/15 text-accent hover:bg-accent/25 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
182
216
  >
183
217
  <Send size={14} />
184
218
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.75",
3
+ "version": "0.27.77",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.75",
3
+ "version": "0.27.77",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.75",
3
+ "version": "0.27.77",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -168,6 +168,20 @@ export class PreviewService {
168
168
  if (!command) {
169
169
  return Promise.resolve({ launched: false, reason: 'no_command' });
170
170
  }
171
+ // If command references an npm script, verify it exists in package.json
172
+ const npmRunMatch = command.match(/npm\s+run\s+(\S+)/);
173
+ if (npmRunMatch) {
174
+ const scriptName = npmRunMatch[1];
175
+ const pkgPath = resolve(baseDir, 'package.json');
176
+ try {
177
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
178
+ if (!pkg.scripts || !pkg.scripts[scriptName]) {
179
+ return Promise.resolve({ launched: false, reason: 'no_dev_script' });
180
+ }
181
+ } catch {
182
+ return Promise.resolve({ launched: false, reason: 'no_dev_script' });
183
+ }
184
+ }
171
185
  const urlPattern = preview.urlPattern
172
186
  ? new RegExp(preview.urlPattern)
173
187
  : /https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0):\d+/;
@@ -114,6 +114,9 @@ CRITICAL — NEVER DO THESE:
114
114
  - NEVER kill the daemon process. No "kill <pid>", "pkill groove", "killall node", etc.
115
115
  - NEVER run "./promote.sh", "./promote-local.sh", or any publish/deploy script.
116
116
  - NEVER start long-running dev servers (vite dev, npm start, next dev, etc.).
117
+ - NEVER use 'git add -f' or 'git add --force' to bypass .gitignore. If a file is gitignored, it should stay gitignored. Only stage files that git tracks normally. If .gitignore prevents staging, report it in your output — do NOT force-add.
118
+ - NEVER use 'git push --force' or 'git push -f'. Force-pushing can destroy shared history.
119
+ - NEVER modify .gitignore to include files that were previously excluded.
117
120
 
118
121
  Restarting the daemon destroys ALL other agents currently running in other teams. Verification is done via "npm run build" and "npm test", which exit cleanly. If code changes require a daemon restart to take effect, report that in your output so the user can restart manually — do NOT do it yourself.
119
122
 
@@ -1161,7 +1164,7 @@ For normal file edits within your scope, proceed without review.
1161
1164
  const workingDir = plan.workingDir;
1162
1165
  preview.launch(teamId, workingDir, plan.preview).then((result) => {
1163
1166
  if (!result.launched) {
1164
- const intentionalSkips = new Set(['no_preview', 'cli', 'none']);
1167
+ const intentionalSkips = new Set(['no_preview', 'cli', 'none', 'no_command', 'no_dev_script']);
1165
1168
  if (intentionalSkips.has(result.reason)) {
1166
1169
  console.log(`[Groove] Preview for team ${teamId} intentionally skipped: ${result.reason}`);
1167
1170
  return;