Commit Graph
2986 Commits
Author SHA1 Message Date
Bernhard Jackiewicz e4bb705d05 test(ui): accept the call-feedback fixes with measured browser checks
One check per plan item, measured in the running page instead of
described: canvas identity and fit count across selection and Expand,
camera movement under the wheel, root marker position, toolbar height,
path and call-order stepping, element under the Send button, the prompt
sent to the browser model, the caller list against query_graph, cached
explanations, Overview labels inside a large area, System structure,
Behavior starts and grouped endpoints. Movements are also cut into
frame strips from a recorded video.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:52:05 +02:00
Bernhard Jackiewicz 70e0513a5f fix(ui): show percent-encoded route paths as people read them
Route labels showed the indexed form, e.g. '/%C3%A9dit'. Labels now
decode with decodeURI, which keeps reserved characters such as %2F
encoded and leaves invalid encodings as indexed; grouping and the route
filter work on the same readable form.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:52:05 +02:00
Bernhard Jackiewicz 82a761f46e fix(ui): add ranked flows to a short Behavior start list
The flows fallback only ran with zero entry points. Once route handlers
count as entry points, Django classifies two, and Behavior offered only
those two. Below eight classified entry points the ranked flows are
added after them, each operation once, so Django offers 61 starts again.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:52:05 +02:00
Bernhard Jackiewicz c970c041d1 fix(ui): keep the whole graph on screen while the first scope picture is empty
Selecting a symbol outside the loaded layout first arranges an empty
preview. The scene took that empty arrangement as its placeholder and
drew a blank canvas for about 250 ms before the scope appeared. An
arrangement without nodes is no picture: the whole graph stays visible
until the scope is laid out.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:52:05 +02:00
Bernhard Jackiewicz da9d5c2c28 style(store): brace the new projection branches and classify nodes once
The diff-scoped clang-tidy gate flagged the branches added for route
handlers, Python test files and bounded overview entries. Brace them and
move the label/path classification of a loaded node into ap_classify, so
ap_load stays under the cognitive complexity threshold. No behaviour
change; the architecture_projection, mcp and store suites stay green.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:52:05 +02:00
Bernhard Jackiewicz 41b1e3ec37 Merge call-feedback stream: local agent evidence, answers, limits and header 2026-10-02 20:23:34 +02:00
Bernhard Jackiewicz 9931199dea Merge call-feedback stream: galaxy scope stability, root framing and path view 2026-10-02 20:23:34 +02:00
Bernhard Jackiewicz 6a3001a3bd Merge call-feedback stream: architecture views, routes and style gate cleanup 2026-10-02 20:23:34 +02:00
Bernhard Jackiewicz 971b2238fc Merge call-feedback stream: backend (C): architecture projection budget, route entry points, Python test files 2026-10-02 20:23:33 +02:00
Bernhard Jackiewicz 9d6a5c85a9 style(store): split a declaration and name the entry point passes
The diff-scoped clang-tidy run (make lint-tidy-diff) flagged a combined
declaration and a bare pass count on lines of this branch. The other
findings on changed lines follow the file's existing style (statements
without braces, the + 1 allocation idiom) or sit in functions that were
already above the complexity threshold.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:17:10 +02:00
Bernhard Jackiewicz 028b341fc9 fix(store): keep a top-level test/ directory as a test signal for Python
The Python exemption for a singular test/ directory was meant for shipped
packages like django/test/, but it also applied to a repository's own
test/ suite. Helpers such as test/helpers.py or SQLAlchemy's
test/orm/_fixtures.py became production components, their indexed is_test
flag was ignored, and a route handler there would count as an entry point.

Only a nested test/ is now exempt; a top-level test/ stays a test
directory for every language, together with its indexed flag. The segment
and suffix checks use small helpers instead of hand-counted lengths.
Django and the cbm control repository are unchanged.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:11:35 +02:00
Bernhard Jackiewicz 7c7dce8300 fix(store): give every overview group its share of listed entries
The 4096 overview entries were picked by one global rank, so groups whose
components are all small got none. On Django the 785-component @no-source
group, js_tests and the .github test group listed zero entries, and the UI
can neither open such a group nor offer its components in the picker.

