Skip to content

Commit 830bf18

Browse files
dreamrecclaude
andcommitted
ui: chat HTML rework — quiet mode + smaller fonts + contextual ASCII flourishes
Three asks from live testing: 1. The aA font-size toggle felt out of place / weird scaling — remove. 2. The chat is too busy reading every tool call. Want a toggle that hides tool detail and shows just user prompts + assistant text. 3. Make the fonts a bit smaller everywhere, and add some little contextual ASCII art that fits the existing retro CRT aesthetic. ## Removed: aA font-size toggle (ex-PR-27) - Deleted `<button id="font-toggle">` HTML - Deleted `#font-toggle`, `.fs-a`, `.fs-A`, `:root.fs-small`, `:root.fs-large` CSS (~50 lines) - Deleted `$fontToggle`, `FONT_KEY`, `applyFontMode`, click handler, Cmd/Ctrl + +/- keyboard shortcut - Deleted `tests/test_chat_html_font_toggle.py` (23 tests) ## Added: quiet-mode toggle Same far-right slot in the status bar. Single button with a hollow (○) / filled (●) circle glyph indicating off / on. State persists per browser via `localStorage["tdpilot.quietMode"]`. When active, three CSS rules hide every tool-call surface: ```css :root.quiet-mode .msg.tool_call, :root.quiet-mode .msg.tool_result, :root.quiet-mode details.tool-pair { display: none; } ``` User prompts, assistant text, errors, and hints stay visible. The status bar at the top still shows the agent's per-turn working state ("thinking" / "calling td_create_node ...") so progress is never blacked out — quiet mode hides DETAIL, not signal. Keyboard shortcut: Cmd/Ctrl + . (period). Replaces the v2.0.0 font-zoom shortcuts. ## Smaller default fonts | Element | Was | Now | |---|---|---| | body / chat content | 13px | 12px | | status bar | 11px | 10px | | input textarea | 13px | 12px | Status-bar letter-spacing also softened from 0.1em → 0.08em so the slightly-smaller text doesn't look too wide. line-height bumped from 1.5 → 1.55 to compensate for the tighter base size. ## Contextual ASCII flourishes On agent turn-end (state transitions from working → idle), `attachFlourishToLastAssistant()` walks back through #history, finds the most recent assistant message that doesn't already carry a flourish, and appends a `.msg-flourish` div with a topic-matched glyph from a 9-bucket pool: default ▰▰▰ ∴ ◇ ◇ ◇ ═══ · · · ──◆── ·:·:· ▱▰▱ light ✺ ✸ ✺ \●/ ─ ✷ ─ ※ ✺ ※ camera [◉] ▤ ▥ ▤ ▣ ─ ▣ ┌─◎─┐ particle · ∴ · ∴ · ⋆ ✦ ⋆ ·:·∴·:· ∗ ⋆ ∗ material ▓▒░ ░▒▓ ░▒▓▒░ ▓ ─ ▓ audio ~~~ ∿ ~~~ ∿∿∿∿ · ∿ · ∿ · geom ◆ ─ ◇ ─ ◆ △ ▽ △ □ ▣ □ ◯ ◉ ◯ error × × × !? ─ ?! × ─ ✗ ─ × success ✓ ✓ ✓ ✦ ─ ▰ ─ ✦ ── ✓ ── · · ✦ · · Topic detection runs the last user message + assistant body through 8 keyword regexes (light/camera/particle/material/audio/ geom/error/success). If any non-default topic matches, 75% chance to draw from that bucket; 25% fallback to default for variety. The chosen topic name appears in tiny uppercase under the glyph — gives the reader a one-word post-context ("CAMERA", "PARTICLE", etc.). CSS keeps it visually quiet: - accent-dim color (purple-grey, not the bright primary accent) - centered with 0.5em letter-spacing (reads as punctuation) - 0.55 opacity - 1px dashed top border separating from the message body - no animation, no emoji, no extra weight ## Tests `tests/test_chat_html_v210_ui.py` — 18 structural assertions: - Font toggle is GONE (HTML, CSS, JS all clean) - Quiet toggle exists with correct DOM structure + a11y attrs - Status bar still has lhs / rhs / quiet-toggle in that order - :root.quiet-mode hides ONLY tool blocks (NOT user / assistant / error / hint) - Cmd/Ctrl + . shortcut wired to applyQuietMode - localStorage key namespaced + try/catch guarded - All 9 flourish topic buckets defined - 7 topic-detection regex literals present - attachFlourishToLastAssistant idempotent (won't double-attach) - Trigger fires on agent working → idle transition - .msg-flourish styling matches the quiet-aesthetic spec ## Results - 1635 tests pass / 12 deselected (was 1640 in v2.0.1: −23 font-toggle tests, +18 v2.1.0 tests) - ruff check + format clean ## Tox rebuild Required (chat HTML is in standalone tox `_SOURCE_FILES`). Pairs with the v2.0.1 release rebuild — same TD Textport recipe covers both this UI rework AND the v2.0.1 source-side fixes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e568782 commit 830bf18

