fix(graph): correct particle direction + remove system message filter

Particle direction:
- Added `reverse` flag to GraphParticle — when true, particle flies
  from target → source (reverse of edge direction)
- Messages FROM teammate TO lead now fly member→lead (was lead→member)
- draw-particles.ts swaps from/to nodes when reverse=true

Reverted system message filter:
- Removed #isSystemMessage — all messages shown as particles again
  (user wants to see idle_notification etc.)
This commit is contained in:
iliya 2026-03-30 19:20:53 +03:00
parent 942588093b
commit 485327d077
3 changed files with 21 additions and 19 deletions

View file

@ -37,7 +37,11 @@ export function drawParticles(
if (!source || !target) continue; if (!source || !target) continue;
if (source.x == null || source.y == null || target.x == null || target.y == null) continue; if (source.x == null || source.y == null || target.x == null || target.y == null) continue;
const cp = computeControlPoints(source.x, source.y, target.x, target.y); // Reverse: swap source/target for particles going in opposite direction
const from = p.reverse ? target : source;
const to = p.reverse ? source : target;
const cp = computeControlPoints(from.x!, from.y!, to.x!, to.y!);
const color = p.color || COLORS.message; const color = p.color || COLORS.message;
const baseSize = (p.size ?? 1) * 3; const baseSize = (p.size ?? 1) * 3;
// Differentiate visual by particle kind // Differentiate visual by particle kind
@ -50,12 +54,12 @@ export function drawParticles(
const phaseOffset = p.id.charCodeAt(Math.min(5, p.id.length - 1)) * 0.1; const phaseOffset = p.id.charCodeAt(Math.min(5, p.id.length - 1)) * 0.1;
const wobbleAmp = BEAM.wobble.amp; const wobbleAmp = BEAM.wobble.amp;
drawParticleTrail(ctx, source, target, cp, p.progress, color, size, wobbleAmp, phaseOffset, time, p.kind); drawParticleTrail(ctx, from, to, cp, p.progress, color, size, wobbleAmp, phaseOffset, time, p.kind);
drawParticleCore(ctx, source, target, cp, p.progress, color, size, wobbleAmp, phaseOffset, time, p.kind); drawParticleCore(ctx, from, to, cp, p.progress, color, size, wobbleAmp, phaseOffset, time, p.kind);
// Label // Label
if (p.label && p.progress > PARTICLE_DRAW.labelMinT && p.progress < PARTICLE_DRAW.labelMaxT) { if (p.label && p.progress > PARTICLE_DRAW.labelMinT && p.progress < PARTICLE_DRAW.labelMaxT) {
const pos = getWobbledPosition(source, target, cp, p.progress, wobbleAmp, phaseOffset, time); const pos = getWobbledPosition(from, to, cp, p.progress, wobbleAmp, phaseOffset, time);
ctx.font = `${PARTICLE_DRAW.labelFontSize}px monospace`; ctx.font = `${PARTICLE_DRAW.labelFontSize}px monospace`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillStyle = hexWithAlpha(color, 0.56); ctx.fillStyle = hexWithAlpha(color, 0.56);

View file

@ -135,6 +135,8 @@ export interface GraphParticle {
size?: number; size?: number;
/** Short label near particle */ /** Short label near particle */
label?: string; label?: string;
/** If true, particle travels from target → source (reverse direction) */
reverse?: boolean;
} }
// ─── Domain Reference (opaque back-pointer) ────────────────────────────────── // ─── Domain Reference (opaque back-pointer) ──────────────────────────────────

View file

@ -494,12 +494,19 @@ export class TeamGraphAdapter {
if (this.#seenMessageIds.has(msgKey)) continue; if (this.#seenMessageIds.has(msgKey)) continue;
this.#seenMessageIds.add(msgKey); this.#seenMessageIds.add(msgKey);
// Skip system/noise messages (idle notifications, JSON blobs)
if (TeamGraphAdapter.#isSystemMessage(msg)) continue;
const edgeId = TeamGraphAdapter.#resolveMessageEdge(msg, teamName, leadId, leadName, edges); const edgeId = TeamGraphAdapter.#resolveMessageEdge(msg, teamName, leadId, leadName, edges);
if (!edgeId) continue; if (!edgeId) continue;
// Determine direction: messages FROM a teammate TO lead should reverse
// (edges are always lead→member, but message goes member→lead)
const fromId = TeamGraphAdapter.#resolveParticipantId(
msg.from ?? '',
teamName,
leadId,
leadName
);
const isFromTeammate = fromId !== leadId;
particles.push({ particles.push({
id: `particle:msg:${teamName}:${msgKey}`, id: `particle:msg:${teamName}:${msgKey}`,
edgeId, edgeId,
@ -507,6 +514,7 @@ export class TeamGraphAdapter {
kind: 'inbox_message', kind: 'inbox_message',
color: msg.color ?? '#66ccff', color: msg.color ?? '#66ccff',
label: TeamGraphAdapter.#buildParticleLabel(msg.summary ?? msg.text, 'inbox'), label: TeamGraphAdapter.#buildParticleLabel(msg.summary ?? msg.text, 'inbox'),
reverse: isFromTeammate,
}); });
} }
} }
@ -694,18 +702,6 @@ export class TeamGraphAdapter {
return `member:${teamName}:${name}`; return `member:${teamName}:${name}`;
} }
/** Filter out system/noise messages that shouldn't show as particles */
static #isSystemMessage(msg: InboxMessage): boolean {
const text = msg.text ?? '';
// JSON system messages (idle_notification, shutdown, etc.)
if (text.startsWith('{"type":') || text.startsWith('{"type" :')) return true;
// Very short system messages
if (text.length < 3) return true;
// System notification source
if (msg.source === 'system_notification') return true;
return false;
}
static #buildParticleLabel( static #buildParticleLabel(
text: string | undefined, text: string | undefined,
kind: 'inbox' | 'comment', kind: 'inbox' | 'comment',