Displayed components are listed first, then each group's own top-ranked
components up to 16, and the global rank fills the rest of the 4096. With
at most 64 groups the shares always fit.

The new overview field is renamed to omitted_component_entries: it counts
overview entries left out, which the root limits.omitted_components
(components not displayed) does not.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 20:02:01 +02:00
Bernhard Jackiewicz 7c81674f9b fix(store): skip test-registered routes and rank indexed entry points first
On Django every handler that the route rule added came from a test-client
call such as self.client.get("/get_view/"), which the extractor indexes as
a route registration: the Route's only CALLS edges come from tests/. The
four false entry points took 11 of the 12 default path slots, and main
dropped from 4 paths to 1.

A handler now counts only when its Route is registered from non-test code
or has no registration call at all (decorator routes). Indexed
is_entry_point nodes come before handlers, so paths serve main first. The
label checks move to load time.

Django: entry points main (4 paths) and delete_selected (7 paths) instead
of five with main at 1 path.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:51:41 +02:00
Bernhard Jackiewicz 6c1337b906 fix(ui): give the add-index entry its own catalog keys
Moving the atlas row's add-index strings into the catalog had reused
projects.menuLabel and menu.projects and changed what they meant, while
their doc comments and the help texts still describe the '[p]rojects'
panel entry. Both keys keep their old text again. The entry reads the
new projects.addIndexLabel and projects.addIndexTitle, and its visible
text is unchanged.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:42:30 +02:00
Bernhard Jackiewicz 9bb07f5495 test(ui): guard the scope heights and cursor zoom of the architecture scenes
The per-scope height reference and zoomToCursor were only checked by
screenshots and a manual probe, because every architecture test mocks
the scene. The height rule moves into source-metrics as
scopeBrickHeights with a drill-in test, and a shallow render checks
that the repository map and both system scene presentations hand
zoomToCursor to their OrbitControls.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:42:01 +02:00
Bernhard Jackiewicz b504f0a0e8 fix(ui): keep neighbouring architecture labels off each other's text
Adaptive label culling shrank every measured label by a fixed 6 px and
4 px per side, so two neighbours could overlap by 12 px and 8 px. The
compact Overview and Endpoints labels have only 8 px and 6 px of padding
plus border, so the end of one label was hidden under the next. Each
label now gives up half of the thinnest padding plus border measured
from the CSS, so two neighbours together never reach the text of
either. The rule lives in a small pure module with unit tests.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:41:43 +02:00
Bernhard Jackiewicz 3ae2aa4941 fix(ui): show exactly a route group's routes when it is opened
"Show these N routes" and a double-click on a route group handed its
prefix to the free-text filter, which matched labels, file paths and
details and added one-hop neighbours. The button promised N routes and
showed a different set: '/admin' also listed '/admin_views/x/' and
routes registered under django/contrib/admin, and the '/' group listed
every route. A filter that names a group prefix now selects routes by
the same first-segment rule that built the group, and its neighbours
add only caller and handler areas. Routes on the root path '/' stay
single, since they share no segment. Other filter text is still a
search.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:40:01 +02:00
Bernhard Jackiewicz 9b26e19faa fix(ui): treat a nested test package as product code in Routes
The test-route heuristic counted any test/ directory as test code, so a
route registered or handled only under a product package such as
django/test/ was hidden by default. The directory rule now accepts test/
only as a test root (test/ or src/test/), next to tests/, __tests__/ and
spec directories, and the file name rules are unchanged.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:39:50 +02:00
Bernhard Jackiewicz 62257fd418 fix(ui): retire a cached explanation when its scope's evidence changed
The explanation cache is keyed by selection and scope shape only, so that
a scope that is still loading finds its explanation. After a re-index of
the same project in the same session, returning to a selection still
showed the explanation of the old graph. Every cached explanation now
keeps the id of the complete evidence it was written from. When the same
selection completes with other evidence, the entry is dropped and the
selection is explained again; while it loads, the cached text stays.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:38:56 +02:00
Bernhard Jackiewicz fdb8142473 fix(ui): let the input limit decide how much evidence a question carries
The input limit only capped token counting. Manual chat always started
from 3,200 characters of evidence, so raising the limit never showed more
of a scope while the capacity note still blamed the model. The first
evidence budget now follows the configured limit (2,048 tokens still give
3,200 characters) and shrinks from there as before.

