Spaces:
Running
design: premium UI polish + agent reliability fixes
Browse filesUI:
- 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 +11 -5
- ui/src/App.jsx +29 -27
- ui/src/components/GraphDiagram.jsx +32 -4
- ui/src/components/Message.jsx +11 -6
- ui/src/components/Sidebar.jsx +9 -2
- ui/src/components/SourceCard.jsx +21 -10
- ui/src/index.css +41 -39
|
@@ -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 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -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="
|
| 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="
|
| 787 |
{[
|
| 788 |
-
{ icon: "github",
|
| 789 |
-
{ icon: "chat",
|
| 790 |
-
{ icon: "explore",
|
| 791 |
-
].map(({ icon,
|
| 792 |
-
<div key={
|
| 793 |
-
style={{ animationDelay: `${80 + i * 90}ms` }}>
|
| 794 |
-
<span className="suggestion-icon"
|
| 795 |
-
<
|
| 796 |
-
<
|
| 797 |
-
<
|
| 798 |
-
</
|
|
|
|
| 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` }}
|
|
@@ -263,11 +263,22 @@ export default function GraphDiagram({ data, onNodeSelect, onEdgeSelect, onAskAb
|
|
| 263 |
|
| 264 |
const wrapRef = useRef(null);
|
| 265 |
|
| 266 |
-
//
|
|
|
|
|
|
|
| 267 |
useEffect(() => {
|
| 268 |
-
setXform({ x: 0, y: 0, scale: 0.85 });
|
| 269 |
setNodePos({});
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" :
|
| 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)"}
|
|
@@ -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.
|
| 390 |
-
?
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
@@ -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="
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 && (
|
|
@@ -131,16 +131,27 @@ export default function SourceCard({ source, index, showRepo = false }) {
|
|
| 131 |
|
| 132 |
{open && (
|
| 133 |
<div className="source-code">
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
@@ -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.
|
| 269 |
-
0
|
| 270 |
-
inset 0 1px 0 rgba(91, 143, 249, 0.12);
|
| 271 |
flex-shrink: 0;
|
| 272 |
position: relative;
|
| 273 |
overflow: hidden;
|
| 274 |
}
|
| 275 |
|
| 276 |
-
|
| 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:
|
| 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(
|
| 583 |
-
border: 1px solid rgba(255,
|
| 584 |
-
border-radius:
|
| 585 |
-
padding:
|
| 586 |
width: fit-content;
|
|
|
|
| 587 |
}
|
| 588 |
|
| 589 |
.pill {
|
| 590 |
-
background:
|
| 591 |
-
border:
|
| 592 |
-
border-radius:
|
| 593 |
-
color:
|
| 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:
|
| 611 |
-
border
|
| 612 |
-
color:
|
| 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(
|
| 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:
|
| 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:
|
| 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
|
| 3741 |
-
radial-gradient(ellipse at
|
|
|
|
| 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:
|
| 3917 |
-
padding:
|
| 3918 |
-
border-top: 1px solid rgba(255, 255, 255,0.
|
| 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:
|
| 3927 |
-
font-size:
|
| 3928 |
-
color: var(--
|
| 3929 |
}
|
| 3930 |
|
| 3931 |
.ec-legend-dot {
|
| 3932 |
-
width:
|
| 3933 |
-
height:
|
| 3934 |
border-radius: 50%;
|
| 3935 |
flex-shrink: 0;
|
| 3936 |
}
|
| 3937 |
|
| 3938 |
.ec-legend-hint {
|
| 3939 |
font-size: 11px;
|
| 3940 |
-
color: var(--
|
| 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 |
|