From 2328e12edff6c1591a54f49adc6d422437e2fba0 Mon Sep 17 00:00:00 2001 From: colwill <62014134+colwill@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:45:36 +0100 Subject: [PATCH] feat: visualiser real-time support --- .gitignore | 3 + README.md | 47 +- assets/vis/vis.css | 283 ++++++- assets/vis/vis.html | 22 +- assets/vis/vis.js | 1060 +++++++++++++++++++++++- docs/EXTENSION.md | 16 +- docs/MCP.md | 2 + extensions/vscode/media/ccc.svg | 1 + extensions/vscode/media/run.svg | 1 + extensions/vscode/package.json | 29 +- extensions/vscode/src/client.ts | 34 +- extensions/vscode/src/extension.ts | 22 + extensions/vscode/src/mcpconfig.ts | 19 + extensions/vscode/src/server.ts | 51 +- extensions/vscode/src/session.ts | 16 +- extensions/vscode/src/visview.ts | 151 ++++ justfile | 24 + src/edit.rs | 376 ++++++++- src/instance.rs | 161 ++++ src/lib.rs | 2 + src/main.rs | 56 +- src/pr_summary.rs | 1 + src/prompts.rs | 144 +++- src/replay.rs | 591 ++++++++++++++ src/scan.rs | 50 +- src/serve.rs | 1221 +++++++++++++++++++++++++++- src/vis.rs | 1210 ++++++++++++++++++++++++++- visual.png | Bin 0 -> 125337 bytes 28 files changed, 5446 insertions(+), 147 deletions(-) create mode 100644 extensions/vscode/media/ccc.svg create mode 100644 extensions/vscode/media/run.svg create mode 100644 extensions/vscode/src/visview.ts create mode 100644 justfile create mode 100644 src/instance.rs create mode 100644 src/replay.rs create mode 100644 visual.png diff --git a/.gitignore b/.gitignore index baa72cf..02db46d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ bench/ # edits applied through ccc's edit tools, read back by `ccc prompts` /.ccc/edits.jsonl + +# timeline steps the ccc servers on this project share +/.ccc/timeline.jsonl diff --git a/README.md b/README.md index 5db7324..1a9050b 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,35 @@ [![Release Collateral Code Check](https://github.com/colwill/ccc/actions/workflows/ccc-release.yaml/badge.svg)](https://github.com/colwill/ccc/actions/workflows/ccc-release.yaml) +

Collateral Code Check Visualiser Example

