Lossless Changelog
component

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.

Michael StatonMusic Editoraugmented with Claude Code on Opus 4.7
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.

CharacterImageClaimed stagesDefault caption
Financial-Analyst/characters/financial-analyst.pngdeck_analysis"Reading the deck"
Researcher/characters/researcher.pngresearch, competitive"Out gathering sources"
Writer/characters/writer.pngwriting"At the typewriter"
Editor/characters/editor.pngenrichment, assembly"Polishing the draft"
Fact-Checker/characters/fact-checker.pngvalidation"Verifying claims"
Scorecard-Generator/characters/scorecard-generator.pngvalidation"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.

YAML
---
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:

FieldWhat it does
idStable kebab-case identifier. Never displayed.
nameThe label shown under the portrait.
imagePath served by SvelteKit's static handler.
roleLonger description for tooltips.
stagesMilestoneStage values claimed by this character (v1 active-state derivation).
default_captionShown when active and no specific milestone has fired yet.
agentsPer-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:

TS
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:

TS
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:

  1. (v2) A milestone whose agent field matches one of the character's agents[].id → use that agent's caption.

  2. (v1) A milestone whose stage is in the character's stages → use the milestone's label directly.

  3. 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:

CSS
@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

TEXT
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:

TEXT
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

  1. v2 — agent-precise captions. A small surgery on src/server/milestones.py: each _Pattern gains an optional agent: str field where the active agent is unambiguous from the log line. The MilestoneExtractor.process() call attaches the agent field to the milestone dict when present. Client-side, the v2 path in captionFor already prefers agent-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 in agents[].caption take over from the v1 milestone-label fallback.

  2. 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.

  3. Tooltip / role surfacing. The role field is set as the <article> element's title attribute, 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.

  4. 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.

  5. 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.