The output field used step 64 with a minimum of 32, which made the
default 512 and values like 128 or 256 step mismatches and kept the
arrows from reaching 512. It now steps by 32; the input field keeps 64.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:38:27 +02:00
Bernhard Jackiewicz 0f202d781f fix(ui): release the local agent when another tab picks another model
The model choice is stored in the browser and follows storage events, but
only this tab's select released the worker. A model chosen in another tab
changed this tab's model while the old worker stayed loaded: the dialog
called the new model "Loaded", turns recorded the wrong model and the new
model's limits ran on the old worker. The dock now remembers which model
its worker was created for and releases it, and clears the explanation
cache, whenever the chosen model differs.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:37:58 +02:00
Bernhard Jackiewicz 37fd34bb70 fix(ui): keep the placeholder picture out of galaxy filters and counts
While the first scoped picture is arranged, the scene keeps the whole
graph on screen. That placeholder skipped the reader's hidden edge
kinds, and the trace edge filter and the selection evidence read their
kinds and render counts from it, as if it were the new scope.

The scene now draws the placeholder with the hidden kinds applied, and
`shown` is the current picture only, so the filter and the counts never
describe the placeholder.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:37:36 +02:00
Bernhard Jackiewicz ee5d4e8fdb fix(ui): list callers only for the selected symbol and only on list questions
Without a detected subject every caller question counted as one about the
selection, and subjects were found only for camelCase names. "Who calls
get_queryset?" or "Wer ruft as_sql auf?" therefore listed the callers of
the selected JSONBAgg. The question is now read around its verb, so
snake_case and single-capital names are subjects too, and only a subject
that names the selection, one of its roots or "it"/"this" gets the graph
list.

The direction patterns also missed common phrasings and read "Welche
Funktion ruft X auf?" as a callee question. Singular "welche <Nomen> ruft
X auf", "rufen ... auf", "wo wird X aufgerufen", "which/what <noun> call
X" and "where is X called" now ask for callers; a plural noun with "ruft",
"was ruft" and "wird von X aufgerufen" ask for callees. Why, how, what-if
and explain questions go to the model even when they mention callers.

A listed answer now offers "Ask the model", which sends the same question
with the same evidence as a fresh request and replaces the list.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:37:27 +02:00
Bernhard Jackiewicz ebef3d75d6 fix(ui): drop a stale galaxy path and leave Escape to overlays and fields
A path or call order selection was only hidden when the root, trace
direction or edge filter changed. Setting the direction back, or
selecting the same root again after "All graph", brought the old path
back. The selection is now dropped on every new scope key, on a new
selection and on reset.

The window Escape handler for the path view ignored escapeTaken and
typing targets, so one Escape could clear the path while Help, Settings
or Projects stayed open, or take the key from a field. It now steps
back like the fullscreen handler does.

The picked target keeps its name, so removing a layer no longer shows
it as a raw node id.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:37:15 +02:00
Bernhard Jackiewicz d53b008db8 fix(ui): keep large Galaxy scopes whole for the local agent
The selection snapshot has a 16,000 character budget. Roots came first
with up to 1,200 characters of documentation each, so in a large scope
the copy cut whole relationship groups, the symbol totals and names. The
answer then said "to 0 symbols", dropped edge types and still called the
list complete. When a side was dropped entirely the reader fell back to
the old path dump, caller questions went to the model and the explanation
key lost its waiting gate.

GalaxyPanel now builds its evidence through galaxyScopeEvidence. Root
documentation stops at the 300 characters the readers use. Counts come
before names, and the names of all groups share an explicit budget, so a
scope fits the snapshot. The reader recognises a Galaxy scope by its
identity and depth, accepts a missing or cut side and reads the snapshot
omissions. The prompt and the listed answer then say that relationships
were left out instead of claiming a complete list.