3 files changed

Lines changed: 460 additions & 402 deletions

File tree

td_component/tdpilot_api_chat.html

Lines changed: 177 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
background: var(--bg);
3939
color: var(--text);
4040
font-family: "JetBrains Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
41-
font-size: 13px;
42-
line-height: 1.5;
41+
font-size: 12px;
42+
line-height: 1.55;
4343
overflow: hidden;
4444
-webkit-font-smoothing: subpixel-antialiased;
4545
}
@@ -67,12 +67,12 @@
6767
/* Top status bar: terminal window-chrome look. */
6868
#status-bar {
6969
flex: 0 0 auto;
70-
padding: 8px 14px;
70+
padding: 7px 14px;
7171
background: var(--bg-2);
7272
border-bottom: 1px solid var(--rule);
73-
font-size: 11px;
73+
font-size: 10px;
7474
color: var(--muted);
75-
letter-spacing: 0.1em;
75+
letter-spacing: 0.08em;
7676
text-transform: uppercase;
7777
display: flex;
7878
justify-content: space-between;
@@ -149,59 +149,49 @@
149149
visibility: hidden;
150150
}
151151
#status-bar.working .cursor { visibility: visible; }
152-
/* PR-27 (v2.0)chat font-size toggle. Two-state glyph control
153-
(`a` / `A`) at the far right of the status bar; the active size
154-
lights up in --accent, the inactive in --muted. Click swaps the
155-
size class on <html>; the body has no other knowledge of the
156-
toggle, so the rest of the chat keeps its existing scale.
157-
Defaults to small (today's behavior) so existing users see no
158-
change unless they opt in. */
159-
#font-toggle {
152+
/* v2.1.0quiet-mode toggle replaces the v2.0.0 font-size toggle.
153+
Single-button control at the far right of the status bar. When
154+
active (filled glyph), the chat hides ALL tool-call / tool-
155+
result blocks so the user sees only their prompts + the
156+
assistant's text replies + errors. Status bar still shows the
157+
agent's working state ("thinking" / "calling tool ...") so
158+
there's no progress blackout. Persisted via localStorage. */
159+
#quiet-toggle {
160160
background: transparent;
161161
border: 1px solid var(--rule);
162162
color: var(--muted);
163163
font-family: inherit;
164-
padding: 1px 7px 2px;
164+
padding: 1px 8px 2px;
165165
margin-left: 10px;
166166
cursor: pointer;
167167
display: inline-flex;
168-
align-items: baseline;
169-
gap: 3px;
170-
letter-spacing: 0; /* override status-bar uppercase tracking */
171-
text-transform: none; /* lowercase 'a' must stay lowercase */
168+
align-items: center;
169+
letter-spacing: 0;
170+
text-transform: none;
172171
line-height: 1;
172+
font-size: 11px;
173173
}
174-
#font-toggle:hover {
174+
#quiet-toggle:hover {
175175
border-color: var(--accent);
176176
background: var(--bg-3);
177177
}
178-
/* Keyboard focus matches the project's existing focus pattern
179-
(#input:focus uses the same accent ring + glow). Browsers
180-
default focus ring would clash with the chat aesthetic. */
181-
#font-toggle:focus {
178+
#quiet-toggle:focus { outline: none; border-color: var(--accent); }
179+
#quiet-toggle:focus:not(:focus-visible) { border-color: var(--rule); }
180+
#quiet-toggle:focus-visible {
182181
outline: none;
183182
border-color: var(--accent);
184183
box-shadow: 0 0 0 1px var(--accent), 0 0 8px -2px rgba(107, 71, 255, 0.4);
185184
}
186-
#font-toggle:focus:not(:focus-visible) {
187-
box-shadow: none;
188-
border-color: var(--rule);
189-
}
190-
#font-toggle:focus-visible {
191-
outline: none;
192-
border-color: var(--accent);
193-
box-shadow: 0 0 0 1px var(--accent), 0 0 8px -2px rgba(107, 71, 255, 0.4);
194-
}
195-
#font-toggle .fs-a { font-size: 9px; color: var(--muted); }
196-
#font-toggle .fs-A { font-size: 13px; color: var(--muted); }
197-
:root.fs-small #font-toggle .fs-a { color: var(--accent); }
198-
:root.fs-large #font-toggle .fs-A { color: var(--accent); }
199-
/* Scales chat content + input only — chrome (status bar, buttons,
200-
modal headers, badges) stays fixed at its designed scale so the
201-
layout doesn't reflow awkwardly. Tool-call summaries inherit
202-
from #history so they scale automatically. */
203-
:root.fs-large #history { font-size: 17px; }
204-
:root.fs-large #input { font-size: 17px; }
185+
#quiet-toggle .qm-glyph { color: var(--muted); }
186+
:root.quiet-mode #quiet-toggle .qm-glyph { color: var(--accent); }
187+
/* Quiet-mode hiding: tool_call + tool_result + the collapsible
188+
details.tool-pair wrapper all disappear. assistant text, user
189+
prompts, errors, and hints stay visible. Status bar at the top
190+
shows the agent's per-turn state ("calling td_create_node..."
191+
etc.) so the user still has a progress signal. */
192+
:root.quiet-mode .msg.tool_call,
193+
:root.quiet-mode .msg.tool_result,
194+
:root.quiet-mode details.tool-pair { display: none; }
205195
@keyframes blink {
206196
0%, 49% { opacity: 1; }
207197
50%, 100% { opacity: 0; }
@@ -279,6 +269,36 @@
279269
}
280270
.msg-copy.copied { color: var(--accent); border-color: var(--accent); }
281271

