umanggarg Claude Sonnet 4.6 commited on
Commit
402be81
Β·
1 Parent(s): a719c9c

Fix duplicate graph, agent error handling, and UI clarity

Browse files

Bugs fixed:
- CodeGraph: cancelled flag prevents StrictMode double-render (duplicate nodes)
- Agent stream: try/except wraps generator, yields event:error with human-readable
message instead of silently closing connection (was showing 'Error: connection lost')
- api.js: handles event:error from agent stream, shows real error in chat bubble

UI clarity:
- Empty state when no repos: 3-step onboarding (Index β†’ Ask β†’ Agent/Graph)
- Empty state when repos exist: suggestion buttons pre-fill the textarea
- Agent mode hint shown below suggestions when agent mode is on
- Input bar shows mode badge '✦ Agent β€” searches multiple times, shows reasoning'
- Ask button says 'Run Agent' in agent mode
- Graph hint link from suggest state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

backend/main.py CHANGED
@@ -457,25 +457,36 @@ async def agent_stream(
457
  import json
458
 
459
  def event_stream():
460
- for event in agent_svc.stream(question, repo_filter=repo):
461
- etype = event["type"]
462
-
463
- if etype == "tool_call":
464
- payload = json.dumps({"tool": event["tool"], "input": event["input"]})
465
- yield f"event: tool_call\ndata: {payload}\n\n"
466
-
467
- elif etype == "tool_result":
468
- payload = json.dumps({"tool": event["tool"], "output": event["output"]})
469
- yield f"event: tool_result\ndata: {payload}\n\n"
470
-
471
- elif etype == "token":
472
- safe = event["text"].replace("\n", "\\n")
473
- yield f"data: {safe}\n\n"
474
-
475
- elif etype == "done":
476
- payload = json.dumps({"iterations": event["iterations"]})
477
- yield f"event: done\ndata: {payload}\n\n"
478
- yield "data: [DONE]\n\n"
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  return StreamingResponse(event_stream(), media_type="text/event-stream")
481
 
 
457
  import json
458
 
459
  def event_stream():
460
+ try:
461
+ for event in agent_svc.stream(question, repo_filter=repo):
462
+ etype = event["type"]
463
+
464
+ if etype == "tool_call":
465
+ payload = json.dumps({"tool": event["tool"], "input": event["input"]})
466
+ yield f"event: tool_call\ndata: {payload}\n\n"
467
+
468
+ elif etype == "tool_result":
469
+ payload = json.dumps({"tool": event["tool"], "output": event["output"]})
470
+ yield f"event: tool_result\ndata: {payload}\n\n"
471
+
472
+ elif etype == "token":
473
+ safe = event["text"].replace("\n", "\\n")
474
+ yield f"data: {safe}\n\n"
475
+
476
+ elif etype == "done":
477
+ payload = json.dumps({"iterations": event["iterations"]})
478
+ yield f"event: done\ndata: {payload}\n\n"
479
+ yield "data: [DONE]\n\n"
480
+ except Exception as e:
481
+ # Surface the real error to the frontend instead of silently closing
482
+ err_msg = str(e)
483
+ if "credit" in err_msg.lower() or "billing" in err_msg.lower():
484
+ err_msg = "Anthropic API credits exhausted. Add credits at console.anthropic.com."
485
+ elif "api_key" in err_msg.lower():
486
+ err_msg = "ANTHROPIC_API_KEY not configured on this server."
487
+ payload = json.dumps({"message": err_msg})
488
+ yield f"event: error\ndata: {payload}\n\n"
489
+ yield "data: [DONE]\n\n"
490
 
491
  return StreamingResponse(event_stream(), media_type="text/event-stream")
492
 
ui/src/App.jsx CHANGED
@@ -239,13 +239,71 @@ export default function App() {
239
  <>
240
  {messages.length === 0 ? (
241
  <div className="empty-state">
242
- <div className="icon">πŸ’¬</div>
243
- <h2>Ask about a codebase</h2>
244
- <p>
245
- {repos.length === 0
246
- ? "Index a GitHub repo using the sidebar, then ask questions about it."
247
- : "Select a repo from the sidebar or ask across all indexed repos."}
248
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  </div>
250
  ) : (
251
  <div className="messages" ref={scrollRef}>
@@ -256,22 +314,29 @@ export default function App() {
256
 
257
  {/* Input */}
258
  <div className="input-bar">
259
- <textarea
260
- ref={textareaRef}
261
- rows={1}
262
- placeholder={placeholder}
263
- value={input}
264
- onChange={(e) => setInput(e.target.value)}
265
- onKeyDown={handleKeyDown}
266
- disabled={streaming}
267
- />
268
- <button
269
- className="btn"
270
- onClick={handleSubmit}
271
- disabled={!input.trim() || streaming}
272
- >
273
- {streaming ? <span className="spinner" /> : "Ask"}
274
- </button>
 
 
 
 
 
 
 
275
  </div>
276
  </>
277
  )}
 
239
  <>
240
  {messages.length === 0 ? (
241
  <div className="empty-state">
242
+ {repos.length === 0 ? (
243
+ // Step 1: no repos yet
244
+ <>
245
+ <div className="onboarding-steps">
246
+ <div className="onboarding-step active">
247
+ <span className="step-num">1</span>
248
+ <div>
249
+ <strong>Paste a GitHub URL in the sidebar</strong>
250
+ <p>e.g. <code>github.com/karpathy/micrograd</code></p>
251
+ <p>The app downloads and indexes every function and class.</p>
252
+ </div>
253
+ </div>
254
+ <div className="onboarding-step">
255
+ <span className="step-num">2</span>
256
+ <div>
257
+ <strong>Ask a question</strong>
258
+ <p>e.g. <em>"How does backward() work?"</em></p>
259
+ <p>The app finds the relevant code and an AI explains it with citations.</p>
260
+ </div>
261
+ </div>
262
+ <div className="onboarding-step">
263
+ <span className="step-num">3</span>
264
+ <div>
265
+ <strong>Try Agent mode or the Graph view</strong>
266
+ <p><strong>Agent</strong> β€” the AI searches multiple times, shows its reasoning step by step.</p>
267
+ <p><strong>Graph</strong> β€” a visual map of which functions call which.</p>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </>
272
+ ) : (
273
+ // Step 2: repos indexed, suggest questions
274
+ <div className="suggest-state">
275
+ <h2>What do you want to know?</h2>
276
+ <p>Try one of these or ask your own:</p>
277
+ <div className="suggestions">
278
+ {[
279
+ "Give me a high-level overview of this repo",
280
+ "How does the main class work?",
281
+ "What does the training loop do?",
282
+ "How is backpropagation implemented?",
283
+ "What are the entry points to this codebase?",
284
+ ].map(q => (
285
+ <button
286
+ key={q}
287
+ className="suggestion-btn"
288
+ onClick={() => { setInput(q); textareaRef.current?.focus(); }}
289
+ >
290
+ {q}
291
+ </button>
292
+ ))}
293
+ </div>
294
+ {agentMode && (
295
+ <div className="mode-hint">
296
+ <strong>Agent mode on</strong> β€” the AI will search the codebase multiple times
297
+ and show you each step before answering. Slower but more thorough.
298
+ </div>
299
+ )}
300
+ {activeRepo && (
301
+ <button className="graph-hint-btn" onClick={() => setView("graph")}>
302
+ Or view the call graph for {activeRepo} β†’
303
+ </button>
304
+ )}
305
+ </div>
306
+ )}
307
  </div>
308
  ) : (
309
  <div className="messages" ref={scrollRef}>
 
314
 
315
  {/* Input */}
316
  <div className="input-bar">
317
+ {agentMode && (
318
+ <div className="input-mode-badge">
319
+ ✦ Agent β€” searches multiple times, shows reasoning
320
+ </div>
321
+ )}
322
+ <div className="input-row">
323
+ <textarea
324
+ ref={textareaRef}
325
+ rows={1}
326
+ placeholder={agentMode ? "Ask a complex question β€” the agent will reason step by step…" : placeholder}
327
+ value={input}
328
+ onChange={(e) => setInput(e.target.value)}
329
+ onKeyDown={handleKeyDown}
330
+ disabled={streaming}
331
+ />
332
+ <button
333
+ className="btn"
334
+ onClick={handleSubmit}
335
+ disabled={!input.trim() || streaming}
336
+ >
337
+ {streaming ? <span className="spinner" /> : agentMode ? "Run Agent" : "Ask"}
338
+ </button>
339
+ </div>
340
  </div>
341
  </>
342
  )}
ui/src/api.js CHANGED
@@ -123,6 +123,16 @@ export function streamAgentQuery({ question, repo, onToolCall, onToolResult, onT
123
  onDone?.(iterations);
124
  });
125
 
 
 
 
 
 
 
 
 
 
 
126
  // Default events: token text (or [DONE] sentinel)
127
  es.onmessage = (e) => {
128
  if (e.data === "[DONE]") {
 
123
  onDone?.(iterations);
124
  });
125
 
126
+ // Named event: server-side error (API credits, config issues, etc.)
127
+ es.addEventListener("error", (e) => {
128
+ try {
129
+ const { message } = JSON.parse(e.data);
130
+ onError?.(message);
131
+ } catch {
132
+ onError?.("Agent error β€” check server logs.");
133
+ }
134
+ });
135
+
136
  // Default events: token text (or [DONE] sentinel)
137
  es.onmessage = (e) => {
138
  if (e.data === "[DONE]") {
ui/src/components/CodeGraph.jsx CHANGED
@@ -89,32 +89,40 @@ export default function CodeGraph({ repo, onAskAbout }) {
89
  useEffect(() => {
90
  if (!repo || !svgRef.current) return;
91
 
92
- // ── Fetch graph data ────────────────────────────────────────────────────
 
 
 
 
 
93
  setLoading(true);
94
  setError(null);
 
95
 
96
  import("../api").then(({ fetchGraph }) =>
97
  fetchGraph(repo)
98
  .then(data => {
 
99
  setStats(data.stats);
100
  setLoading(false);
101
  if (data.nodes.length === 0) {
102
- setError("No function/class nodes found. Re-ingest the repo to extract call data.");
103
  return;
104
  }
105
  _renderGraph(svgRef.current, data, setTooltip, onAskAbout);
106
  })
107
  .catch(err => {
 
108
  setLoading(false);
109
  setError(err.message);
110
  })
111
  );
112
 
113
- // Cleanup: stop any running simulation when repo changes or component unmounts
114
  return () => {
115
- if (svgRef.current) d3.select(svgRef.current).selectAll("*").remove();
 
116
  };
117
- }, [repo]); // re-render whenever active repo changes
118
 
119
  return (
120
  <div className="graph-container">
 
89
  useEffect(() => {
90
  if (!repo || !svgRef.current) return;
91
 
92
+ // cancelled flag prevents StrictMode's double-invoke from rendering twice.
93
+ // React StrictMode in dev runs effects twice (mount β†’ cleanup β†’ mount) to
94
+ // detect side effects. Without this flag, both fetches complete and both
95
+ // call _renderGraph, appending duplicate SVG elements.
96
+ let cancelled = false;
97
+
98
  setLoading(true);
99
  setError(null);
100
+ d3.select(svgRef.current).selectAll("*").remove();
101
 
102
  import("../api").then(({ fetchGraph }) =>
103
  fetchGraph(repo)
104
  .then(data => {
105
+ if (cancelled) return;
106
  setStats(data.stats);
107
  setLoading(false);
108
  if (data.nodes.length === 0) {
109
+ setError("No function/class nodes found. Re-ingest the repo to get call data.");
110
  return;
111
  }
112
  _renderGraph(svgRef.current, data, setTooltip, onAskAbout);
113
  })
114
  .catch(err => {
115
+ if (cancelled) return;
116
  setLoading(false);
117
  setError(err.message);
118
  })
119
  );
120
 
 
121
  return () => {
122
+ cancelled = true;
123
+ d3.select(svgRef.current).selectAll("*").remove();
124
  };
125
+ }, [repo]);
126
 
127
  return (
128
  <div className="graph-container">
ui/src/index.css CHANGED
@@ -349,9 +349,14 @@ body {
349
 
350
  /* ── Input bar ───────────────────────────────────────────────── */
351
  .input-bar {
352
- padding: 12px 20px;
353
  border-top: 1px solid var(--border);
354
  background: var(--surface);
 
 
 
 
 
355
  display: flex;
356
  gap: 10px;
357
  align-items: flex-end;
@@ -384,15 +389,111 @@ body {
384
  flex-direction: column;
385
  align-items: center;
386
  justify-content: center;
387
- gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  color: var(--muted);
389
- text-align: center;
390
- padding: 40px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
 
393
- .empty-state .icon { font-size: 40px; }
394
- .empty-state h2 { font-size: 18px; color: var(--text); }
395
- .empty-state p { font-size: 14px; max-width: 320px; line-height: 1.6; }
 
 
396
 
397
  /* ── Status bar ──────────────────────────────────────────────── */
398
  .status-bar {
 
349
 
350
  /* ── Input bar ───────────────────────────────────────────────── */
351
  .input-bar {
352
+ padding: 10px 20px 12px;
353
  border-top: 1px solid var(--border);
354
  background: var(--surface);
355
+ display: flex;
356
+ flex-direction: column;
357
+ }
358
+
359
+ .input-row {
360
  display: flex;
361
  gap: 10px;
362
  align-items: flex-end;
 
389
  flex-direction: column;
390
  align-items: center;
391
  justify-content: center;
392
+ padding: 40px 20px;
393
+ overflow-y: auto;
394
+ }
395
+
396
+ /* Onboarding: 3-step guide shown when no repos indexed yet */
397
+ .onboarding-steps {
398
+ display: flex;
399
+ flex-direction: column;
400
+ gap: 16px;
401
+ max-width: 480px;
402
+ width: 100%;
403
+ }
404
+
405
+ .onboarding-step {
406
+ display: flex;
407
+ gap: 14px;
408
+ align-items: flex-start;
409
+ background: var(--surface);
410
+ border: 1px solid var(--border);
411
+ border-radius: 10px;
412
+ padding: 14px 16px;
413
+ opacity: 0.5;
414
+ }
415
+ .onboarding-step.active { opacity: 1; border-color: var(--accent); }
416
+
417
+ .step-num {
418
+ background: var(--accent);
419
+ color: #fff;
420
+ border-radius: 50%;
421
+ width: 22px; height: 22px;
422
+ display: flex; align-items: center; justify-content: center;
423
+ font-size: 11px; font-weight: 700;
424
+ flex-shrink: 0;
425
+ margin-top: 1px;
426
+ }
427
+
428
+ .onboarding-step strong { font-size: 14px; color: var(--text); display: block; margin-bottom: 4px; }
429
+ .onboarding-step p { font-size: 12px; color: var(--muted); margin: 2px 0; line-height: 1.5; }
430
+ .onboarding-step code { font-family: "JetBrains Mono", monospace; color: var(--accent); font-size: 11px; }
431
+
432
+ /* Suggest state: shown when repos exist but no messages yet */
433
+ .suggest-state { max-width: 520px; width: 100%; text-align: center; }
434
+ .suggest-state h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
435
+ .suggest-state > p { font-size: 13px; color: var(--muted); margin-bottom: 14px; }
436
+
437
+ .suggestions {
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 7px;
441
+ margin-bottom: 16px;
442
+ }
443
+
444
+ .suggestion-btn {
445
+ background: var(--surface);
446
+ border: 1px solid var(--border);
447
+ border-radius: 8px;
448
+ color: var(--text);
449
+ cursor: pointer;
450
+ font-family: inherit;
451
+ font-size: 13px;
452
+ padding: 9px 14px;
453
+ text-align: left;
454
+ transition: border-color 0.15s, background 0.15s;
455
+ }
456
+ .suggestion-btn:hover { border-color: var(--accent); background: var(--accent-dim); }
457
+
458
+ .mode-hint {
459
+ background: var(--accent-dim);
460
+ border: 1px solid var(--accent);
461
+ border-radius: 8px;
462
+ color: var(--accent);
463
+ font-size: 12px;
464
+ line-height: 1.5;
465
+ margin-bottom: 12px;
466
+ padding: 9px 14px;
467
+ text-align: left;
468
+ }
469
+
470
+ .graph-hint-btn {
471
+ background: none;
472
+ border: none;
473
  color: var(--muted);
474
+ cursor: pointer;
475
+ font-family: inherit;
476
+ font-size: 12px;
477
+ padding: 4px;
478
+ text-decoration: underline;
479
+ text-decoration-style: dotted;
480
+ }
481
+ .graph-hint-btn:hover { color: var(--text); }
482
+
483
+ /* ── Input bar ── mode badge + row layout ────────────────────── */
484
+ .input-mode-badge {
485
+ font-size: 11px;
486
+ font-weight: 600;
487
+ color: var(--accent);
488
+ padding: 4px 0 6px;
489
+ letter-spacing: 0.03em;
490
  }
491
 
492
+ .input-row {
493
+ display: flex;
494
+ gap: 10px;
495
+ align-items: flex-end;
496
+ }
497
 
498
  /* ── Status bar ──────────────────────────────────────────────── */
499
  .status-bar {