The capacity note now appears only when the prompt could not name every
symbol the snapshot carried. The snapshot's own bound of 24 names per
group is no longer reported as too large for the model.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:36:48 +02:00
Bernhard Jackiewicz d74fc714d4 fix(ui): count an answer that ends on its last allowed token as finished
The worker inferred "length" whenever the answer used every allowed token.
Transformers.js also hands the end-of-sequence token to the streamer, so an
answer that ended exactly on the limit was reported as cut and the chat
showed "Shortened (token limit)". The worker now remembers the last token
and reports "length" only when it is not an end-of-sequence id of the
model or tokenizer.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:36:13 +02:00
Bernhard Jackiewicz 016861e0e5 chore(ui): clear the style gate hits outside the chat surfaces
npm run check:style failed on the PR head. This removes the hits that do
not belong to the chat and header work:

- the six long dashes in the ADR preview fixtures become colons;
- the projects menu entry, the coverage selection label and the "Open
  ADR" button take their text from the message catalog again, with the
  wording unchanged;
- the README names the bundled hook by its installation section instead
  of by product name;
- the configuration reference keeps the installer's exact environment
  variable through a narrow exemption in the operational client rule,
  limited to that one variable in that one file, with tests showing that
  any other mention or an authorship claim there still fails.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:17:24 +02:00
Bernhard Jackiewicz 1c36129bfc fix(store): count route handlers as architecture entry points
The projection took entry points only from the indexed is_entry_point
flag, which the extractor sets for main and JS/TS exports. Python web
code never calls main, so Django's Behavior list stayed empty even once
the projection rendered.

Mark a Function or Method as an entry point when it has a HANDLES edge
to a Route node, the same route evidence atlas_flows.c already scores.
Handlers in test code stay out: they are fixtures, and the projection
keeps test and production roles apart everywhere else. The index and the
extractor are unchanged.

For Django 5.2.7 this adds four production handlers to main (five entry
points, 12 paths instead of 4). The other 130 indexed handlers are test
methods and stay excluded.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:17:12 +02:00
Bernhard Jackiewicz b2d75ceb6a feat(ui): group endpoints and hide test routes in Routes
Routes > Endpoints drew every route on its own: for Django dozens of
overlapping labels, nearly all of them only registered, handled or called
from tests/. The model already supported a filter, but the workspace
hard-wired it to an empty string.

Routes sharing a first path segment now fold into one counted group
("/accounts · 12") whose relationships keep their evidence; selecting or
double-clicking a group narrows the view to its routes through the filter,
which the Routes view now offers and which lists matching routes one by
one. A route whose registration, handlers and callers all lie in test code
is hidden by default behind an "Include test routes" toggle that states
how many are hidden; a test calling a production route stays visible.
Endpoint labels use the adaptive label culling of the Overview.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:13:32 +02:00
Bernhard Jackiewicz 2e0b8eaa35 fix(ui): keep the architecture overview readable inside large areas
Opening "django" in the Overview jumped straight to its files: areas were
one folder deep except under src, lib and similar containers. The folder
layout was computed for every file while only 40 were drawn, so the
pruned platforms kept their full size, filled the canvas and flattened
the tilted view; full-path labels overlapped and heights were scaled
against the largest repository area.

An opened area now shows its next folder level as area blocks, applying
the repository area rule to the path below it (so graph-ui shows src/app
as one step, and django shows contrib, db, utils ...). The node cap runs
before the folder layout, so platforms are sized to what is drawn; a
filter that only narrows the drawn map keeps its positions. Outside areas
are named as siblings of the opened one and sit in their own lane in front
of its platform. Labels are relative to the opened area with the full
path in the tooltip, heights compare the bricks of the current scope, and
the Overview uses adaptive labels whose culling measures the real label
boxes instead of the larger service-map estimate. The location trail
offers every drill-in level.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:13:32 +02:00
Bernhard Jackiewicz 648daf71e4 fix(store): classify Python test files by name in the architecture projection
ap_test_path missed Django's app convention tests.py and treated every
file below a singular test/ directory as test code, so django/test/,
Django's public testing API, became test components.

