Route objects already read `{ index: true, element }` as the page at the
parent's address and a route object around others as their layout. JSX
routes did neither: `<Route path="/" element={<Layout/>}><Route index
element={<Home/>}/>…</Route>` bound `/` to Layout, the index child was never
read, and Layout's links counted only from `/`.
The JSX branch of scanRoutes now:
- reads `<Route index>` / `index={true}` (and a `path=""` leaf with an
element) as the page at its parent's address, and lets it claim that
address from the layout or the path-only `<Route path>` around it;
- treats a `<Route element>` with `<Route>`s inside as their layout
(`layout:` references), a pathless or `path=""` one included, and threads
those layouts into a table mapped inside it when the table is written in
the same file;
- reads an index route only where its parent's address is written down: an
enclosing route with a readable path, or the router itself
(`createRoutesFromElements(…)`, `<BrowserRouter>`). At the top of a
component's own `<Routes>` the address is wherever another file mounts the
component, and under `path={paths.x}` it is not spelled out;
- reads `path={"agents"}` as a path.
Reading what an element renders: tags inside an attribute are no longer the
page unless the attribute hands one over (`component`, `element`, `page`),
so `<Suspense fallback={<Loader/>}>` no longer binds the spinner; the line
break Prettier writes after `element={` no longer hides the element; a guard
that wraps only `<Outlet/>` is the guard; and `element={<Outlet/>}` renders
nothing of its own.
Validated against the merged #2400 build: 16 control repos byte-identical,
578 of 581 new layout edges name a component of an enclosing <Route> (3
inherit a pre-existing default-import resolution bug), 27 of 27 new
navigations precise, no navigation lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 KiB
Changelog entries held for the viewer launch
These describe codegraph ui and its screens. They were taken out of ## [Unreleased] when a fixes release shipped with the viewer withheld behind CODEGRAPH_UI=1 (src/bin/viewer-gate.ts). At launch, move them back under ## [Unreleased] in CHANGELOG.md, bring back the Highlights below, and delete this file.
Highlights, as written for the viewer launch
codegraph ui— your graph in a browser. A local, read-only viewer for the project you already indexed: your code with its callers and callees in the margin, a map of the whole repository, and a strip that shows how one symbol reaches another.- See your app the way its users meet it. A Screens tab draws every screen and the navigation between them, for Expo Router, React Router, Next.js, TanStack Router, Vue Router / Nuxt, SvelteKit and Angular apps.
- See what happens from a screen or an endpoint. A Steps tab draws what one action sets in motion — the handlers it fires, the state it writes, the calls that leave your code, and every way it can answer — with the condition on each arrow.
- APIs too, and across tiers. Endpoints in Express, NestJS, Fastify, Koa, Hono, FastAPI, Flask, Django, Spring, ASP.NET, Vapor and Gin, with a page's
fetchfollowing through to the route that serves it, a queued job to its consumer, an event to its handler. - Read a handler in the order its code runs. The same picture laid out by when things happen rather than by distance, so a reply sits below the token it carries. Where the code chooses, the condition is said once and each arrow answers it.
- Find what nothing reaches, and where everything starts. Dead code and entry points as screens of their own, a type hierarchy showing what is built on what, and trails you can name, save and come back to after the code has changed.
- Conditions and arguments for more languages. What a call passes, and the
if/switch/tryit runs under, are now read for Python, Java, Kotlin, C#, Go and C as well as JavaScript, TypeScript and Swift. - Upgrading: re-index your projects after this release — several of the new readings rest on edges that are written while indexing.
New Features
-
The Steps tab draws what an Angular screen does. Each handler a template binds is drawn with the event that fires it, like
submitFormon the form's(ngSubmit)ortoggleFavoriteon a button's(click), followed by the requests it sends and the screen it opens. That includes handlers in child components the screen renders. Re-index Angular projects after upgrading. -
The Screens tab draws Angular apps. Every screen in an Angular app's routes, with the navigation between them:
router.navigate(…), a guard's redirect and each template'srouterLink, under the condition the code checks first. A button in a child component, like an article's favorite button, is drawn from the screen that renders it. A layout's tabs and buttons are drawn from every screen inside that layout. Re-index Angular projects after upgrading. -
A big screen's picture stops wrapping into a column. How wide a screen's lines run before they wrap was worked out with a formula, and the formula was wrong for the way these pictures are actually drawn: a part of a screen spends lines on its own structure — a step that fires things gets a line to itself, and what it fires starts another — so estimating the lines from the boxes alone badly undercounted them, and one screen's 98 boxes wrapped into a 4,356px column. Laying a picture out is cheap and exact, so the widths are now simply tried and the one that comes out closest to the shape of a window is kept. Across one app's 51 screens the tallest picture went from 4,356px to 3,796px, total height fell 8%, and — because a shorter picture is also a picture whose lines have less far to go — lines running over other boxes fell by a third and lines crossing each other went from 13 to 5.
-
A screen's picture stops being mostly empty. The parts of a screen were tiled a row at a time, each row as tall as its tallest member — so one short region beside a tall one left the whole rest of that row blank. Measured across one app: the median screen's canvas was only 55% picture and 45% nothing, and its busiest screen was 44% — 4,860px tall to hold about 2,160px of content, all of which a reader has to scroll through. Each part now goes as high as it can and then as far left as it can, so a short one tucks under another short one instead of waiting for the tall one beside it. Reading order is unchanged: parts are still placed in the screen's own source order, and an earlier one is never pushed below a later one. That screen is now 3,584px instead of 4,860px, and close to square rather than a ribbon.
-
The screen's own line into each part of itself stops sweeping across the picture. A screen's picture holds back the screen's whole fan-out and draws one line into each region instead — but on a screen with ten regions those regions tile into bands, so the line into a region two bands down had to travel the height of the whole picture to get there. Measured across one app's 51 screens: those lines were 17% of everything drawn and caused 79% of every remaining crossing. Two fixes. The line now lands on the box nearest the region's top-left that the screen actually leads to, rather than on whichever box the walk happened to meet first — which, once a screen is drawn in clusters, could be lines down inside the region. And when the region is still too far for a line to be followed, the link is said in words like any other, with no exception for the screen itself. Across those 51 screens, lines crossing each other went from 47 to 10, with no screen above 10; the busiest screen went from 19 to 2.
-
A busy screen's picture no longer draws itself as a web. On a screen whose whole body is one component, the region grouping had nothing to divide — 98 of 100 boxes landed in a single column — and the picture came back as a 5,600px ribbon whose 113 lines crossed each other 652 times, each one running over five other boxes' names. Three things are fixed. A screen is now laid out in clusters: a step, then the steps it sets in motion on the line under it, stepped in — so a box and the thing it fires are one line apart instead of seven, which is what the crossings were made of. A region's lines run to a width its size earns rather than a fixed one, so a big screen comes out about as wide as it is tall instead of a narrow ribbon. And a link whose two boxes end up too far apart to follow is now said in words at both ends —
→ resumeInferenceunder the box that leads there,← CaptureViewunder the box it arrives at — rather than drawn as a line across the whole picture; select either box and every one of its real lines draws, exactly as before. On the screen that prompted this, the lines drawn at rest cross each other once, down from 652, with half of them still drawn as real lines. Steps that fire nothing still share a line, so a flat screen is unchanged. Nothing needs a re-index. -
A region that holds a cycle no longer stretches the picture. Rows inside a region were settled by relaxation, which never converges on a graph that has a loop in it: one real screen's 65 boxes were pushed to rows 294-301 while the rest sat at rows 0-2, and the order they were drawn in had nothing to do with what leads to what. Cycle-closing links are now set aside before the rows are settled, as the in-order reading already did — the loop is still drawn, it just cannot stretch the picture.
-
A busy screen's picture is laid out by the parts of the screen. A screen is a set of handlers with no order between them, so on a hub screen the old rows-by-distance collapsed into one enormous row — the main screen of one app put 89 boxes side by side on a canvas over 28,000px wide, every line a near-horizontal sweep across all of it. The Steps tab now groups a screen's picture by region — the component that owns each handler, named in a small caption over its boxes — with each region a column where a step sits above what it sets in motion, tiled in the screen's own source order. At rest the picture hides only two things: the screen's own fan-out — one line into each region stands in for it — and lines that point back up; every other line draws where it leads, between two regions included, and selecting a step brings out its whole story in the side panel, link by link. A box nothing points at is the screen's own doing — run on render or mount, or from a binding written inline — the key says so, and selecting it lights its line from the screen with what fires it. The same app's widest screen now lays out under 3,500px with every line local, and the whole picture fits on screen when it opens. Endpoints, handlers and the in-order reading are untouched, and nothing needs a re-index: the regions come from the same walk that draws the steps.
-
Where the code chooses, the picture says so once. A helper that ends
return (await hasSeenWelcome(id)) ? '/home/' : '/welcome/'sends the app to one of two screens, but the Steps picture drew that as two separate arrows, each carrying the whole condition with one of them negated and both cut off at the same forty characters — and before you clicked anything, neither arrow was labelled at all, so nothing said it was a choice. Now sibling arrows out of one box that are the arms of oneif,switchor ternary are drawn as the choice they are: the condition is written once under the box that decides it, and each arrow out says only which way it is —yes,no, or a case's own value. They are the only arrows labelled before you select anything, so the picture reads at a glance without becoming a wall of text. A one-sided guard — an early exit, anifwith only one side drawn — still carries its condition on the arrow, and an arrow that is reached whether or not the condition holds never claims a side. Nothing needs a re-index: the decision is read from the source at request time. -
A dialog's buttons fire what they run.
Alert.prompt('Add Folder', …, [{ onPress: (name) => createBackgroundFolder(name) }])is two facts: the prompt is a call that leaves the index, and its button fires the handler. The handler's line now arrives from the dialog's own box — with the condition on it — instead of from the screen, so the confirm-then-act chains a mobile app is full of read as chains: the delete alert leads to the delete, which leads to the request it sends. The same holds for anything bound inside the arguments of a call that leaves the index. Nothing needs a re-index: it is read from the source at request time. -
The Steps tab draws a handler in the order its code runs. The picture of what an endpoint sets in motion put the lookup, the token signing, the 200 and the 401 side by side, because each is one step from the anchor — true, and not how the code reads. Now a handler, an endpoint or any function opens as the same picture laid out by when things happen: a line means and then, so the 200 sits below the token signing it is built from and the 401 branches off the check that chose it. Where the code forks both ways, a small box asks the condition once and each line out of it answers —
yes,no, a case's value — so the two arms of oneifread as one decision instead of two lines that each carry the whole predicate, one of them negated; selecting a step next to the decision lights the whole fork through it. A lone guard — an early exit, anifwith one drawn side — keeps its condition on the line, and an arm that answers the request, returns or throws simply has nothing leaving it. A call written inside another call's arguments happens first, so the token is signed before the reply that carries it. A helper is drawn where it is called (via generateToken), a body that repeats says so (for each item of items), and work registered to run later (later · then) or started at once (together · Promise.all) says that rather than pretending to be a sequence. A screen still opens as before — its handlers fire on events and have no order between them — and either reading is one click, or one&view=order/&view=treein the link, away. Nothing to re-index: it is read from the source at request time, and where the conditions cannot be read the picture is a plain sequence rather than an invented structure. -
Double-click a box to go there. On the Steps tab a double-click on any step starts the picture from it — the same as the panel's Start here — so an endpoint the page calls, or another screen drawn as a boundary, opens as its own chapter in one gesture; on the Screens tab a double-click on a screen opens what happens from it. A boundary's panel now says it is not entered rather than that nothing leaves it.
-
The Steps tab now draws an API as well as an app. Anchor on an endpoint —
POST /usersin Express, NestJS, Fastify, FastAPI, Flask, Django, Spring (Java or Kotlin), ASP.NET, Vapor or Gin — and the viewer starts at the handler the route runs (or at the route itself when the handler is an inline arrow), says what fires it (FIRES FROM POST /users · after authenticate, validate(…)— the middleware arguments at the registration, or the guard decorators on the method and its class, or a FastAPIdependencies=[…]), and draws what the request sets in motion: the database calls with the model and whether they read or write (prisma.user.create({ data })·database · user · write), jobs put on a queue, emails, payments, cache reads, token checks, calls to other services, files and processes — and the responses, one box per status a handler can send (201,404) whose panel rows are the endpoint's contract as the code has it:WHEN NOT user → 404 · NotFoundException('no such user'),always → 201 · res.status(201).json(user). A queue consumer or a scheduled job anchored by name says the decorator that fires it (@Process('email')). The legend, the panel and the chooser use the project's own words — endpoint, data call, another tier — and the bare Steps tab lists an API's endpoints by router file when there are no screens. Re-index is not needed: everything new is read from the source at request time. -
Calls are read as written, so the database is the database. The index keeps only the last segment of a deep member call (
createforprisma.user.create), and a bare name matches by name alone — often to the wrongcreate. The Steps walk now reads each call from the source as written, classifiesprisma.user.create,this.usersRepository.save,session.commit,owners.saveand_context.TodoItems.Addby the whole chain and by the receiver's declared type (OwnerRepository owners,private readonly usersService: UsersService,val owners: OwnerRepository, read from the class body), and followsthis.usersService.findByEmail(…)into the class the type names instead of the name-only guess. A hop resolved this way says so in the panel. -
A Steps tab in
codegraph ui— what happens from here. Pick a screen (or search any symbol and choose What happens from here) and the viewer draws everything it sets in motion as typed steps: the handlers wired to its taps and listeners, the calls that cross into native code, the native events that come back, the store actions it writes, and the calls that leave the app into the network, storage, the device or telemetry — one box per step, an arrow for every way one leads to the next, and on each arrow the condition under which it happens. The plumbing between two steps (hooks, helpers, the components in between) is folded into the arrow and listed in the side panel, exactly as the Screens tab folds a tap's chain into one transition — and every call the panel lists says what it passes, read from the source as written (SecureStore.setItemAsync('userEmail', values.email),axios.post('/auth/login', { email, password })), so a step is not just that something was stored or sent but what. And each handler says what fires it — the JSX prop and its element (onPress · <Button>), the option it is written under (onSubmit · useFormik(…)), the listener or effect it runs from — read from the source at the call site, soonPress={() => handleLogin(values)}and Formik'sonSubmitmakehandleLogina step of its own with the event on the arrow into it. Any step is the next anchor, any link opens as a Flow strip, a cap the walk hit is announced on the step it hit it at, and the picture travels in the URL. React Native + Expo apps get the full picture today; any project gets handlers, stores and calls that leave the index. -
A Screens tab in
codegraph ui— the app the way its user meets it. One box per screen, an arrow for every way of getting from one to another, and on each arrow the condition under which it happens. Click a screen and each of its transitions is labelled beside the screen at the other end of the line with the last condition checked before it happens —→ isCollectedabove/object-detail— laid out so that no two labels overlap and none sits under a line; hover a label, a line, or its row in the side panel for the whole condition and the chain the tap travels through (HomeSearchResults → ItemCard → openObjectDetail), with a link to each navigation call. A screen that returns to where it came from is drawn around the boxes rather than through them, shared chrome (a top bar rendered on ten screens) is one node a row above what it opens rather than the same arrows from every box, a screen that opens many others is wide enough to follow each line back to it and its lines take separate paths through the gap so they fan out instead of stacking, hovering picks the line nearest the pointer, and a helper that chooses the destination after login shows its fork. Projects whose graph holds screen navigation land on this tab. Expo Router apps today. -
Read your graph in a browser:
codegraph ui. Point it at a project you've already indexed and it opens a viewer for it on your own machine. Pick a symbol and you see who calls it on the left, its real source in the middle with a marker on every line that calls something, and what it calls on the right, each one drawn level with the line that calls it. Hover either end and both light up; click anything to step into it. Test callers fold into a single line so real callers stay in view, edges CodeGraph isn't confident about are folded away as "uncertain" rather than shown as fact, and a symbol no test reaches within three caller hops says so on a badge. A blast-radius strip counts what a change would reach. Search with/or Cmd-K across every symbol and file, start from suggested entry points (routes, hubs, files that run code when imported), and follow a trail of the path you walked that lives in the URL, so you can send someone the exact route you took. Click any file path for that file's outline in source order between everything it depends on and everything that depends on it.Run
codegraph uiin an indexed project, orcodegraph ui /path/to/projectfor one indexed elsewhere (codegraph webis an alias). It takes port 4747, or the next free one;--port <n>pins a specific port and--no-openjust prints the URL for a headless box or an SSH session. SetCODEGRAPH_BROWSER=<command>to choose the browser, orCODEGRAPH_BROWSER=noneto never open one.The viewer listens on
127.0.0.1only, so nothing on your network can reach it, and requests claiming to come from any other host are refused. It opens an index that already exists, never creates one, and never changes your graph or a line of your code — the one thing it writes is a trail you asked it to save (see below), and--read-onlyturns even that off. It sends nothing anywhere. -
A map of the whole project, in
codegraph ui. The Map tab draws your repository at module granularity — one box per directory — with dependencies pointing down, so the top of the picture is what runs first and the bottom is what everything else stands on. Nothing is placed by hand and nothing floats: a module sits one layer above whatever it depends on, line weight is how many calls, imports and type references cross the link, and the same project always draws the same picture. Hover a link for what crosses it, including the busiest symbol pairs behind the weight; click a module to isolate its links, list its dependencies and dependents with counts, and jump straight into one of its files.It says what it leaves out. Links carrying only a handful of references stay hidden until you select a module they touch, references CodeGraph isn't confident about are excluded from every count on the screen and the number is printed, and mutual dependencies, module loops and circular imports between files are listed rather than straightened away. The vertical order rests on the dependencies your code actually writes down — imports, qualified names, inheritance, typed receivers — because a method name shared by two unrelated folders should not be able to move a box.
It opens on your project's source directory; a picker switches to any other top-level folder or the whole repository, a checkbox brings tests in, and
depthsplits a large folder into its sub-folders — useful on a monorepo. What you're looking at lives in the address, so the view is shareable. -
Ask how one symbol reaches another, in
codegraph ui. Type "how does execute reach getFile" into the search box — orexecute -> getFile— and the Flow strip draws the call path between them, left to right, one card per hop. Each card is opened at the exact line that makes the next call rather than at the top of the function, so reading the strip is reading the handful of lines that actually carry the work; the identifier being called is a link, and clicking a card opens it in the symbol screen with the trail already set to the path you've read so far.A dashed link is a hop nobody can see in the source — a callback, an interface dispatch, a React re-render, a JSX child — and it names the mechanism and, where CodeGraph knows it, the exact line the handler was wired at. When a name means several definitions, the strip says so and names the one the path runs through, offers the alternatives in a picker, and can draw them together as one branching diagram.
The "Read as flow" button on the trail turns a walk you did by hand into the same strip. It is the same path finder
codegraph_exploreleads its answers with, so the picture and what your agent tells you can't disagree. -
When a path runs out, the Flow strip says where — and why. A flow that doesn't reach what you asked about now ends in a small block: "Where the graph stops." It names the kind of dispatch that ended it — a computed member call, a
getattr, a reflective invoke, a typed message bus — and the line it's on, and the card beside it opens at that exact line so you can read the code the block is talking about. Where the key is written in the source (handlers['save']) it shows the key and shortlists the symbols that could be on the other side, marking any you already named; where the key is a runtime value it says so rather than guessing.It also lists what CodeGraph chose not to follow: name-only matches it wasn't confident enough about, with their confidence, and a count of the other calls the symbol makes that this path didn't need. Nothing is invented — no edge is guessed and none is added to your graph — and a flow that does reach what you asked for never shows the block at all. It's the same finding
codegraph_exploreannounces to your agent when a flow breaks, so the screen and the answer agree. -
Read a whole file, with its call graph in the margin, in
codegraph ui. The file screen gained a Source tab: the file itself, top to bottom, with the same gutter markers as the symbol view and the same right-hand list of what each line calls, positioned level with the line that calls it. A 6,800-line file scrolls at full speed — only the lines on screen are ever drawn, and the text pages in behind you while the markers are there from the first frame.In the left margin is an arc for every call that stays inside the file, drawn from the calling line to the line the callee is defined on. Nothing is laid out by an algorithm — the author already put the symbols in order, so source order does the work, and this is the one place a file's internal call structure is legible at a glance. Hover a line to light the arcs the function under your cursor takes part in, and click an arc to jump to the other end. On a file with more than forty of them the picture narrows to the symbol you're reading instead of drawing a wash of overlapping sweeps, with the total in the header. A rail on the far left lists the file's symbols and follows you as you scroll, when the window is wide enough for it.
-
The viewer keeps up with your project while it's open. Save a file and
codegraph uisays so within about a third of a second: a banner on the file's screen explaining that the index hasn't caught up yet, and the file's current source in place of a body sliced at line numbers it no longer has — the call arcs, gutter markers and call list go away with the old numbering rather than pointing at the wrong lines. It's the same answercodegraph_nodegives your agent about a file that changed after its last sync.When anything re-indexes the project — your agent's background sync,
codegraph sync, a git hook — whatever is on screen re-reads the graph and a small "Index updated · reloaded" note appears. A symbol that moved because you added a line above it is followed to its new place, with your trail intact, instead of turning into a dead link.Nothing polls: the viewer watches for these two things and is told about them. If it loses touch with the server it retries a few times with a growing delay, then stops and says "Not live" in the top bar rather than hammering a port that isn't answering.
-
Take a flow or a map with you: copy it as an image, or save it as an SVG. The Flow strip and the Map both gained Copy image and Download SVG. Copy image puts a PNG on your clipboard, ready to paste into a pull-request comment or a chat — the fastest way to say "here is what your change actually touches" without asking anyone to install anything. Download SVG saves a file for a README: it is real text rather than a bitmap, so it stays sharp at any size and the symbol names in it are selectable.
Both render the light theme whichever one you are reading in, because the image is going to be read on somebody else's screen, and both carry a caption saying what the picture is. What comes out is exactly what is on screen — the same hops, the same dashed dynamic-dispatch links with their wiring sites, the same "where the graph stops" block, the same modules dimmed or brought forward by your selection — because the image is drawn from the same measurements the screen is, not photographed off it. An eight-hop strip comes out around half a megabyte, well inside what GitHub takes inline.
-
"Where does anything start?" has a screen now. The Entry points tab in
codegraph ui(or presse) is the first thing worth opening on a codebase you have never seen. Every route with the symbol that serves it and thefile:lineyou will find it at, grouped by the file the URL is registered in — your router, not your handlers — and headed with the framework CodeGraph detected it from. Under that, the files that actually do something when they load (a CLI, a worker entry, a build script), the tests ranked by how much of the project each one exercises, and the symbols the most code depends on.None of it is guessed from a filename: a file "runs something" because the graph recorded a call from the file itself, and a project with fewer than three routes simply has no Routes section rather than an empty one. Every list says how much of itself it is showing, and says "at least" wherever the real total can only be a floor.
Any row that names a symbol can start a flow: press
Flow ›, then type a second symbol or press→ hereon another row, and you get the path between them — so "how doesPOST /v1/payroll/cycles/{cycleID}/runreach the database" is two clicks. Typing into the search box now finds entry points too, under their own heading below the symbol matches, so a URL comes back with its handler attached instead of on its own. -
Syntax colouring in
codegraph uinow comes from CodeGraph's own reading of your code. The viewer used to run a second syntax highlighter over source CodeGraph had already parsed, with its own separate set of grammars. It doesn't any more: the colouring is taken straight from the parse that built your graph, so a file is coloured by exactly the grammar that decided what its symbols are. Three things you will notice — the name a definition declares now stands out on the line that declares it, wherever it appears; calls written inside a string (${user.name()},#{...},$"{...}") are read as code and are now clickable links like every other call site; and built-in type words such asstring,intandvoidlook the same in every language instead of one way in Go and another in TypeScript. A big file paints far faster, most visibly in TypeScript, which was by a wide margin the slowest before.Two formats change for the worse and it is worth saying so: Liquid, Razor, YAML, Twig, XML and
.propertiesfiles are shown without colouring now, and in.svelte,.vueand.astrofiles the<script>block is coloured but the surrounding markup is not. Nothing about navigation changes there — call sites in those files still link, exactly as before.This also takes about 3 MB of grammar files and two dependencies out of the install.
-
The viewer's screens are now a component library other tools can render. The Symbol view, the Flow strip and the Map are packaged as
@colbymchenry/codegraph-ui— the same componentscodegraph uidraws, not a copy of them — so another application can show you a symbol's callers, a call path or your architecture over its own copy of the graph. Everything a screen knows arrives through one small interface it is handed, so the tool doing the rendering decides where the data comes from and where a click goes; a design-token stylesheet ships with it so the screens can be themed to match whatever they are embedded in. It is versioned with the engine, so the reader and the graph it reads always match.Nothing changes for
codegraph uiitself — it is the same viewer, now the library's first user. The package is prepared, not yet on npm. -
See what a type is built on, and what is built on it, in
codegraph ui. Open a class, interface, struct, trait or enum and a small tree now sits above its members: what it extends and implements, going all the way up rather than stopping at the direct parent, and everything that extends or implements it, going down. Inheritance is drawn with a solid line and implementation with a dashed one, so the two never read as the same relationship, and every row opens the type it names.For an interface, the list below it is the answer to a question source code cannot give you: a call through that interface can land on any of them, and where there are enough of them to make a static answer meaningless the block says so in a sentence. Go's implicit interface satisfaction is included — a struct that satisfies an interface without either file mentioning the other appears in the fan, marked as matched by CodeGraph rather than written down, along with the line it was matched at. Long fans fold behind a "+N more implementations" button rather than being cut off, and if there is more below than was walked the tree says that too.
Members that redeclare something from a type above are marked in the outline ("overrides Base", or "satisfies Clock" for an interface), so a 40-member class shows at a glance which parts are its own and which are a contract it is filling. The number of implementations shown here is the same number
codegraph_explorereports to your agent when it announces an interface dispatch. -
Find the code nothing reaches, in
codegraph ui. A new Dead code tab lists the symbols no import, call or reference anywhere in your project reaches — biggest first, grouped by the file they live in, with the number of lines each one would take with it. A class nobody uses brings its methods along as a single finding rather than eleven. Every row opens the code.The screen is built to be believed rather than to look impressive. A line above the list says, and keeps saying, that this means "no static reference in the index" and not "unused" — reflection, a framework registry and a template can all reach code a graph cannot follow. Under the list, every reason a candidate was left off is printed with its count, so you can see the list is twenty findings out of two and a half thousand candidates rather than twenty out of twenty-one.
Those exclusions are the feature. Anything exported, or declared in a header, is off the list by default, because something outside your repository can import it — one switch adds them back, with a warning band. So are test and generated files, abstract and interface declarations, anything a decorator registers, members that override something further up, names the language calls for you (
constructor,__enter__,main), vendored directories, and files nothing in your project reaches at all. Two more rules catch what the graph itself missed: a name CodeGraph failed to resolve somewhere is never called unreferenced, and neither is a name shared with a symbol that is used — the twin may simply have been picked instead. Last, before any row is shown, the files that could reach it are read and the identifier counted: written down twice, something uses it and we did not see it.On the Map, a module nothing depends on now says so in its own count line instead of counting itself — usually your entry points, sometimes something you forgot to delete. Tool-generated files and modules are dimmed wherever they appear: on the map, in its file list, in search results and on the file screen.
-
Keep a walk you want to come back to: saved trails in
codegraph ui. Follow a path through the code, press Save trail on the trail bar, give it a name, and it's kept. Saved trails are listed on the empty screen and on the Entry points tab, above the suggestions — a walk somebody named beats any ranking — and opening one puts you back at the symbol you left with the whole path in the trail bar. Explaining "how a request is served" to a new teammate is now a link and a name rather than a paragraph.A saved trail survives your project changing. Each step is remembered by what it is — its qualified name, its kind, the file it was in — rather than by where it sat, so editing the file above a function doesn't lose it. When something does move, the trail says so on its own row: which step moved to another file, which one was renamed or deleted, and which part of the walk still opens. It never quietly stitches over a missing step, because a trail is a path and a step that skips one would show a call that doesn't exist.
Trails are plain JSON, one file per trail, under
.codegraph/ui/trails/— already ignored by git, so they stay yours by default. Export hands you the file if you'd rather commit one for the team. This is the only thing the viewer writes: it still never indexes, never changes your graph, and never touches a line of your code. Start it withcodegraph ui --read-onlyand it won't write even that — saved trails can still be opened, just not saved or deleted.
Fixes
- Steps diagrams retain database operations made through external client chains without inventing internal dependencies.
Fixes — Screens, links and navigation
-
Routes served by a component are listed. Entry points and the Steps picker listed a route only when a function, method or class served it. A Vue Router, Nuxt, Svelte or Astro screen is served by a component, so those apps' routes weren't listed at all. They are now, each with the component that serves it.
-
Where the app goes after login is a fork, not two always-es. A navigation whose destination comes back from a helper —
router.replace(await resolvePostLoginRoute())overreturn (await hasSeenWelcome(…)) ? '/home/' : '/welcome/'— drew both screens with no condition, reading as if the welcome screen always shows. The two arms share a line, and only a column can tell them apart; each synthesized edge now carries its literal's own position, so the guard reader says which arm it is:WHEN await hasSeenWelcome(…)→ home, and its negation → welcome. And the scan starts at the helper's body, so a literal-union return type —Promise<'/welcome/' | '/home/'>, whose routes are string literals too, written first — no longer stands in for the navigation itself. Re-index after upgrading to pick the positions up. -
A screen that talks to native code keeps its own navigations. In a React Native or Expo app, a
router.pushwritten inside a listener for a native event was credited to whichever screen had started that round trip, not to the screen the push is written on. In one app that moved seven transitions off the capture screen and onto the review screen it opens — leaving the review screen looking as though nothing in the app could reach it, stranded in the "no transition reaches this" band at the bottom of the Screens tab, and printing Swift conditions likeThread.isMainThreadon a JavaScript navigation. A navigation now belongs to the screen whose file it is written in; an event arriving from native code, from an HTTP call or off a queue is no longer read backwards as if it were a caller. -
A link written under a condition says so on the Screens tab. A checkout stepper whose tabs are each enabled by their own prop, and a navbar whose admin links only render for an admin, both read as always — every transition written in markup was drawn with no condition at all, while the ones written as calls carried theirs. They are read the same way now: a store's checkout tabs say
step1…step4, its navbar saysuserInfo && userInfo.isAdminfor the admin links and!userInfofor sign-in, and 59 of that store's 74 transitions carry the condition they actually run under, up from 20. A template language with no condition rules of its own still says nothing rather than guessing. -
A link in markup no longer reads as a helper's return value.
<Link to='/shipping'>was labelledreturn /shipping, which in this picture means the destination came back from somewhere else and was inferred. It is written right there, so it now readslink /shipping— and an internal<a href>readsa. Only a destination that genuinely arrives from elsewhere still saysreturn. -
API endpoints are no longer drawn on the Screens tab. A store's thirty Express endpoints sat beside its nineteen pages as boxes nothing navigates to and nothing leaves — in a picture that is only about navigation — and they stretched the row of unreachable pages hundreds of boxes wide. A screen is now a route named by its path alone; a route named with the method that reaches it (
GET /api/orders,POST /api/users/login,ANY /api/users) is a request, not somewhere a user can be. Every route still appears on Entry points, which is the list of everything a request or a user can arrive at. -
A screen you could reach but never leave. Three separate things left a page's own navigation off the map, and a store's home and sign-in pages showed nothing leaving them. One component, several addresses: a screen rendered at more than one route — a listing page that is also the search and the paginated results — handed all of its navigation to whichever route happened to be written first, and the rest were drawn as dead ends; every address it serves now gets it. A link written as a choice:
<Link to={redirect ? \/register?redirect=${redirect}` : '/register'}>is how a link that carries state is written, and markup was read by a weaker reader than calls were, so it saw nothing; both now use the same one. **A destination whose other half is unknowable:**const redirect = location.search ? location.search.split('=')[1] : '/'followed byhistory.push(redirect)is how every app sends a user on after signing in — the/` is where it lands by default, and reading neither half lost the whole transition. Where both halves ARE readable and disagree, it is still a fork and still nothing. -
A server action written through a wrapper starts its transition on the right page.
export const signIn = validatedAction(schema, async (data) => { … redirect('/dashboard') })— the arrow inside is no symbol of its own — now belongs tosignInon the Screens tab, andsignInis attributed to the page whose component hands it touseActionState(signIn, …), read from the source when the graph holds no such edge. -
React Router apps that keep their routes in a table get a Screens picture. The ASP.NET Core React template's
AppRoutesarray, aroutesarray handed touseRoutesorcreateBrowserRouterfrom another file, and admin kits'[MainRoutes, LoginRoutes]route objects drew no screens, because their routes were never read. They are now, with the links andnavigate(…)calls between them. A parent route object's component is drawn as the layout around the screens inside it: its own links count from each of those screens, and when anindex: truechild claims its address, that child is the screen there. Re-index React projects after upgrading. -
A React Router layout written as JSX is drawn around every screen inside it. In
<Route path="/" element={<Layout />}><Route index element={<Home />} />…</Route>, the screen at/is Home rather than Layout, and the links in Layout's header count from each screen nested in it instead of only from/. A guard with no path of its own, like<Route element={<PrivateRoute />}>, does the same for its redirect to the login page. Re-index React projects after upgrading.
Fixes — Steps — what a call does, and when
-
A Steps picture with nothing past its anchor says why. Starting Steps from a helper that only computes drew one box and no explanation. The panel now says that nothing it sets in motion is a step the picture draws, explains that calls between plain functions fold into the lines, and links to the symbol's callers and callees.
-
Steps no longer goes blank on a function full of checks. Reading a function in its code's order doubled the ways forward at every
ifwith noelse. A body with dozens of checks that draw nothing, like jsoup'sparse, never finished, and the page died with a stack overflow. Ways that arrive at the same place are now one way, carrying only the conditions they all share. -
A Swift app's Steps picture labels what a call leaves for correctly.
rawValue.data(using: .utf8)was drawn as a network call, Foundation'sTimeras telemetry,Calendarand a SiriKitintentas device calls, andviewModel.votes.firstIndex(of:)as a database read. Now only a session'sdata,uploadanddownloadare network,Timer,Calendarandintentare no effect at all, a view model is not a table, and a keychain is storage. A call through a type the app declares itself, like IceCubes'Notificationsendpoint enum, is no longer read as the library of the same name. -
A server-rendered endpoint's Steps picture shows how it answers. A Spring MVC handler answers by what it returns: a view name, a constant holding one,
"redirect:/owners/" + id, aModelAndViewor aRedirectView. Each is now a reply, a render (200) or a redirect (302), under the condition it's returned in. petclinic'sPOST /owners/newdraws the form re-renderedWHEN result.hasErrors()and the redirect otherwise. A Laravel controller'sview(…),redirect(…),redirect()->back(),back(),to_route(…)andresponse()->json(…)are replies too, so BookStack's book page draws the page it renders beside its404and its old-slug redirect. PHP's own programming-error exceptions (InvalidArgumentException,LogicException, …) thrown deep in a helper are no longer drawn as one of the endpoint's answers. -
A handler called from under a binding says what it passes. A press that runs
tryCatchSync(onClosePress)drew a box for the wrapper and stopped — leaving the one thing a reader asks ("what is being wrapped?") unsaid, even though every other call-shaped site already prints its arguments. The panel and tooltip now saytryCatchSync(onClosePress)— the argument is the answer. -
A step the walk stopped at keeps its whole name. A boundary — another screen, or a cap the walk hit — ends its name with an ellipsis by design, but the box was not sized for it, so a longer name lost its last letters instead (
/scan-to-verif…for/scan-to-verify …). The anchor's start mark clipped a long path the same way (/sheets/forgot-passw…). The box now makes room for both. -
A server action, or any handler written inside a wrapper, draws what it really does.
const signIn = validatedAction(schema, async (data) => { … })showed one call out of nine, because the constant held a reference to its schema and that counted as having a body of its own. Its picture is now whole — the lookup, the early returns, thePromise.all, the redirect. -
A call is no longer followed to a same-named method of its own class.
crypto.createHash('sha256').update(…)inside a service that happens to have anupdatemethod was followed into that method, so a login endpoint read as though it updated the user, extra replies and all. A method of your own class is writtenthis.update(…); a receiver that is notthisnow ends the walk instead of guessing. -
A step is ordered by the call it was actually reached at. An endpoint whose handler is written inline at the registration (
router.post('/users/login', async (req, res) => { … })) had every call in the handler read as part of the registration itself, which put its reply before the work that produces it. -
A row reads in the code's order. The boxes one step away from a handler now sit left to right as the code runs them — the database read, the token signed while the reply is built, the reply, the error reply — instead of in an arbitrary order; a call written inside another call's arguments (
generateToken(…)inres.json({ token: generateToken(…) })) comes before it, and its link saysinside res.json(…)in the panel and the tooltip. -
Each way an endpoint answers is its own box. A handler that replies
200or401now draws two reply boxes instead of one200 · 401, so each line from the handler carries its own condition on the picture —→ user && (await user.matchPassword(password))into the 200,→ NOT (…)into the 401 — the way a screen's transitions do; replies whose status the code does not spell out share one box labelled by the call. -
A reply that sets no status is a 200.
res.json(user),res.send(…),reply.send(…),NextResponse.json(…), aJSONResponseorjsonify(…)with no status in the chain now count as200, so an endpoint's success reply has a box of its own beside the401's and its row carries its code; a status set by the statement just before (res.status(202); res.json(user)) is that reply's. An Express handler written inline at the registration keeps its own replies too — they were filtered out with the framework noise. -
The Steps walk stays out of test doubles. A production handler that calls an interface method is no longer followed into
TestUserDataRepository(or any implementation in a test folder); the counts in the summary no longer say "outside the indexs". -
Conditions read as words, and each scenario gets its own row. In the Screens and Steps tabs a condition is now written the way you would say it —
WHEN NOT (isUploadInProgress || elapsed < 5000) AND user?.organization_id, the joining words set apart from the code — with the code inside each guard left as written, and a transition or step with several call sites (four early returns that each go home) is listed as four rows, the clauses they all share said once above them, instead of one string joined with||. A guard that is itself an either/or keeps its parentheses everywhere conditions are shown, includingcodegraph_explore's Flow section. -
Every call now says when it happens. In
codegraph ui, a symbol's callee and caller rails and the Flow strip's connectors carry the branch conditions the call site sits under —when !isUploading && isCollected— andcodegraph_explore's Flow section prints the same on each hop (↓ calls (when isCollected)). The conditions come from theif/else/ ternary /switch/&&branches around the call, the early returns before it (if (busy) returnreads as!busy), and Swift'sguard; an inline callback inherits the conditions of the place it is defined. Read from the source as it is now, never stored: nothing about your index changes. TypeScript, JavaScript and Swift today.
Fixes — Symbols, tests and the viewer
-
Dead code no longer lists methods a framework calls for you. A method with a framework's decorator on it — Angular's
@HostListeneror@HostBinding, NestJS's@Cron,@OnEventor@Process, TypeORM's@BeforeInsert, Spring's@InitBinder— was listed as unused whenever the decorator came from a package outside your project. So was a lifecycle hook like Angular'sngOnInitor NestJS'sonModuleIniton a class that skips theimplementsclause, though the framework calls it by name all the same. Both are now left off and counted under the list. What counts as a hook is learned from your own classes that do write the interface out, not from a built-in list, so an unused helper on the same component is still listed. Nothing needs a re-index. -
Dead code no longer lists the types you add to a library's or the browser's. An interface written inside
declare module 'chart.js' { … }ordeclare global { … }, like chart.js'sPluginOptionsByType, TanStack Router'sRegister, the browser'sWindow, or a custom element's entry inHTMLElementTagNameMap, merges into a type declared outside your project. The library or the browser reads it from there, so nothing in your code names it, and every one was listed as unused. TypeScript exports what these blocks declare without anexportkeyword, and CodeGraph now records them the same way, so they are left off and counted with the exported symbols. Adeclare namespacein an ordinary module file still belongs to that file and is checked as before. Re-index TypeScript projects after upgrading. -
One slow file can no longer freeze the viewer. Code is highlighted with the same grammars the index uses. The COBOL grammar never finishes a line like
., which free-format COBOL paragraphs end with, so opening a Flow through cobolcraft frozecodegraph ui, and every page after it waited forever. Highlighting now runs where it can be stopped: a piece of code that takes too long is shown unhighlighted, and everything else keeps working. -
The Map opens on a picture, not a single box. A project whose code sits in one flat folder, like Express's
lib/, an R package'sR/, an Erlang app'ssrc/or fmt'sinclude/fmt/, opened as one box with nothing to say. It now opens on the whole repository when that draws more. Files at the repository's top level also count when the Map picks where the program lives: git's hundreds of top-level.cfiles had madebuiltin/look like most of the code. -
A Flow between two symbols finds names of any shape. A Flow opened from a Steps link, an entry point or the search box names its two ends by the symbols' own names, and names that don't look like plain identifiers were dropped: an Objective-C selector like
initWithFrame:, a Ruby method likesave!orvalid?, and any name shorter than three letters, likeok. The strip then said the other end "names nothing in this index", even though it was there. Both ends are now looked up exactly as written. -
Entry points and the Symbol view no longer go blank. The Symbol view of every Vue, Svelte and Astro component stayed on "Loading…", because its member list held the component's script symbols twice, along with the component and its file. Entry points went blank on apps with inline route handlers like
app.get('/x', async (c) => { … }), which came back once per call in the handler, each call listed as if it were the handler. Each member is now listed once, and each route is one row: the handler it is bound to, or "inline handler" when the handler is written at the registration. -
Entry points' most-depended-on list shows your code, not a vendored bundle's. A minified library or bundled docs script (
n,t,Bufferwith thousands of callers, all from inside the bundle) could top the list. Symbols in generated files are now left out of it, like test files. -
Stopping
codegraph uistops the server. Killing the command by its process id, as a process manager, an editor task orkilldoes, left the server running on its port with no way to reach it, until the machine restarted. The server now notices it has been left behind and shuts down, closing the index first. Ctrl+C was never affected. -
A SwiftUI view with a preview is no longer listed as a file that runs something. A
#Preview { … }sits at the top level of a view's file, so every view with one showed up under entry points as if it ran code. A preview is Xcode's canvas, not code the app runs: its calls no longer count, and a file whose only top-level code is previews leaves the list. -
The Map opens a Maven or Gradle project on its packages. A Java project keeps every file under
src/main/java/org/<company>/<app>/, and those folders hold nothing but the next one, so the Map drew the whole program as onesrc/main/java/orgbox and no grouping option reached further. A folder with one subfolder and no files of its own no longer counts as a level: petclinic opens onowner,vet,modelandsystem, each labelledsrc/main/java/…/petclinic/owner, with the full path on hover. -
In
codegraph ui, routes whose handlers live in more than 60 different files are all linked to their handler, instead of the later ones showing "not in the index". (#1975) -
The viewer continues to count module-level initializer calls as top-level file activity in entry points and file screens.
-
The Map groups a repository the way that repository is shaped. It always drew top-level directories, so a project whose whole program lives under one
src/opened as a picture of four boxes —src,ios,.github,(root files)— with two thirds of the code inside one of them and nothing to say about it. The Map now picks its own grouping: the shallowest one that is not a single box holding the program, so a mobile app opens onsrc/app,src/components,src/api,ios/CaptureViewand the rest, and a project packaged asfrontend/src/…opens on the screens, components and reducers instead of on the wordfrontend. A repository whose top-level directories really are its modules is left exactly where it was. A new Grouping control on the right says which one was chosen and lets you take it a level in or out, and a leaf directory is now named for itself rather than as…/(root files). Each box now also says how much leans on it — how many files elsewhere reference straight into it — with a bar along its bottom edge scaled against the most depended-on box on screen, so the folder you have to be careful with is the one you can see at a glance rather than the one with the longest name. The Map also has a Key now, like the Screens and Steps tabs — including what the dashed maroon lines mean, which only appear once you select a module: that module reaching back UP into something that depends on it. -
The Symbol tab opens the Symbol tab. With no symbol open and no trail to return to, clicking Symbol in the top bar took you to the landing page — which, on any project that has screens, is the Screens tab. So the button said Symbol and gave you somebody else's view. It now has an address of its own (
#/s) that opens the "nothing selected" screen: the search prompt and the where-to-start list of routes, entry files and the symbols the most code depends on. -
The Map covers a multi-root project. A React Native app's
ios/beside itssrc/— or any second root holding a fifth of the code — is now on the picture, one level deeper, instead of the map silently drawing only the larger root. -
Fixed a long-running
codegraph uisession serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, orcodegraph sync— rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it. -
A viewer trail now holds up to 64 hops everywhere — the trail bar, saved trails and "Read as flow". Thanks @inth3shadows for the report and @danusha2345. (#1976)
-
codegraph uion Windows moves on from a port another program holds. Windows refuses a port that another program keeps for itself, or one inside a range it reserves, with a different error than a port that is simply in use, so the viewer gave up at the first such port instead of trying the next one, and blamed ports below 1024 needing elevated privileges — a rule Windows doesn't have. It now moves on to the next free port, and when none is left, or the port you pinned with--portis refused, it says what Windows did and how to list the reserved ranges. Thanks @ijbranch for the report and @sx4im. (#2299) -
A file's outside imports are listed in more languages. The file screen lists the imports that name nothing in your project, like a third-party package or a system header. In C and C++, Objective-C, JavaScript and TypeScript, Python, C#, Lua, R, Solidity, Svelte and Vue, an import that named no file had been linked to its own import statement, so it was listed nowhere. Re-index after upgrading.
-
What an Angular template uses is no longer dead code. A signal,
computedvalue, getter or method that a component's template calls from a property binding, an interpolation, a structural directive or a control-flow block —[name]="icon()",{{ label() }},*matRowDef="let row; columns: displayedColumns()",@if (loading())— or hands to a child, like[displayWith]="displayFn", now counts as reached, so it leaves the Dead code tab. Steps draws it as part of what the screen renders, not as something the user does. Re-index Angular projects after upgrading.
Entries whose graph and codegraph_explore half already shipped
The release notes carry a reworded version of each of these. Keep only the viewer half at launch.
-
A Next.js app lands on the Screens tab like a mobile app. App Router pages (
app/(group)/blog/[slug]/page.tsx→/blog/:slug) and Pages Router pages are screens bound to the component they export;<Link href>, an internal<a href>,router.push/router.replace(next/navigationandnext/router),redirect()/permanentRedirect()in a server action or a page, and the middleware'sNextResponse.redirect(new URL('/login', req.url))are the transitions between them — each attributed back to the page it starts on with the plumbing folded and the condition on the arrow, a link written in markup drawn dashed as an inferred hop.app/api/**/route.tsexports (GET,POST, …) are endpoints bound to their functions,pages/api/*handlers areANY /api/…, and a page's Steps picture fires from its load (FIRES FROM page load · /users), draws the data it reads, the handlers it wires, the server actions it crosses to and the pages it leads to as boundaries. A response's status written as{ status: 201 }is read too. Re-index after upgrading. -
TanStack Router apps land on the Screens tab too. Routes are read both ways a TanStack app declares them: file-based, where
createFileRoute('/posts/$postId')carries the whole path as a literal, and code-based, where eachcreateRoute({ path, getParentRoute })names a fragment that is composed through its parent into/posts/$postId.navigate({ to })fromuseNavigate, a thrownredirect({ to })from a loader orbeforeLoad, and<Link to>/<Navigate to>are the transitions between them. TanStack is the one router here whose destination is the route PATTERN rather than a filled address —<Link to="/posts/$postId" params={{ postId }}>names the route and passes the values beside it — so a destination is read as a pattern and matched against the route it names. Addresses that are not pages are left off the map: a_authsegment is a pathless layout and never appears in the URL, a(group)folder is invisible, a__rootroute wraps everything without being a page, and a file that renders an<Outlet />is the layout around an address while the index route beside it is the page at it. A computedto, a pattern no route serves, and anavigate({ search })that only changes the query are left out rather than guessed. Re-index after upgrading. -
Vue Router and SvelteKit apps land on the Screens tab too. Both drew their screens as islands with no transitions, so the tab stayed hidden; now the navigation between pages is read for each. Vue: the routes are read out of
createRouter({ routes: [...] })— path, name, and the view each entry names, including a lazycomponent: () => import('@/views/Login')— androuter.push/router.replace/$router.push, Nuxt'snavigateTo, and<router-link>/<RouterLink>/<NuxtLink>are the transitions. Vue apps usually navigate by route NAME rather than by path, sorouter.push({ name: 'profile' })and:to="{ name: 'profile' }"resolve by name, androuter.push({ path: '/', query })by path. SvelteKit:goto('/login'),redirect(303, '/article/' + slug)from a load or a form action — whose destination is its second argument, after the status — and the plain<a href="/login">that is a link in a SvelteKit app. A SvelteKit page also opens with a body now — it is joined to the page file that serves it and to the+page.server.jsbeside it — so its Steps picture draws its loader's work, its form actions, and the auth guard the loader performs (redirect(302, '/login')underif (!locals.user)) as a transition to the sign-in page, with the condition on the arrow. A computed destination, a path or name nothing declares, a relative path in a nested route, and a conditional whose two arms go to different pages are left out rather than guessed. Re-index after upgrading. -
A React Router app lands on the Screens tab too.
<Route path='/payment' component={PaymentScreen}>(v5),<Route path='/payment' element={<PaymentScreen/>}>(v6) andcreateBrowserRouter([{ path, element }])already named a project's screens; now the navigation between them is drawn as well.history.push('/placeorder')andhistory.replace,navigate('/placeorder')fromuseNavigate,redirect()in a loader or an action, and<Link to>/<NavLink to>/<Navigate to>/ react-router-bootstrap's<LinkContainer to>each become a transition — attributed back to the screen it starts on, with the plumbing folded and the condition on the arrow, a link written in markup drawn dashed as an inferred hop. A route with an optional parameter (/cart/:id?) is reached by both/cartand/cart/5. Until now a React Router project's screens were drawn as islands with no transitions at all, and a screen's Steps picture left out every page it sends you to — a checkout step showed its saved payment method but not that it goes on to place the order. A computed destination (history.push(redirect)), a path no route serves, and a relative path inside a nested route are left out rather than guessed, and an ordinarypaths.push('/x')on an array is never mistaken for navigation. Re-index after upgrading. -
The Steps tab follows a web app across its tiers. A page's
fetch('/api/users', { method: 'POST' })(or anaxios/ky/got/$fetchcall, including one through a project instance made withaxios.create({ baseURL })) now reaches the route that serves it in the same index — drawn as a crossing to the server (⇢ POST /api/users) with the handler named on the box and the registration site in the panel, a boundary by default and entered with Continue through, so the picture reads page → handler → the endpoint → its database write → its response. A job put on a BullMQ / Bull queue lands on the@Processmethod,Workerorqueue.processhandler that consumes it; a NestJSEventEmitter2event lands on its@OnEventlisteners (globs included); a socket message crosses from a client'ssocket.emitto the gateway's@SubscribeMessageand back from the server'semitto the component that registeredsocket.on; and a Next.js server action called from a client file is a crossing to the server by its'use server'directive. Each of these is a synthesized hop — dashed, with where it was wired up — andcodegraph_explore's Flow section names them too. Only a literal path or event name pairs: a variable url, a path no route serves, or one two routes serve alike produce nothing. Re-index to pick the new edges up. -
Conditions and arguments for Python, Java, Kotlin, C#, Go and C. The
whenon a call —if/elif/else,switch/when/match, the ternary and Kotlin'sifexpression,try/except/catch,and/or, and the early exits before it (if err != nil { return }reads aserr == nil,if not item.title: raiseasitem.title) — and what each call passes (HTTPException(status_code=422, detail="bad price"),c.JSON(http.StatusCreated, gin.H{…})) are now read for those languages too, incodegraph ui's rails, Flow strip and Steps tab and incodegraph_explore's Flow section. As before: read from the source as it stands, never stored, and a language without rules yields nothing rather than a wrong label. -
A
<Card/>is the Card that file imports, not the first one in the repo. When two components share a name —Section,Picker,FrameCard, one per feature folder — the render link out of a component picked whichever happened to be indexed first, ignoring what the file actually declares or imports. That cost twice over: a component gained a link to something it never renders, and the one it does render was left with nothing pointing at it, so callers, impact and the Screens tab all dead-ended there. In one Expo app it left a folder sheet's navigation standing alone on Screens with no screen behind it, while the link pointed at an unrelated card in another sheet; in Excalidraw every<Excalidraw/>in the app pointed at a documentation preview file instead of the component itself. A tag now resolves to the component that file declares, then to the one it imports — through a@/path alias as readily as a relative path — and a JavaScript tag prefers a JavaScript component over a same-named class in a mobile app's native half. No links are added or removed, only pointed at the right place. Re-index after upgrading. -
A link that goes to one of several places now draws all of them. A destination written as a choice —
!isAdmin ? keyword ? \/search/${keyword}/page/${x}` : `/page/${x}` : `/admin/productlist/${x}`, which is how a paginator shared between a storefront and an admin list is written — used to draw nothing at all, because one edge carried one destination and picking an arm would have been a guess. Every arm is now its own transition, labelled with the path THAT arm takes, so a store's paginated addresses are on the map instead of sitting there unreachable. The same goes forredirect(307, user ? \/profile/@${user.username}` : '/login')in a loader, and forrouter.push(cond ? '/a' : '/b')`. Arms that name the same route still make one transition, and an arm nothing can read still contributes nothing. -
A client call with a type argument binds too.
useSWR<Team>('/api/team', fetcher)andky.get<User>('/api/users/1')reach their routes like the untyped forms. -
A SvelteKit layout is no longer a second screen at a page's address.
+layout.svelteand+error.sveltesit at the same path as the+page.sveltebeside them and were each indexed as a route, so one address appeared in the index two and three times over. Only a page is a route now. -
An Express handler written through a wrapper is the endpoint's handler.
const authUser = asyncHandler(async (req, res) => { … })— theexpress-async-handleridiom — now names the handler in Entry points and starts the Steps walk at it, with the database reads, the token check and the401row read from the arrow's body. -
Files under an
e2e/directory count as tests. Their calls no longer appear as production callers in Steps, dead-code and test badges.