---
phase: quick
plan: 260329-ton
type: execute
wave: 1
depends_on: []
files_modified:
  - src/utils/markdown.ts
  - src/utils/markdown.test.ts
  - src/app.ts
  - src/ui/screens.ts
  - src/forest/reward.ts
autonomous: true
requirements: []
must_haves:
  truths:
    - "AI-generated companion texts with *italic* render as <em> in the UI"
    - "AI-generated companion texts with **bold** render as <strong> in the UI"
    - "Plain text without markdown renders unchanged"
    - "HTML special characters in generated text are escaped (no XSS)"
  artifacts:
    - path: "src/utils/markdown.ts"
      provides: "renderInlineMarkdown() function"
      exports: ["renderInlineMarkdown"]
    - path: "src/utils/markdown.test.ts"
      provides: "Unit tests for markdown rendering"
  key_links:
    - from: "src/app.ts"
      to: "src/utils/markdown.ts"
      via: "innerHTML = renderInlineMarkdown(text)"
      pattern: "renderInlineMarkdown"
    - from: "src/ui/screens.ts"
      to: "src/utils/markdown.ts"
      via: "innerHTML = renderInlineMarkdown(introText)"
      pattern: "renderInlineMarkdown"
    - from: "src/forest/reward.ts"
      to: "src/utils/markdown.ts"
      via: "innerHTML = renderInlineMarkdown(comment)"
      pattern: "renderInlineMarkdown"
---

<objective>
Render inline Markdown (*italic*, **bold**) in AI-generated companion texts as HTML instead of displaying raw asterisks.

Purpose: AI-generated texts (greetings, letter intros, forest comments) contain Markdown formatting like *f* for emphasis, but `.textContent` displays them as plaintext with visible asterisks. Switching to `.innerHTML` with a safe inline markdown renderer fixes this.

Output: A `renderInlineMarkdown()` utility function used at all 3 AI-text insertion points.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/STATE.md
@src/utils/markdown.ts (will be created)
@src/app.ts
@src/ui/screens.ts
@src/forest/reward.ts
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create renderInlineMarkdown utility with tests</name>
  <files>src/utils/markdown.ts, src/utils/markdown.test.ts</files>
  <behavior>
    - renderInlineMarkdown("hello") returns "hello" (plain text unchanged)
    - renderInlineMarkdown("press *f*") returns "press &lt;em&gt;f&lt;/em&gt;" (single asterisks become em)
    - renderInlineMarkdown("very **important**") returns "very &lt;strong&gt;important&lt;/strong&gt;" (double asterisks become strong)
    - renderInlineMarkdown("**bold** and *italic*") handles both in one string
    - renderInlineMarkdown("a &lt;script&gt;alert(1)&lt;/script&gt;") escapes HTML entities BEFORE markdown processing (XSS safe)
    - renderInlineMarkdown("2 * 3 * 4") does NOT wrap standalone asterisks with spaces (only wrapping content like *word*)
    - renderInlineMarkdown("") returns "" (empty string)
  </behavior>
  <action>
    Create src/utils/markdown.ts exporting a single function renderInlineMarkdown(text: string): string.

    Implementation approach:
    1. First escape HTML special chars: & < > " ' (using string replace chains, no DOM needed)
    2. Then apply regex for **bold**: /\*\*(.+?)\*\*/g -> <strong>$1</strong>
    3. Then apply regex for *italic*: /\*(.+?)\*/g -> <em>$1</em>
    (Order matters: bold first so ** is not consumed as two single *)

    No external dependencies. Pure function, no DOM.

    Create src/utils/markdown.test.ts with vitest tests covering all behaviors above.
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx vitest run src/utils/markdown.test.ts</automated>
  </verify>
  <done>All markdown rendering tests pass. Function handles bold, italic, mixed, HTML escaping, and edge cases.</done>
</task>

<task type="auto">
  <name>Task 2: Replace .textContent with .innerHTML + renderInlineMarkdown at all AI-text insertion points</name>
  <files>src/app.ts, src/ui/screens.ts, src/forest/reward.ts</files>
  <action>
    Import renderInlineMarkdown from "../utils/markdown" (or adjust relative path) in each file and replace .textContent with .innerHTML + renderInlineMarkdown() at these 3 specific locations:

    1. src/app.ts line 227:
       BEFORE: textEl.textContent = text;
       AFTER:  textEl.innerHTML = renderInlineMarkdown(text);

    2. src/ui/screens.ts line 422:
       BEFORE: companionText.textContent = introText;
       AFTER:  companionText.innerHTML = renderInlineMarkdown(introText);

    3. src/forest/reward.ts line 207:
       BEFORE: companionTextEl.textContent = comment;
       AFTER:  companionTextEl.innerHTML = renderInlineMarkdown(comment);

    IMPORTANT: Do NOT change any other .textContent assignments in these files. The other assignments use hardcoded German strings (no markdown) or set empty strings. Only the 3 lines above receive AI-generated text that may contain markdown.
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx vitest run && npx tsc --noEmit</automated>
  </verify>
  <done>All 3 AI-text insertion points use innerHTML + renderInlineMarkdown. TypeScript compiles without errors. Existing tests still pass.</done>
</task>

</tasks>

<verification>
- npx vitest run passes (all tests including new markdown tests)
- npx tsc --noEmit passes (no type errors)
- grep -n "renderInlineMarkdown" src/app.ts src/ui/screens.ts src/forest/reward.ts shows exactly 3 usage sites (plus imports)
</verification>

<success_criteria>
- AI-generated companion texts with *italic* or **bold** markdown render as styled HTML
- Plain text and hardcoded strings are unaffected
- HTML in generated text is escaped (no injection risk)
- All existing tests continue to pass
</success_criteria>

<output>
After completion, create `.planning/quick/260329-ton-markdown-in-companion-texten-zu-html-ren/260329-ton-SUMMARY.md`
</output>
