world: per-file heat map — persistent touch-count residual (V4)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Files that see repeated touches decay back to visual silence within a
couple of seconds because `n.heat` decays at 0.5/sec. Hard to tell a
hot-spot file (touched 20 times over a run) from a virgin one; both
look the same at rest.

Adds a monotonically-increasing per-node `touchCount` on the engine
and a log-scaled residual on the render that never decays. Files
climbing the touch count grow subtly, tint whiter, and pull more
emissive — even at heat=0.

- GNode gains `touchCount`, initialized 0 on ensureNode and on the
  ROOT sentinel.
- onTouch bumps `touchCount` only for `file:` prefix nodes so
  landmarks + structural orbs don't accumulate a hot halo just because
  agents converge on them.
- WorldCanvas node loop computes `residual = min(0.7, log(1+count)/log(50))`
  and adds it into radius, whiten-lerp, and emissiveIntensity.

Feel: hot files stay softly-warm indefinitely (residual stops climbing
around ~50 touches). Cold files stay cold. Live activity still dominates
via the transient `heat` bump; residual is the ghost trail.
This commit is contained in:
Omar Sobh
2026-07-09 15:51:21 -07:00
parent a49cf86623
commit 959e766e36
2 changed files with 20 additions and 3 deletions
@@ -695,14 +695,16 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
nodeMeshes.set(n.id, m); nodeMeshes.set(n.id, m);
gourceGroup.add(m); gourceGroup.add(m);
} }
const r = n.r * (1 + n.heat * 0.4); // touchCount → log-scaled residual heat that never decays.
const residual = Math.min(0.7, Math.log(1 + (n.touchCount ?? 0)) / Math.log(50));
const r = n.r * (1 + n.heat * 0.4 + residual * 0.15);
m.position.set(n.x, n.y, 0); m.position.set(n.x, n.y, 0);
m.scale.set(r, r, r); m.scale.set(r, r, r);
const mat = m.material as THREE.MeshStandardMaterial; const mat = m.material as THREE.MeshStandardMaterial;
col.set(n.color).lerp(white, n.heat * 0.6); col.set(n.color).lerp(white, n.heat * 0.6 + residual * 0.35);
mat.color.copy(col); mat.color.copy(col);
mat.emissive.copy(col); mat.emissive.copy(col);
mat.emissiveIntensity = 0.35 + n.heat * 0.6; mat.emissiveIntensity = 0.35 + n.heat * 0.6 + residual * 0.4;
mat.opacity = n.alpha; mat.opacity = n.alpha;
mat.transparent = n.alpha < 0.99; // solid when present; only fades on appear/disappear mat.transparent = n.alpha < 0.99; // solid when present; only fades on appear/disappear
let g = nodeGlows.get(n.id); let g = nodeGlows.get(n.id);
+15
View File
@@ -28,6 +28,11 @@ export interface GNode {
alpha: number; // fade in/out (world nodes) alpha: number; // fade in/out (world nodes)
lastSeen: number; lastSeen: number;
fixed?: boolean; fixed?: boolean;
/** Cumulative touch count (per session). Grows monotonically on every
* world.touch that lands here. Used by the render to compute a
* persistent "heat map" tint so files touched many times stay visibly
* warmer than virgin files even at heat=0. */
touchCount: number;
} }
export interface GPawn { export interface GPawn {
@@ -144,6 +149,7 @@ export class WorldEngine {
alpha: 1, alpha: 1,
lastSeen: 0, lastSeen: 0,
fixed: true, fixed: true,
touchCount: 0,
}); });
} }
@@ -189,6 +195,7 @@ export class WorldEngine {
burst: 0, burst: 0,
alpha: 1, alpha: 1,
lastSeen: this.now, lastSeen: this.now,
touchCount: 0,
}; };
this.nodes.set(id, n); this.nodes.set(id, n);
} }
@@ -291,6 +298,14 @@ export class WorldEngine {
// permanent hot spot regardless of intent. // permanent hot spot regardless of intent.
node.heat = Math.min(1, node.heat + w * 0.6); node.heat = Math.min(1, node.heat + w * 0.6);
node.burst = Math.max(node.burst, w); // file ops (weight 1) → explosive burst node.burst = Math.max(node.burst, w); // file ops (weight 1) → explosive burst
// Persistent per-file heat map — tracks TOTAL touches so hot files
// stay visibly warmer even after `heat` decays (see the render's
// touchCount → residual emissive intensity in WorldCanvas). We only
// count file: touches; landmarks and structural nodes shouldn't
// accumulate a "hot" halo just because agents converge on them.
if (e.nodeId.startsWith("file:")) {
node.touchCount = (node.touchCount ?? 0) + 1;
}
// activity type → signal colour: file=coral, tool=cyan, run=green, // activity type → signal colour: file=coral, tool=cyan, run=green,
// repo=sky-blue, loop=amber // repo=sky-blue, loop=amber
if (e.nodeId.startsWith("tool:")) p.fireColor = w >= 0.9 ? "#ff6f61" : "#5ec8d8"; if (e.nodeId.startsWith("tool:")) p.fireColor = w >= 0.9 ? "#ff6f61" : "#5ec8d8";