Python discovers tests by file name, so recognize tests.py and
conftest.py next to the existing test_*.py and *_test.py rules, and stop
a singular test/ directory from being the only signal for a .py file
without a test name. The indexed is_test flag of such a file comes from
the same directory rule at extraction time, so it is set aside there as
well; otherwise the class (unflagged) and its methods (flagged) would get
different roles and split into separate components. tests/, __tests__/
and test/ for other languages keep their meaning.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:09:18 +02:00
Bernhard Jackiewicz 01cc2c0c28 fix(ui): keep the scoped galaxy toolbar on one row
At 1600 CSS px the scoped toolbar broke into a second row with the chat
closed, and the path controls made it longer. The search field now starts
from a smaller basis and the scope name is capped earlier, the status and
group count no longer wrap internally, "N connection groups" reads "N
groups" (the full sentence stays in the tooltip), and inside a scope the
node and edge render limits step back into a compact "Limits" menu. The
whole-graph toolbar keeps them inline. With the chat open the row still
wraps, but only the status, group count and limits move to the second row.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:06:11 +02:00
Bernhard Jackiewicz 4cb8d6dd50 fix(ui): rank path targets by name and name the active step
In the browser check, searching the path picker for "Aggregate" listed
__all__ first, because a qualified-name match sorted alphabetically ahead
of the node actually called Aggregate. Name matches now rank first (exact,
prefix, anywhere), as in the node search. The ring on the active step also
carries the node's name, since plain labels of path nodes are too small to
read at the framed distance.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:05:43 +02:00
Bernhard Jackiewicz 8961287899 fix(ui): offer flow entries when Behavior has no entry points
For Django the Behavior start list held only "Choose an operation...":
the limited projection emits no entry points, Python entry points are
only functions named main, the auto-pick accepted only main, and the
behavior source_id 0 the server sends for "nothing selected" was taken
as a real node ID because ?? does not fall through on 0.

source_id 0 now means no selection. When the projection classifies no
entry point, the start list comes from /api/flows (ranked route handlers
and call-graph roots), and among those a conventional handler name such
as handle or run is suggested after main. A limited projection shows the
warnings that limited it in the empty journey instead of "No direct call
witnesses were returned".

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 19:02:16 +02:00
Bernhard Jackiewicz db4c441cc9 style(ui): replace long dashes in the browser agent code
The style gate forbids U+2013 and found nine in the reader context notes,
the chat source ranges and their tests. Ranges now use a hyphen.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:59:26 +02:00
Bernhard Jackiewicz 080c79f2b4 feat(ui): add a path and call order view to the scoped galaxy
The call asked for step-by-step tracing, modelled on Graphify's shortest-path
view. In the scoped Galaxy workspace the toolbar now offers:

- "Path to...": a searchable list of the nodes already loaded in the scope.
  The shortest path from the root follows the scope's trace direction
  (outgoing, incoming walked backwards, or both) over the loaded edges only.
- "Call order": the root's outgoing CALLS sorted by call-site line.

The path or call order is highlighted and everything else dimmed; its edges
are drawn over the scene in their type colour with type labels on the path
edges only, and the active step carries a ring. A compact step list shows
"A --CALLS--> B" with the hop (or call-site line), Previous/Next moves the
highlight, and Clear or Escape returns to the normal view. A new root,
direction or edge filter drops the path; an expansion recomputes it. Hops
are the only distance used here, in line with the plan (G6). Nothing is
fetched from the server, and /api/trace is unchanged.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:59:05 +02:00
Bernhard Jackiewicz 369e9993b5 feat(ui): show the local agent as a robot with a status lamp
The header read "Agent active · Qwen2.5 Coder 0.5B" next to the chat
toggle. It is now a robot icon drawn on the same grid as the chat toggle,
with the status lamp at its corner; a click still opens the agent
configuration. The state and the model name move into the tooltip and
the accessible name, and the model is chosen in the configuration.

The header's agent and chat labels now come from the workspace strings,
which removes the seven hardcoded chrome strings the style gate found in
AtlasChrome.tsx.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:59:03 +02:00
Bernhard Jackiewicz 9e27ae3048 feat(ui): set and keep local agent token limits per model
The agent configuration offered model, automatic explanations and
history only, and the model choice and the automatic flag reset on every
reload.

