Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.gortex/
5 changes: 4 additions & 1 deletion src/components/chrome/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useDashboard } from '@/lib/hooks'

export function StatusBar() {
const scope = useTweaks((s) => s.scope)
const activeRepo = useTweaks((s) => s.activeRepo)
const { data, error } = useDashboard()
const nodes = data?.stats.total_nodes
const edges = data?.stats.total_edges
Expand Down Expand Up @@ -33,7 +34,9 @@ export function StatusBar() {
</span>
<span className="sep">·</span>
<span className="seg">
scope <b style={{ color: 'var(--fg-0)' }}>{scope === 'federated' ? 'all repos' : 'single'}</b>
scope <b style={{ color: 'var(--fg-0)' }}>
{scope === 'federated' ? 'all repos' : activeRepo ? `single · ${activeRepo}` : 'single'}
</b>
</span>
<span className="spacer" />
<span className="seg">{version}</span>
Expand Down
31 changes: 28 additions & 3 deletions src/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,34 @@ function Kpi({
}

function RepoCard({ r }: { r: Repo }) {
const router = useRouter()
const set = useTweaks((s) => s.set)
const kinds = [
{ label: 'functions', value: r.funcs, color: 'var(--k-function)' },
{ label: 'methods', value: r.methods, color: 'var(--k-method)' },
{ label: 'types', value: r.types, color: 'var(--k-type)' },
{ label: 'interfaces', value: r.interfaces, color: 'var(--k-interface)' },
{ label: 'variables', value: r.vars, color: 'var(--k-variable)' },
]
const drillIn = () => {
set('scope', 'single')
set('activeRepo', r.id)
router.push('/graph')
}
return (
<div className="repo-card">
<div
className="repo-card"
style={{ cursor: 'pointer' }}
role="button"
tabIndex={0}
onClick={drillIn}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
drillIn()
}
}}
>
<div className="repo-hd">
<span style={{ background: r.color, width: 6, height: 18, borderRadius: 2, display: 'inline-block' }} />
<div>
Expand Down Expand Up @@ -314,6 +333,7 @@ export function Dashboard() {
const router = useRouter()
const { data, loading, error, refetch } = useDashboard()
const scope = useTweaks((s) => s.scope)
const activeRepo = useTweaks((s) => s.activeRepo)
// Separate from the global workspace `scope` — this one partitions
// the caveats card into first-party code ("yours"), test fixtures,
// and vendored dependencies. "yours" is the default because the raw
Expand Down Expand Up @@ -547,12 +567,17 @@ export function Dashboard() {
<div className="card-hd">
<span className="ti">Repositories</span>
<span className="mono faint" style={{ fontSize: 11 }}>
{snap.stats.repos} indexed · {scope === 'federated' ? 'federated' : 'single repo'} view
{scope === 'single' && activeRepo
? `scoped to ${activeRepo}`
: `${snap.stats.repos} indexed · federated view`}
</span>
</div>
<div className="card-bd" style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 14 }}>
<div className="repo-grid">
{snap.repos.slice(0, 6).map((r) => (
{(scope === 'single' && activeRepo
? snap.repos.filter((r) => r.id === activeRepo)
: snap.repos.slice(0, 6)
).map((r) => (
<RepoCard key={r.id + ':' + r.owner} r={r} />
))}
</div>
Expand Down
34 changes: 28 additions & 6 deletions src/components/graph/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,28 @@ function RepoFilterPanel({

export function GraphView() {
const showMinimap = useTweaks((s) => s.showMinimap)
const scope = useTweaks((s) => s.scope)
const activeRepo = useTweaks((s) => s.activeRepo)
const setTweak = useTweaks((s) => s.set)
const { data: repos, loading, error } = useRepos()
const { data: dash } = useDashboard()
const { data: graph, loading: graphLoading, error: graphError } = useGraph()
const repoList = repos ?? []
const effectiveRepo = scope === 'single' ? (activeRepo || repoList[0]?.id) : undefined
const { data: graph, loading: graphLoading, error: graphError } = useGraph(
effectiveRepo ? { repo: effectiveRepo } : undefined,
)
const [mode, setMode] = useState<Mode>('constellation')
const [filtered, setFiltered] = useState<Set<string>>(new Set())
const [filterKinds, setFilterKinds] = useState<Set<string>>(new Set())

useEffect(() => {
if (repos && filtered.size === 0) {
if (!repos) return
if (effectiveRepo) {
setFiltered(new Set([effectiveRepo]))
} else if (filtered.size === 0) {
setFiltered(new Set(repos.map((r) => r.id)))
}
}, [repos, filtered.size])
}, [repos, effectiveRepo, filtered.size])

// Default kinds: everything except `file`. File nodes inflate the graph
// without adding useful topology, so they start off; user can opt in.
Expand All @@ -125,15 +135,18 @@ export function GraphView() {
else n.add(id)
setFiltered(n)
}
const only = (id: string) => setFiltered(new Set([id]))
const only = (id: string) => {
setFiltered(new Set([id]))
setTweak('scope', 'single')
setTweak('activeRepo', id)
}
const toggleKind = (kind: string) => {
const n = new Set(filterKinds)
if (n.has(kind)) n.delete(kind)
else n.add(kind)
setFilterKinds(n)
}

const repoList = repos ?? []
const visibleRepos = repoList.filter((r) => !filtered.size || filtered.has(r.id))
const kinds = dash?.kinds ?? []

Expand All @@ -145,9 +158,18 @@ export function GraphView() {
<div className="sub">
{loading || graphLoading
? 'Loading graph…'
: `${filtered.size} of ${repoList.length} repos · ${(graph?.nodes.length ?? dash?.stats.total_nodes ?? 0).toLocaleString()} nodes · ${(graph?.edges.length ?? dash?.stats.total_edges ?? 0).toLocaleString()} edges`}
: effectiveRepo
? `Single repo · ${effectiveRepo} · ${(graph?.nodes.length ?? 0).toLocaleString()} nodes · ${(graph?.edges.length ?? 0).toLocaleString()} edges`
: `${filtered.size} of ${repoList.length} repos · ${(graph?.nodes.length ?? dash?.stats.total_nodes ?? 0).toLocaleString()} nodes · ${(graph?.edges.length ?? dash?.stats.total_edges ?? 0).toLocaleString()} edges`}
</div>
</div>
{effectiveRepo && (
<div className="actions">
<button type="button" className="btn ghost" onClick={() => setTweak('scope', 'federated')}>
<Icon name="expand" size={11} /> Show all repos
</button>
</div>
)}
</div>
{(error || graphError) && (
<div style={{ padding: 22, color: 'var(--danger)', fontSize: 13 }}>
Expand Down
44 changes: 41 additions & 3 deletions src/components/pages/ServicesView.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
'use client'

import { useRouter } from 'next/navigation'
import { Icon } from '@/components/primitives/Icon'
import { StackedBar } from '@/components/primitives/Charts'
import { useRepos } from '@/lib/hooks'
import { useTweaks } from '@/lib/tweaks'

export function ServicesView() {
const router = useRouter()
const { data, loading, error, refetch } = useRepos()
const repos = data ?? []
const scope = useTweaks((s) => s.scope)
const activeRepo = useTweaks((s) => s.activeRepo)
const set = useTweaks((s) => s.set)
const allRepos = data ?? []
const repos = scope === 'single' && activeRepo
? allRepos.filter((r) => r.id === activeRepo)
: allRepos

const drillIn = (id: string) => {
set('scope', 'single')
set('activeRepo', id)
router.push('/graph')
}

return (
<>
<div className="page-hd">
<div>
<h1>Services</h1>
<div className="sub">
{loading ? 'Loading…' : `${repos.length} indexed services · click to drill in`}
{loading
? 'Loading…'
: scope === 'single' && activeRepo
? `Scoped to ${activeRepo} · click to open its graph`
: `${repos.length} indexed services · click to drill in`}
</div>
</div>
<div className="actions">
{scope === 'single' && activeRepo && (
<button type="button" className="btn ghost" onClick={() => set('scope', 'federated')}>
Show all
</button>
)}
<button type="button" className="btn" onClick={refetch}>
<Icon name="history" size={12} /> Refresh
</button>
Expand All @@ -38,7 +63,20 @@ export function ServicesView() {
<div style={{ padding: 18, overflow: 'auto' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 10 }}>
{repos.map((r) => (
<div key={r.id + ':' + r.owner} className="card" style={{ padding: 14 }}>
<div
key={r.id + ':' + r.owner}
className="card"
style={{ padding: 14, cursor: 'pointer' }}
role="button"
tabIndex={0}
onClick={() => drillIn(r.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
drillIn(r.id)
}
}}
>
<div className="hstack" style={{ gap: 8 }}>
<span style={{ width: 8, height: 28, borderRadius: 3, background: r.color }} />
<div>
Expand Down
10 changes: 6 additions & 4 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,15 @@ export const api = {
},

// Fetches the full step list + files for a single process. Uses the
// `get_processes` MCP tool with the `id` parameter so the response
// includes every step's node ID — list endpoints deliberately omit
// these to keep the summary light.
// `analyze` facade's `processes` kind with the `id` parameter so the
// response includes every step's node ID — list endpoints deliberately
// omit these to keep the summary light. The facade routes
// analyze(kind=processes) to the get_processes handler without needing
// the legacy tool promoted into the live registry (core/defer surface).
processDetail: async (id: string): Promise<ProcessDetail | null> => {
if (!id) return null
try {
return await callToolJSON<ProcessDetail>('get_processes', { id })
return await callToolJSON<ProcessDetail>('analyze', { kind: 'processes', id })
} catch { return null }
},

Expand Down
3 changes: 3 additions & 0 deletions src/lib/tweaks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Tweaks = {
theme: Theme
layout: Layout
scope: Scope
activeRepo: string | null
caveats: CaveatDensity
graphStyle: 'constellation' | 'tree' | 'sankey' | '3d'
density: Density
Expand All @@ -23,6 +24,7 @@ const initial: Tweaks = {
theme: 'ink',
layout: 'tri',
scope: 'federated',
activeRepo: null,
caveats: 'inline',
graphStyle: 'constellation',
density: 'comfortable',
Expand Down Expand Up @@ -50,6 +52,7 @@ export const useTweaks = create<Store>()(
theme: s.theme,
layout: s.layout,
scope: s.scope,
activeRepo: s.activeRepo,
caveats: s.caveats,
graphStyle: s.graphStyle,
density: s.density,
Expand Down