Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,40 @@ Embedded storage is selected automatically on Linux x64. On Apple silicon macOS,

Set `CODEGRAPH_RAW_TOOLS=1` to expose the lower-level handlers instead of the four grouped tools.

## Dashboard

CodeGraph ships a browser dashboard for exploring the graph visually: a force,
tree, or ring view of files, functions, classes, and interfaces, semantic and
Cypher search, a source viewer with syntax highlighting, and an operations tab
for indexing and embedding coverage.

It runs as a second binary that serves both the UI and the REST API on one
port, so nothing else needs to be started.

```bash
# published package: the binary ships inside codegraph-mcp, so name the
# package explicitly. A bare "npx codegraph-dashboard" would look for a
# package of that name, which does not exist.
npx -p codegraph-mcp codegraph-dashboard

# already installed globally
npm install --global codegraph-mcp && codegraph-dashboard

# source checkout
pnpm dashboard
```
Comment thread
Phoenixrr2113 marked this conversation as resolved.

Then open <http://localhost:3001>. Set `API_PORT` to use a different port.

The dashboard is optional. The MCP server does not start it, and running the
MCP server does not require it.

| Variable | Purpose |
| --- | --- |
| `API_PORT` | Port for the dashboard and REST API (default 3001) |
| `CODEGRAPH_DASHBOARD_DIR` | Override the location of the built dashboard assets |
| `CODEGRAPH_CORS_ORIGINS` | Comma separated origin allowlist, for a shared deployment |

## Configuration

| Variable | Purpose |
Expand Down Expand Up @@ -166,8 +200,10 @@ FalkorDBLite's Linux x64 and Apple silicon macOS binaries are installed with the
| [`@codegraph/cli`](packages/cli/) | Source-checkout command-line tools |
| [`codegraph-mcp`](packages/npm-package/) | Public npm distribution staging and entry point |
| [`@codegraph/mcpb`](packages/mcpb/) | Platform-local MCPB desktop extension build |
| [`@codegraph/api`](packages/api/) | REST API consumed by the dashboard |
| [`@codegraph/dashboard`](packages/dashboard/) | Static dashboard UI served by the API |

The Next.js application lives in [`apps/web`](apps/web/), and the reproducible search benchmark lives in [`benchmarks/cgbench-v1`](benchmarks/cgbench-v1/).
The marketing site lives in [`apps/web`](apps/web/), and the reproducible search benchmark lives in [`benchmarks/cgbench-v1`](benchmarks/cgbench-v1/).

## License