# Collateral Code Check (`ccc`) -Collateral Code Check tells you, and your AI agent, what a change touches before you commit it - the functions, tests, services and cross-service contracts it reaches - by: +What is becoming obvious is that understanding the changes being made by LLMs/Agents to projects is an increasingly complex problem. - - highlighting which tests will be ran with your changes +`ccc` reduces that problem by providing tools to understand code, visualise changes for humans and allow LLMs/Agents to make the changes they need to with significant oversight by the human in charge. + +## So what is ccc? + +Collateral Code Check - (collateral as-in collateral damage) shows everything a change touches (before you commit it!) visually in your IDE, in your pipeline or anywhere else you need to see it. This includes; functions, tests, services and cross-service contracts it reaches. + +It does this by: + + - Highlighting what pipeline tests run with your changes - - showing if your change violates language linting rules + - Showing if your change violates language linting rules + + - Showing new or modified cross-service calls before you commit - - showing when you create or modify cross-service calls before you commit + - Provides agents/models with accurate real-time insights - - providing language models with accurate real-time insights into your changes + - Provides agents/models with restricted edit controls (no sed, sudo etc) - - triggering specific testing tools based on your changes + - Visualise changes from agents/models before they are applied with the visualiser. - - making your agent's changes for it - a project-wide rename, a dead-code delete, a new function beside an old one - as one confined, previewed, revertible call that costs a fraction of the tokens read-then-edit does with an extra layer of safety ([how much](#edits-go-through-ccc)) +Additionally, benefits for heavy agent/model usage are: + +- Project-wide rename, dead-code delete, new functions beside an old one are a single confined, previewed and revertible atomic call that costs a fraction of the tokens read-then-edit does with an extra layer of safety ([more info](#edits-go-through-ccc)) **ccc** stands on the shoulders of [Tree-Sitter](https://github.com/tree-sitter/tree-sitter). It scans a project and builds the **ccc** code map in memory. This is a human and machine readable map of the source tree including every source file; its @@ -149,8 +162,7 @@ ccc insights --html page.html # output format is html, as a single page `ccc run --vis` serves a 2D map of the project at `http://127.0.0.1:6767/vis` and opens it in your browser. It follows the C4 model from the whole system down to the code, then goes one level -further: the logic of a single function, drawn as a node graph in the style of Unreal Engine -Blueprints. +further: the logic of a single function, drawn as a node graph. | level | boxes | arrows | |---|---|---| @@ -171,6 +183,23 @@ is a call the resolver found evidence for, and a branch is a node in the syntax anything was seen to take. Calls that reach nothing in the project (the standard library, a dependency) are folded into grey pills, and the toolbar can show them in full or hide them. +Along the foot of the page runs the timeline of what agents did through ccc - the same in a browser +as in the editor: + +- **A definition at a time.** An edit is split into the definitions it touches - imports first, + then what it adds, then what it changes, then what it removes - so a replay builds the change up + one function, type or constant at a time, in the order the agent staged it. +- **Looks too.** `find`, `references`, `file` and `dependencies` calls land as looks at what they + named, so you can see what an agent read before it wrote anything. +- **At your pace.** Live keeps the time between steps as it was; ½×, ¼×, ⅛× and ⅒× stretch it, and + following runs behind by however many steps are still waiting. +- **One step on its own.** While a step is on show (◎), what it did not touch fades back, and a big + scene draws only what is wired to it, laid out together to fit the screen. Click the canvas to + bring the rest back. + +Every ccc server on a project shares its steps through `.ccc/timeline.jsonl` (kept out of git), so +the visualiser shows an agent's work whichever server the agent talks to. + The data behind the page is plain JSON, with or without `--vis`: ```sh diff --git a/assets/vis/vis.css b/assets/vis/vis.css index 8aa8f4b..2ba7ddd 100644 --- a/assets/vis/vis.css +++ b/assets/vis/vis.css @@ -139,12 +139,18 @@ button, input { font: inherit; color: inherit; } .brand { display: flex; - align-items: baseline; - gap: 8px; + align-items: center; white-space: nowrap; } -.brand b { font-weight: 800; letter-spacing: -.01em; } -.brand span { color: var(--muted); font-size: 12px; } +/* the logo is white on clear, so it is drawn as a mask in the text's colour - it reads on either theme */ +.brand .logo { + display: block; + width: 72px; + height: 16px; + background: var(--text); + -webkit-mask: url("data:image/png;base64,__CCC_LOGO__") center / contain no-repeat; + mask: url("data:image/png;base64,__CCC_LOGO__") center / contain no-repeat; +} .crumbs { display: flex; @@ -253,6 +259,20 @@ button, input { font: inherit; color: inherit; } touch-action: none; } .viewport.panning { cursor: grabbing; } +/* zoomed out this far the grid is noise, not a measure */ +.viewport.far { background-image: none; } +/* the logo, faint and tiled, under everything drawn - fixed to the window, + so zooming and panning the canvas never move it */ +.viewport::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: var(--text); + opacity: .01; + -webkit-mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='440' height='220'%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='30' y='40' width='110' height='25'/%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='250' y='150' width='110' height='25'/%3E%3C/svg%3E") 0 0 / 440px 220px repeat; + mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='440' height='220'%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='30' y='40' width='110' height='25'/%3E%3Cimage href='data:image/png;base64,__CCC_LOGO__' x='250' y='150' width='110' height='25'/%3E%3C/svg%3E") 0 0 / 440px 220px repeat; +} .surface { position: absolute; @@ -639,9 +659,262 @@ button, input { font: inherit; color: inherit; } } .note-bar[hidden] { display: none; } +/* ---------- edit timeline ---------- */ + +/* what a step changed: added green, modified purple, removed red; a step not + written yet is dashed amber */ +.node.chg { + --chg: var(--primary); + border-color: var(--chg); + box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--chg) 30%, transparent); + animation: chg-pulse 1.4s ease-out 2; +} +.node.chg-added { --chg: var(--green); } +.node.chg-removed { --chg: var(--red); opacity: .85; } +.node.chg-pending { --chg: var(--orange); border-style: dashed; } +/* what an agent looked at, rather than changed */ +.node.chg-read { --chg: var(--blue); } +@keyframes chg-pulse { + 0% { box-shadow: var(--shadow), 0 0 0 0 color-mix(in srgb, var(--chg) 60%, transparent); } + 70% { box-shadow: var(--shadow), 0 0 0 16px color-mix(in srgb, var(--chg) 0%, transparent); } + 100% { box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--chg) 30%, transparent); } +} +@media (prefers-reduced-motion: reduce) { + .node.chg { animation: none; } + .viewport.glide .surface { transition: none !important; } +} +.viewport.glide .surface { transition: transform .6s cubic-bezier(.2, .7, .2, 1); } +.viewport.glide { transition: background-size .6s, background-position .6s; } +/* a step on show, seen alone: whatever it did not touch fades back */ +.node.faded, .wire.faded, .elabel.faded, #frames.faded { opacity: .2; } + +.timeline { + position: fixed; + left: 12px; + right: 12px; + bottom: 12px; + z-index: 14; + display: flex; + align-items: center; + gap: 8px; + padding: 5px 6px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: var(--shadow-bar); +} +.timeline[hidden] { display: none; } +/* no edits yet: only the switches for what comes stay, in the corner */ +.timeline.empty { left: auto; } +.timeline.empty > :not(.tl-ws):not(.tl-follow):not(.tl-speed):not(.tl-focus) { display: none; } +body.with-tl-switches .zoom { bottom: 62px; } +body.with-timeline .legend { bottom: 62px; } +body.with-timeline .zoom { bottom: 62px; } +body.with-timeline .panel { bottom: 62px; } +.tl-controls { display: flex; gap: 2px; } +.timeline button { + height: 28px; + min-width: 28px; + padding: 0 8px; + border: 0; + border-radius: 4px; + background: none; + color: var(--tool); + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} +.timeline button:hover:not(:disabled) { background: var(--hover); color: var(--tool-active); } +.timeline button:disabled { opacity: .4; cursor: default; } +.tl-track { + position: relative; + flex: 1 1 160px; + min-width: 80px; + height: 28px; + border-radius: 4px; + background: var(--surface-2); + cursor: pointer; + touch-action: none; + outline: none; +} +.tl-track:focus-visible { box-shadow: 0 0 0 2px var(--primary-soft); } +.tl-ticks { position: absolute; inset: 4px 4px; display: flex; align-items: flex-end; gap: 1px; pointer-events: none; } +.tl-ticks .tick { flex: 1 1 0; max-width: 14px; height: 55%; border-radius: 2px; background: var(--grey); opacity: .75; } +.tl-ticks .tick.staged { background: var(--orange); } +.tl-ticks .tick.applied { background: var(--green); } +.tl-ticks .tick.reverted { background: var(--red); } +.tl-ticks .tick.discarded { background: var(--grey); opacity: .45; } +.tl-ticks .tick.edited { background: var(--blue); } +.tl-ticks .tick.read { background: var(--primary); opacity: .4; height: 30%; } +.tl-ticks .tick.at { height: 100%; opacity: 1; box-shadow: 0 0 0 2px var(--primary); } +/* a change made by hand is a little person: the tick its body, this its head */ +.tl-ticks .tick.human { position: relative; height: 40%; } +.tl-ticks .tick.human.at { height: 70%; } +.tl-ticks .tick.human::before { + content: ''; + position: absolute; + left: 50%; + bottom: calc(100% + 1px); + width: 5px; + height: 5px; + border-radius: 50%; + background: inherit; + transform: translateX(-50%); +} +/* a step that only moved layout, while those are hidden */ +.tl-ticks .tick.ws:not(.at) { height: 20%; opacity: .3; } +.timeline .tl-ws, .timeline .tl-speed, .timeline .tl-focus { border: 1px solid var(--border); border-radius: 14px; font-weight: 400; } +.timeline .tl-speed { min-width: 46px; font-variant-numeric: tabular-nums; } +.timeline .tl-ws[aria-pressed="true"], .timeline .tl-focus[aria-pressed="true"] { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } +.panel .chg-tag.ws { background: var(--grey); } +.person, .glyph { flex: none; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; fill: currentColor; } +.tl-step .person, .replay-badge .person, .tl-ask .person, .ask-row .person { color: var(--blue); } +.tl-ask .glyph, .ask-row .glyph { color: var(--primary); } +/* where one ask's steps end and the next one's begin */ +.tl-ticks .tl-sep { flex: none; align-self: stretch; width: 2px; margin: 0 2px; border-radius: 1px; background: var(--border); } +.timeline .tl-ask { + flex: 0 1 auto; + min-width: 0; + max-width: 28ch; + overflow: hidden; + text-overflow: ellipsis; + border: 1px solid var(--border); + border-radius: 14px; + font-weight: 400; + font-size: 12px; +} +.timeline .tl-ask[hidden] { display: none; } +.panel .ask-session { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin: 12px 0 4px; + color: var(--muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: .04em; +} +.panel .ask-session button { flex: none; white-space: nowrap; text-transform: none; letter-spacing: 0; } +.panel .ask-row { display: flex; align-items: stretch; gap: 2px; margin: 2px 0; border-radius: 6px; } +.panel .ask-row.at { background: var(--primary-soft); } +.panel .ask-go { + flex: 1 1 auto; + display: flex; + align-items: flex-start; + gap: 6px; + min-width: 0; + padding: 6px; + border: 0; + background: none; + color: var(--text); + text-align: left; + cursor: pointer; +} +.panel .ask-go:hover { background: var(--hover); } +.panel .ask-go .glyph, .panel .ask-go .person { margin-top: 2px; } +.panel .ask-text { + flex: 1 1 auto; + min-width: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.panel .ask-n { flex: none; color: var(--muted); font-size: 11px; } +.panel .ask-play { flex: none; border: 0; background: none; color: var(--tool); cursor: pointer; } +.panel .ask-play:hover { color: var(--primary); } +.timeline .tl-step { + flex: 0 1 auto; + min-width: 0; + max-width: 34ch; + overflow: hidden; + text-overflow: ellipsis; + font: 12px var(--mono); + font-weight: 400; + color: var(--muted); +} +.timeline .tl-step:disabled { opacity: 1; } +.tl-side { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; } +.tl-side[hidden] { display: none; } +.timeline .tl-side button { border-radius: 0; font-weight: 400; font-size: 12px; } +.timeline .tl-side button[aria-pressed="true"] { background: var(--primary-soft); color: var(--primary); } +.timeline .tl-follow { border: 1px solid var(--border); border-radius: 14px; font-weight: 400; } +.timeline .tl-follow[aria-pressed="true"] { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } + +.replay-badge { + position: fixed; + left: 12px; + top: 64px; + z-index: 12; + padding: 4px 10px; + border: 1px solid var(--orange); + border-radius: 14px; + background: var(--surface); + color: var(--text); + font-size: 12px; + box-shadow: var(--shadow-bar); + max-width: calc(100vw - 24px); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.replay-badge[hidden] { display: none; } + +.panel .chg-row { display: flex; align-items: baseline; gap: 8px; } +.panel .chg-tag { + flex: none; + padding: 0 6px; + border-radius: 8px; + font-size: 11px; + color: #fff; + background: var(--primary); +} +.panel .chg-tag.added { background: var(--green); } +.panel .chg-tag.removed { background: var(--red); } +.panel .chg-tag.read { background: var(--blue); } +.panel .diff-file { margin: 8px 0 2px; font: 12px var(--mono); } +.panel .diff-file span { color: var(--muted); } +.panel .diff pre { + margin: 0; + padding: 6px 8px; + max-height: 340px; + overflow: auto; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-2); + font: 11px/1.45 var(--mono); +} +.panel .diff .add { display: block; background: color-mix(in srgb, var(--green) 16%, transparent); } +.panel .diff .del { display: block; background: color-mix(in srgb, var(--red) 14%, transparent); } +.panel .diff .hunk { display: block; color: var(--primary); } +.panel .diff .meta { display: block; color: var(--faint); } + @media (max-width: 900px) { - .brand span { display: none; } .search input { width: 160px; } .panel { width: auto; left: 12px; top: auto; height: 45vh; } .panel:not([hidden]) ~ .zoom { right: 12px; bottom: calc(45vh + 24px); } + body.with-timeline .panel { bottom: 62px; } + body.with-timeline .panel:not([hidden]) ~ .zoom { bottom: calc(45vh + 74px); } +} + +/* a VS Code side bar: one column - the crumbs get a row of their own above + search and the toggles, and the rest of the chrome folds away */ +@media (max-width: 560px) { + .brand, .crumbs .lvl, .spacer, .legend, #layout-reset { display: none; } + .bar { height: 84px; flex-wrap: wrap; align-content: center; gap: 4px 6px; padding: 4px 8px; } + .crumbs { flex: 1 1 100%; order: -1; } + .crumbs button { max-width: 150px; padding: 3px 5px; } + .search { flex: 1 1 0; min-width: 0; } + .search input { width: 100%; height: 30px; } + .search svg { top: 8px; } + .tools .chip { padding: 0 6px; font-size: 11px; } + .viewport { top: 84px; } + .panel, .note-bar, .replay-badge { top: 94px; } + .timeline { flex-wrap: wrap; left: 6px; right: 6px; bottom: 6px; } + .tl-track { order: 5; flex-basis: 100%; } + .timeline .tl-step { flex: 1 1 0; max-width: none; } + .timeline .tl-ask { max-width: 12ch; } + body.with-timeline .zoom, body.with-timeline .panel { bottom: 88px; } + .panel { left: 6px; right: 6px; } } diff --git a/assets/vis/vis.html b/assets/vis/vis.html index 1062be7..d43e3aa 100644 --- a/assets/vis/vis.html +++ b/assets/vis/vis.html @@ -8,7 +8,7 @@
-
cccCollateral Code Check
+