Spaces:
Running
Running
fix: send eof event to frontend, inject new files into explorer
Browse files- Fix SSE generator: yield eof event before breaking so frontend receives it
- Fix: result panel now correctly transitions to FIX READY after analysis
- Add generated files to explorer tree with NEW/MOD labels
- New files (✨ NEW) and modified files (✏️ MOD) clickable in sidebar
- Click generated files to view content without GitHub fetch
- backend/api.py +2 -2
- frontend/src/app/[owner]/[repo]/page.tsx +32 -3
backend/api.py
CHANGED
|
@@ -109,16 +109,15 @@ async def analyze_endpoint(issue_url: str, repo_url: str, run_confidence: bool =
|
|
| 109 |
async def event_generator():
|
| 110 |
while True:
|
| 111 |
try:
|
| 112 |
-
# Wait up to 15s before sending a heartbeat ping to keep connection alive
|
| 113 |
msg = await asyncio.wait_for(queue.get(), timeout=15.0)
|
| 114 |
if msg["type"] == "eof":
|
|
|
|
| 115 |
break
|
| 116 |
yield {
|
| 117 |
"event": msg["type"],
|
| 118 |
"data": json.dumps(msg["data"])
|
| 119 |
}
|
| 120 |
except asyncio.TimeoutError:
|
| 121 |
-
# Send a heartbeat comment to prevent browser SSE timeout
|
| 122 |
yield {"event": "heartbeat", "data": json.dumps({"alive": True})}
|
| 123 |
|
| 124 |
return EventSourceResponse(event_generator())
|
|
@@ -264,6 +263,7 @@ async def refine_endpoint(session_id: str, feedback: str):
|
|
| 264 |
try:
|
| 265 |
msg = await asyncio.wait_for(queue.get(), timeout=15.0)
|
| 266 |
if msg["type"] == "eof":
|
|
|
|
| 267 |
break
|
| 268 |
yield {
|
| 269 |
"event": msg["type"],
|
|
|
|
| 109 |
async def event_generator():
|
| 110 |
while True:
|
| 111 |
try:
|
|
|
|
| 112 |
msg = await asyncio.wait_for(queue.get(), timeout=15.0)
|
| 113 |
if msg["type"] == "eof":
|
| 114 |
+
yield {"event": "eof", "data": json.dumps({"done": True})}
|
| 115 |
break
|
| 116 |
yield {
|
| 117 |
"event": msg["type"],
|
| 118 |
"data": json.dumps(msg["data"])
|
| 119 |
}
|
| 120 |
except asyncio.TimeoutError:
|
|
|
|
| 121 |
yield {"event": "heartbeat", "data": json.dumps({"alive": True})}
|
| 122 |
|
| 123 |
return EventSourceResponse(event_generator())
|
|
|
|
| 263 |
try:
|
| 264 |
msg = await asyncio.wait_for(queue.get(), timeout=15.0)
|
| 265 |
if msg["type"] == "eof":
|
| 266 |
+
yield {"event": "eof", "data": json.dumps({"done": True})}
|
| 267 |
break
|
| 268 |
yield {
|
| 269 |
"event": msg["type"],
|
frontend/src/app/[owner]/[repo]/page.tsx
CHANGED
|
@@ -47,6 +47,8 @@ export default function RepoDashboard() {
|
|
| 47 |
const [fixedFileView, setFixedFileView] = useState<{path: string, content: string} | null>(null);
|
| 48 |
const [sessionId, setSessionId] = useState("");
|
| 49 |
const [feedback, setFeedback] = useState("");
|
|
|
|
|
|
|
| 50 |
|
| 51 |
const logsEndRef = useRef<HTMLDivElement>(null);
|
| 52 |
|
|
@@ -163,13 +165,25 @@ export default function RepoDashboard() {
|
|
| 163 |
const data = JSON.parse((e as MessageEvent).data);
|
| 164 |
const r = data.result;
|
| 165 |
setResult(r);
|
|
|
|
| 166 |
if (data.session_id) setSessionId(data.session_id);
|
| 167 |
if (r?.fixed_files) {
|
| 168 |
const paths = Object.keys(r.fixed_files);
|
|
|
|
| 169 |
if (paths.length > 0) {
|
| 170 |
setFixedFileView({ path: paths[0], content: r.fixed_files[paths[0]] });
|
| 171 |
setSelectedFilePath(paths[0]);
|
| 172 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
});
|
| 175 |
eventSource.addEventListener("error", (e: Event) => {
|
|
@@ -252,10 +266,21 @@ export default function RepoDashboard() {
|
|
| 252 |
<div style={{ flex: 1, overflowY: 'auto', fontSize: '0.78rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
|
| 253 |
{repoInfo.tree.map((file, i) => {
|
| 254 |
const isAnalyzing = analyzingFiles.includes(file.path);
|
|
|
|
|
|
|
| 255 |
return (
|
| 256 |
<div
|
| 257 |
key={i}
|
| 258 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
className={isAnalyzing ? 'analyzing-glow' : ''}
|
| 260 |
style={{
|
| 261 |
padding: '5px 8px',
|
|
@@ -263,15 +288,19 @@ export default function RepoDashboard() {
|
|
| 263 |
borderRadius: '4px',
|
| 264 |
marginBottom: '1px',
|
| 265 |
backgroundColor: selectedFilePath === file.path ? 'rgba(139, 92, 246, 0.15)' : 'transparent',
|
| 266 |
-
color: selectedFilePath === file.path ? 'var(--primary)' : 'inherit',
|
| 267 |
whiteSpace: 'nowrap',
|
| 268 |
overflow: 'hidden',
|
| 269 |
textOverflow: 'ellipsis',
|
| 270 |
transition: 'all 0.2s ease'
|
| 271 |
}}
|
| 272 |
>
|
| 273 |
-
<span style={{ marginRight: 6 }}>
|
|
|
|
|
|
|
| 274 |
{file.path}
|
|
|
|
|
|
|
| 275 |
</div>
|
| 276 |
);
|
| 277 |
})}
|
|
|
|
| 47 |
const [fixedFileView, setFixedFileView] = useState<{path: string, content: string} | null>(null);
|
| 48 |
const [sessionId, setSessionId] = useState("");
|
| 49 |
const [feedback, setFeedback] = useState("");
|
| 50 |
+
const [newFiles, setNewFiles] = useState<string[]>([]); // files generated by agent, not in original tree
|
| 51 |
+
const resultRef = useRef<any>(null); // avoid stale closure in eof handler
|
| 52 |
|
| 53 |
const logsEndRef = useRef<HTMLDivElement>(null);
|
| 54 |
|
|
|
|
| 165 |
const data = JSON.parse((e as MessageEvent).data);
|
| 166 |
const r = data.result;
|
| 167 |
setResult(r);
|
| 168 |
+
resultRef.current = r;
|
| 169 |
if (data.session_id) setSessionId(data.session_id);
|
| 170 |
if (r?.fixed_files) {
|
| 171 |
const paths = Object.keys(r.fixed_files);
|
| 172 |
+
// Show first fixed file in editor
|
| 173 |
if (paths.length > 0) {
|
| 174 |
setFixedFileView({ path: paths[0], content: r.fixed_files[paths[0]] });
|
| 175 |
setSelectedFilePath(paths[0]);
|
| 176 |
}
|
| 177 |
+
// Inject any new files into the explorer tree
|
| 178 |
+
setRepoInfo(prev => {
|
| 179 |
+
if (!prev) return prev;
|
| 180 |
+
const existingPaths = new Set(prev.tree.map(f => f.path));
|
| 181 |
+
const addedPaths = paths.filter(p => !existingPaths.has(p));
|
| 182 |
+
setNewFiles(addedPaths);
|
| 183 |
+
if (addedPaths.length === 0) return prev;
|
| 184 |
+
const newEntries = addedPaths.map(p => ({ path: p, size: 0, type: 'blob' }));
|
| 185 |
+
return { ...prev, tree: [...prev.tree, ...newEntries] };
|
| 186 |
+
});
|
| 187 |
}
|
| 188 |
});
|
| 189 |
eventSource.addEventListener("error", (e: Event) => {
|
|
|
|
| 266 |
<div style={{ flex: 1, overflowY: 'auto', fontSize: '0.78rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
|
| 267 |
{repoInfo.tree.map((file, i) => {
|
| 268 |
const isAnalyzing = analyzingFiles.includes(file.path);
|
| 269 |
+
const isNew = newFiles.includes(file.path);
|
| 270 |
+
const isFixed = result?.fixed_files && file.path in result.fixed_files && !isNew;
|
| 271 |
return (
|
| 272 |
<div
|
| 273 |
key={i}
|
| 274 |
+
onClick={() => {
|
| 275 |
+
if (isNew || isFixed) {
|
| 276 |
+
// Show the generated content directly (no need to fetch from GitHub)
|
| 277 |
+
setSelectedFilePath(file.path);
|
| 278 |
+
setFixedFileView({ path: file.path, content: result.fixed_files[file.path] });
|
| 279 |
+
} else {
|
| 280 |
+
handleFileClick(file.path);
|
| 281 |
+
setFixedFileView(null);
|
| 282 |
+
}
|
| 283 |
+
}}
|
| 284 |
className={isAnalyzing ? 'analyzing-glow' : ''}
|
| 285 |
style={{
|
| 286 |
padding: '5px 8px',
|
|
|
|
| 288 |
borderRadius: '4px',
|
| 289 |
marginBottom: '1px',
|
| 290 |
backgroundColor: selectedFilePath === file.path ? 'rgba(139, 92, 246, 0.15)' : 'transparent',
|
| 291 |
+
color: isNew ? '#a3be8c' : isFixed ? '#ebcb8b' : selectedFilePath === file.path ? 'var(--primary)' : 'inherit',
|
| 292 |
whiteSpace: 'nowrap',
|
| 293 |
overflow: 'hidden',
|
| 294 |
textOverflow: 'ellipsis',
|
| 295 |
transition: 'all 0.2s ease'
|
| 296 |
}}
|
| 297 |
>
|
| 298 |
+
<span style={{ marginRight: 6 }}>
|
| 299 |
+
{isAnalyzing ? '🧠' : isNew ? '✨' : isFixed ? '✏️' : '📄'}
|
| 300 |
+
</span>
|
| 301 |
{file.path}
|
| 302 |
+
{isNew && <span style={{ marginLeft: 6, fontSize: '0.65rem', color: '#a3be8c', opacity: 0.8 }}>NEW</span>}
|
| 303 |
+
{isFixed && <span style={{ marginLeft: 6, fontSize: '0.65rem', color: '#ebcb8b', opacity: 0.8 }}>MOD</span>}
|
| 304 |
</div>
|
| 305 |
);
|
| 306 |
})}
|