E5K7 commited on
Commit
0482043
·
1 Parent(s): 342230a

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 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={() => handleFileClick(file.path)}
 
 
 
 
 
 
 
 
 
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 }}>{isAnalyzing ? '🧠' : '📄'}</span>
 
 
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
  })}