Expand Down
12 changes: 11 additions & 1 deletion packages/dashboard/src/components/dashboard/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@ export function AppShell({ projectId }: { projectId?: string | null }) {
<SearchPanel
apiUrl={API_URL}
onHighlight={handleSearchHighlight}
onSelectResult={(name) => setHighlightedNames(new Set([name]))}
onSelectResult={(result) => {
setHighlightedNames(new Set([result.name]))
// Open the detail panel too. The search payload already carries
// filePath and line numbers, which is everything the panel needs.
setSelectedNode({
id: `${result.nodeType}:${result.filePath ?? ''}:${result.name}`,
label: result.name,
type: result.nodeType,
properties: result,
})
}}
/>
</ResizablePanel>

Expand Down
7 changes: 6 additions & 1 deletion packages/dashboard/src/components/dashboard/graph-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ export function GraphCanvas({ apiUrl, onNodeSelect, highlightedNames, hiddenEdge
const nodeType = (n.label ?? nodeData.type ?? 'Unknown') as string
return {
data: {
// Spread first: the graph payload carries its own internal "id",
// and spreading it last replaced the cytoscape node id. Every edge
// referencing the real id was then treated as an orphan and
// dropped, so functions, classes and interfaces rendered with no
// edges at all while files, which carry no inner id, looked fine.
...nodeData,
id: n.id as string,
label: displayName,
type: nodeType,
filePath: n.filePath as string | undefined,
...nodeData,
},
}
})
Expand Down
9 changes: 6 additions & 3 deletions packages/dashboard/src/components/dashboard/search-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { useState, useCallback } from 'react'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'

interface SearchResult {
export interface SearchResult {
name: string
nodeType: string
filePath?: string
callerCount?: number
importerCount?: number
// The search endpoint returns the full node payload (startLine, docstring,
// params and so on). Those extra fields are what the detail panel renders.
[key: string]: unknown
}

interface SearchPanelProps {
apiUrl: string
onHighlight: (names: string[]) => void
onSelectResult: (name: string) => void
onSelectResult: (result: SearchResult) => void
}

export function SearchPanel({ apiUrl, onHighlight, onSelectResult }: SearchPanelProps) {
Expand Down Expand Up @@ -82,7 +85,7 @@ export function SearchPanel({ apiUrl, onHighlight, onSelectResult }: SearchPanel
<button
key={`${r.name}-${i}`}
className="w-full border-b border-border/50 px-3 py-2 text-left transition-colors hover:bg-accent/50"
onClick={() => onSelectResult(r.name)}
onClick={() => onSelectResult(r)}
>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] font-normal">
Expand Down
10 changes: 8 additions & 2 deletions packages/dashboard/src/lib/cytoscape-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,14 @@ export type LayoutName = 'cose' | 'concentric' | 'breadthfirst'
export const LAYOUT_OPTIONS: Record<LayoutName, cytoscape.LayoutOptions> = {
cose: {
name: 'cose',
animate: true,
animationDuration: 500,
// cose runs numIter (1000 by default) simulation steps. Animating them left
// the graph drifting for many seconds, during which nodes moved out from
// under the pointer and could not reliably be clicked. Animating only the
// final transition still fit the viewport to the pre-layout positions, so
// the graph settled off screen. Positioning without animation gives final
// coordinates immediately, which makes both the fit and the hit testing
// correct.
animate: false,
nodeRepulsion: () => 8000,
idealEdgeLength: () => 80,
gravity: 0.3,
Expand Down
46 changes: 31 additions & 15 deletions packages/graph/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,27 @@ export interface GraphQueries {
// Query Operations Implementation
// ============================================================================

/**
* Find the graph node an edge endpoint refers to.
*
* Prefers name plus path, which identifies a specific declaration. Falls back to
* the file itself only when the endpoint carries no name, since matching on path
* alone cannot distinguish declarations that share a file.
*/
function findEndpoint(
nodes: GraphNode[],
props: Record<string, unknown>,
): GraphNode | undefined {
const name = props['name'];
const filePath = props['filePath'];

if (typeof name === 'string' && name.length > 0) {
const exact = nodes.find((n) => n.displayName === name && n.filePath === filePath);
if (exact) return exact;
}
return nodes.find((n) => n.filePath === filePath && n.label === 'File');
}

class GraphQueriesImpl implements GraphQueries {
private readonly dialect: CypherDialect;
private readonly templates: ReturnType<typeof buildCypherTemplates>;
Expand Down Expand Up @@ -300,21 +321,16 @@ class GraphQueriesImpl implements GraphQueries {
const fromLabels = extractLabels(row.a, [], this.dialect);
const toLabels = row.toLabels ?? extractLabels(row.b, [], this.dialect);

// Get source node from our nodes array
const fromNode = nodes.find((n) => {
return (
n.filePath === fromProps['filePath'] ||
(n.displayName === fromProps['name'] && n.filePath === fromProps['filePath'])
);
});

// Get target node - or create External node if needed
let toNode = nodes.find((n) => {
return (
n.filePath === toProps['filePath'] ||
(n.displayName === toProps['name'] && n.filePath === toProps['filePath'])
);
});
// Resolve each endpoint by identity.
//
// This previously tested filePath on its own first, and every function,
// class and interface declared in a file shares that path. The lookup
// therefore returned whichever node for the file happened to come first,
// usually the File node, so real endpoints ended up with no edges at all
// and rendered as a disconnected grid. The name check that followed could
// never run because the || had already short-circuited.
const fromNode = findEndpoint(nodes, fromProps);
let toNode = findEndpoint(nodes, toProps);

// If target is External and not in nodes, add it
const isExternalTarget = toLabels.includes('External') ||
Expand Down
18 changes: 18 additions & 0 deletions packages/npm-package/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ npm install --global codegraph-mcp

Then configure an MCP client to run \`codegraph-mcp\`. The server uses stdio and keeps logs on stderr.

## Dashboard

This package also installs \`codegraph-dashboard\`, a browser UI for exploring the
graph: force, tree, and ring views, semantic and Cypher search, a source viewer,
and an operations tab for indexing and embedding coverage.

\`\`\`bash
# The binary ships inside this package, so name the package explicitly.
npx -p codegraph-mcp codegraph-dashboard

# or, once installed globally
codegraph-dashboard
\`\`\`

It serves the UI and the REST API on http://localhost:3001. Set \`API_PORT\` to
change the port. The dashboard is optional: the MCP server neither starts it nor
depends on it.

## Offline start

\`\`\`bash
Expand Down
Loading