Pabloler21 Claude Opus 4.8 commited on
Commit
b084563
Β·
1 Parent(s): 05c3128

docs: add Day 3 treasure panel implementation plan

Browse files

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

.gitignore CHANGED
@@ -209,6 +209,9 @@ tempCodeRunnerFile.py
209
  # PyPI configuration file
210
  .pypirc
211
 
 
 
 
212
  # Marimo
213
  marimo/_static/
214
  marimo/_lsp/
 
209
  # PyPI configuration file
210
  .pypirc
211
 
212
+ # Superpowers brainstorming
213
+ .superpowers/
214
+
215
  # Marimo
216
  marimo/_static/
217
  marimo/_lsp/
docs/superpowers/plans/2026-06-10-day3-treasure-panel.md ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Day 3 β€” Treasure Panel Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Show the memories Hollow extracts from the visitor in a "Treasure" side panel, and confirm Hollow uses them in later turns.
6
+
7
+ **Architecture:** The backend is already complete β€” `TurnUpdate.new_memories`, the extraction prompt, `apply_update`'s dedupe, and `build_system_prompt`'s treasure injection all exist and are tested. This day is **presentation only**. We add a pure `render_treasure(treasure) -> str` function in a new `render.py` module (no Gradio import β†’ unit-testable in isolation, the same separation pattern Day 2 used when it moved logic into `memory.py`), then wire a `gr.HTML` panel into a two-column layout in `app.py`. All visitor-derived memory text is HTML-escaped before injection.
8
+
9
+ **Tech Stack:** Python 3.11, Gradio 6.x (`gr.Blocks`, `gr.Row`, `gr.Column`, `gr.HTML`, `.then()` chaining), `html` stdlib, pytest via uv.
10
+
11
+ ---
12
+
13
+ ## File map
14
+
15
+ | File | Action | Responsibility |
16
+ |------|--------|----------------|
17
+ | `render.py` | **Create** | `render_treasure` β€” pure HTML string from a memory list, with escaping |
18
+ | `tests/test_render.py` | **Create** | Unit tests for `render_treasure` (placeholder, items, escaping) |
19
+ | `app.py` | **Modify** | Two-column layout; `gr.HTML` treasure panel; wire into both `chat()` returns |
20
+
21
+ **No changes** to `engine.py`, `memory.py`, `character.py`, `schemas.py` β€” the memory pipeline is done.
22
+
23
+ ---
24
+
25
+ ## Design notes (read before starting)
26
+
27
+ - **Empty state = Hollow's voice.** When `treasure` is empty, the panel shows `...nothing yet. but i'm listening.` β€” chosen so the panel reads as Hollow paying attention, not as empty UI.
28
+ - **Escaping is mandatory.** Memories originate from the visitor's typed text (the LLM extracts them). Raw injection into `gr.HTML` would break on `<`, `>`, `&` and allow HTML injection. Every memory passes through `html.escape()`.
29
+ - **The 7th output.** `chat()` currently returns 6 values from **two** `return` statements (the empty-message early return and the normal return). Both must be extended with the treasure HTML or Gradio raises an output-count mismatch.
30
+ - **`_start_turn` is NOT touched for treasure.** It does not receive `state`, and a memory shared this turn is only extracted inside `chat()` β€” there is nothing new to show during the loading placeholder. Treasure updates live only in `chat()`.
31
+ - **Inline styles are the Day-3 base.** The dark styling here is intentionally minimal (matches the approved mockup). Day 5 replaces it with global CSS in `styles.css`. Keep all styling inside `render.py` so Day 5 has one place to change.
32
+
33
+ ---
34
+
35
+ ## Task 1: Create `tests/test_render.py` with failing tests for `render_treasure`
36
+
37
+ **Files:**
38
+ - Create: `tests/test_render.py`
39
+
40
+ - [ ] **Step 1: Write the failing tests**
41
+
42
+ Create `tests/test_render.py`:
43
+
44
+ ```python
45
+ from render import render_treasure
46
+
47
+
48
+ class TestRenderTreasure:
49
+ def test_empty_shows_placeholder(self):
50
+ out = render_treasure([])
51
+ assert "nothing yet. but i'm listening." in out
52
+
53
+ def test_empty_has_no_memory_item(self):
54
+ # The memory-item style marker must not appear when there are no memories
55
+ out = render_treasure([])
56
+ assert "border-left:2px solid #5a3a7a" not in out
57
+
58
+ def test_single_memory_appears(self):
59
+ out = render_treasure(["has a dog named Nala"])
60
+ assert "has a dog named Nala" in out
61
+
62
+ def test_multiple_memories_all_appear(self):
63
+ out = render_treasure(["has a dog named Nala", "grew up near the sea"])
64
+ assert "has a dog named Nala" in out
65
+ assert "grew up near the sea" in out
66
+
67
+ def test_html_tags_are_escaped(self):
68
+ out = render_treasure(["<script>alert('x')</script>"])
69
+ assert "<script>" not in out
70
+ assert "&lt;script&gt;" in out
71
+
72
+ def test_ampersand_is_escaped(self):
73
+ out = render_treasure(["mom & dad"])
74
+ assert "mom &amp; dad" in out
75
+
76
+ def test_header_present_in_both_states(self):
77
+ assert "Treasure" in render_treasure([])
78
+ assert "Treasure" in render_treasure(["x"])
79
+ ```
80
+
81
+ - [ ] **Step 2: Run tests to confirm they fail with ImportError**
82
+
83
+ ```
84
+ .venv\Scripts\pytest tests/test_render.py -v
85
+ ```
86
+
87
+ Expected: FAIL with `ModuleNotFoundError: No module named 'render'`. The tests must fail before we implement.
88
+
89
+ ---
90
+
91
+ ## Task 2: Create `render.py` to make the tests pass
92
+
93
+ **Files:**
94
+ - Create: `render.py`
95
+
96
+ - [ ] **Step 1: Create `render.py`**
97
+
98
+ ```python
99
+ import html
100
+
101
+ _PLACEHOLDER = "...nothing yet. but i'm listening."
102
+
103
+ _PANEL = (
104
+ "background:#0c0a12;border:1px solid #2a1f3d;border-radius:4px;"
105
+ "padding:10px;min-height:460px;"
106
+ )
107
+ _HEADER = (
108
+ "font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#5a4570;"
109
+ "padding-bottom:6px;border-bottom:1px solid #1e1628;margin-bottom:8px;"
110
+ )
111
+ _ITEM = (
112
+ "background:#110d1a;border:1px solid #2a1f3d;border-left:2px solid #5a3a7a;"
113
+ "border-radius:0 3px 3px 0;padding:5px 8px;margin-bottom:6px;"
114
+ "font-size:11px;color:#9a80b8;line-height:1.4;"
115
+ )
116
+ _EMPTY = "font-size:11px;color:#4a3f56;font-style:italic;"
117
+
118
+
119
+ def render_treasure(treasure: list[str]) -> str:
120
+ header = f'<div style="{_HEADER}">✦ Treasure</div>'
121
+ if not treasure:
122
+ body = f'<div style="{_EMPTY}">{_PLACEHOLDER}</div>'
123
+ else:
124
+ body = "".join(
125
+ f'<div style="{_ITEM}">{html.escape(m)}</div>' for m in treasure
126
+ )
127
+ return f'<div style="{_PANEL}">{header}{body}</div>'
128
+ ```
129
+
130
+ Note: `_PLACEHOLDER` is a trusted constant (no visitor data), so it is not escaped β€” its apostrophe is valid in HTML text content. Only the visitor-derived `m` values are passed through `html.escape()`. `✦` is the ✦ star glyph, kept as an escape so the file stays ASCII-safe.
131
+
132
+ - [ ] **Step 2: Run the tests**
133
+
134
+ ```
135
+ .venv\Scripts\pytest tests/test_render.py -v
136
+ ```
137
+
138
+ Expected: all 7 tests PASS.
139
+
140
+ - [ ] **Step 3: Run the full suite to confirm nothing else broke**
141
+
142
+ ```
143
+ .venv\Scripts\pytest tests/ -v
144
+ ```
145
+
146
+ Expected: all 23 tests PASS (16 from `test_memory.py` + 7 new).
147
+
148
+ - [ ] **Step 4: Commit**
149
+
150
+ ```
151
+ git add render.py tests/test_render.py
152
+ git commit -m "feat: add render.py with render_treasure, tests passing"
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Task 3: Wire the treasure panel into `app.py`
158
+
159
+ **Files:**
160
+ - Modify: `app.py`
161
+
162
+ - [ ] **Step 1: Replace the full contents of `app.py`**
163
+
164
+ ```python
165
+ import gradio as gr
166
+
167
+ from character import build_system_prompt, OPENING_LINE
168
+ from engine import run_turn
169
+ from memory import apply_update, get_tier
170
+ from render import render_treasure
171
+
172
+
173
+ def _init_state():
174
+ return {
175
+ "affinity": 15,
176
+ "treasure": [],
177
+ "claimed": [],
178
+ "history": [],
179
+ "turn": 0,
180
+ }
181
+
182
+
183
+ def _start_turn(user_msg: str, history: list):
184
+ if not user_msg.strip():
185
+ return gr.update(), gr.update(interactive=False), history
186
+ new_history = history + [
187
+ {"role": "user", "content": user_msg},
188
+ {"role": "assistant", "content": "..."},
189
+ ]
190
+ return gr.update(interactive=False), gr.update(interactive=False), new_history
191
+
192
+
193
+ def chat(user_msg: str, state: dict, history: list):
194
+ if not user_msg.strip():
195
+ tier = get_tier(state["affinity"])
196
+ return (
197
+ gr.update(interactive=True),
198
+ gr.update(interactive=True),
199
+ history,
200
+ state,
201
+ state["affinity"],
202
+ f"*{tier}*",
203
+ render_treasure(state["treasure"]),
204
+ )
205
+
206
+ system = build_system_prompt(state["affinity"], state["treasure"])
207
+ chat_messages = [{"role": "system", "content": system}] + state["history"] + [
208
+ {"role": "user", "content": user_msg}
209
+ ]
210
+
211
+ reply, raw_json = run_turn(chat_messages, user_msg)
212
+ state = apply_update(state, raw_json)
213
+
214
+ state["history"].append({"role": "user", "content": user_msg})
215
+ state["history"].append({"role": "assistant", "content": reply})
216
+ state["turn"] += 1
217
+
218
+ new_history = history[:-1] + [{"role": "assistant", "content": reply}]
219
+
220
+ tier = get_tier(state["affinity"])
221
+ return (
222
+ gr.update(value="", interactive=True),
223
+ gr.update(interactive=True),
224
+ new_history,
225
+ state,
226
+ state["affinity"],
227
+ f"*{tier}*",
228
+ render_treasure(state["treasure"]),
229
+ )
230
+
231
+
232
+ with gr.Blocks(title="Hollow") as demo:
233
+ state = gr.State(_init_state)
234
+
235
+ gr.Markdown("## Hollow\n*something is here, at the edge of the wood*")
236
+
237
+ with gr.Row():
238
+ with gr.Column(scale=2):
239
+ chatbot = gr.Chatbot(
240
+ value=[{"role": "assistant", "content": OPENING_LINE}],
241
+ label="",
242
+ height=500,
243
+ )
244
+
245
+ with gr.Row():
246
+ affinity_slider = gr.Slider(
247
+ minimum=0, maximum=100, value=15,
248
+ interactive=False, label="Bond", scale=4,
249
+ )
250
+ tier_label = gr.Markdown("*Hollow*", scale=1)
251
+
252
+ with gr.Row():
253
+ msg_input = gr.Textbox(
254
+ placeholder="say something...",
255
+ show_label=False,
256
+ scale=9,
257
+ autofocus=True,
258
+ )
259
+ send_btn = gr.Button("β†’", scale=1)
260
+
261
+ with gr.Column(scale=1):
262
+ treasure_panel = gr.HTML(value=render_treasure([]))
263
+
264
+ send_btn.click(
265
+ fn=_start_turn,
266
+ inputs=[msg_input, chatbot],
267
+ outputs=[msg_input, send_btn, chatbot],
268
+ ).then(
269
+ fn=chat,
270
+ inputs=[msg_input, state, chatbot],
271
+ outputs=[msg_input, send_btn, chatbot, state, affinity_slider, tier_label, treasure_panel],
272
+ )
273
+
274
+ msg_input.submit(
275
+ fn=_start_turn,
276
+ inputs=[msg_input, chatbot],
277
+ outputs=[msg_input, send_btn, chatbot],
278
+ ).then(
279
+ fn=chat,
280
+ inputs=[msg_input, state, chatbot],
281
+ outputs=[msg_input, send_btn, chatbot, state, affinity_slider, tier_label, treasure_panel],
282
+ )
283
+
284
+ if __name__ == "__main__":
285
+ demo.launch()
286
+ ```
287
+
288
+ Key changes from the Day-2 version:
289
+ - `from render import render_treasure` added.
290
+ - Chat + meters + input wrapped in `gr.Column(scale=2)`; a new `gr.Column(scale=1)` holds `treasure_panel = gr.HTML(value=render_treasure([]))` so the placeholder shows on load.
291
+ - Both `chat()` returns now append `render_treasure(state["treasure"])` as a 7th value.
292
+ - Both `.then(...)` chat events add `treasure_panel` as the 7th output. `_start_turn` outputs are unchanged.
293
+
294
+ - [ ] **Step 2: Confirm the app imports without error**
295
+
296
+ ```
297
+ .venv\Scripts\python -c "import app; print('app imports OK')"
298
+ ```
299
+
300
+ Expected: `app imports OK` with no traceback. (This catches output-count or syntax mistakes before launching.)
301
+
302
+ - [ ] **Step 3: Run the full test suite**
303
+
304
+ ```
305
+ .venv\Scripts\pytest tests/ -v
306
+ ```
307
+
308
+ Expected: all 23 tests PASS.
309
+
310
+ - [ ] **Step 4: Commit**
311
+
312
+ ```
313
+ git add app.py
314
+ git commit -m "feat: treasure panel in app.py two-column layout"
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Task 4: Manual behavioral verification (the Day-3 checkpoint)
320
+
321
+ This checkpoint is **behavioral**, not visual β€” it must be run against the live local model (Ollama), not just confirmed by a rendered panel.
322
+
323
+ **Files:** none (verification only).
324
+
325
+ - [ ] **Step 1: Make sure Ollama is serving `qwen3:8b`**
326
+
327
+ ```
328
+ ollama list
329
+ ```
330
+
331
+ Expected: `qwen3:8b` appears in the list. If Ollama is not running, start it first.
332
+
333
+ - [ ] **Step 2: Launch the app**
334
+
335
+ ```
336
+ .venv\Scripts\python app.py
337
+ ```
338
+
339
+ Open `http://127.0.0.1:7860`.
340
+
341
+ - [ ] **Step 3: Verify the empty state**
342
+
343
+ On load, the right column shows the Treasure panel with the header `✦ Treasure` and the placeholder `...nothing yet. but i'm listening.`
344
+
345
+ - [ ] **Step 4: Verify a memory appears (turn 1)**
346
+
347
+ Type: `I have a dog named Nala.` and press Enter.
348
+ After Hollow replies, a memory item (e.g. `has a dog named Nala`) appears in the Treasure panel.
349
+
350
+ > Note: extraction is the LLM's job. The exact wording may vary; what matters is that a Nala-related memory shows up. If nothing appears, the issue is extraction (Day 2), not this panel β€” check the engine/extraction output, do not patch the panel.
351
+
352
+ - [ ] **Step 5: Verify Hollow recalls it (turn 2 β€” the real checkpoint)**
353
+
354
+ Type: `Do you remember my pet?` and press Enter.
355
+ Hollow's reply should reference Nala/the dog. This proves the treasure was injected into the next turn's system prompt via `build_system_prompt`.
356
+
357
+ > Timing by design: the memory shared in turn 1 is injected starting in turn 2 β€” the prompt that generates a reply cannot contain a memory that same reply hasn't been extracted from yet. See `app.py` `chat()`: `build_system_prompt(...)` runs before `apply_update(...)`.
358
+
359
+ - [ ] **Step 6: Verify escaping (defensive)**
360
+
361
+ Type: `My password is <secret>.` and press Enter.
362
+ If a memory is extracted from it, the Treasure panel renders the angle brackets as literal text β€” the panel does not break and no tag is interpreted.
363
+
364
+ **Checkpoint Day 3 done:** a memory the visitor shares appears in the Treasure panel, and Hollow references it on a later turn. βœ…
365
+
366
+ ---
367
+
368
+ ## Task 5: Deploy to HF Space
369
+
370
+ **Files:** none (deploy only).
371
+
372
+ - [ ] **Step 1: Push to the Space**
373
+
374
+ ```
375
+ git push space main
376
+ ```
377
+
378
+ - [ ] **Step 2: Verify on the live Space**
379
+
380
+ Open `https://huggingface.co/spaces/Pabloler21/hollow` once the Space rebuilds. Confirm:
381
+ - The Treasure panel is visible in the right column with the placeholder on load.
382
+ - After sharing a memory, it appears in the panel.
383
+ - On a later turn, Hollow references the shared memory.
384
+
385
+ ---
386
+
387
+ ## Self-review notes
388
+
389
+ - **Spec coverage:** Day-3 plan items β€” extend `TurnUpdate` (already done Day 2), extraction of personal facts (already done Day 2), dedupe + treasure injection (already done Day 2), and the side panel listing memories (Tasks 1–3). The behavioral checkpoint is Task 4. All covered.
390
+ - **Escaping:** handled in `render.py` (Task 2) and tested (Task 1, `test_html_tags_are_escaped`, `test_ampersand_is_escaped`).
391
+ - **Output count:** both `chat()` returns and both `.then()` events extended together (Task 3) β€” no mismatch.
392
+ - **Type consistency:** `render_treasure(treasure: list[str]) -> str` defined in Task 2, imported and called identically in `app.py` (Task 3) and tests (Task 1).