|
38 | 38 | background: var(--bg); |
39 | 39 | color: var(--text); |
40 | 40 | 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; |
43 | 43 | overflow: hidden; |
44 | 44 | -webkit-font-smoothing: subpixel-antialiased; |
45 | 45 | } |
|
67 | 67 | /* Top status bar: terminal window-chrome look. */ |
68 | 68 | #status-bar { |
69 | 69 | flex: 0 0 auto; |
70 | | - padding: 8px 14px; |
| 70 | + padding: 7px 14px; |
71 | 71 | background: var(--bg-2); |
72 | 72 | border-bottom: 1px solid var(--rule); |
73 | | - font-size: 11px; |
| 73 | + font-size: 10px; |
74 | 74 | color: var(--muted); |
75 | | - letter-spacing: 0.1em; |
| 75 | + letter-spacing: 0.08em; |
76 | 76 | text-transform: uppercase; |
77 | 77 | display: flex; |
78 | 78 | justify-content: space-between; |
|
149 | 149 | visibility: hidden; |
150 | 150 | } |
151 | 151 | #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.0 — quiet-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 { |
160 | 160 | background: transparent; |
161 | 161 | border: 1px solid var(--rule); |
162 | 162 | color: var(--muted); |
163 | 163 | font-family: inherit; |
164 | | - padding: 1px 7px 2px; |
| 164 | + padding: 1px 8px 2px; |
165 | 165 | margin-left: 10px; |
166 | 166 | cursor: pointer; |
167 | 167 | 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; |
172 | 171 | line-height: 1; |
| 172 | + font-size: 11px; |
173 | 173 | } |
174 | | - #font-toggle:hover { |
| 174 | + #quiet-toggle:hover { |
175 | 175 | border-color: var(--accent); |
176 | 176 | background: var(--bg-3); |
177 | 177 | } |
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 { |
182 | 181 | outline: none; |
183 | 182 | border-color: var(--accent); |
184 | 183 | box-shadow: 0 0 0 1px var(--accent), 0 0 8px -2px rgba(107, 71, 255, 0.4); |
185 | 184 | } |
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; } |
205 | 195 | @keyframes blink { |
206 | 196 | 0%, 49% { opacity: 1; } |
207 | 197 | 50%, 100% { opacity: 0; } |
|
279 | 269 | } |
280 | 270 | .msg-copy.copied { color: var(--accent); border-color: var(--accent); } |
281 | 271 |
|
| 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 | + |
282 | 302 | /* Phase 2 (1.8.0) — node-path chips inside tool result bodies. |
283 | 303 | Inline chips for TD paths (`/project1/noise1`) so the user can |
284 | 304 | click to ask about that node. The chip stays inline-flow with |
|
657 | 677 | border-radius: 0; |
658 | 678 | padding: 8px 10px; |
659 | 679 | font-family: inherit; |
660 | | - font-size: 13px; |
| 680 | + font-size: 12px; |
661 | 681 | line-height: 1.4; |
662 | 682 | resize: none; |
663 | 683 | outline: none; |
|
727 | 747 | <span class="rhs"> |
728 | 748 | <span id="status-text">idle</span><span id="model-badge"></span><span id="token-meter"></span><span class="cursor">█</span> |
729 | 749 | </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" |
733 | 753 | aria-pressed="false"> |
734 | | - <span class="fs-a">a</span><span class="fs-A">A</span> |
| 754 | + <span class="qm-glyph">●</span> |
735 | 755 | </button> |
736 | 756 | </div> |
737 | 757 | <!-- |
@@ -821,32 +841,101 @@ <h4>quickstart</h4> |
821 | 841 | const $modelBadge = document.getElementById('model-badge'); |
822 | 842 | const $tokenMeter = document.getElementById('token-meter'); |
823 | 843 | 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')); |
848 | 866 | }); |
849 | 867 |
|
| 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 | + |
850 | 939 | // Render the initial welcome from the same template fullSync uses, so |
851 | 940 | // the wizard DOM is guaranteed present. |
852 | 941 | $history.innerHTML = WELCOME_HTML; |
@@ -1711,7 +1800,17 @@ <h4>quickstart</h4> |
1711 | 1800 | } |
1712 | 1801 |
|
1713 | 1802 | 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 | + } |
1715 | 1814 | render(); |
1716 | 1815 | } |
1717 | 1816 |
|
@@ -1910,17 +2009,13 @@ <h4>quickstart</h4> |
1910 | 2009 | $input.focus(); |
1911 | 2010 | return; |
1912 | 2011 | } |
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 === '.') { |
1922 | 2017 | e.preventDefault(); |
1923 | | - applyFontMode('small'); |
| 2018 | + applyQuietMode(!document.documentElement.classList.contains('quiet-mode')); |
1924 | 2019 | return; |
1925 | 2020 | } |
1926 | 2021 | // End — jump to bottom of history. Useful after scrolling up to |
|
0 commit comments