It now has an input (context) and an output limit for the selected
model, bounded by the model policy: the answer stays within the model's
output ceiling and question plus answer within its browser context.
Limits, the chosen model and "Explain selections automatically" are
stored in this browser under a versioned key and validated on read, as
the view preferences are. Manual chat uses the limits for its prompt
budget and asks the worker for that many output tokens; automatic
explanations keep their short budget but never exceed the limits.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:57:24 +02:00
Bernhard Jackiewicz 41f73838c7 fix(store): bound the overview entries so Django-sized graphs render
get_architecture(system_structure) returned status "limited" for Django
5.2.7 (52,402 nodes, 274,549 edges, below both graph budgets). The
overview listed one entry with a full representative symbol for each of
its 12,700 components. That section alone grew the mutable document to
16.7 MB, the capped components and dependency witnesses took it to
29.4 MB, and serializing the result then crossed the 32 MB response
budget, so the whole projection fell back to the empty limited render.

List at most 4096 overview entries, chosen by the same rank that picks
the displayed components, so every displayed component keeps its entry.
Groups still carry every component id and all counts, and the omission
is explicit: overview.complete is false and overview.limits reports
omitted_components. Graphs with up to 4096 components are unchanged
apart from that new zero count.

Django now renders "ready" with 12,700 components, 256 displayed, 2048
dependencies and 4096 overview entries in a 4.1 MB result (peak 23.2 MB
of the budget, below the 8 MiB job result cap).

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:55:02 +02:00
Bernhard Jackiewicz 0e8b2c17ba fix(ui): keep the galaxy root pinned, marked and framed
After expanding JSONBAgg to two layers the starting node could not be found:
nothing marked it, the fit centred the cloud instead of the root, and the
screen-space separation pushed the root out of the middle.

- The organic layout keeps roots at the origin: a fresh layout is re-centred
  on them, an expansion keeps the centred positions, and a root the previous
  picture does not hold is placed at the origin instead of drifting.
- The scene draws a ring and an always-visible name on each root (up to 12
  roots, DOM like the hierarchy ring), and its plain label is not repeated.
- Screen separation never moves a pinned root; pinned disks are hashed at the
  largest scale so later disks still see them.
- The scoped fit centres on the roots with extents measured symmetrically
  around them, so the farthest node still fits.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:53:46 +02:00
Bernhard Jackiewicz 0fb9504085 fix(ui): reuse selection explanations and wait for complete scopes
Every return to a selection showed "Explaining selection..." and ran the
model again: there was one explanation slot, and the selection key held
the whole evidence text, so counts and the loading state changed it
while the scope loaded. An explanation could also start on the preview
and describe the state before the expansion.

A Galaxy selection is now identified by project, workspace, the selected
scope, direction, edge types and depth. Finished explanations are kept
per selection (at most 32) and shown again on return without a new
generation; "Explain again" replaces the entry. An explanation starts
only once the scope is complete; until then the card says it is waiting.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:53:20 +02:00
Bernhard Jackiewicz ef6725570f fix(ui): frame the next scope picture along the kept view direction
Without a refit, a scope's complete answer could arrive far smaller than the
preview the camera had been fitted to, and keeping the preview's positions
pinned froze layout-derived nodes far out of the complete picture. Positions
are again only carried over from complete pictures. Instead the scene steps
the camera back or forward along its view direction until the outermost
drawn node sits at the margin, never closer to the pivot than a fit would
stand, and only while the last camera move was a fit the reader has not
touched since.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:53:09 +02:00
Bernhard Jackiewicz b38ba0827b fix(ui): answer caller and callee questions from the loaded graph
"Who calls JSONBAgg?" got no caller: the model saw one relationship,
decoded greedily without repetition controls and repeated one line until
the 512 token budget ran out.

Caller and callee questions about the current Galaxy selection, in
English or German, are now answered without the model from the loaded
scope: every edge type with its complete count, the related names by
file and an explicit "+N more" beyond the evidence bound, CALLS first.
The answer says when the scope does not follow that direction or is
still loading, instead of reporting no callers.

For everything else:
- manual chat gets a repetition penalty and an n-gram block as well;
  the n-gram block now checks the answer against itself only, because
  counting the prompt forces quoted names and paths off course
- the worker reports why generation ended and the dock marks answers
  cut at the output limit as "Shortened (token limit)"
- a scope larger than the prompt can name shows "<nodes> nodes /
  <edges> edges: too large for the local <model> model; showing <n>"
