umanggarg Claude Sonnet 4.6 commited on
Commit
34ceb36
Β·
1 Parent(s): 4734880

design: premium UI polish + agent reliability fixes

Browse files

UI:
- Atmospheric radial glow behind diagram canvas (centered, works fullscreen)
- Directional gradient arrows in diagrams (bright at source, fades to target)
- Diagram now centers in viewport on initial render
- Legend readability bumped (12px, text-2 color, larger dots)
- Repo list: GitHub mark icon + owner/repo split (owner dimmed, repo bold)
- Landing headline 70px, logo 72px, subtitle 15px restored closer to original
- Source card hover color fixed (warm amber β†’ cool dark blue)
- Avatar: compass for RAG, ✦ for agent (keyed on msg.mode, not msg.iterations)
- Suggestion cards now generate proper natural language questions
- focusFiles clears on new chat / repo switch (no stale breadcrumb bar)
- Try-these chips now show GitHub mark icon
- onboarding steps use vertical suggestion-btn style

Backend:
- agent read_file sources now store actual file text (was always empty string)
- Retry once on transient MCP TaskGroup connection errors (300ms backoff)

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

backend/services/agent.py CHANGED
@@ -734,10 +734,16 @@ class AgentService:
734
 
735
  # Execute all new calls concurrently β€” MCP calls are async HTTP round trips
736
  async def _run_tool(tc: dict) -> str:
737
- try:
738
- return await self.mcp.call_tool(tc["name"], tc["input"])
739
- except Exception as e:
740
- return f"Tool error: {e}"
 
 
 
 
 
 
741
 
742
  parallel_results = await asyncio.gather(*[_run_tool(tc) for tc in new_calls])
743
 
@@ -768,7 +774,7 @@ class AgentService:
768
  "repo": repo, "filepath": filepath, "language": lang,
769
  "chunk_type": "file", "name": filepath.rsplit("/", 1)[-1],
770
  "start_line": 1, "end_line": result.count("\n"),
771
- "score": 1.0, "text": "",
772
  }
773
 
774
  display = result[:500] + "…" if len(result) > 500 else result
 
734
 
735
  # Execute all new calls concurrently β€” MCP calls are async HTTP round trips
736
  async def _run_tool(tc: dict) -> str:
737
+ # Retry once on transient MCP connection failures (TaskGroup /
738
+ # HTTP errors from the SDK's internal connection management).
739
+ for attempt in range(2):
740
+ try:
741
+ return await self.mcp.call_tool(tc["name"], tc["input"])
742
+ except Exception as e:
743
+ if attempt == 0 and "TaskGroup" in str(e):
744
+ await asyncio.sleep(0.3)
745
+ continue
746
+ return f"Tool error: {e}"
747
 
748
  parallel_results = await asyncio.gather(*[_run_tool(tc) for tc in new_calls])
749
 
 
774
  "repo": repo, "filepath": filepath, "language": lang,
775
  "chunk_type": "file", "name": filepath.rsplit("/", 1)[-1],
776
  "start_line": 1, "end_line": result.count("\n"),
777
+ "score": 1.0, "text": result,
778
  }
779
 
780
  display = result[:500] + "…" if len(result) > 500 else result
ui/src/App.jsx CHANGED
@@ -158,6 +158,7 @@ export default function App() {
158
  setCurrentSessionId(null);
159
  setMessages([]);
160
  setLastSources([]);
 
161
  setSessions(readSessions(activeRepo));
162
  // Auto-navigate to Explore view when a specific repo is selected.
163
  // Reset to chat for "All repos" or landing page β€” diagram needs a single repo.
@@ -203,6 +204,7 @@ export default function App() {
203
  sessionIdRef.current = null;
204
  setCurrentSessionId(null);
205
  setMessages([]);
 
206
  }
207
  }
208
 
@@ -618,6 +620,7 @@ export default function App() {
618
  sessionIdRef.current = null;
619
  setCurrentSessionId(null);
620
  setMessages([]);
 
621
  setStreaming(false);
622
  }
623
 
