fix(web): relations sparkline no longer overflows the n column + PAD/relation tooltips (v0.20.1)

- cap the unicode sparkline (relations last-8, faders last-7) + overflow:hidden
  so it can't grow one-char-per-sample and paint over the evidence-count (n)
  column — the "n sitting behind the graph" bug
- add native title mouseover hints on all 3 PAD faders (P/A/D meaning + range)
  and every relationship metric row (warmth / trust·ability / benevolence /
  integrity / agency)
- state.lastSnap lets the console re-render without a refetch

Verified: pytest tests/test_web_* (84) + node Playwright injected a 24-sample
history (sparkline->n bounding-box overlap = 0; sparklines capped; tooltips present).
This commit is contained in:
2026-07-06 22:12:22 -07:00
parent 1fcb17730e
commit 10acaec33d
4 changed files with 22 additions and 11 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ upstream API key stays server-side (INV-003).
_As of 2026-07-06 (session cont.):_
**SHIPPED — web UI redesign via Claude Design (`v0.20.0`, MINOR, operator-approved).** The Claude Design prototype **`Ratatoskr Console.dc.html`** (project `bc0b65d1-a33e-422a-8bc1-3635c9112775`) was pulled via `DesignSync get_file` (design scopes already granted this session — no `/design-login` needed) and adapted into `src/ratatoskr/web/static/index.html`: translated OUT of the `.dc.html` dialect (`<x-dc>`/`<sc-if>`/`<sc-for>`/`{{}}`/`DCLogic`/external `_ds/` CSS — none runnable) into single-file/no-CDN/vanilla, with ALL real `/api/*` fetch + SSE wired into its DOM (endpoint set + SSE vocab unchanged from the prior SPA — ported verbatim, only DOM hooks re-targeted). New shape = a **3-column command-console**: left engine-ticker rail (DEBUG+ADMIN+tool/turn-lifecycle MERGED into one timeline via `tickerAdd` + a tools-armed chip list + a FULL-detail Bifrost rail pane) · center conversation (per-turn INLINE chain-of-thought, replacing the Think pane) · right RESIZABLE affect console (dominant/canonical-mood centerpiece + bipolar PAD faders EACH with a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ADDED (round 2, operator-requested): a **light/dark theme toggle** (dark default; FULL token override — surfaces+fg+borders+accent-as-text, since the designer's light theme only did surfaces → would've been light-on-light) + a **full-detail Bifrost pane** (endpoint/connected/consumer/caps/tools) + fixed the **engine-ticker spine** (was a container-anchored `::before` that scrolled out of view on auto-scroll → re-anchored to a content-height `.ticker-inner` wrapper) + **per-fader PAD turn-to-turn Δ+sparkline** (fills the room beside each meter, from the deduped-per-turn AFFECT_HIST) + an **INLINED data-URI favicon** (operator's `/home/lkraven/rata.png` — chibi aurora squirrel — downscaled 1024→64px via PIL, ~8.6KB base64, kills the /favicon.ico 404). ALL server routes UNCHANGED (**84 web tests green**). Verified BOTH lenses: `pytest tests/test_web_*` (84) + node Playwright drove the real UI end-to-end against personal :8081 (session open → Sindra seeded greeting → live turn SSE → affect console + relations + bifrost detail; theme toggle + PAD deltas + ticker spine + no-favicon-404 all confirmed, dark+light screenshots). `:8765` restarted on the new code. Contract `web_debug_surface.contract.md` amended in-commit (v0.20.0 presenter renames: `renderBifrostState``renderBifrost`, `renderAffectPane``renderConsole`, `setPersonaStrip` removed; INV-001/INV-004 held). **HONEST-SHAPE call (INV-001, agent-discretion within settled policy):** the dominant-emotion centerpiece shows a real OCC emotion (Tier-1) OR the CANONICAL mood word (Tier-3 e.g. Sindra→"positive and energized", dimmed) OR "—", NEVER a fabricated emotion; the affect-derived grid drops non-emitted intensity/decay-τ, shows only real/client-derived cells. **OPEN (operator's call):** the per-fader PAD Δ placement is a sensible default — operator offered to have the designer spec the exact treatment (hooks are in place to swap it).
**SHIPPED — web UI redesign via Claude Design (`v0.20.0`, MINOR, operator-approved).** The Claude Design prototype **`Ratatoskr Console.dc.html`** (project `bc0b65d1-a33e-422a-8bc1-3635c9112775`) was pulled via `DesignSync get_file` (design scopes already granted this session — no `/design-login` needed) and adapted into `src/ratatoskr/web/static/index.html`: translated OUT of the `.dc.html` dialect (`<x-dc>`/`<sc-if>`/`<sc-for>`/`{{}}`/`DCLogic`/external `_ds/` CSS — none runnable) into single-file/no-CDN/vanilla, with ALL real `/api/*` fetch + SSE wired into its DOM (endpoint set + SSE vocab unchanged from the prior SPA — ported verbatim, only DOM hooks re-targeted). New shape = a **3-column command-console**: left engine-ticker rail (DEBUG+ADMIN+tool/turn-lifecycle MERGED into one timeline via `tickerAdd` + a tools-armed chip list + a FULL-detail Bifrost rail pane) · center conversation (per-turn INLINE chain-of-thought, replacing the Think pane) · right RESIZABLE affect console (dominant/canonical-mood centerpiece + bipolar PAD faders EACH with a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ADDED (round 2, operator-requested): a **light/dark theme toggle** (dark default; FULL token override — surfaces+fg+borders+accent-as-text, since the designer's light theme only did surfaces → would've been light-on-light) + a **full-detail Bifrost pane** (endpoint/connected/consumer/caps/tools) + fixed the **engine-ticker spine** (was a container-anchored `::before` that scrolled out of view on auto-scroll → re-anchored to a content-height `.ticker-inner` wrapper) + **per-fader PAD turn-to-turn Δ+sparkline** (fills the room beside each meter, from the deduped-per-turn AFFECT_HIST) + an **INLINED data-URI favicon** (operator's `/home/lkraven/rata.png` — chibi aurora squirrel — downscaled 1024→64px via PIL, ~8.6KB base64, kills the /favicon.ico 404). ALL server routes UNCHANGED (**84 web tests green**). Verified BOTH lenses: `pytest tests/test_web_*` (84) + node Playwright drove the real UI end-to-end against personal :8081 (session open → Sindra seeded greeting → live turn SSE → affect console + relations + bifrost detail; theme toggle + PAD deltas + ticker spine + no-favicon-404 all confirmed, dark+light screenshots). `:8765` restarted on the new code. Contract `web_debug_surface.contract.md` amended in-commit (v0.20.0 presenter renames: `renderBifrostState``renderBifrost`, `renderAffectPane``renderConsole`, `setPersonaStrip` removed; INV-001/INV-004 held). **HONEST-SHAPE call (INV-001, agent-discretion within settled policy):** the dominant-emotion centerpiece shows a real OCC emotion (Tier-1) OR the CANONICAL mood word (Tier-3 e.g. Sindra→"positive and energized", dimmed) OR "—", NEVER a fabricated emotion; the affect-derived grid drops non-emitted intensity/decay-τ, shows only real/client-derived cells. **OPEN (operator's call):** the per-fader PAD Δ placement is a sensible default — operator offered to have the designer spec the exact treatment (hooks are in place to swap it). **`v0.20.1` patch (operator-reported UI):** fixed the relations sparkline overflowing onto the `n` (evidence-count) column — the sparkline grows one char/sample (HIST_CAP=24) and overflowed its fixed grid cell, covering `n`; now capped (relations last-8, faders last-7) + `overflow:hidden` clip; verified via Playwright injecting a 24-sample history (sparkline→n bounding-box overlap = 0). ADDED native `title` mouseover hints on all 3 PAD faders + every relationship metric row (meaning + range; static METRIC_HINTS, esc()'d). Added `state.lastSnap` (console can re-render without a refetch). Playwright-verified.
**SHIPPED THIS SESSION (all pushed; origin/main == `d75c4e8`; code tip `v0.19.9`) — details in Recent decisions:** the whole **#347 authored-history-write** arc landed end-to-end — OpenAPI re-vendor 2.2.0->2.3.0 (`75da676`), the CONSUMER side (`v0.19.6`: `write_authored_history` + `get_session_messages` + `--seed-first-message`, **live-proven on personal :8081** via a rule-based Heimdall allow — the PDP is rule-based NOT scope-on-key, policy user_id=ratatoskr->ALLOW/others->DENY-hide-404), persona_state `{pad:{pleasure,arousal,dominance}}` canonical alignment (#317) + Tier-3 prose re-vendor (`v0.19.7`), the **first-message-preset AUTO-SEED** (`v0.19.8`: new module `ratatoskr.first_message` wired into all 3 session-create paths, best-effort never-raise/never-block; heid-code-review + heid-bug-hunt hardened), and the web now RENDERS the seeded first-message (`v0.19.9`: new `GET /api/sessions/{id}/messages` route + SPA `loadTranscript`, Playwright-verified). Coverage-map re-converged **REST 19/41**. **Sindra:** her card was PATCHed (the `Startup:` workaround moved into a #347 first-message; non-destructive PATCH — OCEAN/persona/memory intact), and she's currently **RESET clean (0/0)** on the provider stores.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.20.0"
version = "0.20.1"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+19 -8
View File
@@ -395,7 +395,8 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
.fader .d { font-family: var(--font-mono); font-size: 9px; min-height: 11px; color: var(--fg-muted); }
.fader .d.up { color: var(--aus-bright-green); }
.fader .d.dn { color: var(--aus-bright-red); }
.fader .sp { font-family: var(--font-mono); font-size: 9px; color: var(--aus-cyan); letter-spacing: 1px; min-height: 11px; }
.fader .sp { font-family: var(--font-mono); font-size: 9px; color: var(--aus-cyan); letter-spacing: 1px; min-height: 11px; max-width: 46px; overflow: hidden; white-space: nowrap; text-align: center; }
.fader[title], .mrow[title], #affect-derived .dcell[title] { cursor: help; }
#fader-p .fill { background: linear-gradient(180deg, var(--aus-bright-green), var(--aus-green)); box-shadow: 0 0 10px rgba(81,224,138,0.35); }
#fader-a .fill { background: linear-gradient(180deg, var(--aus-blue), var(--aus-bright-blue)); box-shadow: 0 0 10px rgba(99,136,216,0.3); }
#fader-d .fill { background: linear-gradient(180deg, var(--aus-bright-cyan), var(--aus-cyan)); box-shadow: 0 0 10px rgba(66,220,209,0.35); }
@@ -419,7 +420,7 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
.mrow .md { font-size: 9px; }
.mrow .md.up { color: var(--aus-bright-green); }
.mrow .md.dn { color: var(--aus-bright-red); }
.mrow .msp { color: var(--aus-cyan); letter-spacing: 1px; font-size: 9px; }
.mrow .msp { color: var(--aus-cyan); letter-spacing: 1px; font-size: 9px; overflow: hidden; white-space: nowrap; }
.mrow .mn { color: var(--fg-muted); font-size: 9px; }
.mrow .mdesc { color: var(--fg-3); font-style: italic; font-size: 9.5px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#affect-console.narrow .mrow { grid-template-columns: 92px 50px 46px minmax(0,1fr); }
@@ -562,17 +563,17 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
</div>
<div class="pad-row">
<div class="faders">
<div class="fader">
<div class="fader" title="Pleasure (valence) — how positive vs negative the mood feels. 1 miserable ‥ 0 neutral ‥ +1 elated. Live, per-turn. Below: Δ since last turn + session sparkline.">
<div class="bar" id="fader-p"><div class="fill"></div><div class="mid"></div></div>
<span class="k">P</span><span class="v" id="pad-p-val"></span>
<span class="d" id="pad-p-d"></span><span class="sp" id="pad-p-sp"></span>
</div>
<div class="fader">
<div class="fader" title="Arousal (activation) — how energized vs calm the agent is. 1 sleepy ‥ 0 neutral ‥ +1 excited. Live, per-turn. Below: Δ since last turn + session sparkline.">
<div class="bar" id="fader-a"><div class="fill"></div><div class="mid"></div></div>
<span class="k">A</span><span class="v" id="pad-a-val"></span>
<span class="d" id="pad-a-d"></span><span class="sp" id="pad-a-sp"></span>
</div>
<div class="fader">
<div class="fader" title="Dominance (control) — how in-control vs submissive the agent feels. 1 submissive ‥ 0 neutral ‥ +1 dominant. Live, per-turn. Below: Δ since last turn + session sparkline.">
<div class="bar" id="fader-d"><div class="fill"></div><div class="mid"></div></div>
<span class="k">D</span><span class="v" id="pad-d-val"></span>
<span class="d" id="pad-d-d"></span><span class="sp" id="pad-d-sp"></span>
@@ -930,10 +931,11 @@ function renderRelations(snap) {
const d = trendDelta(hist);
const dcls = d.startsWith("▲") ? "up" : d.startsWith("▼") ? "dn" : "";
const nlab = (n !== undefined && n !== null) ? "n" + esc(n) : "";
return `<div class="mrow"><span class="mk">${esc(label)}</span>`
const hint = METRIC_HINTS[label] || "";
return `<div class="mrow"${hint ? ` title="${esc(hint)}"` : ""}><span class="mk">${esc(label)}</span>`
+ `<span class="mv">${esc(num(val))}</span>`
+ `<span class="md ${dcls}">${esc(d)}</span>`
+ `<span class="msp">${esc(sparkline(hist))}</span>`
+ `<span class="msp">${esc(sparkline((hist || []).slice(-8)))}</span>`
+ `<span class="mn">${nlab}</span>`
+ `<span class="mdesc">${esc(trendDesc(hist))}</span></div>`;
};
@@ -983,10 +985,19 @@ function setFaderTrend(axis) {
const dEl = $("pad-" + axis + "-d"), spEl = $("pad-" + axis + "-sp");
dEl.textContent = d;
dEl.className = "d " + (d.startsWith("▲") ? "up" : d.startsWith("▼") ? "dn" : "");
spEl.textContent = sparkline(hist);
spEl.textContent = sparkline(hist.slice(-7)); // cap width — the fader column is narrow
}
// mouseover hints for each relationship metric (native title tooltips).
const METRIC_HINTS = {
"warmth": "Warmth — affective closeness / bond toward this entity. 0 cold (stranger) ‥ 1 deep warm bond. Durable: accumulates across turns.",
"trust·ability": "Ability trust — belief this entity is competent / capable. 0 ‥ 1.",
"trust·benev": "Benevolence trust — belief this entity means well toward you. 0 ‥ 1.",
"trust·integ": "Integrity trust — belief this entity is honest / principled. 0 ‥ 1.",
"agency": "Agency — this entity's perceived autonomy / capacity to act. 0 ‥ 1.",
};
// Unified console render for both Tier-1 persona_state and Tier-3 affect snapshots.
function renderConsole(snap) {
state.lastSnap = snap;
const pad = snap.pad || {};
setFader("p", pad.pleasure); setFader("a", pad.arousal); setFader("d", pad.dominance);
setFaderTrend("p"); setFaderTrend("a"); setFaderTrend("d");
Generated
+1 -1
View File
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.20.0"
version = "0.20.1"
source = { editable = "." }
dependencies = [
{ name = "httpx" },