一键导入
hgraf-add-renderer-overlay
Add a new canvas-layer overlay to the Gantt renderer — RAF draw order, layer composition, viewport transform, hit testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a new canvas-layer overlay to the Gantt renderer — RAF draw order, layer composition, viewport transform, hit testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Understand and safely change heartbeat interval, stuck threshold, and timeout constants — client cadence, server detection math, frontend signal.
Add a new AnnotationKind — proto, storage, ingress RPC, optional downstream delivery to agents, frontend authoring UI, tests.
Add a new agent Capability — proto enum, client advertisement, server ingest, frontend gating in the control UI.
Add a new ControlKind — capability advertisement, client handler, server routing, ack semantics, frontend button, and tests — all the way through.
Add a new drift kind end-to-end. Goldfive owns the detector + enum; harmonograf reflects the kind through the intervention aggregator and the UI timeline.
Script a complex multi-turn FakeLlm scenario — function calls, tool chains, drift events, side-effect hooks, partial responses — for deterministic adk test coverage.
| name | hgraf-add-renderer-overlay |
| description | Add a new canvas-layer overlay to the Gantt renderer — RAF draw order, layer composition, viewport transform, hit testing. |
You need to draw something on the Gantt canvas that is not a span — a highlight, a guide line, a badge, a warning icon, a selection outline, a context-window chart, a minimap cursor. This is renderer-layer-only: no proto changes, no server work.
If you also need new data from the server, see hgraf-add-gantt-overlay.md (batch 1), which covers the full client→proto→server→storage→frontend loop. This skill specializes on the canvas/draw-loop side.
frontend/src/gantt/renderer.ts:1-150 — the GanttRenderer class, the draw() method, the layer ordering.frontend/src/gantt/contextOverlay.ts — it's the canonical example of a compact, composable overlay with its own state and RAF draw contribution.frontend/src/gantt/viewport.ts — world ↔ screen coordinate transforms. Your overlay will receive (ctx, viewport, layout) and must not mutate them.A clean overlay looks like:
// frontend/src/gantt/myOverlay.ts
import type { GanttLayout } from './layout';
import type { Viewport } from './viewport';
export interface MyOverlayState {
visible: boolean;
highlightSpanId: string | null;
}
export function drawMyOverlay(
ctx: CanvasRenderingContext2D,
vp: Viewport,
layout: GanttLayout,
state: MyOverlayState,
): void {
if (!state.visible || !state.highlightSpanId) return;
const bar = layout.barsBySpanId.get(state.highlightSpanId);
if (!bar) return;
ctx.save();
ctx.strokeStyle = 'var(--hg-overlay-highlight, #ffcc00)';
ctx.lineWidth = 2;
ctx.strokeRect(bar.x, bar.y, bar.w, bar.h);
ctx.restore();
}
Key rules:
vp, layout, or the source store.ctx.save() / ctx.restore() every call. Leaking canvas state between overlays is the most common overlay bug.GanttRenderer.draw() in frontend/src/gantt/renderer.ts walks a fixed sequence of draw phases. Grep this._drawSpanBars and this._drawLabels to find the slot between them and insert your call:
// Inside draw()
this._drawSpanBars(ctx, vp, layout);
this._drawRowSeparators(ctx, vp, layout);
drawMyOverlay(ctx, vp, layout, this._myOverlayState);
this._drawLabels(ctx, vp, layout);
this._drawSelection(ctx, vp, layout);
Order matters. Overlays that should appear underneath span bars go before _drawSpanBars; highlights that should appear over labels go at the very end.
Add a field on GanttRenderer:
private _myOverlayState: MyOverlayState = { visible: false, highlightSpanId: null };
setMyOverlayState(next: Partial<MyOverlayState>): void {
this._myOverlayState = { ...this._myOverlayState, ...next };
this._requestDraw();
}
Always call this._requestDraw() (the RAF scheduler) after any mutation — the renderer does not re-draw on state change unless you ask it to.
If UI code (a drawer tab, a toolbar toggle, a keyboard shortcut) needs to toggle the overlay, put the handle on uiStore rather than passing the renderer directly:
// frontend/src/state/uiStore.ts
ganttRenderer: GanttRenderer | null; // already exists
toggleMyOverlay(visible: boolean): void {
set({ myOverlayVisible: visible });
get().ganttRenderer?.setMyOverlayState({ visible });
},
If clicks on the overlay should do something, plumb a hit-test into GanttRenderer.spatialIndex — see frontend/src/gantt/spatialIndex.ts. For static decorations, skip this entirely. Interactive overlays should re-use the existing handleClick path in GanttCanvas.tsx by consulting the spatial index first, then falling back to overlay hit logic.
Use CSS custom properties for colors. Grep --hg- in frontend/src/theme/themes.ts to see the existing variable namespace and add a new one there so both light and dark themes provide a value. Never hard-code a hex color in an overlay — it will invert badly across themes.
The renderer runs the full draw() cycle every RAF tick while the viewport animates. Your overlay runs inside that budget — aim for <0.5ms per draw on a 10k-span session:
fillRect / strokeRect / arc — primitive paths are fast.ctx.measureText inside draw loops; it forces a font-shaping round trip. Measure once and cache.layout.barsBySpanId.get(id) or layout.rowsByAgentId.get(id) for O(1) lookups.Benchmark with the stress page: frontend/src/gantt/StressPage.tsx renders large sessions; toggle your overlay and watch the frame meter.
frontend/src/__tests__/gantt/renderer.test.ts — snapshot test that the overlay draws the expected pixels on a fixture layout. Use the existing canvas mock.getComputedStyle to return a known color and assert the overlay used it.cd frontend && pnpm test -- --run renderer
cd frontend && pnpm typecheck
cd frontend && pnpm dev # visually verify overlay toggles on/off, animates, and survives theme switches
ctx.save() / ctx.restore() poisons every later overlay. Wrap every overlay body in save/restore even if it seems unnecessary._requestDraw(): setting state outside the draw loop does nothing visible until the next external event causes a redraw. Always call the scheduler.layout.barsBySpanId uses screen coordinates already transformed by the viewport. Do not apply vp.worldToScreen on top of that — you'll double-transform.useUiStore directly breaks the renderer's isolation. Keep the overlay pure and push state in from the outside via setMyOverlayState._drawSelection. Check the order against hgraf-add-gantt-overlay.md's data-layer example which draws at the ghost-plan layer, well before spans.