@@ -767,7 +770,7 @@ export default function App() {
767
  // No repo selected yet (landing), or All repos selected but nothing indexed
768
  <div className="onboarding-steps">
769
  <div className="onboarding-header">
770
- <svg width="60" height="60" viewBox="0 0 24 24" fill="none"
771
  style={{ marginBottom: 12, filter: "drop-shadow(0 0 8px rgba(91,143,249,0.70))" }}>
772
  {/* N β€” pulses bright */}
773
  <path className="compass-north" d="M12 2 L14.5 7 L12 12 L9.5 7 Z" fill="var(--accent)"/>
@@ -783,19 +786,20 @@ export default function App() {
783
  <div className="onboarding-headline">Map <em>any</em> codebase</div>
784
  <div className="onboarding-sub">Index any public repo and ask questions about the code β€” architecture, data flow, classes, functions, and more.</div>
785
  </div>
786
- <div className="onboarding-grid">
787
  {[
788
- { icon: "github", num: 1, title: "Paste a GitHub URL", body: "Index any public repo β€” every function and class." },
789
- { icon: "chat", num: 2, title: "Ask about the code", body: "\"How does the main loop work?\" β€” cited answers." },
790
- { icon: "explore", num: 3, title: "Explore the structure", body: "Concept map of key components and how they connect." },
791
- ].map(({ icon, num, title, body }, i) => (
792
- <div key={num} className="onboarding-step active"
793
- style={{ animationDelay: `${80 + i * 90}ms` }}>
794
- <span className="suggestion-icon" style={{ marginBottom: 4 }}>{ICONS[icon]}</span>
795
- <div>
796
- <strong>{title}</strong>
797
- <p>{body}</p>
798
- </div>
 
799
  </div>
800
  ))}
801
  </div>
@@ -813,13 +817,12 @@ export default function App() {
813
  </div>
814
  <div className="suggestions">
815
  {[
816
- { icon: "architecture", title: "Map the architecture", body: `Walk ${activeRepo.split("/")[1]} from entry point to output` },
817
- { icon: "functions", title: "Key functions", body: "Most important functions and how they connect" },
818
- { icon: "diagram", title: "Generate a diagram", body: "Visual map of the main components" },
819
- { icon: "shield", title: "Error handling", body: "How edge cases are managed across the codebase" },
820
- { icon: "flow", title: "Data flow", body: "How data moves from input to final result" },
821
- ].map(({ icon, title, body }, i) => {
822
- const q = `${title}: ${body}`;
823
  return (
824
  <button key={title} className="suggestion-btn"
825
  style={{ animationDelay: `${80 + i * 80}ms` }}
@@ -842,13 +845,12 @@ export default function App() {
842
  <>
843
  <div className="suggestions">
844
  {[
845
- { icon: "architecture", title: "Overall architecture", body: `How is ${activeRepo.split("/")[1]} structured?` },
846
- { icon: "entry", title: "Entry points", body: "Main entry points and how the code flows" },
847
- { icon: "classes", title: "Key classes", body: "What each major class does" },
848
- { icon: "flow", title: "Data processing", body: "How data is transformed through the system" },
849
- { icon: "package", title: "Dependencies", body: "External libraries and how they're used" },
850
- ].map(({ icon, title, body }, i) => {
851
- const q = `${title}: ${body}`;
852
  return (
853
  <button key={title} className="suggestion-btn"
854
  style={{ animationDelay: `${80 + i * 80}ms` }}
 
158
  setCurrentSessionId(null);
159
  setMessages([]);
160
  setLastSources([]);
161
+ setFocusFiles(null);
162
  setSessions(readSessions(activeRepo));
163
  // Auto-navigate to Explore view when a specific repo is selected.
164
  // Reset to chat for "All repos" or landing page β€” diagram needs a single repo.
 
204
  sessionIdRef.current = null;
205
  setCurrentSessionId(null);
206
  setMessages([]);
207
+ setFocusFiles(null);
208
  }
209
  }
210
 
 
620
  sessionIdRef.current = null;
621
  setCurrentSessionId(null);
622
  setMessages([]);
623
+ setFocusFiles(null);
624
  setStreaming(false);
625
  }
626
 
 
770
  // No repo selected yet (landing), or All repos selected but nothing indexed
771
  <div className="onboarding-steps">
772
  <div className="onboarding-header">
773
+ <svg width="72" height="72" viewBox="0 0 24 24" fill="none"
774
  style={{ marginBottom: 12, filter: "drop-shadow(0 0 8px rgba(91,143,249,0.70))" }}>
775
  {/* N β€” pulses bright */}
776
  <path className="compass-north" d="M12 2 L14.5 7 L12 12 L9.5 7 Z" fill="var(--accent)"/>
 
786
  <div className="onboarding-headline">Map <em>any</em> codebase</div>
787
  <div className="onboarding-sub">Index any public repo and ask questions about the code β€” architecture, data flow, classes, functions, and more.</div>
788
  </div>
789
+ <div className="suggestions" style={{ marginBottom: 0 }}>
790
  {[
791
+ { icon: "github", title: "Paste a GitHub URL", body: "Index any public repo β€” every function and class." },
792
+ { icon: "chat", title: "Ask about the code", body: "\"How does the main loop work?\" β€” finds and explains it." },
793
+ { icon: "explore", title: "Explore the structure", body: "Concept map of key components and how they connect." },
794
+ ].map(({ icon, title, body }, i) => (
795
+ <div key={title} className="suggestion-btn"
796
+ style={{ animationDelay: `${80 + i * 90}ms`, cursor: "default" }}>
797
+ <span className="suggestion-icon">{ICONS[icon]}</span>
798
+ <span className="suggestion-content">
799
+ <span className="suggestion-title">{title}</span>
800
+ <span className="suggestion-body">{body}</span>
801
+ </span>
802
+ <svg className="suggestion-arrow" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.2 }}><path d="M3 8h10M9 4l4 4-4 4"/></svg>
803
  </div>
804
  ))}
805
  </div>
 
817
  </div>
818
  <div className="suggestions">
819
  {[
820
+ { icon: "architecture", title: "Map the architecture", body: `Walk ${activeRepo.split("/")[1]} from entry point to output`, q: `How is ${activeRepo.split("/")[1]} structured? Trace the main execution path from the entry point all the way to the output.` },
821
+ { icon: "functions", title: "Key functions", body: "Most important functions and how they connect", q: `What are the most important functions in ${activeRepo.split("/")[1]} and how do they call each other?` },
822
+ { icon: "diagram", title: "Generate a diagram", body: "Visual map of the main components", q: `Generate a diagram of the main components in ${activeRepo.split("/")[1]} and how they relate to each other.` },
823
+ { icon: "shield", title: "Error handling", body: "How edge cases are managed across the codebase", q: `How does ${activeRepo.split("/")[1]} handle errors and edge cases?` },
824
+ { icon: "flow", title: "Data flow", body: "How data moves from input to final result", q: `How does data flow through ${activeRepo.split("/")[1]} from input to final result?` },
825
+ ].map(({ icon, title, body, q }, i) => {
 
826
  return (
827
  <button key={title} className="suggestion-btn"
828
  style={{ animationDelay: `${80 + i * 80}ms` }}
 
845
  <>
846
  <div className="suggestions">
847
  {[
848
+ { icon: "architecture", title: "Overall architecture", body: `How is ${activeRepo.split("/")[1]} structured?`, q: `How is ${activeRepo.split("/")[1]} structured overall? What are the main components and how do they fit together?` },
849
+ { icon: "entry", title: "Entry points", body: "Main entry points and how the code flows", q: `What are the main entry points of ${activeRepo.split("/")[1]} and how does execution flow through them?` },
850
+ { icon: "classes", title: "Key classes", body: "What each major class does", q: `What are the key classes in ${activeRepo.split("/")[1]} and what is each one responsible for?` },
851
+ { icon: "flow", title: "Data processing", body: "How data is transformed through the system", q: `How is data transformed and processed as it flows through ${activeRepo.split("/")[1]}?` },
852
+ { icon: "package", title: "Dependencies", body: "External libraries and how they're used", q: `What external libraries does ${activeRepo.split("/")[1]} depend on and how does it use them?` },
853
+ ].map(({ icon, title, body, q }, i) => {
 
854
  return (
855
  <button key={title} className="suggestion-btn"
856
  style={{ animationDelay: `${80 + i * 80}ms` }}
ui/src/components/GraphDiagram.jsx CHANGED
@@ -263,11 +263,22 @@ export default function GraphDiagram({ data, onNodeSelect, onEdgeSelect, onAskAb
263
 
264
  const wrapRef = useRef(null);
265
 
266
- // Reset view and any manual node positions when the diagram data changes
 
 
267
  useEffect(() => {
268
- setXform({ x: 0, y: 0, scale: 0.85 });
269
  setNodePos({});
270
- }, [data]);
 
 
 
 
 
 
 
 
 
 
271
 
272
  // Non-passive wheel zoom β€” passive: false required to call preventDefault()
273
  useEffect(() => {
@@ -439,6 +450,23 @@ export default function GraphDiagram({ data, onNodeSelect, onEdgeSelect, onAskAb
439
  <marker id="gd-arrow-hi" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto">
440
  <polygon points="0 0, 7 2.5, 0 5" fill="#7DABFF" />
441
  </marker>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  </defs>
443
 
444
  {layoutEdges.map((edge, i) => {
@@ -477,7 +505,7 @@ export default function GraphDiagram({ data, onNodeSelect, onEdgeSelect, onAskAb
477
  <path
478
  d={d}
479
  fill="none"
480
- stroke={isHi ? "#7DABFF" : "rgba(91,143,249,0.22)"}
481
  strokeWidth={isHi ? 2 : 1.5}
482
  strokeDasharray={edge.label === "uses" ? "5 3" : undefined}
483
  markerEnd={isHi ? "url(#gd-arrow-hi)" : "url(#gd-arrow)"}
 
263
 
264
  const wrapRef = useRef(null);
265
 
266
+ // Center the diagram in the viewport whenever layout changes.
267
+ // We read the wrapper's actual rendered dimensions (not CSS values) via
268
+ // getBoundingClientRect so the offset is always pixel-perfect.
269
  useEffect(() => {
 
270
  setNodePos({});
271
+ if (!wrapRef.current || !layoutNodes.length) {
272
+ setXform({ x: 0, y: 0, scale: 0.85 });
273
+ return;
274
+ }
275
+ const { width, height } = wrapRef.current.getBoundingClientRect();
276
+ const scale = 0.85;
277
+ // Offset so the scaled canvas sits in the centre of the wrapper
278
+ const cx = (width - canvasW * scale) / 2;
279
+ const cy = (height - canvasH * scale) / 2;
280
+ setXform({ x: cx, y: Math.max(cy, 24), scale });
281
+ }, [layoutNodes]); // layoutNodes recomputes whenever data changes
282
 
283
  // Non-passive wheel zoom β€” passive: false required to call preventDefault()
284
  useEffect(() => {
 
450
  <marker id="gd-arrow-hi" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto">
451
  <polygon points="0 0, 7 2.5, 0 5" fill="#7DABFF" />
452
  </marker>
453
+ {/* Per-edge directional gradients β€” bright at source, fades toward target.
454
+ gradientUnits="userSpaceOnUse" maps x1/y1/x2/y2 to canvas coordinates
455
+ so the gradient always aligns with the actual arrow path. */}
456
+ {layoutEdges.map((edge, i) => {
457
+ const from = getPosFor(edge.source);
458
+ const to = getPosFor(edge.target);
459
+ if (!from || !to) return null;
460
+ return (
461
+ <linearGradient key={`grad-${i}`} id={`edge-grad-${i}`}
462
+ gradientUnits="userSpaceOnUse"
463
+ x1={from.x + CARD_W} y1={from.y + CARD_H / 2}
464
+ x2={to.x} y2={to.y + CARD_H / 2}>
465
+ <stop offset="0%" stopColor="#7DABFF" stopOpacity="0.55" />
466
+ <stop offset="100%" stopColor="#5B8FF9" stopOpacity="0.08" />
467
+ </linearGradient>
468
+ );
469
+ })}
470
  </defs>
471
 
472
  {layoutEdges.map((edge, i) => {
 
505
  <path
506
  d={d}
507
  fill="none"
508
+ stroke={isHi ? "#7DABFF" : `url(#edge-grad-${i})`}
509
  strokeWidth={isHi ? 2 : 1.5}
510
  strokeDasharray={edge.label === "uses" ? "5 3" : undefined}
511
  markerEnd={isHi ? "url(#gd-arrow-hi)" : "url(#gd-arrow)"}
ui/src/components/Message.jsx CHANGED
@@ -11,6 +11,7 @@ import SourceCard from "./SourceCard";
11
  // Lazy-load MermaidBlock β€” deferred so mermaid.js doesn't bloat the initial bundle.
12
  const MermaidBlock = lazy(() => import("./MermaidBlock"));
13
 
 
14
  // ReactMarkdown renders fenced code blocks as <pre><code>...</code></pre>.
15
  // If we override only `code`, ReactMarkdown wraps the whole thing in a <p>,
16
  // giving <p><pre>...</pre></p> β€” invalid HTML (pre can't be inside p).
@@ -386,12 +387,16 @@ const Message = forwardRef(function Message({ msg, onDiagramThis, onRetry, showR
386
  This matches the ✦ badge on agent sessions in the sidebar,
387
  making the visual language consistent: ✦ = agent mode. */}
388
  <div className="message-avatar assistant" aria-hidden="true">
389
- {msg.iterations
390
- ? <span style={{ fontSize: 13, lineHeight: 1, color: "white", opacity: 0.9 }}>✦</span>
391
- : <svg width="14" height="14" viewBox="0 0 18 18" fill="none">
392
- <path d="M5.5 5L2 9l3.5 4" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" strokeOpacity="0.95"/>
393
- <path d="M12.5 5L16 9l-3.5 4" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" strokeOpacity="0.95"/>
394
- <circle cx="9" cy="9" r="1.2" fill="white" fillOpacity="0.7"/>
 
 
 
 
395
  </svg>
396
  }
397
  </div>
 
11
  // Lazy-load MermaidBlock β€” deferred so mermaid.js doesn't bloat the initial bundle.
12
  const MermaidBlock = lazy(() => import("./MermaidBlock"));
13
 
14
+
15
  // ReactMarkdown renders fenced code blocks as <pre><code>...</code></pre>.
16
  // If we override only `code`, ReactMarkdown wraps the whole thing in a <p>,
17
  // giving <p><pre>...</pre></p> β€” invalid HTML (pre can't be inside p).
 
387
  This matches the ✦ badge on agent sessions in the sidebar,
388
  making the visual language consistent: ✦ = agent mode. */}
389
  <div className="message-avatar assistant" aria-hidden="true">
390
+ {msg.mode === "agent"
391
+ ? /* Agent mode β€” ✦ sparkle signals autonomous reasoning / ReAct loop */
392
+ <span style={{ fontSize: 14, lineHeight: 1, color: "white", opacity: 0.9 }}>✦</span>
393
+ : /* RAG mode β€” compass brand mark, signals "Cartographer navigating the code" */
394
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
395
+ <path d="M12 2 L14.5 7 L12 12 L9.5 7 Z" fill="white" opacity="0.95"/>
396
+ <path d="M12 22 L13.5 17 L12 12 L10.5 17 Z" fill="white" opacity="0.45"/>
397
+ <path d="M22 12 L17 10.5 L12 12 L17 13.5 Z" fill="white" opacity="0.45"/>
398
+ <path d="M2 12 L7 10.5 L12 12 L7 13.5 Z" fill="white" opacity="0.45"/>
399
+ <circle cx="12" cy="12" r="1.5" fill="white"/>
400
  </svg>
401
  }
402
  </div>
ui/src/components/Sidebar.jsx CHANGED
@@ -334,7 +334,7 @@ export default function Sidebar({ repos, reposLoading, activeRepo, onSelectRepo,
334
  title={label}
335
  >
336
  <svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" style={{ opacity: 0.6, flexShrink: 0 }}>
337
- <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Z"/>
338
  </svg>
339
  {slug.split("/")[1]}
340
  </button>
@@ -461,7 +461,14 @@ export default function Sidebar({ repos, reposLoading, activeRepo, onSelectRepo,
461
  onClick={() => onSelectRepo(activeRepo === r.slug ? null : r.slug)}
462
  >
463
  <div className="repo-item-main">
464
- <span className="repo-slug">{r.slug}</span>
 
 
 
 
 
 
 
465
  <div className="repo-item-meta">
466
  {/* Staleness indicator β€” shown when index is > 3 days old */}
467
  {staleness && !justDone && (
 
334
  title={label}
335
  >
336
  <svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" style={{ opacity: 0.6, flexShrink: 0 }}>
337
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
338
  </svg>
339
  {slug.split("/")[1]}
340
  </button>
 
461
  onClick={() => onSelectRepo(activeRepo === r.slug ? null : r.slug)}
462
  >
463
  <div className="repo-item-main">
464
+ {/* GitHub mark β€” reinforces these are GitHub repos without taking space */}
465
+ <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor" style={{ opacity: 0.3, flexShrink: 0 }}>
466
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
467
+ </svg>
468
+ {/* Split slug: owner dimmed + repo name prominent β€” scanability trick from Linear */}
469
+ <span className="repo-slug">
470
+ <span className="repo-owner">{r.slug.split("/")[0]}/</span>{r.slug.split("/")[1]}
471
+ </span>
472
  <div className="repo-item-meta">
473
  {/* Staleness indicator β€” shown when index is > 3 days old */}
474
  {staleness && !justDone && (
ui/src/components/SourceCard.jsx CHANGED
@@ -131,16 +131,27 @@ export default function SourceCard({ source, index, showRepo = false }) {
131
 
132
  {open && (
133
  <div className="source-code">
134
- <SyntaxHighlighter
135
- language={lang}
136
- style={oneDark}
137
- customStyle={{ fontSize: 12, margin: 0, background: '#06060F', borderRadius: 0 }}
138
- lineNumberStyle={{ color: 'rgba(255,255,255,0.18)', fontSize: 11, minWidth: 36, paddingRight: 12 }}
139
- showLineNumbers
140
- startingLineNumber={source.start_line} /* start_line = class range when expanded */
141
- >
142
- {source.text}
143
- </SyntaxHighlighter>
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
  )}
146
  </div>
 
131
 
132
  {open && (
133
  <div className="source-code">
134
+ {source.text ? (
135
+ <SyntaxHighlighter
136
+ language={lang}
137
+ style={oneDark}
138
+ customStyle={{ fontSize: 12, margin: 0, background: '#06060F', borderRadius: 0 }}
139
+ lineNumberStyle={{ color: 'rgba(255,255,255,0.18)', fontSize: 11, minWidth: 36, paddingRight: 12 }}
140
+ showLineNumbers
141
+ startingLineNumber={source.start_line}
142
+ >
143
+ {source.text}
144
+ </SyntaxHighlighter>
145
+ ) : (
146
+ <div style={{ padding: "10px 14px", fontSize: 12, color: "var(--muted)", fontFamily: "var(--mono)" }}>
147
+ Code preview not stored β€”{" "}
148
+ <a href={githubUrl} target="_blank" rel="noopener noreferrer"
149
+ style={{ color: "var(--accent-soft)", textDecoration: "none" }}
150
+ onClick={e => e.stopPropagation()}>
151
+ open on GitHub β†—
152
+ </a>
153
+ </div>
154
+ )}
155
  </div>
156
  )}
157
  </div>
ui/src/index.css CHANGED
@@ -265,21 +265,14 @@ textarea:focus-visible {
265
  align-items: center;
266
  justify-content: center;
267
  box-shadow:
268
- 0 0 0 1px rgba(91, 143, 249, 0.30),
269
- 0 4px 16px rgba(91, 143, 249, 0.18),
270
- inset 0 1px 0 rgba(91, 143, 249, 0.12);
271
  flex-shrink: 0;
272
  position: relative;
273
  overflow: hidden;
274
  }
275
 
276
- .sidebar-brand-icon::after {
277
- content: '';
278
- position: absolute;
279
- inset: 0;
280
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.25) 0%, transparent 60%);
281
- border-radius: inherit;
282
- }
283
 
284
  /* Superscript label β€” "GITHUB RAG" small caps above the serif title */
285
  .sidebar-brand-tag {
@@ -477,7 +470,7 @@ textarea:focus-visible {
477
  .repo-item {
478
  display: flex;
479
  align-items: center;
480
- padding: 7px 10px;
481
  border-radius: var(--radius);
482
  cursor: pointer;
483
  /* 2px left accent β€” cleaner, less dominant */
@@ -546,6 +539,12 @@ textarea:focus-visible {
546
  letter-spacing: -0.01em;
547
  }
548
 
 
 
 
 
 
 
549
  .repo-count {
550
  font-size: 10px;
551
  color: rgba(200, 210, 240, 0.55);
@@ -579,18 +578,19 @@ textarea:focus-visible {
579
  .mode-pills {
580
  display: inline-flex;
581
  gap: 2px;
582
- background: rgba(0, 0, 0, 0.25);
583
- border: 1px solid rgba(255, 255, 255, 0.06);
584
- border-radius: 9px;
585
- padding: 2px;
586
  width: fit-content;
 
587
  }
588
 
589
  .pill {
590
- background: transparent;
591
- border: 1px solid transparent;
592
- border-radius: 6px;
593
- color: rgba(200, 210, 240, 0.50);
594
  cursor: pointer;
595
  font-family: var(--sans);
596
  font-size: 11.5px;
@@ -607,13 +607,11 @@ textarea:focus-visible {
607
  }
608
 
609
  .pill.active {
610
- background: linear-gradient(160deg, #4A7FE8 0%, #5B8FF9 60%, #7DABFF 100%);
611
- border-color: rgba(200, 220, 255, 0.18);
612
- color: #fff;
613
  font-weight: 600;
614
- box-shadow:
615
- 0 2px 10px rgba(91, 143, 249, 0.40),
616
- inset 0 1px 0 rgba(255, 255, 255, 0.22);
617
  }
618
 
619
  /* ══════════════════════════════════════════════════════════
@@ -975,7 +973,7 @@ textarea:focus-visible {
975
 
976
  .source-item:hover {
977
  border-color: var(--accent-border);
978
- background: rgba(40, 35, 29, 0.98);
979
  box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(91, 143, 249,0.08);
980
  }
981
 
@@ -1394,7 +1392,7 @@ textarea:focus-visible {
1394
  }
1395
 
1396
  .onboarding-headline {
1397
- font-size: 50px;
1398
  font-weight: 300;
1399
  font-family: "Anthropic Serif", Georgia, serif;
1400
  letter-spacing: -0.040em;
@@ -1415,7 +1413,7 @@ textarea:focus-visible {
1415
  }
1416
 
1417
  .onboarding-sub {
1418
- font-size: 14px;
1419
  color: var(--text-2);
1420
  line-height: 1.55;
1421
  letter-spacing: -0.012em;
@@ -3736,9 +3734,13 @@ textarea:focus-visible {
3736
  overflow: hidden;
3737
  cursor: grab;
3738
  position: relative;
 
 
 
3739
  background:
3740
- radial-gradient(ellipse at 20% 50%, rgba(91, 143, 249, 0.05) 0%, transparent 60%),
3741
- radial-gradient(ellipse at 80% 30%, rgba(91, 143, 249, 0.02) 0%, transparent 50%);
 
3742
  }
3743
 
3744
  .ec-canvas-wrapper:active {
@@ -3913,9 +3915,9 @@ textarea:focus-visible {
3913
  .ec-legend {
3914
  display: flex;
3915
  align-items: center;
3916
- gap: 14px;
3917
- padding: 7px 16px;
3918
- border-top: 1px solid rgba(255, 255, 255,0.05);
3919
  flex-shrink: 0;
3920
  flex-wrap: wrap;
3921
  }
@@ -3923,21 +3925,21 @@ textarea:focus-visible {
3923
  .ec-legend-item {
3924
  display: flex;
3925
  align-items: center;
3926
- gap: 5px;
3927
- font-size: 11px;
3928
- color: var(--muted);
3929
  }
3930
 
3931
  .ec-legend-dot {
3932
- width: 7px;
3933
- height: 7px;
3934
  border-radius: 50%;
3935
  flex-shrink: 0;
3936
  }
3937
 
3938
  .ec-legend-hint {
3939
  font-size: 11px;
3940
- color: var(--faint);
3941
  margin-left: auto;
3942
  }
3943
 
 
265
  align-items: center;
266
  justify-content: center;
267
  box-shadow:
268
+ 0 0 0 1px rgba(91, 143, 249, 0.22),
269
+ 0 2px 8px rgba(91, 143, 249, 0.10);
 
270
  flex-shrink: 0;
271
  position: relative;
272
  overflow: hidden;
273
  }
274
 
275
+ /* sidebar-brand-icon::after removed β€” was causing white glare on compass edges */
 
 
 
 
 
 
276
 
277
  /* Superscript label β€” "GITHUB RAG" small caps above the serif title */
278
  .sidebar-brand-tag {
 
470
  .repo-item {
471
  display: flex;
472
  align-items: center;
473
+ padding: 6px 10px;
474
  border-radius: var(--radius);
475
  cursor: pointer;
476
  /* 2px left accent β€” cleaner, less dominant */
 
539
  letter-spacing: -0.01em;
540
  }
541
 
542
+ /* owner/ prefix β€” dimmed so the eye jumps straight to the repo name */
543
+ .repo-owner {
544
+ color: var(--muted);
545
+ font-weight: 400;
546
+ }
547
+
548
  .repo-count {
549
  font-size: 10px;
550
  color: rgba(200, 210, 240, 0.55);
 
578
  .mode-pills {
579
  display: inline-flex;
580
  gap: 2px;
581
+ background: rgba(255,255,255,0.04);
582
+ border: 1px solid rgba(255,255,255,0.08);
583
+ border-radius: var(--radius-md);
584
+ padding: 3px;
585
  width: fit-content;
586
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.30);
587
  }
588
 
589
  .pill {
590
+ background: none;
591
+ border: none;
592
+ border-radius: calc(var(--radius-md) - 3px);
593
+ color: var(--text-2);
594
  cursor: pointer;
595
  font-family: var(--sans);
596
  font-size: 11.5px;
 
607
  }
608
 
609
  .pill.active {
610
+ background: rgba(255,255,255,0.10);
611
+ border: none;
612
+ color: var(--text);
613
  font-weight: 600;
614
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.12);
 
 
615
  }
616
 
617
  /* ══════════════════════════════════════════════════════════
 
973
 
974
  .source-item:hover {
975
  border-color: var(--accent-border);
976
+ background: rgba(15, 18, 35, 0.98);
977
  box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(91, 143, 249,0.08);
978
  }
979
 
 
1392
  }
1393
 
1394
  .onboarding-headline {
1395
+ font-size: 70px;
1396
  font-weight: 300;
1397
  font-family: "Anthropic Serif", Georgia, serif;
1398
  letter-spacing: -0.040em;
 
1413
  }
1414
 
1415
  .onboarding-sub {
1416
+ font-size: 15px;
1417
  color: var(--text-2);
1418
  line-height: 1.55;
1419
  letter-spacing: -0.012em;
 
3734
  overflow: hidden;
3735
  cursor: grab;
3736
  position: relative;
3737
+ /* Atmospheric glow β€” spotlight from top-center bleeds behind the graph,
3738
+ making nodes pop against the dark canvas. Inspired by Raycast/Arc splash screens.
3739
+ Two layers: a warm blue core + a cooler wide bloom that fills the canvas. */
3740
  background:
3741
+ radial-gradient(ellipse 70% 55% at 50% 50%, rgba(91, 143, 249, 0.13) 0%, transparent 70%),
3742
+ radial-gradient(ellipse 110% 90% at 50% 50%, rgba(91, 143, 249, 0.05) 0%, transparent 80%),
3743
+ var(--bg);
3744
  }
3745
 
3746
  .ec-canvas-wrapper:active {
 
3915
  .ec-legend {
3916
  display: flex;
3917
  align-items: center;
3918
+ gap: 16px;
3919
+ padding: 9px 16px;
3920
+ border-top: 1px solid rgba(255, 255, 255,0.06);
3921
  flex-shrink: 0;
3922
  flex-wrap: wrap;
3923
  }
 
3925
  .ec-legend-item {
3926
  display: flex;
3927
  align-items: center;
3928
+ gap: 6px;
3929
+ font-size: 12px;
3930
+ color: var(--text-2);
3931
  }
3932
 
3933
  .ec-legend-dot {
3934
+ width: 8px;
3935
+ height: 8px;
3936
  border-radius: 50%;
3937
  flex-shrink: 0;
3938
  }
3939
 
3940
  .ec-legend-hint {
3941
  font-size: 11px;
3942
+ color: var(--muted);
3943
  margin-left: auto;
3944
  }
3945