272+
/* v2.1.0 — contextual ASCII flourishes appended to assistant
273+
messages on turn-end. The pool is keyword-matched against the
274+
last user message + last assistant text so geometry / lighting /
275+
audio / particle / camera / error / done topics each draw from
276+
a small theme-specific glyph set. Flourishes are visually quiet
277+
— accent-dim color, lower opacity, centered, generous letter-
278+
spacing — so they read as a punctuation mark, not a decoration
279+
stealing focus. Default pool is generic geometric glyphs. */
280+
.msg-flourish {
281+
margin-top: 10px;
282+
padding-top: 6px;
283+
border-top: 1px dashed var(--rule);
284+
text-align: center;
285+
color: var(--accent-dim);
286+
font-family: inherit;
287+
font-size: 11px;
288+
letter-spacing: 0.5em;
289+
opacity: 0.55;
290+
user-select: none;
291+
line-height: 1;
292+
}
293+
.msg-flourish .topic {
294+
display: block;
295+
margin-top: 4px;
296+
font-size: 9px;
297+
letter-spacing: 0.2em;
298+
text-transform: uppercase;
299+
opacity: 0.7;
300+
}
301+
282302
/* Phase 2 (1.8.0) — node-path chips inside tool result bodies.
283303
Inline chips for TD paths (`/project1/noise1`) so the user can
284304
click to ask about that node. The chip stays inline-flow with
@@ -657,7 +677,7 @@
657677
border-radius: 0;
658678
padding: 8px 10px;
659679
font-family: inherit;
660-
font-size: 13px;
680+
font-size: 12px;
661681
line-height: 1.4;
662682
resize: none;
663683
outline: none;
@@ -727,11 +747,11 @@
727747
<span class="rhs">
728748
<span id="status-text">idle</span><span id="model-badge"></span><span id="token-meter"></span><span class="cursor"></span>
729749
</span>
730-
<button id="font-toggle" type="button"
731-
title="Toggle chat font size (small / large)"
732-
aria-label="Toggle chat font size"
750+
<button id="quiet-toggle" type="button"
751+
title="Quiet mode — hide tool-call detail (Cmd/Ctrl + .)"
752+
aria-label="Toggle quiet mode"
733753
aria-pressed="false">
734-
<span class="fs-a">a</span><span class="fs-A">A</span>
754+
<span class="qm-glyph"></span>
735755
</button>
736756
</div>
737757
<!--
@@ -821,32 +841,101 @@ <h4>quickstart</h4>
821841
const $modelBadge = document.getElementById('model-badge');
822842
const $tokenMeter = document.getElementById('token-meter');
823843
const $scrollJump = document.getElementById('scroll-jump');
824-
const $fontToggle = document.getElementById('font-toggle');
825-
826-
// PR-27 (v2.0) — chat font-size toggle. Two states; the active one
827-
// is reflected as a class on <html>. Persisted per-browser via
828-
// localStorage so the choice survives reloads. Restored before any
829-
// history rendering happens so users with a saved 'large' setting
830-
// never see a small-text flash on page load.
831-
const FONT_KEY = 'tdpilot.fontMode';
832-
function applyFontMode(mode) {
833-
if (mode !== 'small' && mode !== 'large') mode = 'small';
834-
document.documentElement.classList.remove('fs-small', 'fs-large');
835-
document.documentElement.classList.add('fs-' + mode);
836-
// aria-pressed reflects "non-default state" — large = pressed,
837-
// small = not pressed. Lets screen readers announce the toggle
838-
// state ("toggle button, pressed" / "not pressed") on focus.
839-
$fontToggle.setAttribute('aria-pressed', mode === 'large' ? 'true' : 'false');
840-
try { localStorage.setItem(FONT_KEY, mode); } catch (_) { /* private mode */ }
841-
}
842-
let _savedFontMode = 'small';
843-
try { _savedFontMode = localStorage.getItem(FONT_KEY) || 'small'; } catch (_) { /* private mode */ }
844-
applyFontMode(_savedFontMode);
845-
$fontToggle.addEventListener('click', () => {
846-
const cur = document.documentElement.classList.contains('fs-large') ? 'large' : 'small';
847-
applyFontMode(cur === 'large' ? 'small' : 'large');
844+
const $quietToggle = document.getElementById('quiet-toggle');
845+
846+
// v2.1.0 — quiet-mode toggle. Replaces the v2.0.0 font-size toggle
847+
// (the `aA` glyph control) which had inconsistent scaling and felt
848+
// out of place. Quiet mode hides the tool-call / tool-result blocks
849+
// so the chat stays readable as a conversation: user prompts +
850+
// assistant text + errors only. The agent's per-turn working state
851+
// is still surfaced via the status bar at the top, so there's no
852+
// progress-blackout. State persists per-browser via localStorage.
853+
const QUIET_KEY = 'tdpilot.quietMode';
854+
function applyQuietMode(on) {
855+
if (on) document.documentElement.classList.add('quiet-mode');
856+
else document.documentElement.classList.remove('quiet-mode');
857+
$quietToggle.setAttribute('aria-pressed', on ? 'true' : 'false');
858+
$quietToggle.querySelector('.qm-glyph').textContent = on ? '●' : '○';
859+
try { localStorage.setItem(QUIET_KEY, on ? '1' : '0'); } catch (_) { /* private mode */ }
860+
}
861+
let _savedQuiet = false;
862+
try { _savedQuiet = localStorage.getItem(QUIET_KEY) === '1'; } catch (_) { /* private mode */ }
863+
applyQuietMode(_savedQuiet);
864+
$quietToggle.addEventListener('click', () => {
865+
applyQuietMode(!document.documentElement.classList.contains('quiet-mode'));
848866
});
849867

868+
// v2.1.0 — contextual ASCII flourishes. On agent turn-end (status
869+
// transitions to idle after a non-idle state), pick a small glyph
870+
// from a topic-matched pool and append it to the latest assistant
871+
// message as a quiet visual punctuation. Retro-monochrome aesthetic
872+
// — geometric / typographic, no emoji, no color beyond --accent-dim.
873+
const ASCII_FLOURISHES = {
874+
default: ['▰▰▰', '∴', '◇ ◇ ◇', '═══', '· · ·', '──◆──', '·:·:·', '▱▰▱', '── ✦ ──'],
875+
light: ['✺ ✸ ✺', '\\●/', '─ ✷ ─', '※ ✺ ※'],
876+
camera: ['[◉]', '▤ ▥ ▤', '▣ ─ ▣', '┌─◎─┐'],
877+
particle: ['· ∴ · ∴ ·', '⋆ ✦ ⋆', '·:·∴·:·', '∗ ⋆ ∗'],
878+
material: ['▓▒░ ░▒▓', '░▒▓▒░', '▓ ─ ▓'],
879+
audio: ['~~~ ∿ ~~~', '∿∿∿∿', '· ∿ · ∿ ·'],
880+
geom: ['◆ ─ ◇ ─ ◆', '△ ▽ △', '□ ▣ □', '◯ ◉ ◯'],
881+
error: ['× × ×', '!? ─ ?!', '× ─ ✗ ─ ×'],
882+
success: ['✓ ✓ ✓', '✦ ─ ▰ ─ ✦', '── ✓ ──', '· · ✦ · ·'],
883+
};
884+
const TOPIC_RULES = [
885+
{ topic: 'light', re: /\b(light|lamp|illuminat|sun|shadow|brightness|expos)/i },
886+
{ topic: 'camera', re: /\b(camera|view|render|frame|lens|fov|projection|cam[0-9]?)\b/i },
887+
{ topic: 'particle', re: /\b(particle|pop|noise|swirl|flow|fluid|sand|spark)/i },
888+
{ topic: 'material', re: /\b(material|texture|color|paint|pbr|phong|shader)/i },
889+
{ topic: 'audio', re: /\b(audio|sound|music|chop|wave|spectrum|fft)/i },
890+
{ topic: 'geom', re: /\b(sop|geom|mesh|sphere|box|grid|line|extrude|topo)/i },
891+
{ topic: 'error', re: /\b(error|fail|broken|crash|exception|traceback)/i },
892+
{ topic: 'success', re: /\b(done|complete|fixed|working|success|ready|shipped|live)/i },
893+
];
894+
function pickFlourish(contextText) {
895+
const text = String(contextText || '');
896+
const matched = ['default'];
897+
for (const { topic, re } of TOPIC_RULES) {
898+
if (re.test(text)) matched.push(topic);
899+
}
900+
// Prefer a matched topic over default (skew last) — pick uniform random
901+
// from matched, but if any non-default topic matched, weight it 2× by
902+
// putting default at the tail of the choice list.
903+
const specific = matched.filter((t) => t !== 'default');
904+
const pool = specific.length > 0 && Math.random() > 0.25
905+
? ASCII_FLOURISHES[specific[Math.floor(Math.random() * specific.length)]]
906+
: ASCII_FLOURISHES.default;
907+
const glyph = pool[Math.floor(Math.random() * pool.length)];
908+
const topic = specific.length > 0 ? specific[0] : '';
909+
return { glyph, topic };
910+
}
911+
function attachFlourishToLastAssistant() {
912+
// Walk backwards through #history children to find the most recent
913+
// assistant message that doesn't already carry a flourish.
914+
const msgs = $history.querySelectorAll('.msg.assistant');
915+
if (msgs.length === 0) return;
916+
const last = msgs[msgs.length - 1];
917+
if (last.querySelector('.msg-flourish')) return;
918+
// Use the last user message as the primary topic source (most
919+
// accurate signal of what the turn is about), with the assistant
920+
// body as fallback so a generic prompt like "fix it" still picks
921+
// up topic from the response.
922+
const userMsgs = $history.querySelectorAll('.msg.user .body');
923+
const lastUser = userMsgs.length > 0 ? userMsgs[userMsgs.length - 1].textContent : '';
924+
const assistantBody = last.querySelector('.body');
925+
const ctx = (lastUser || '') + ' ' + (assistantBody ? assistantBody.textContent : '');
926+
const { glyph, topic } = pickFlourish(ctx);
927+
const f = document.createElement('div');
928+
f.className = 'msg-flourish';
929+
f.textContent = glyph;
930+
if (topic) {
931+
const t = document.createElement('span');
932+
t.className = 'topic';
933+
t.textContent = topic;
934+
f.appendChild(t);
935+
}
936+
last.appendChild(f);
937+
}
938+
850939
// Render the initial welcome from the same template fullSync uses, so
851940
// the wizard DOM is guaranteed present.
852941
$history.innerHTML = WELCOME_HTML;
@@ -1711,7 +1800,17 @@ <h4>quickstart</h4>
17111800
}
17121801

17131802
function setAgentStatus(text) {
1714-
agentState = (text || 'idle').toLowerCase();
1803+
const newState = (text || 'idle').toLowerCase();
1804+
const wasWorking = isWorkingAgentState(agentState);
1805+
agentState = newState;
1806+
// v2.1.0 — when the agent transitions from a working state back
1807+
// to idle, the assistant has finished responding. Append a
1808+
// contextual ASCII flourish to the last assistant message as
1809+
// quiet visual punctuation. Wrapped in try/catch so a flourish
1810+
// bug never blocks the status transition.
1811+
if (wasWorking && newState === 'idle') {
1812+
try { attachFlourishToLastAssistant(); } catch (_) { /* defensive */ }
1813+
}
17151814
render();
17161815
}
17171816

@@ -1910,17 +2009,13 @@ <h4>quickstart</h4>
19102009
$input.focus();
19112010
return;
19122011
}
1913-
// PR-27 (v2.0) — Cmd/Ctrl + `+` / `-` toggles chat font size
1914-
// independently of the browser-native zoom (which still works).
1915-
// `=` matches `+` for the unshifted keycap on US/EU layouts.
1916-
if (cmd && (e.key === '+' || e.key === '=')) {
1917-
e.preventDefault();
1918-
applyFontMode('large');
1919-
return;
1920-
}
1921-
if (cmd && e.key === '-') {
2012+
// v2.1.0 — Cmd/Ctrl + `.` toggles quiet mode. Discoverable via
2013+
// the title= tooltip on the toggle button at the far right of
2014+
// the status bar. Period chosen because `.` evokes "minimal /
2015+
// condensed" and isn't claimed by browsers.
2016+
if (cmd && e.key === '.') {
19222017
e.preventDefault();
1923-
applyFontMode('small');
2018+
applyQuietMode(!document.documentElement.classList.contains('quiet-mode'));
19242019
return;
19252020
}
19262021
// End — jump to bottom of history. Useful after scrolling up to

0 commit comments

Comments
 (0)