- history is trimmed oldest first to the input limit, with a note,
  before the request is refused

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:51:01 +02:00
Bernhard Jackiewicz 447e2d9183 fix(ui): take scoped hierarchy hops from the discovered levels
scopedHierarchy derived each node's hop column from its z coordinate, which
only the arranged hop rings encode. While a scope loads, its preview is built
from the overall layout and keeps the server's global coordinates, so the
columns were wrong until the complete answer arrived. The hop now comes from
the levels the traversal records (roots at 0); z is only a fallback for
callers without levels.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:48:34 +02:00
Bernhard Jackiewicz 69b2ac6b59 fix(ui): send trace_path direction and depth in the server's vocabulary
RpcIntelligenceClient.tracePath sent direction "callers"/"callees" and
"max_depth", but the trace_path schema in src/mcp/mcp.c accepts
"inbound"/"outbound"/"both" and "depth". The server therefore ignored both
arguments and always traced with its defaults. The client now takes and
sends the schema's names.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:48:14 +02:00
Bernhard Jackiewicz c3fe308763 fix(ui): describe Galaxy selections to the local agent as readable facts
The prompt for a Galaxy selection was a dump of JSON paths: snapshot,
view, generation, rendered sizes and omission paths. Relationship items
were cut to 24 before anyone looked at their direction and then to three
before the budget applied, so "who calls JSONBAgg" reached the model with
one outgoing INHERITS edge and no caller.

GalaxyPanel now classifies every scope edge against the roots first and
publishes incoming and outgoing groups per edge type with complete
counts and the related names by file. The agent prompt states the
selection with kind and file:lines, both sides in words, the scope
(depth, direction, filters) and its completeness. Names are listed until
the budget ends and followed by an explicit "+N more"; the packet
reports how many it could name. Other evidence kinds keep the generic
formatter.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:43:44 +02:00
Bernhard Jackiewicz e46f3765b4 fix(ui): say why the system structure is empty
For Django, get_architecture returns status "limited" with zero groups and
the warning that the response exceeded its memory budget. The empty map
said only "No component projection is available within this analysis
budget", or, with Group cycles enabled, that no cycles matched the
filters; the actual reason sat in the collapsed Evidence and limits.

A limited or group-less projection now picks its empty state before any
filter message and shows the warnings that limited it. The two standing
interpretation caveats every response carries are left out there; any
other or reworded warning stays visible.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:42:55 +02:00
Bernhard Jackiewicz 0b03df4c27 fix(ui): zoom the galaxy toward the cursor
Wheel zoom moved the camera exactly along its view direction for every
cursor position, so the reader could not zoom into the part of the graph
they were pointing at. The Galaxy OrbitControls now use zoomToCursor, which
three-stdlib 2.36 supports and drei passes through. Fits and fly-tos still
set the pivot themselves, and the distance limits apply to the new radius.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:42:48 +02:00
Bernhard Jackiewicz e341a4988e fix(ui): keep the galaxy canvas mounted while a scope re-lays out
The organic layout returned nothing while it computed, so every selection,
expansion and preview-to-complete step unmounted the scene and built a new
WebGL canvas, and each new picture counted as a fit request. The camera
jumped to a new orientation every time, and the quick unmount let React
Three Fiber connect its events to a canvas that was already gone, which is
the intermittent "reading 'addEventListener'" page error.

The layout hook now exposes the previous arrangement as stale while the next
one runs, and the panel keeps showing it (or the whole graph before the first
scoped picture). In the scoped Galaxy a fit is requested by the scope itself
(root, direction, edge types), not by each picture; nodes that arrive later
are brought into view by stepping the camera back along its view direction,
as long as the reader has not moved it. A fly-to set before the scene mounted
is not replayed.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:42:11 +02:00
Bernhard Jackiewicz 6e400a35e7 fix(ui): zoom the architecture scenes toward the cursor
Wheel zoom in the Overview, Routes, System structure and Behavior scenes
moved the orthographic camera along its view direction only, so scrolling
over a corner of the map always zoomed into the screen centre. The
installed three-stdlib OrbitControls support zoomToCursor for orthographic
cameras: they keep the point under the cursor fixed and move the orbit
target with it, which the fit and scope-camera code already reads back.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-10-02 18:39:51 +02:00