The Crew Shows Up — Live Character Indicators on the Run Panel
Six steam-punk character portraits — Financial-Analyst, Researcher, Writer, Editor, Fact-Checker, Scorecard-Generator — now sit above the JobView's 3-column grid during a memo run. The character whose stage is active glows with a slow breathing border; the rest stay visible but quiet. A live caption under the active character names the work in progress, swapping as the underlying milestones advance. The cast is authored in a single human-editable markdown file (`src/lib/characters.md`) — frontmatter holds names, stage mappings, default captions, and forward-compat agent-precise captions for v2. Mike retunes the wording without touching code.
feat(jobview): live character cast with breathing-border indication
Six steam-punk character portraits sit above the 3-column grid during
a memo run. The character whose stage is currently active glows with a
slow breathing border; the rest stay visible but quiet (low-contrast
border, no animation, no caption). Multiple characters can glow at
once when they share a stage (Fact-Checker + Scorecard-Generator
during validation) — that's the honest read of the data we have.
Files:
* src/lib/characters.md — frontmatter manifest with 6 characters,
each carrying id/name/image/role/stages/default_caption and a
forward-compat agents[] list of agent-precise captions for v2.
* src/lib/characters.ts loads the manifest via Vite's ?raw import,
splits frontmatter, parses YAML with js-yaml. Exposes:
- characters: Character[]
- deriveActiveCharacters(milestones): Set<id>
- captionFor(id, milestones): prefers v2 agent-tagged captions,
falls back to most recent matching milestone's label, then to
default_caption.
* src/lib/components/CharacterRow.svelte renders the row with
circular portrait masks, name labels, and live captions. Active
state flips a breathing CSS animation on the border (~1.6s ease).
* JobView.svelte mounts <CharacterRow> between the run-status header
and the 3-column grid.
* Six PNGs at static/characters/{id}.png, served at /characters/{id}.
Mike controls every caption by editing src/lib/characters.md.Changelog - 2026-05-03_01
The Crew Shows Up — Live Character Indicators on the Run Panel
The fastest-growing agentic AI products lean hard on personification: agents have faces, names, titles. Users don't think "the LangGraph workflow is at the validation node" — they think Fact-Checker is reviewing my work right now. Same shift, decades earlier, from "the database is running a query" to "Clippy needs your attention." Personality is the difference between watching a progress bar and watching a team work for you.
memopop-native already had six steam-punk character portraits committed to assets/. They were unused. As of tonight, they're wired into the JobView so that during a memo run, the user sees their crew at work in real time.
What Lands on Screen
A horizontal row of six portraits sits above the existing 3-column grid (checklist / file tree / log). The character whose stage is currently active glows. The rest stay visible but quiet — dark borders that blend with the page background, no animation, no caption.
| Character | Image | Claimed stages | Default caption |
| Financial-Analyst | /characters/financial-analyst.png | deck_analysis | "Reading the deck" |
| Researcher | /characters/researcher.png | research, competitive | "Out gathering sources" |
| Writer | /characters/writer.png | writing | "At the typewriter" |
| Editor | /characters/editor.png | enrichment, assembly | "Polishing the draft" |
| Fact-Checker | /characters/fact-checker.png | validation | "Verifying claims" |
| Scorecard-Generator | /characters/scorecard-generator.png | validation | "Scoring the memo" |
Multiple characters can glow simultaneously. During the validation stage, both Fact-Checker and Scorecard-Generator light up — that's the honest read of the data we have today. The orchestrator emits stage-level milestones; both characters claim that stage; both glow. v2 (described below) will tighten this so exactly one character glows at a time, but v1 ships today with what the orchestrator already gives us.
How the User Authors the Cast
The cast lives in src/lib/characters.md — a single markdown file with YAML frontmatter. The file is the manifest the app reads at build time. Mike edits it; the next dev-server reload picks up the changes.
---
title: "memopop-native Character Cast"
version: 1
characters:
- id: editor
name: "Editor"
image: "/characters/editor.png"
role: "Polishes the draft into a deliverable"
stages: [enrichment, assembly]
default_caption: "Polishing the draft"
agents:
- id: enrich_links
caption: "Wiring up references"
- id: enrich_socials
caption: "Hunting LinkedIn profiles"
- id: toc
caption: "Building the table of contents"
- id: assemble_citations
caption: "Compiling final draft"
---Field guide:
| Field | What it does |
id | Stable kebab-case identifier. Never displayed. |
name | The label shown under the portrait. |
image | Path served by SvelteKit's static handler. |
role | Longer description for tooltips. |
stages | MilestoneStage values claimed by this character (v1 active-state derivation). |
default_caption | Shown when active and no specific milestone has fired yet. |
agents | Per-agent captions (forward-compat for v2). |
Retitling "Wiring up references" → "Linking sources" requires no code changes. Just edit the markdown.
How It's Wired
Manifest delivery
characters.ts imports the markdown via Vite's ?raw suffix and parses the frontmatter at module load:
import yaml from 'js-yaml';
import manifestRaw from './characters.md?raw';
function splitFrontmatter(raw: string): { data: unknown; body: string } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) throw new Error('characters.md is missing or has malformed YAML frontmatter');
const [, yamlBlock, body] = match;
return { data: yaml.load(yamlBlock), body };
}The manifest is baked into the bundle. No runtime filesystem reads, no Tauri commands, no fetch round-trips. A dev-server reload picks up edits.
Active-state derivation
deriveActiveCharacters(milestones) returns the set of character ids that should glow right now:
let currentStage: MilestoneStage | null = null;
for (let i = milestones.length - 1; i >= 0; i--) {
if (milestones[i].stage !== 'complete' && milestones[i].stage !== 'start') {
currentStage = milestones[i].stage;
break;
}
}
if (!currentStage) return active;
for (const ch of characters) {
if (ch.stages.includes(currentStage)) {
active.add(ch.id);
}
}The most recent non-start, non-complete milestone defines the current stage. Every character whose stages include that stage glows. start and complete are deliberately skipped — those are plumbing milestones, not personalities, so the run begins and ends with no character lit.
Caption resolution
captionFor(id, milestones) walks milestones newest-first and picks the first match:
(v2) A milestone whose
agentfield matches one of the character'sagents[].id→ use that agent'scaption.(v1) A milestone whose
stageis in the character'sstages→ use the milestone'slabeldirectly.Fallback —
default_caption.
Step 1 is a no-op today because milestones don't carry agent. But the code path is wired so a server-side patch (described in What's Next) flips precision on without a client refactor.
Breathing border
A CSS animation on the active frame:
@keyframes breathe {
0%, 100% {
box-shadow:
0 0 0 2px #a855f7 inset,
0 0 12px 2px rgba(168, 85, 247, 0.55);
opacity: 1;
}
50% {
box-shadow:
0 0 0 2px #a855f7 inset,
0 0 18px 4px rgba(168, 85, 247, 0.85);
opacity: 0.92;
}
}1.6 second cycle, ease-in-out, infinite. Slow enough to read as "alive" not "selected." The halo expands and contracts; the opacity dips slightly at peak so the eye picks up the rhythm. Resting characters get a flat low-contrast border (#e5e7eb on light, #2a2a2c on dark) — visibly present but they don't pop.
Design Decisions
Markdown over TypeScript for the manifest. A .ts file with a typed Character[] const would have skipped the YAML parser dependency entirely. Rejected because the spec asks for prose alongside the data — context for future-Mike or future-Claude reading the file blind. The markdown file carries a "Field guide" section after the frontmatter that the running app never reads but a human editing the cast definitely does.
Build-time bundle, not runtime read. Two paths considered: (a) bundle via ?raw import, manifest baked into the JS, edits require dev-server reload; (b) Tauri command reading assets/characters.md at app start, hot-reloads without rebuild. Shipped (a). The manifest is small and changes infrequently in early use. The simplicity wins.
Static folder, not Tauri asset protocol. SvelteKit's static/ is the simplest path: static/characters/financial-analyst.png is served at /characters/financial-analyst.png with no protocol setup, no scope allowlists. The repo-level assets/ folder keeps the original PNG exports + the design source .afphoto; the app's static/characters/ has the runtime versions at clean kebab-case names.
Multi-character glow. Easy to argue exactly one character should glow at any moment — that would feel cleaner. But the orchestrator emits stage-level events (~10 buckets), and inside one stage multiple agents run (validation runs validate_citations → fact_check → validator → scorecard_evaluator → integrate_scorecard). Forcing single-glow with stage-level data means picking one character arbitrarily. Multi-glow honors the data we have. v2 fixes the ambiguity at the source.
start and complete claim no character. Those stages are plumbing — "the run started," "the run finished." Not personalities. Once complete fires, every character returns to resting, the breathing stops, and the failure banner / completion banner take over.
Captions, not status text. "Wiring up references" reads like a postcard from a real person. "Status: enrich_links" reads like a log line. The spec explicitly opted for the postcard voice and gave Mike a single file where he controls every word.
How to Test
1. Quit MemoPop fully (Cmd+Q). The new manifest is bundled in the JS, so a
running dev server picks up the edit on next reload — but a packaged
build needs a fresh launch.
2. Open MemoPop.
3. Click an outline → Try this on a company → fill the deal form → Generate.
4. Watch the row above the run panel. For ~5 seconds it stays quiet — the
`start` milestone fires but isn't claimed by any character.
5. As soon as deck analysis kicks in, Financial-Analyst lights up with
"Parsing the pitch deck" underneath.
6. Watch the cast advance through Researcher → Writer → Editor →
Fact-Checker + Scorecard-Generator. The captions update as the
underlying milestones fire.
7. Run completes. All characters return to resting. The completion banner
takes over.Or, edit the cast and verify it round-trips:
1. Open src/lib/characters.md.
2. Change Editor's default_caption from "Polishing the draft" to
"Sharpening the prose".
3. Save. Bun's dev server hot-reloads.
4. The JobView (mid-run, or on a fresh run) shows the new caption.What's Next
v2 — agent-precise captions. A small surgery on
src/server/milestones.py: each_Patterngains an optionalagent: strfield where the active agent is unambiguous from the log line. TheMilestoneExtractor.process()call attaches theagentfield to the milestone dict when present. Client-side, the v2 path incaptionForalready prefersagent-matched captions, so this is purely additive — no frontmatter rewrite, no new event types. After this lands, only Fact-Checker glows during fact-check, then only Scorecard-Generator glows during scorecard. Mike's hand-tuned captions inagents[].captiontake over from the v1 milestone-label fallback.Done state visualization. v1 returns characters to resting once the stage advances. A small checkmark in the corner, or a "tipping their hat" frame variant, would make completed work visible at a glance. Out of scope for v1 — see if the breathing-only language reads cleanly first.
Tooltip / role surfacing. The
rolefield is set as the<article>element'stitleattribute, which gives a basic browser tooltip on hover. A custom tooltip component with the role text in a steam-punk-styled bubble would honor the aesthetic better. Defer until the rest of the UI gets a tooltip pass.Caption truncation refinement. v1 milestone labels can exceed the 30-char comfort window (e.g.,
"Drafting: CAGR (Compound Annual Revenue Growth)"). Currently they wrap to a 2-line clamp with ellipsis. Once v2 captions land, this concern fades — those are hand-tuned to fit. But a hover-to-expand affordance on truncated lines is a nice polish for the v1 fallback path.Resume runs and the cast. A resumed run skips agents whose work is already on disk. The cast still lights up as their stages' milestones fire — but those milestones come from the resume CLI's "Running agent: X" prints, not from real work. The user sees Researcher light up briefly even though the resume actually skipped that agent. Probably acceptable — the run banner already labels it as a resume — but worth watching for confusion in the field.
The acceptance check from the spec: a stranger watches the row for 10 seconds during a memo run and points at the glowing portrait saying "that one is researching right now," without ever reading the milestones panel. If that lands, v1 is good.
