ワンクリックで
widget-rendering
Pi TUI crew widget data sources, display priority, and rendering performance.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Pi TUI crew widget data sources, display priority, and rendering performance.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | widget-rendering |
| description | Pi TUI crew widget data sources, display priority, and rendering performance. |
| origin | pi-crew |
| triggers | ["empty agent","ghost run","widget timing","display priority","snapshot cache","widget render","UI refresh"] |
The crew widget (src/ui/crew-widget.ts) displays active runs and their agents in the Pi TUI. It must render synchronously at TTY refresh rate without blocking. Understanding the data sources and timing rules is essential for debugging display issues.
The widget has three sources, used in priority order:
liveAgents Map (real-time, highest priority)In-memory map from live-agent-manager.ts. Provides:
activeTools Map (toolName → description)When used: Agents with liveHandle && liveHandle.status === "running" get the live activity description (tool labels, response text, turn counter).
When NOT used: After evictStaleLiveAgentHandles() removes a handle, widget falls back to agent records on disk.
RunSnapshotCache from run-snapshot-cache.ts caches parsed manifests and agents for 500ms. Reduces disk reads during rapid refresh.
When used: As the fallback when no live handle exists. Prevents excessive disk reads on every render tick.
Invalidation: Cache is invalidated when:
invalidate() is called on a specific runagents.json on disk (durables, lowest priority)readCrewAgents(run) reads artifactsRoot/agents.json. Provides:
When used: For completed agents, or when snapshot cache misses.
for each active run:
for each agent in run:
if liveAgents has this agent (by agentId or taskId):
→ use live activity description (tool labels, response text)
→ use live status (running/queued/waiting)
→ use live session stats (context %, turns, tokens)
else if snapshot cache has fresh data:
→ use cached agent status
→ use cached tool count, tokens, progress
else:
→ read agents.json from disk
→ use disk agent status
if status is completed/failed/cancelled:
→ apply linger rules (finishedAgents: 1min, errors: 2min)
activeWidgetRuns() determines which runs to show. Key filter: isDisplayActiveRun(manifest, tasks) from process-status.ts.
Rule: hasStaleAsyncProcess()
A run with an async PID is considered stale (hidden) if:
STALE_ACTIVE_RUN_MS = 30 * 60 * 1000)Rule: isDisplayActiveRun()
export function isDisplayActiveRun(manifest: TeamRunManifest, tasks: TeamTaskState[]): boolean {
if (manifest.status === "running" || manifest.status === "waiting") {
if (manifest.async?.pid) {
if (hasStaleAsyncProcess(manifest.async.pid, manifest.updatedAt)) return false;
}
const hasActiveTask = tasks.some((t) => t.status === "running" || t.status === "queued" || t.status === "waiting");
if (!hasActiveTask) return false;
return true;
}
return false;
}
This filters out ghost runs (PID dead, manifest still "running") that are more than 30 minutes old.
On every widget refresh, evictStaleLiveAgentHandles() is called at the start of activeWidgetRuns():
export function activeWidgetRuns(...): WidgetRun[] {
evictStaleLiveAgentHandles(); // prevent memory leaks
const runs = preloadedManifests ?? ...;
// ...
}
Eviction rule: Remove handles where:
updatedAt is more than 10 minutes agoconst STALE_HANDLE_MS = 10 * 60 * 1000;
if (handle.status !== "running" && handle.status !== "queued" && handle.status !== "waiting") {
const age = now - new Date(handle.updatedAt).getTime();
if (age > STALE_HANDLE_MS) {
liveAgents.delete(agentId);
safeDisposeLiveSession(handle);
}
}
Why called on every refresh: Ensures the in-memory Map stays bounded even during long Pi sessions. Completed agents linger for 10 minutes (for visibility), then get evicted.
renderTick() must be non-blockingEvery render cycle (renderTick / requestAnimationFrame) must complete in <16ms to maintain 60fps. The widget must not:
fs.readFileSync on hot pathsloadConfig() during renderreaddirSync)Solution: Preload everything async before the first render.
| Scenario | Interval |
|---|---|
| Live agents running | 160ms (LIVE_REFRESH_MS) |
| No live agents, recent activity | 2s |
| Idle | 10s |
agentActivity(agent, liveHandle?) generates the activity string shown in the widget:
function agentActivity(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
if (liveHandle && liveHandle.status === "running") {
const live = describeLiveActivity(liveHandle);
// Prefer richer agent.progress data if live is just the fallback
if (live === "thinking…" && agent.progress?.currentTool)
return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
return live;
}
// Fallback chain from agent records
if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool]}…`;
if (recent output) return lastOutput line;
if (activityState === "needs_attention") return "needs attention";
if (status === "queued") return "queued";
if (status === "running") {
if (age < 5s && no tool) return "spawning…";
return "thinking…";
}
if (status === "failed") return agent.error ?? "failed";
return "done";
}
Tool name extraction: TOOL_LABELS maps tool names to readable labels:
const TOOL_LABELS = {
read: "reading",
bash: "running command",
edit: "editing",
write: "writing",
grep: "searching",
find: "finding files",
ls: "listing",
};
Root cause: agents.json still has status "running" while the actual PID is dead.
Fix path: reconcileAllStaleRuns now calls upsertCrewAgent when repairing tasks, syncing agent status files. Also purgeStaleActiveRunIndex and cancelOrphanedRuns now sync agent records.
Root cause: agentActivity fallback chain returned "done" with no name. The agent description construction had fallbacks to empty strings.
Fix applied: agentActivity now uses handle.agent ?? handle.role ?? agent.agent ?? agent.role ?? "Agent" as the description base. Plus (running) suffix uses handle.status not agent.status.
Root cause: active-run-index.json is missing on restart, so purgeStaleActiveRunIndex() doesn't know about orphaned runs. reconcileAllStaleRuns (disk scan) was never called.
Fix applied: reconcileAllStaleRuns is now called at session start in register.ts.
Root cause: Worker spawns and crashes in <1 frame. Structured logs (worker.spawned + rapid worker.exit) expose the crash cause.
Fix: Event log tracing skill documents this pattern.
Before modifying widget rendering or display logic, verify:
If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
readFileSync, readdirSync, fs.statSync in the render path causes frame drops. Preload everything async.readCrewAgents returns [] (no agents yet), the cache must be invalidated on next tick to prevent showing empty for too long.evictStaleLiveAgentHandles, the Map grows indefinitely. Call it on every refresh.src/ui/crew-widget.ts — render, refresh, activeWidgetRuns, evictStaleLiveAgentHandles, agentActivity, describeLiveActivitysrc/ui/run-snapshot-cache.ts — SnapshotCache, get, refreshIfStale, TTL=500mssrc/runtime/crew-agent-records.ts — readCrewAgents, agents.jsonsrc/runtime/process-status.ts — hasStaleAsyncProcess, isDisplayActiveRunsrc/runtime/background-runner.ts — active run filtering with async PID checksrc/state/active-run-registry.ts — purgeStaleActiveRunIndexRender-path performance for the widget is non-negotiable. Treat every render(width), widget update, and powerbar refresh as a hot synchronous path.
RenderScheduler. Always go through RenderScheduler.schedule() rather than issuing direct/repeated renders. This batches pending paints and avoids redundant refresh storms.snapshotCache.get(runId) on render paths. If a synchronous fallback is genuinely unavoidable, classify it as first-load/rare and document why it can't be preloaded.fs.readFileSync, fs.readdirSync, fs.statSync, network APIs, or large JSON parsing from pane render methods.renderTick().RunSnapshotCache TTL at 500ms or less so the widget never shows stale state. Watch TTL interactions: the preload interval must be shorter than the cache TTL, otherwise render-time refresh gaps appear.Anti-patterns: calling loadConfig(), manifestCache.list(), or refreshIfStale() inside renderTick() without preloaded frame data; directory scans or large JSON parsing in widget render/update functions; showing stale health warnings for terminal runs.
cd pi-crew
npx tsc --noEmit
node --experimental-strip-types --test test/unit/crew-widget.test.ts test/unit/run-snapshot-cache.test.ts test/unit/live-agent-manager.test.ts
npm test
Iterative multi-round codebase audit with diminishing-returns detection. Run 5-20+ rounds, each focusing on one specific area. Built from 19 rounds of dogfooding pi-crew on itself.
Multi-phase orchestration for planners and executors.
Spawn 3 adversarial subagents (Skeptic, Pragmatist, Critic) to evaluate a decision, architecture choice, or plan. Anti-anchoring: each role receives ONLY the question, not conversation history. Aggregates votes into consensus recommendation with dissent tracking. Use when facing critical decisions, architecture choices, security tradeoffs, or plan reviews where single-perspective analysis is insufficient.
Background worker, heartbeat, stale-run, crash-recovery, and deadletter workflow. Use when debugging stuck/dead workers or changing async run reliability.
Child Pi worker spawning, lifecycle callbacks, and failure modes.
Use when constructing worker prompts, reading artifacts/logs, summarizing runs, compacting context, or handing work between agents.