feat(typography): establish three-font contract (#5918)

* feat(typography): establish three-font contract

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(terminal): refit after live font changes

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: OpenCode:gpt-5.3-codex-spark
Assisted-by: Claude Code:claude-opus-4-8

* docs(typography): preserve global serif guardrail

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Claude Code:claude-opus-4-8

* fix(typography): enforce font tokens in rendered editors

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark

* fix(terminal): resync after stylesheet font changes

Observe live extension stylesheet changes and completed stylesheet loads so open terminals refresh font metrics and backend dimensions.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark
Assisted-by: Greptile:undisclosed

* test(typography): remove dynamic harness execution

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.3-codex-spark
Assisted-by: nesquena-hermes:undisclosed

* test(typography): skip missing Playwright browser

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: nesquena-hermes:undisclosed

* fix(terminal): refit after web fonts load

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: Greptile:undisclosed

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
This commit is contained in:
starship-sandnesquena-hermes authored and GitHub committed 2026-08-08 22:40:21 +00:00
1 parent 84f9be2088
commit 1480718767
11 files changed
+1083 -75

No files matched your search

+30
View File
@@ -144,6 +144,36 @@ font size. Persists alongside theme and skin.
--- ---
## Typography Tokens
The three font tokens are declared in `static/style.css`:
- `--font-ui`: user interface chrome, controls, composer chrome, labels, and
ordinary non-code UI text
- `--font-conversation`: user/assistant message prose
- `--font-mono`: code, tool details, logs, identifiers, and terminal text
By default:
```css
--font-conversation: var(--font-ui);
--font-mono: ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
```
For CSS-based custom skins and extension stylesheets/scripts, override the three
font tokens directly (`--font-ui`, `--font-conversation`, `--font-mono`) on the
skin root (`:root[data-skin="..."]`), including any mode-specific variant.
`window.registerHermesSkin()` currently accepts only the documented design-token
allowlist (no `--font-*` tokens), so custom font families should be supplied via
CSS rather than skin registration.
Leaving `--font-conversation` untouched keeps prose coupled to `--font-ui`;
override it only when a different reading face is explicitly required. Avoid
hard-coded selector-level font declarations when one of these tokens already
expresses the intent.
---
## How It Works Internally ## How It Works Internally
1. **Theme:** `document.documentElement.classList.toggle('dark', isDark)` — 1. **Theme:** `document.documentElement.classList.toggle('dark', isDark)` —
+18 -9
View File
@@ -96,16 +96,25 @@ unless the code and tests prove it is implemented.
## Typography and content ## Typography and content
Use split typography intentionally: Use three explicit font tokens:
- assistant prose uses the same system sans stack as the rest of the UI by - `--font-ui`: shell chrome, controls, composer, labels, and ordinary UI text
default (`--font-ui` in `static/style.css`), - `--font-conversation`: user/assistant message prose; by default this is
- editorial serif assistant prose is historical/proposal or skin-scoped only; `var(--font-ui)` in `static/style.css`
do not reintroduce a global assistant serif without explicit design approval - `--font-mono`: code, file paths, command lines, tool payloads, technical logs,
plus code and test evidence, and terminal output
- user bubbles and functional UI also use the system sans stack unless a scoped
skin intentionally overrides typography, Use semantic tokens for typography. Keep prose on `--font-conversation` by
- monospace is for code, file paths, commands, tool names, and compact metadata. default so it tracks `--font-ui` whenever a skin intentionally retunes UI type.
Override `--font-conversation` only for a skin that intentionally wants a
distinct prose face.
Conversation prose must remain the system sans by default. Do not introduce a
global conversation serif through the `--font-conversation` token or
selector-level overrides without explicit design approval plus code and test
evidence; keep distinct editorial prose typography explicitly opt-in or
skin-scoped.
Avoid hard-coding selector-level font stacks when a token already carries the
intent.
Keep scale tight. Avoid introducing near-duplicate one-off font sizes, colors, Keep scale tight. Avoid introducing near-duplicate one-off font sizes, colors,
radius values, or spacing values when an existing token works. radius values, or spacing values when an existing token works.
+1 -1
View File
@@ -3871,5 +3871,5 @@ async function shutdownServer() {
function _showServerStopped() { function _showServerStopped() {
var stoppedMsg = (typeof t === 'function' ? t('settings_shutdown_stopped_message') : 'Server stopped. You can close this tab.'); var stoppedMsg = (typeof t === 'function' ? t('settings_shutdown_stopped_message') : 'Server stopped. You can close this tab.');
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;color:var(--muted);font-family:system-ui,ui-sans-serif;font-size:14px"><p>' + stoppedMsg + '</p></div>'; document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;color:var(--muted);font-family:var(--font-ui);font-size:14px"><p>' + stoppedMsg + '</p></div>';
} }
+1 -1
View File
@@ -1718,7 +1718,7 @@
<div class="preview-html-wrap" id="previewHtmlWrap" style="display:none;flex:1;border-radius:8px;overflow:hidden;border:1px solid var(--border2)"> <div class="preview-html-wrap" id="previewHtmlWrap" style="display:none;flex:1;border-radius:8px;overflow:hidden;border:1px solid var(--border2)">
<iframe id="previewHtmlIframe" style="width:100%;height:100%;border:none;background:#fff" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox" title="HTML preview"></iframe> <iframe id="previewHtmlIframe" style="width:100%;height:100%;border:none;background:#fff" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox" title="HTML preview"></iframe>
</div> </div>
<textarea id="previewEditArea" style="display:none;flex:1;width:100%;background:var(--code-bg);color:var(--pre-text);border:1px solid var(--border2);border-radius:8px;padding:12px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;line-height:1.6;resize:none;outline:none" oninput="_previewDirty=true;updateEditBtn()"></textarea> <textarea id="previewEditArea" style="display:none;flex:1;width:100%;background:var(--code-bg);color:var(--pre-text);border:1px solid var(--border2);border-radius:8px;padding:12px;font-family:var(--font-mono);font-size:12px;line-height:1.6;resize:none;outline:none" oninput="_previewDirty=true;updateEditBtn()"></textarea>
</div> </div>
</aside> </aside>
</div> </div>
+1 -1
View File
@@ -12312,7 +12312,7 @@ function _openAuxAdvancedOptions(taskCfg,cfg){
_auxAdvancedInputHtml('auxAdvancedBaseUrl',t('settings_aux_advanced_base_url')||'Base URL',_auxAdvancedValue(cfg,'base_url'),t('settings_aux_advanced_base_url_desc')||'Optional provider endpoint override.','text','inputmode="url"')+ _auxAdvancedInputHtml('auxAdvancedBaseUrl',t('settings_aux_advanced_base_url')||'Base URL',_auxAdvancedValue(cfg,'base_url'),t('settings_aux_advanced_base_url_desc')||'Optional provider endpoint override.','text','inputmode="url"')+
serviceTierField+ serviceTierField+
timingFields+ timingFields+
`<label style="display:grid;gap:4px;font-size:12px;color:var(--text)"><span style="font-weight:600">${esc(t('settings_aux_advanced_extra_body')||'Extra body JSON')}</span><textarea id="auxAdvancedExtraBody" rows="6" style="width:100%;box-sizing:border-box;padding:7px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px;font-family:var(--mono,monospace)">${esc(extraBody)}</textarea><span style="font-size:10px;color:var(--muted);line-height:1.35">${esc(t('settings_aux_advanced_extra_body_desc')||'Optional JSON object merged into the model request body.')}</span></label>`+ `<label style="display:grid;gap:4px;font-size:12px;color:var(--text)"><span style="font-weight:600">${esc(t('settings_aux_advanced_extra_body')||'Extra body JSON')}</span><textarea id="auxAdvancedExtraBody" rows="6" style="width:100%;box-sizing:border-box;padding:7px 8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:12px;font-family:var(--font-mono)">${esc(extraBody)}</textarea><span style="font-size:10px;color:var(--muted);line-height:1.35">${esc(t('settings_aux_advanced_extra_body_desc')||'Optional JSON object merged into the model request body.')}</span></label>`+
_auxAdvancedInputHtml('auxAdvancedApiKey',t('settings_aux_advanced_api_key')||'API key override','',apiKeyHint,'text','autocomplete="one-time-code" inputmode="text" readonly onfocus="this.removeAttribute(&quot;readonly&quot;)"',';-webkit-text-security:disc')+ _auxAdvancedInputHtml('auxAdvancedApiKey',t('settings_aux_advanced_api_key')||'API key override','',apiKeyHint,'text','autocomplete="one-time-code" inputmode="text" readonly onfocus="this.removeAttribute(&quot;readonly&quot;)"',';-webkit-text-security:disc')+
`<label style="display:${cfg&&cfg.api_key_set?'flex':'none'};align-items:center;gap:8px;font-size:12px;color:var(--text)"><input id="auxAdvancedApiKeyClear" type="checkbox" style="width:15px;height:15px;accent-color:var(--accent)"><span>${esc(t('settings_aux_advanced_api_key_clear')||'Clear existing API key override')}</span></label>`; `<label style="display:${cfg&&cfg.api_key_set?'flex':'none'};align-items:center;gap:8px;font-size:12px;color:var(--text)"><input id="auxAdvancedApiKeyClear" type="checkbox" style="width:15px;height:15px;accent-color:var(--accent)"><span>${esc(t('settings_aux_advanced_api_key_clear')||'Clear existing API key override')}</span></label>`;
} }
+57 -53
View File
@@ -1,4 +1,9 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Typography contract ──
--font-ui: shell chrome, controls, composer, labels, and ordinary UI.
--font-conversation: user/assistant message prose (defaults to --font-ui).
--font-mono: inline code, fenced code blocks, logs, tool/metadata strings, and terminal.
*/
/* ── Light mode (default) — warm gold-tinted palette from Hermes brand ── */ /* ── Light mode (default) — warm gold-tinted palette from Hermes brand ── */
:root { :root {
--bg:#FEFCF7;--sidebar:#FAF7F0;--border:#E0D8C8;--border2:rgba(0,0,0,0.15); --bg:#FEFCF7;--sidebar:#FAF7F0;--border:#E0D8C8;--border2:rgba(0,0,0,0.15);
@@ -15,6 +20,8 @@
--message-body-font-size:14px;--message-body-line-height:1.75;--message-code-font-size:12.5px;--message-pre-code-font-size:13px;--message-table-font-size:12px; --message-body-font-size:14px;--message-body-line-height:1.75;--message-code-font-size:12.5px;--message-pre-code-font-size:13px;--message-table-font-size:12px;
--file-tree-toggle-width:10px; --file-tree-toggle-width:10px;
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif; --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
--font-conversation:var(--font-ui);
--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
--surface-subtle:rgba(0,0,0,.025);--surface-subtle-hover:rgba(0,0,0,.045); --surface-subtle:rgba(0,0,0,.025);--surface-subtle-hover:rgba(0,0,0,.045);
--border-subtle:rgba(0,0,0,.08);--border-muted:rgba(0,0,0,.12); --border-subtle:rgba(0,0,0,.08);--border-muted:rgba(0,0,0,.12);
font-family:var(--font-ui);font-size:14px;line-height:1.6; font-family:var(--font-ui);font-size:14px;line-height:1.6;
@@ -141,7 +148,6 @@
--user-bubble-bg:#EFEFEF;--user-bubble-border:#DEDEDE;--user-bubble-text:#242424;--user-bubble-placeholder:#707070; --user-bubble-bg:#EFEFEF;--user-bubble-border:#DEDEDE;--user-bubble-text:#242424;--user-bubble-placeholder:#707070;
--user-selection-bg:rgba(0,0,0,0.16);--user-selection-text:#111111; --user-selection-bg:rgba(0,0,0,0.16);--user-selection-text:#111111;
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
font-family:var(--font-ui); font-family:var(--font-ui);
} }
:root.dark[data-skin="graphite"]{ :root.dark[data-skin="graphite"]{
@@ -203,7 +209,7 @@
:root.dark[data-skin="graphite"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);} :root.dark[data-skin="graphite"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);}
:root[data-skin="graphite"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;} :root[data-skin="graphite"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;}
:root[data-skin="graphite"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;} :root[data-skin="graphite"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;}
:root[data-skin="graphite"] .msg-body{font-family:var(--font-ui);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;} :root[data-skin="graphite"] .msg-body{font-family:var(--font-conversation);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;}
:root[data-skin="graphite"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;} :root[data-skin="graphite"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;}
:root[data-skin="graphite"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;} :root[data-skin="graphite"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;}
:root[data-skin="graphite"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;} :root[data-skin="graphite"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;}
@@ -251,7 +257,7 @@
:root[data-skin="graphite"] .btn.primary:hover, :root[data-skin="graphite"] .btn.primary:hover,
:root[data-skin="graphite"] .update-primary:hover, :root[data-skin="graphite"] .update-primary:hover,
:root[data-skin="graphite"] .clarify-submit:hover{background:var(--accent-hover)!important;border-color:var(--accent-hover)!important;} :root[data-skin="graphite"] .clarify-submit:hover{background:var(--accent-hover)!important;border-color:var(--accent-hover)!important;}
/* ── Skin: Codex (OpenAI terminal IDE) ── /* ── Skin: Codex (OpenAI terminal IDE) ──
Full palette rewrite with minimal editor surfaces, crisp contrast, Full palette rewrite with minimal editor surfaces, crisp contrast,
and restrained OpenAI green action chrome. Uses native system fonts so it stays local, and restrained OpenAI green action chrome. Uses native system fonts so it stays local,
fast, and license-safe while feeling closer to modern code workbenches. */ fast, and license-safe while feeling closer to modern code workbenches. */
@@ -268,7 +274,6 @@
--user-bubble-bg:#EBEBEB;--user-bubble-border:#D9D9D9;--user-bubble-text:#252523;--user-bubble-placeholder:#6A6A68; --user-bubble-bg:#EBEBEB;--user-bubble-border:#D9D9D9;--user-bubble-text:#252523;--user-bubble-placeholder:#6A6A68;
--user-selection-bg:rgba(46,122,96,0.20);--user-selection-text:#0D0D0D; --user-selection-bg:rgba(46,122,96,0.20);--user-selection-text:#0D0D0D;
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
font-family:var(--font-ui); font-family:var(--font-ui);
} }
:root.dark[data-skin="codex"]{ :root.dark[data-skin="codex"]{
@@ -330,7 +335,7 @@
:root.dark[data-skin="codex"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);} :root.dark[data-skin="codex"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);}
:root[data-skin="codex"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;} :root[data-skin="codex"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;}
:root[data-skin="codex"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;} :root[data-skin="codex"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;}
:root[data-skin="codex"] .msg-body{font-family:var(--font-ui);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;} :root[data-skin="codex"] .msg-body{font-family:var(--font-conversation);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;}
:root[data-skin="codex"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;} :root[data-skin="codex"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;}
:root[data-skin="codex"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;} :root[data-skin="codex"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;}
:root[data-skin="codex"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;} :root[data-skin="codex"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;}
@@ -395,7 +400,6 @@
--user-bubble-bg:#F0EEE6;--user-bubble-border:#E2DED2;--user-bubble-text:#30302E;--user-bubble-placeholder:#87867F; --user-bubble-bg:#F0EEE6;--user-bubble-border:#E2DED2;--user-bubble-text:#30302E;--user-bubble-placeholder:#87867F;
--user-selection-bg:rgba(217,119,87,0.24);--user-selection-text:#141413; --user-selection-bg:rgba(217,119,87,0.24);--user-selection-text:#141413;
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
font-family:var(--font-ui); font-family:var(--font-ui);
} }
:root.dark[data-skin="terracotta"]{ :root.dark[data-skin="terracotta"]{
@@ -457,7 +461,7 @@
:root.dark[data-skin="terracotta"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);} :root.dark[data-skin="terracotta"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);}
:root[data-skin="terracotta"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;} :root[data-skin="terracotta"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;}
:root[data-skin="terracotta"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;} :root[data-skin="terracotta"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;}
:root[data-skin="terracotta"] .msg-body{font-family:var(--font-ui);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;} :root[data-skin="terracotta"] .msg-body{font-family:var(--font-conversation);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;}
:root[data-skin="terracotta"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;} :root[data-skin="terracotta"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;}
:root[data-skin="terracotta"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;} :root[data-skin="terracotta"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;}
:root[data-skin="terracotta"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;} :root[data-skin="terracotta"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;}
@@ -522,7 +526,6 @@
--user-bubble-bg:#EFEFEF;--user-bubble-border:#DEDEDE;--user-bubble-text:#242424;--user-bubble-placeholder:#707070; --user-bubble-bg:#EFEFEF;--user-bubble-border:#DEDEDE;--user-bubble-text:#242424;--user-bubble-placeholder:#707070;
--user-selection-bg:rgba(0,0,0,0.16);--user-selection-text:#111111; --user-selection-bg:rgba(0,0,0,0.16);--user-selection-text:#111111;
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
font-family:var(--font-ui); font-family:var(--font-ui);
} }
:root.dark[data-skin="github"]{ :root.dark[data-skin="github"]{
@@ -584,7 +587,7 @@
:root.dark[data-skin="github"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);} :root.dark[data-skin="github"] .composer-box{box-shadow:0 1px 2px rgba(0,0,0,0.28);}
:root[data-skin="github"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;} :root[data-skin="github"] .composer-box:focus-within{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--focus-ring)!important;}
:root[data-skin="github"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;} :root[data-skin="github"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;font-weight:430;letter-spacing:0;line-height:1.45;}
:root[data-skin="github"] .msg-body{font-family:var(--font-ui);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;} :root[data-skin="github"] .msg-body{font-family:var(--font-conversation);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;}
:root[data-skin="github"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;} :root[data-skin="github"][data-font-size="small"] .msg-body{font-size:11.5px;line-height:1.6;}
:root[data-skin="github"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;} :root[data-skin="github"][data-font-size="large"] .msg-body{font-size:15px;line-height:1.62;}
:root[data-skin="github"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;} :root[data-skin="github"][data-font-size="xlarge"] .msg-body{font-size:17px;line-height:1.62;}
@@ -1551,7 +1554,7 @@
.system-health-foot{font-size:11px;color:var(--muted);line-height:1.45;opacity:.82;} .system-health-foot{font-size:11px;color:var(--muted);line-height:1.45;opacity:.82;}
.app-titlebar-icon{display:inline-flex;align-items:center;color:var(--accent);} .app-titlebar-icon{display:inline-flex;align-items:center;color:var(--accent);}
.app-titlebar-title{font-size:12px;font-weight:600;color:var(--text);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw;} .app-titlebar-title{font-size:12px;font-weight:600;color:var(--text);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw;}
.app-titlebar-sub{font-size:10px;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;white-space:nowrap;flex-shrink:0;} .app-titlebar-sub{font-size:10px;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;font-family:var(--font-mono);white-space:nowrap;flex-shrink:0;}
.app-titlebar-sub[hidden]{display:none;} .app-titlebar-sub[hidden]{display:none;}
.app-titlebar-title-popover{position:fixed;z-index:300;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);box-shadow:0 4px 16px rgba(0,0,0,.18);word-break:break-word;line-height:1.4;max-width:calc(100vw - 16px);pointer-events:auto;} .app-titlebar-title-popover{position:fixed;z-index:300;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);box-shadow:0 4px 16px rgba(0,0,0,.18);word-break:break-word;line-height:1.4;max-width:calc(100vw - 16px);pointer-events:auto;}
.app-titlebar-hamburger,.app-titlebar-spacer{display:none;width:44px;height:44px;flex-shrink:0;} .app-titlebar-hamburger,.app-titlebar-spacer{display:none;width:44px;height:44px;flex-shrink:0;}
@@ -1991,7 +1994,7 @@
.approval-card.collapsed .approval-header{margin-bottom:0;} .approval-card.collapsed .approval-header{margin-bottom:0;}
.approval-card.collapsed .approval-desc,.approval-card.collapsed .approval-cmd,.approval-card.collapsed .approval-counter,.approval-card.collapsed .approval-btns{display:none;} .approval-card.collapsed .approval-desc,.approval-card.collapsed .approval-cmd,.approval-card.collapsed .approval-counter,.approval-card.collapsed .approval-btns{display:none;}
.approval-desc{font-size:12px;color:var(--muted);margin-bottom:8px;line-height:1.5;} .approval-desc{font-size:12px;color:var(--muted);margin-bottom:8px;line-height:1.5;}
.approval-cmd{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:"SF Mono",ui-monospace,monospace;font-size:12px;color:var(--pre-text);white-space:pre-wrap;word-break:break-all;margin-bottom:14px;max-height:120px;overflow-y:auto;} .approval-cmd{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:var(--font-mono);font-size:12px;color:var(--pre-text);white-space:pre-wrap;word-break:break-all;margin-bottom:14px;max-height:120px;overflow-y:auto;}
.approval-counter{font-size:11px;color:var(--muted);margin:0 0 12px;font-variant-numeric:tabular-nums;letter-spacing:0.01em;} .approval-counter{font-size:11px;color:var(--muted);margin:0 0 12px;font-variant-numeric:tabular-nums;letter-spacing:0.01em;}
.approval-btns{display:flex;gap:8px;flex-wrap:wrap;align-items:center;} .approval-btns{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.approval-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;border:1px solid var(--border2);background:var(--hover-bg);color:var(--text);cursor:pointer;transition:all .15s;white-space:nowrap;} .approval-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;border:1px solid var(--border2);background:var(--hover-bg);color:var(--text);cursor:pointer;transition:all .15s;white-space:nowrap;}
@@ -2301,7 +2304,7 @@
.role-icon{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;} .role-icon{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;}
.role-icon.user{background:var(--accent-bg);color:var(--accent-text);border:1px solid var(--accent-bg-strong);} .role-icon.user{background:var(--accent-bg);color:var(--accent-text);border:1px solid var(--accent-bg-strong);}
.role-icon.assistant{background:var(--accent-bg-strong);color:var(--accent-text);border:1px solid var(--accent-bg-strong);} .role-icon.assistant{background:var(--accent-bg-strong);color:var(--accent-text);border:1px solid var(--accent-bg-strong);}
.msg-body{font-size:var(--message-body-font-size);line-height:var(--message-body-line-height);color:var(--text);padding-left:30px;max-width:680px;overflow-wrap:anywhere;} .msg-body{font-family:var(--font-conversation);font-size:var(--message-body-font-size);line-height:var(--message-body-line-height);color:var(--text);padding-left:30px;max-width:680px;overflow-wrap:anywhere;}
.msg-body p{margin-bottom:10px;}.msg-body p:last-child{margin-bottom:0;} .msg-body p{margin-bottom:10px;}.msg-body p:last-child{margin-bottom:0;}
.msg-body td p,.msg-body th p{margin:0;} .msg-body td p,.msg-body th p{margin:0;}
.msg-body ul,.msg-body ol{margin:6px 0 10px 20px;}.msg-body li{margin-bottom:3px;} .msg-body ul,.msg-body ol{margin:6px 0 10px 20px;}.msg-body li{margin-bottom:3px;}
@@ -2317,7 +2320,7 @@
.msg-body > h1:first-child,.msg-body > h2:first-child,.msg-body > h3:first-child, .msg-body > h1:first-child,.msg-body > h2:first-child,.msg-body > h3:first-child,
.msg-body > h4:first-child,.msg-body > h5:first-child,.msg-body > h6:first-child{margin-top:0;} .msg-body > h4:first-child,.msg-body > h5:first-child,.msg-body > h6:first-child{margin-top:0;}
.msg-body strong{color:var(--strong);font-weight:600;}.msg-body em{color:var(--em);font-style:italic;} .msg-body strong{color:var(--strong);font-weight:600;}.msg-body em{color:var(--em);font-style:italic;}
.msg-body code{font-family:"SF Mono","Fira Code",ui-monospace,monospace;font-size:var(--message-code-font-size);background:var(--code-inline-bg);padding:1px 5px;border-radius:4px;color:var(--code-text);} .msg-body code{font-family:var(--font-mono);font-size:var(--message-code-font-size);background:var(--code-inline-bg);padding:1px 5px;border-radius:4px;color:var(--code-text);}
.msg-body pre{background:var(--code-bg);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;contain:content;margin:10px 0;} .msg-body pre{background:var(--code-bg);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;contain:content;margin:10px 0;}
.msg-body pre code{background:none;padding:0;border-radius:0;color:var(--pre-text);font-size:var(--message-pre-code-font-size);line-height:1.6;} .msg-body pre code{background:none;padding:0;border-radius:0;color:var(--pre-text);font-size:var(--message-pre-code-font-size);line-height:1.6;}
.msg-body pre.md-source-block,.preview-md pre.md-source-block{white-space:pre-wrap;overflow-x:hidden;overflow-wrap:anywhere;} .msg-body pre.md-source-block,.preview-md pre.md-source-block{white-space:pre-wrap;overflow-x:hidden;overflow-wrap:anywhere;}
@@ -2385,7 +2388,7 @@
.diff-inline-error{color:#ef4444;font-size:13px;padding:8px 12px;border:1px solid rgba(239,68,68,.2);border-radius:8px;margin:6px 0;} .diff-inline-error{color:#ef4444;font-size:13px;padding:8px 12px;border:1px solid rgba(239,68,68,.2);border-radius:8px;margin:6px 0;}
/* JSON/YAML tree viewer */ /* JSON/YAML tree viewer */
.code-tree-wrap{position:relative;} .code-tree-wrap{position:relative;}
.tree-view{padding:4px 0;font-family:'JetBrains Mono',monospace;font-size:13px;} .tree-view{padding:4px 0;font-family:var(--font-mono);font-size:13px;}
.tree-hidden{display:none;} .tree-hidden{display:none;}
.tree-toggle-btn{background:none;border:1px solid var(--border);border-radius:4px;padding:1px 8px;font-size:10px;color:var(--muted);cursor:pointer;font-weight:600;} .tree-toggle-btn{background:none;border:1px solid var(--border);border-radius:4px;padding:1px 8px;font-size:10px;color:var(--muted);cursor:pointer;font-weight:600;}
.tree-toggle-btn:hover{color:var(--text);border-color:var(--muted);} .tree-toggle-btn:hover{color:var(--text);border-color:var(--muted);}
@@ -2818,7 +2821,7 @@
.workspace-panel-heading.workspace-panel-heading--enabled{cursor:pointer;} .workspace-panel-heading.workspace-panel-heading--enabled{cursor:pointer;}
.workspace-panel-heading.workspace-panel-heading--enabled:hover, .workspace-panel-heading.workspace-panel-heading--enabled:hover,
.workspace-panel-heading.workspace-panel-heading--enabled:focus-visible{color:var(--text);background:var(--hover-bg);outline:none;} .workspace-panel-heading.workspace-panel-heading--enabled:focus-visible{color:var(--text);background:var(--hover-bg);outline:none;}
.git-badge{grid-column:1 / -1;grid-row:2;justify-self:start;font-size:9px;font-weight:600;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;letter-spacing:.02em;white-space:nowrap;font-family:'SF Mono',ui-monospace,monospace;overflow:hidden;min-width:0;max-width:100%;} .git-badge{grid-column:1 / -1;grid-row:2;justify-self:start;font-size:9px;font-weight:600;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;letter-spacing:.02em;white-space:nowrap;font-family:var(--font-mono);overflow:hidden;min-width:0;max-width:100%;}
.topbar-source-badge{display:inline-flex;align-items:center;margin-left:6px;padding:2px 7px;border-radius:999px;background:var(--accent-bg);color:var(--accent-text);font-size:10px;font-weight:700;letter-spacing:.02em;vertical-align:middle;} .topbar-source-badge{display:inline-flex;align-items:center;margin-left:6px;padding:2px 7px;border-radius:999px;background:var(--accent-bg);color:var(--accent-text);font-size:10px;font-weight:700;letter-spacing:.02em;vertical-align:middle;}
.git-badge.dirty{color:var(--accent-text);background:var(--accent-bg);} .git-badge.dirty{color:var(--accent-text);background:var(--accent-bg);}
.panel-actions{grid-column:2;grid-row:1;display:flex;gap:4px;flex-shrink:0;margin-left:auto;} .panel-actions{grid-column:2;grid-row:1;display:flex;gap:4px;flex-shrink:0;margin-left:auto;}
@@ -2897,7 +2900,7 @@
.preview-area{flex:1;overflow:auto;padding:14px;flex-direction:column;gap:8px;display:none;opacity:0;transition:opacity .15s;} .preview-area{flex:1;overflow:auto;padding:14px;flex-direction:column;gap:8px;display:none;opacity:0;transition:opacity .15s;}
.preview-area.visible{display:flex;opacity:1;} .preview-area.visible{display:flex;opacity:1;}
.preview-path{font-size:11px;color:var(--muted);padding-bottom:8px;border-bottom:1px solid var(--border);flex-shrink:0;} .preview-path{font-size:11px;color:var(--muted);padding-bottom:8px;border-bottom:1px solid var(--border);flex-shrink:0;}
.preview-code{font-family:"SF Mono","Fira Code",ui-monospace,monospace;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--pre-text);} .preview-code{font-family:var(--font-mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;color:var(--pre-text);}
/* Workspace preview syntax highlighting (#3337): Prism's prism-tomorrow theme /* Workspace preview syntax highlighting (#3337): Prism's prism-tomorrow theme
styles BOTH pre[class*="language-"] and code[class*="language-"] with its own styles BOTH pre[class*="language-"] and code[class*="language-"] with its own
gray background + padding. Prism propagates the language-* class onto the gray background + padding. Prism propagates the language-* class onto the
@@ -2925,7 +2928,7 @@
.preview-md h5{font-size:14px;margin:16px 0 6px;text-transform:uppercase;letter-spacing:0.04em;} .preview-md h5{font-size:14px;margin:16px 0 6px;text-transform:uppercase;letter-spacing:0.04em;}
.preview-md h6{font-size:13px;margin:14px 0 6px;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;} .preview-md h6{font-size:13px;margin:14px 0 6px;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;}
.preview-md ul,.preview-md ol{margin:4px 0 10px 18px;}.preview-md li{margin-bottom:3px;} .preview-md ul,.preview-md ol{margin:4px 0 10px 18px;}.preview-md li{margin-bottom:3px;}
.preview-md code{font-family:"SF Mono",ui-monospace,monospace;font-size:11.5px;background:var(--code-inline-bg);padding:1px 5px;border-radius:4px;color:var(--code-text);} .preview-md code{font-family:var(--font-mono);font-size:11.5px;background:var(--code-inline-bg);padding:1px 5px;border-radius:4px;color:var(--code-text);}
.preview-md pre{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;overflow-x:auto;margin:8px 0;} .preview-md pre{background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;overflow-x:auto;margin:8px 0;}
.preview-md pre code{background:none;padding:0;color:var(--pre-text);font-size:11.5px;line-height:1.55;} .preview-md pre code{background:none;padding:0;color:var(--pre-text);font-size:11.5px;line-height:1.55;}
/* Keep original theme background — prevent prism-tomorrow from overriding --code-bg */ /* Keep original theme background — prevent prism-tomorrow from overriding --code-bg */
@@ -3398,7 +3401,7 @@
.cmd-item-name{font-size:13px;color:var(--text);font-weight:500;} .cmd-item-name{font-size:13px;color:var(--text);font-weight:500;}
.cmd-item-parent{color:var(--muted);font-weight:400;} .cmd-item-parent{color:var(--muted);font-weight:400;}
.cmd-item-subarg{font-weight:600;} .cmd-item-subarg{font-weight:600;}
.cmd-item-path-value{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;font-size:12px;} .cmd-item-path-value{font-family:var(--font-mono);font-size:12px;}
.cmd-item-arg{color:var(--muted);font-weight:400;font-style:italic;} .cmd-item-arg{color:var(--muted);font-weight:400;font-style:italic;}
.cmd-item-desc{font-size:11px;color:var(--muted);margin-top:1px;} .cmd-item-desc{font-size:11px;color:var(--muted);margin-top:1px;}
.cmd-item-badge{flex-shrink:0;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:999px;border:1px solid var(--border2);color:var(--muted);background:var(--hover-bg);} .cmd-item-badge{flex-shrink:0;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:999px;border:1px solid var(--border2);color:var(--muted);background:var(--hover-bg);}
@@ -3437,7 +3440,7 @@ body.tts-enabled .msg-tts-btn{display:inline-flex;align-items:center;}
@keyframes tts-pulse{0%,100%{opacity:1}50%{opacity:.5}} @keyframes tts-pulse{0%,100%{opacity:1}50%{opacity:.5}}
/* ── Edit message inline ── */ /* ── Edit message inline ── */
.msg-edit-area{width:100%;background:rgba(255,255,255,.05);border:1px solid var(--accent-bg);border-radius:8px;color:var(--text);padding:10px 12px;font-size:14px;font-family:inherit;line-height:1.6;resize:none;outline:none;min-height:60px;box-sizing:border-box;box-shadow:0 0 0 3px var(--accent-bg);margin-top:4px;} .msg-edit-area{width:100%;background:rgba(255,255,255,.05);border:1px solid var(--accent-bg);border-radius:8px;color:var(--text);padding:10px 12px;font-size:14px;font-family:var(--font-conversation);line-height:1.6;resize:none;outline:none;min-height:60px;box-sizing:border-box;box-shadow:0 0 0 3px var(--accent-bg);margin-top:4px;}
.msg-edit-bar{display:flex;gap:8px;margin-top:8px;margin-bottom:4px;} .msg-edit-bar{display:flex;gap:8px;margin-top:8px;margin-bottom:4px;}
.msg-edit-send{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:opacity .15s;} .msg-edit-send{background:var(--accent);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:opacity .15s;}
.msg-edit-send:hover{opacity:.85;} .msg-edit-send:hover{opacity:.85;}
@@ -3742,7 +3745,7 @@ body.resizing .sidebar{transition:none!important;}
.tool-worklog-group[data-tool-worklog-group="1"]:not([data-run-activity-group="1"]) .as-dot{ .tool-worklog-group[data-tool-worklog-group="1"]:not([data-run-activity-group="1"]) .as-dot{
display:none; display:none;
} }
.tool-worklog-running-label{display:none;color:var(--accent-text);font-family:'SF Mono',ui-monospace,monospace;font-size:12px;font-weight:600;line-height:1.4;} .tool-worklog-running-label{display:none;color:var(--accent-text);font-family:var(--font-mono);font-size:12px;font-weight:600;line-height:1.4;}
.tool-worklog-group[data-tool-worklog-running="1"] .tool-worklog-running-label{display:inline-flex;} .tool-worklog-group[data-tool-worklog-running="1"] .tool-worklog-running-label{display:inline-flex;}
.tool-worklog-body{ .tool-worklog-body{
padding-left:0; padding-left:0;
@@ -3971,7 +3974,7 @@ body.resizing .sidebar{transition:none!important;}
.tool-worklog-tool-group-label, .tool-worklog-tool-group-label,
.tg-sum{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .tg-sum{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tool-worklog-tool-group-running, .tool-worklog-tool-group-running,
.tg-run{display:none;color:var(--accent-text);font-family:'SF Mono',ui-monospace,monospace;font-size:12px;font-weight:600;line-height:1.4;white-space:nowrap;} .tg-run{display:none;color:var(--accent-text);font-family:var(--font-mono);font-size:12px;font-weight:600;line-height:1.4;white-space:nowrap;}
.tool-worklog-tool-group-body, .tool-worklog-tool-group-body,
.tool-group-body{overflow:hidden;max-height:0;transition:max-height .3s ease;} .tool-group-body{overflow:hidden;max-height:0;transition:max-height .3s ease;}
.tool-worklog-tool-group.open .tool-worklog-tool-group-body, .tool-worklog-tool-group.open .tool-worklog-tool-group-body,
@@ -4055,9 +4058,9 @@ body.resizing .sidebar{transition:none!important;}
.tool-card-detail-lead pre{margin:0;color:var(--text);font-family:var(--font-mono);font-size:var(--message-code-font-size);line-height:1.55;white-space:pre-wrap;word-break:break-word;} .tool-card-detail-lead pre{margin:0;color:var(--text);font-family:var(--font-mono);font-size:var(--message-code-font-size);line-height:1.55;white-space:pre-wrap;word-break:break-word;}
.tool-card-args{margin-bottom:6px;} .tool-card-args{margin-bottom:6px;}
.tool-card-args div{font-size:var(--message-code-font-size);line-height:1.6;} .tool-card-args div{font-size:var(--message-code-font-size);line-height:1.6;}
.tool-arg-key{color:var(--blue);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--message-code-font-size);display:block;margin-bottom:2px;} .tool-arg-key{color:var(--blue);font-family:var(--font-mono);font-size:var(--message-code-font-size);display:block;margin-bottom:2px;}
.tool-arg-val{color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--message-code-font-size);white-space:pre-wrap;word-break:break-word;display:block;overflow-x:auto;} .tool-arg-val{color:var(--muted);font-family:var(--font-mono);font-size:var(--message-code-font-size);white-space:pre-wrap;word-break:break-word;display:block;overflow-x:auto;}
.tool-card-result pre{font-size:var(--message-code-font-size);color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;margin:0;line-height:1.55;width:100%;max-width:100%;box-sizing:border-box;min-width:0;} .tool-card-result pre{font-size:var(--message-code-font-size);color:var(--muted);font-family:var(--font-mono);white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;margin:0;line-height:1.55;width:100%;max-width:100%;box-sizing:border-box;min-width:0;}
/* ── Transparent Stream event rows ── */ /* ── Transparent Stream event rows ── */
/* Inline rhythm: transparent stream events are transcript metadata inside an /* Inline rhythm: transparent stream events are transcript metadata inside an
@@ -4623,7 +4626,7 @@ body.resizing .sidebar{transition:none!important;}
color:var(--strong); color:var(--strong);
} }
.handoff-summary-body code{ .handoff-summary-body code{
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
font-size:.92em; font-size:.92em;
background:var(--code-inline-bg); background:var(--code-inline-bg);
color:var(--code-text); color:var(--code-text);
@@ -4678,7 +4681,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-card-label{ .compression-card-label{
font-size:11px; font-size:11px;
font-weight:700; font-weight:700;
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
letter-spacing:.03em; letter-spacing:.03em;
color:var(--muted); color:var(--muted);
flex-shrink:0; flex-shrink:0;
@@ -4686,7 +4689,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-card-command{ .compression-card-command{
font-size:11px; font-size:11px;
color:var(--text); color:var(--text);
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
overflow:hidden; overflow:hidden;
text-overflow:ellipsis; text-overflow:ellipsis;
white-space:nowrap; white-space:nowrap;
@@ -4724,7 +4727,7 @@ body.resizing .sidebar{transition:none!important;}
} }
.compression-card-focus .compression-card-focus-label{ .compression-card-focus .compression-card-focus-label{
color:var(--muted); color:var(--muted);
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
} }
.compression-card-error-body{ .compression-card-error-body{
color:#fecaca; color:#fecaca;
@@ -4760,7 +4763,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-reference-kind{ .compression-reference-kind{
font-size:11px; font-size:11px;
font-weight:700; font-weight:700;
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
letter-spacing:.03em; letter-spacing:.03em;
flex-shrink:0; flex-shrink:0;
white-space:nowrap; white-space:nowrap;
@@ -4812,7 +4815,7 @@ body.resizing .sidebar{transition:none!important;}
} }
.compression-reference-body pre{ .compression-reference-body pre{
margin:0; margin:0;
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
font-size:11px; font-size:11px;
line-height:1.55; line-height:1.55;
white-space:pre-wrap; white-space:pre-wrap;
@@ -4850,7 +4853,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-complete-label{ .compression-complete-label{
font-size:11px; font-size:11px;
font-weight:700; font-weight:700;
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
letter-spacing:.03em; letter-spacing:.03em;
flex-shrink:0; flex-shrink:0;
} }
@@ -4861,7 +4864,7 @@ body.resizing .sidebar{transition:none!important;}
text-overflow:ellipsis; text-overflow:ellipsis;
white-space:nowrap; white-space:nowrap;
color:var(--text); color:var(--text);
font-family:'SF Mono',ui-monospace,monospace; font-family:var(--font-mono);
font-size:11px; font-size:11px;
} }
.compression-complete-toggle{ .compression-complete-toggle{
@@ -4969,7 +4972,7 @@ main.main > #mainPlugin{display:none;}
.settings-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--border);} .settings-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--border);}
.settings-section-title{font-size:18px;font-weight:600;letter-spacing:-.01em;color:var(--text);line-height:1.3;margin-bottom:4px;} .settings-section-title{font-size:18px;font-weight:600;letter-spacing:-.01em;color:var(--text);line-height:1.3;margin-bottom:4px;}
.settings-section-meta{font-size:13px;color:var(--muted);line-height:1.55;} .settings-section-meta{font-size:13px;color:var(--muted);line-height:1.55;}
.settings-version-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;background:var(--surface);color:var(--muted);font-size:11px;font-weight:600;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;flex-shrink:0;align-self:flex-start;letter-spacing:.02em;} .settings-version-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;background:var(--surface);color:var(--muted);font-size:11px;font-weight:600;font-family:var(--font-mono);flex-shrink:0;align-self:flex-start;letter-spacing:.02em;}
#checkUpdatesBlock{display:inline-flex;align-items:center;gap:0.5em;font-size:12px;flex-shrink:0;align-self:flex-start;flex-wrap:wrap;} #checkUpdatesBlock{display:inline-flex;align-items:center;gap:0.5em;font-size:12px;flex-shrink:0;align-self:flex-start;flex-wrap:wrap;}
#checkUpdatesBlock .btn-tiny{padding:4px 10px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-size:11px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:border-color .15s,color .15s;} #checkUpdatesBlock .btn-tiny{padding:4px 10px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-size:11px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:border-color .15s,color .15s;}
#checkUpdatesBlock .btn-tiny:hover{border-color:var(--accent);color:var(--accent);} #checkUpdatesBlock .btn-tiny:hover{border-color:var(--accent);color:var(--accent);}
@@ -5382,7 +5385,7 @@ main.main > #mainPlugin{display:none;}
border:1px solid var(--border); border:1px solid var(--border);
border-radius:8px; border-radius:8px;
font-size:12px; font-size:12px;
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace; font-family:var(--font-mono);
outline:none; outline:none;
transition:border-color .15s ease, box-shadow .15s ease; transition:border-color .15s ease, box-shadow .15s ease;
} }
@@ -5569,7 +5572,7 @@ main.main > #mainPlugin{display:none;}
.provider-card-model-tag{ .provider-card-model-tag{
display:inline-block; display:inline-block;
padding:2px 8px; padding:2px 8px;
font-size:10.5px;font-family:ui-monospace,SFMono-Regular,\"SF Mono\",Menlo,Consolas,monospace; font-size:10.5px;font-family:var(--font-mono);
color:var(--muted); color:var(--muted);
background:var(--surface); background:var(--surface);
border:1px solid var(--border); border:1px solid var(--border);
@@ -5640,7 +5643,7 @@ main.main > #mainPlugin{display:none;}
.cron-script-badge{flex-shrink:0;font-size:12px;line-height:1;opacity:.92;} .cron-script-badge{flex-shrink:0;font-size:12px;line-height:1;opacity:.92;}
.cron-script-job-banner{margin-bottom:12px;} .cron-script-job-banner{margin-bottom:12px;}
.cron-script-job-banner .detail-alert-title{font-weight:600;color:rgba(245,158,11,.98);} .cron-script-job-banner .detail-alert-title{font-weight:600;color:rgba(245,158,11,.98);}
.detail-script{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:'SF Mono',ui-monospace,monospace;word-break:break-word;} .detail-script{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:var(--font-mono);word-break:break-word;}
.detail-hint{font-size:11px;color:var(--muted);line-height:1.5;} .detail-hint{font-size:11px;color:var(--muted);line-height:1.5;}
.cron-script-card .detail-hint,.cron-script-card-hint{margin-top:8px;} .cron-script-card .detail-hint,.cron-script-card-hint{margin-top:8px;}
.detail-badge.cron-mode-badge.script{background:rgba(148,163,184,.14);color:var(--text-secondary);} .detail-badge.cron-mode-badge.script{background:rgba(148,163,184,.14);color:var(--text-secondary);}
@@ -5949,7 +5952,8 @@ main.main > #mainPlugin{display:none;}
.msg-body { padding-left: var(--msg-rail); padding-top: 8px; max-width: var(--msg-max); } .msg-body { padding-left: var(--msg-rail); padding-top: 8px; max-width: var(--msg-max); }
.msg-body code, .msg-body code,
.msg-body pre, .msg-body pre,
.msg-body table { font-family: 'SF Mono', ui-monospace, monospace; } .msg-body table { font-family:var(--font-mono); }
body code[class*="language-"],body pre[class*="language-"],body pre code[class*="language-"]{font-family:var(--font-mono);}
.msg-body:empty { display: none; } .msg-body:empty { display: none; }
.assistant-turn { width: 100%; } .assistant-turn { width: 100%; }
.assistant-turn-blocks { display: flex; flex-direction: column; } .assistant-turn-blocks { display: flex; flex-direction: column; }
@@ -6279,7 +6283,7 @@ main.main > #mainPlugin{display:none;}
transition: max-height .22s ease, opacity .18s ease, padding .22s ease, border-top-color .22s ease; transition: max-height .22s ease, opacity .18s ease, padding .22s ease, border-top-color .22s ease;
} }
.thinking-card.open .thinking-card-body { max-height: 260px; overflow-y: auto; opacity: 1; padding: 8px 12px; border-top-color: var(--accent-bg-strong); } .thinking-card.open .thinking-card-body { max-height: 260px; overflow-y: auto; opacity: 1; padding: 8px 12px; border-top-color: var(--accent-bg-strong); }
.thinking-card-body pre { font-family: 'SF Mono', ui-monospace, monospace; font-size: 11px; line-height: 1.6; color: var(--muted); white-space: pre-wrap; word-break: break-word; margin: 0; } .thinking-card-body pre { font-family:var(--font-mono); font-size: 11px; line-height: 1.6; color: var(--muted); white-space: pre-wrap; word-break: break-word; margin: 0; }
/* ── Tool cards: quiet disclosure rows ── */ /* ── Tool cards: quiet disclosure rows ── */
.tool-card { background:transparent;border:0;border-left:0;border-radius:0;margin-top:0;margin-bottom:0; } .tool-card { background:transparent;border:0;border-left:0;border-radius:0;margin-top:0;margin-bottom:0; }
@@ -6417,7 +6421,7 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.skill-file-breadcrumb{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border-subtle,var(--border));} .skill-file-breadcrumb{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border-subtle,var(--border));}
.skill-file-back{font-size:12px;color:var(--accent-text);text-decoration:none;font-weight:500;} .skill-file-back{font-size:12px;color:var(--accent-text);text-decoration:none;font-weight:500;}
.skill-file-back:hover{text-decoration:underline;} .skill-file-back:hover{text-decoration:underline;}
.skill-file-path{font-size:12px;color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;} .skill-file-path{font-size:12px;color:var(--muted);font-family:var(--font-mono);}
.skill-file-code{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-size:12px;overflow:auto;max-width:960px;margin:0 auto;} .skill-file-code{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-size:12px;overflow:auto;max-width:960px;margin:0 auto;}
@media(max-width:700px){ @media(max-width:700px){
@@ -6432,7 +6436,7 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.detail-row:last-child{border-bottom:none;} .detail-row:last-child{border-bottom:none;}
.detail-row-label{flex:0 0 140px;color:var(--muted);font-size:12px;} .detail-row-label{flex:0 0 140px;color:var(--muted);font-size:12px;}
.detail-row-value{flex:1;min-width:0;color:var(--text);word-break:break-word;} .detail-row-value{flex:1;min-width:0;color:var(--text);word-break:break-word;}
.detail-row-value code{background:var(--sidebar);border:1px solid var(--border);border-radius:4px;padding:1px 6px;font-size:12px;font-family:'SF Mono',ui-monospace,monospace;} .detail-row-value code{background:var(--sidebar);border:1px solid var(--border);border-radius:4px;padding:1px 6px;font-size:12px;font-family:var(--font-mono);}
.detail-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;border:1px solid var(--border);color:var(--muted);} .detail-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;border:1px solid var(--border);color:var(--muted);}
.detail-badge.active{background:var(--accent-bg);color:var(--accent-text);border-color:var(--accent-bg-strong,var(--accent-bg));} .detail-badge.active{background:var(--accent-bg);color:var(--accent-text);border-color:var(--accent-bg-strong,var(--accent-bg));}
.detail-badge.ok{background:rgba(100,200,100,.12);color:rgba(100,200,100,.95);border-color:rgba(100,200,100,.3);} .detail-badge.ok{background:rgba(100,200,100,.12);color:rgba(100,200,100,.95);border-color:rgba(100,200,100,.3);}
@@ -6457,7 +6461,7 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.detail-card-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;} .detail-card-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.detail-expand-toggle{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;padding:3px 8px;font-size:11px;line-height:1.3;cursor:pointer;} .detail-expand-toggle{border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:6px;padding:3px 8px;font-size:11px;line-height:1.3;cursor:pointer;}
.detail-expand-toggle:hover{color:var(--text);border-color:var(--accent);} .detail-expand-toggle:hover{color:var(--text);border-color:var(--accent);}
.detail-prompt{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:'SF Mono',ui-monospace,monospace;max-height:240px;overflow-y:auto;} .detail-prompt{background:var(--sidebar);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:12px;white-space:pre-wrap;line-height:1.55;color:var(--text);font-family:var(--font-mono);max-height:240px;overflow-y:auto;}
.detail-prompt.expanded{max-height:none;overflow-y:visible;} .detail-prompt.expanded{max-height:none;overflow-y:visible;}
.detail-run-item{border-top:1px solid var(--border);padding:8px 0;} .detail-run-item{border-top:1px solid var(--border);padding:8px 0;}
.detail-run-item:first-child{border-top:none;} .detail-run-item:first-child{border-top:none;}
@@ -6475,11 +6479,11 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.workspace-artifact-empty{padding:16px 8px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center;} .workspace-artifact-empty{padding:16px 8px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center;}
.workspace-artifact-item{display:block;width:100%;text-align:left;background:var(--surface-subtle);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:6px;cursor:pointer;} .workspace-artifact-item{display:block;width:100%;text-align:left;background:var(--surface-subtle);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:6px;cursor:pointer;}
.workspace-artifact-item:hover{border-color:var(--accent);} .workspace-artifact-item:hover{border-color:var(--accent);}
.workspace-artifact-filename{font-family:'SF Mono',ui-monospace,monospace;font-size:12px;overflow-wrap:anywhere;} .workspace-artifact-filename{font-family:var(--font-mono);font-size:12px;overflow-wrap:anywhere;}
.workspace-artifact-directory{display:flex;min-width:0;overflow:hidden;white-space:nowrap;font-family:'SF Mono',ui-monospace,monospace;font-size:11px;color:var(--muted);} .workspace-artifact-directory{display:flex;min-width:0;overflow:hidden;white-space:nowrap;font-family:var(--font-mono);font-size:11px;color:var(--muted);}
.workspace-artifact-directory-head{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;} .workspace-artifact-directory-head{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;}
.workspace-artifact-directory-tail{flex:0 0 auto;min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;} .workspace-artifact-directory-tail{flex:0 0 auto;min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;}
.workspace-artifact-meta{display:inline-block;margin-top:4px;padding:1px 5px;border:1px solid var(--border);border-radius:999px;color:var(--muted);font-size:11px;line-height:1.3;} .workspace-artifact-meta{display:inline-block;margin-top:4px;padding:1px 5px;border:1px solid var(--border);border-radius:999px;color:var(--muted);font-size:11px;line-height:1.3;}
.workspace-artifacts-count{opacity:.65;font-size:11px;} .workspace-artifacts-count{opacity:.65;font-size:11px;}
.rightpanel[data-active-tab="artifacts"] .breadcrumb-bar,.rightpanel[data-active-tab="artifacts"] .file-tree,.rightpanel[data-active-tab="artifacts"] #wsEmptyState{display:none!important;} .rightpanel[data-active-tab="artifacts"] .breadcrumb-bar,.rightpanel[data-active-tab="artifacts"] .file-tree,.rightpanel[data-active-tab="artifacts"] #wsEmptyState{display:none!important;}
.rightpanel[data-active-tab="artifacts"] .workspace-artifacts{display:block;} .rightpanel[data-active-tab="artifacts"] .workspace-artifacts{display:block;}
@@ -6526,8 +6530,8 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
.cron-schedule-preset-field input[type="time"]{min-width:120px;} .cron-schedule-preset-field input[type="time"]{min-width:120px;}
.cron-schedule-preset-field input[type="number"]{width:64px;} .cron-schedule-preset-field input[type="number"]{width:64px;}
.cron-schedule-preset-time-hint{flex-basis:100%;margin-top:2px;} .cron-schedule-preset-time-hint{flex-basis:100%;margin-top:2px;}
.cron-schedule-preview{font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text);opacity:.85;} .cron-schedule-preview{font-family:var(--font-mono);color:var(--text);opacity:.85;}
.detail-form-row textarea{resize:vertical;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;} .detail-form-row textarea{resize:vertical;font-family:var(--font-mono);font-size:12px;}
.detail-form-row .detail-form-hint{font-size:11px;color:var(--muted);line-height:1.5;} .detail-form-row .detail-form-hint{font-size:11px;color:var(--muted);line-height:1.5;}
.detail-form-warning{font-size:11px;line-height:1.5;border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.1);color:rgba(245,158,11,.98);border-radius:8px;padding:8px 10px;} .detail-form-warning{font-size:11px;line-height:1.5;border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.1);color:rgba(245,158,11,.98);border-radius:8px;padding:8px 10px;}
.cron-once-warning{margin-top:2px;} .cron-once-warning{margin-top:2px;}
@@ -6893,7 +6897,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
font-size:11px; font-size:11px;
color:var(--muted); color:var(--muted);
} }
.kanban-modal-hint code{background:var(--input-bg);padding:1px 5px;border-radius:4px;font-family:'SF Mono',ui-monospace,Menlo,monospace;font-size:11px;color:var(--text);} .kanban-modal-hint code{background:var(--input-bg);padding:1px 5px;border-radius:4px;font-family:var(--font-mono);font-size:11px;color:var(--text);}
.kanban-modal-hint em{color:var(--text);font-style:normal;font-weight:600;} .kanban-modal-hint em{color:var(--text);font-style:normal;font-weight:600;}
.kanban-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;} .kanban-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}
.kanban-modal-error{ .kanban-modal-error{
@@ -7038,7 +7042,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-profile-lane-unassigned .kanban-profile-lane-head{border-bottom:1px solid color-mix(in srgb,var(--warning,#f5c542) 45%,var(--border));padding-bottom:6px} .kanban-profile-lane-unassigned .kanban-profile-lane-head{border-bottom:1px solid color-mix(in srgb,var(--warning,#f5c542) 45%,var(--border));padding-bottom:6px}
.kanban-board-in-lane{min-height:0;overflow-x:auto} .kanban-board-in-lane{min-height:0;overflow-x:auto}
.kanban-card-topline{display:flex;gap:6px;align-items:center;margin-bottom:4px;font-size:10px;color:var(--muted)} .kanban-card-topline{display:flex;gap:6px;align-items:center;margin-bottom:4px;font-size:10px;color:var(--muted)}
.kanban-card-id{font-family:var(--mono);opacity:.8} .kanban-card-id{font-family:var(--font-mono);opacity:.8}
.kanban-badge{border:1px solid var(--border);border-radius:999px;padding:1px 6px;font-size:10px;color:var(--muted)} .kanban-badge{border:1px solid var(--border);border-radius:999px;padding:1px 6px;font-size:10px;color:var(--muted)}
.kanban-badge.priority{color:var(--accent)} .kanban-badge.priority{color:var(--accent)}
.kanban-badge.tenant{color:var(--text)} .kanban-badge.tenant{color:var(--text)}
@@ -7051,7 +7055,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-card-stale-amber{border-color:rgba(245,197,66,.55)} .kanban-card-stale-amber{border-color:rgba(245,197,66,.55)}
.kanban-card-stale-red{border-color:rgba(255,95,95,.65)} .kanban-card-stale-red{border-color:rgba(255,95,95,.65)}
.kanban-column.drop-target{outline:2px solid var(--accent);outline-offset:-2px} .kanban-column.drop-target{outline:2px solid var(--accent);outline-offset:-2px}
.hermes-kanban-md p{margin:0 0 4px}.hermes-kanban-md code{font-family:var(--mono);font-size:.95em}.hermes-kanban-md h1,.hermes-kanban-md h2,.hermes-kanban-md h3,.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{margin:10px 0 4px;font-weight:650;color:var(--text)}.hermes-kanban-md h1{font-size:15px}.hermes-kanban-md h2{font-size:14px}.hermes-kanban-md h3{font-size:13px}.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{font-size:12px}.hermes-kanban-md ul,.hermes-kanban-md ol{margin:4px 0;padding-left:20px}.hermes-kanban-md li{margin:2px 0}.hermes-kanban-md li.checked{opacity:.6}.hermes-kanban-md li input[type=checkbox]{margin:0 4px 0 0;vertical-align:middle}.hermes-kanban-md table{border-collapse:collapse;margin:6px 0;font-size:11px;width:100%}.hermes-kanban-md td{border:1px solid var(--border);padding:4px 6px;vertical-align:top}.hermes-kanban-md blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--accent);color:var(--muted);font-size:12px}.hermes-kanban-md pre{background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:8px;margin:6px 0;overflow-x:auto;font-size:11px;line-height:1.4;color:var(--text)}.hermes-kanban-md hr{border:none;border-top:1px solid var(--border);margin:8px 0} .hermes-kanban-md p{margin:0 0 4px}.hermes-kanban-md code{font-family:var(--font-mono);font-size:.95em}.hermes-kanban-md h1,.hermes-kanban-md h2,.hermes-kanban-md h3,.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{margin:10px 0 4px;font-weight:650;color:var(--text)}.hermes-kanban-md h1{font-size:15px}.hermes-kanban-md h2{font-size:14px}.hermes-kanban-md h3{font-size:13px}.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{font-size:12px}.hermes-kanban-md ul,.hermes-kanban-md ol{margin:4px 0;padding-left:20px}.hermes-kanban-md li{margin:2px 0}.hermes-kanban-md li.checked{opacity:.6}.hermes-kanban-md li input[type=checkbox]{margin:0 4px 0 0;vertical-align:middle}.hermes-kanban-md table{border-collapse:collapse;margin:6px 0;font-size:11px;width:100%}.hermes-kanban-md td{border:1px solid var(--border);padding:4px 6px;vertical-align:top}.hermes-kanban-md blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--accent);color:var(--muted);font-size:12px}.hermes-kanban-md pre{background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:8px;margin:6px 0;overflow-x:auto;font-size:11px;line-height:1.4;color:var(--text)}.hermes-kanban-md hr{border:none;border-top:1px solid var(--border);margin:8px 0}
@media (max-width: 640px){ @media (max-width: 640px){
.kanban-board{scroll-snap-type:x mandatory;} .kanban-board{scroll-snap-type:x mandatory;}
@@ -7086,10 +7090,10 @@ main.main.showing-logs > #mainLogs{display:flex;}
.logs-copy{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);border-radius:8px;padding:7px 10px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s,color .15s;} .logs-copy{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);border-radius:8px;padding:7px 10px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s,color .15s;}
.logs-copy:hover{background:var(--hover-bg);border-color:var(--border2);} .logs-copy:hover{background:var(--hover-bg);border-color:var(--border2);}
.logs-copy.compact{padding:6px 10px;white-space:nowrap;} .logs-copy.compact{padding:6px 10px;white-space:nowrap;}
.logs-status{font-size:12px;color:var(--muted);margin-top:3px;font-family:'SF Mono',ui-monospace,monospace;} .logs-status{font-size:12px;color:var(--muted);margin-top:3px;font-family:var(--font-mono);}
.logs-main-body{padding:18px 24px;} .logs-main-body{padding:18px 24px;}
.logs-content{max-width:1200px;} .logs-content{max-width:1200px;}
.logs-output{min-height:320px;max-height:calc(100vh - 170px);overflow:auto;background:var(--code-bg);border:1px solid var(--border);border-radius:12px;padding:12px 0;font-family:'SF Mono','Fira Code',ui-monospace,monospace;font-size:12px;line-height:1.55;color:var(--pre-text);white-space:pre;} .logs-output{min-height:320px;max-height:calc(100vh - 170px);overflow:auto;background:var(--code-bg);border:1px solid var(--border);border-radius:12px;padding:12px 0;font-family:var(--font-mono);font-size:12px;line-height:1.55;color:var(--pre-text);white-space:pre;}
.logs-output.wrap{white-space:pre-wrap;overflow-wrap:anywhere;} .logs-output.wrap{white-space:pre-wrap;overflow-wrap:anywhere;}
.log-line{padding:0 14px;min-height:1.55em;border-left:3px solid transparent;} .log-line{padding:0 14px;min-height:1.55em;border-left:3px solid transparent;}
.log-line:hover{background:rgba(255,255,255,.04);} .log-line:hover{background:rgba(255,255,255,.04);}
+115 -6
View File
@@ -15,6 +15,11 @@ const TERMINAL_UI={
resizing:false, resizing:false,
resizeStartY:0, resizeStartY:0,
resizeStartHeight:0, resizeStartHeight:0,
lastAppliedTheme:null,
lastAppliedFontFamily:null,
fontFitFrame:null,
fontLoadGeneration:0,
fontLoadRequest:null,
}; };
const TERMINAL_HEIGHT_DEFAULT=260; const TERMINAL_HEIGHT_DEFAULT=260;
@@ -114,8 +119,78 @@ function _terminalTheme(){
}; };
} }
function syncComposerTerminalTheme(){ function _terminalMonoFont(){
if(TERMINAL_UI.term)TERMINAL_UI.term.options.theme=_terminalTheme(); return _terminalCssVar(
'--font-mono',
'ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace'
);
}
function _terminalThemesEqual(left,right){
if(left===right)return true;
if(!left||!right)return false;
const leftKeys=Object.keys(left);
const rightKeys=Object.keys(right);
return leftKeys.length===rightKeys.length&&leftKeys.every(key=>Object.prototype.hasOwnProperty.call(right,key)&&left[key]===right[key]);
}
function _scheduleTerminalFontFit(term){
if(TERMINAL_UI.fontFitFrame!==null)return;
TERMINAL_UI.fontFitFrame=requestAnimationFrame(()=>{
TERMINAL_UI.fontFitFrame=null;
if(TERMINAL_UI.term!==term||!TERMINAL_UI.open||TERMINAL_UI.collapsed)return;
_fitTerminal();
});
}
function _watchTerminalFontLoad(term,fontFamily){
const request={
generation:TERMINAL_UI.fontLoadGeneration+1,
term,
fontFamily,
};
TERMINAL_UI.fontLoadGeneration=request.generation;
TERMINAL_UI.fontLoadRequest=request;
const fontSet=document.fonts;
if(!fontSet||typeof fontSet.load!=='function')return;
const fontSize=Number(term.options&&term.options.fontSize);
const fontShorthand=(Number.isFinite(fontSize)&&fontSize>0?fontSize:13)+'px '+fontFamily;
let load;
try{
load=document.fonts.load(fontShorthand);
}catch(_){
return;
}
Promise.resolve(load).then(fontFaces=>{
if(!fontFaces||fontFaces.length===0)return;
if(TERMINAL_UI.fontLoadRequest!==request
||TERMINAL_UI.fontLoadGeneration!==request.generation
||TERMINAL_UI.term!==term
||!TERMINAL_UI.open
||TERMINAL_UI.collapsed
||TERMINAL_UI.lastAppliedFontFamily!==fontFamily)return;
_scheduleTerminalFontFit(term);
},()=>{});
}
function syncComposerTerminalAppearance(forceFontLoad=false){
if(!TERMINAL_UI.term)return;
const term=TERMINAL_UI.term;
const theme=_terminalTheme();
const fontFamily=_terminalMonoFont();
if(!_terminalThemesEqual(TERMINAL_UI.lastAppliedTheme,theme)){
term.options.theme=theme;
TERMINAL_UI.lastAppliedTheme={...theme};
}
const fontFamilyChanged=TERMINAL_UI.lastAppliedFontFamily!==fontFamily;
if(fontFamilyChanged){
term.options.fontFamily=fontFamily;
TERMINAL_UI.lastAppliedFontFamily=fontFamily;
_scheduleTerminalFontFit(term);
}
if(fontFamilyChanged||forceFontLoad){
_watchTerminalFontLoad(term,fontFamily);
}
} }
function _xtermReady(){ function _xtermReady(){
@@ -130,13 +205,15 @@ function _ensureXterm(){
surface.textContent='Terminal library failed to load. Check network access to cdn.jsdelivr.net.'; surface.textContent='Terminal library failed to load. Check network access to cdn.jsdelivr.net.';
return null; return null;
} }
const theme=_terminalTheme();
const fontFamily=_terminalMonoFont();
const term=new window.Terminal({ const term=new window.Terminal({
cursorBlink:true, cursorBlink:true,
fontSize:13, fontSize:13,
fontFamily:'Menlo, Monaco, Consolas, "Liberation Mono", monospace', fontFamily,
scrollback:1000, scrollback:1000,
convertEol:false, convertEol:false,
theme:_terminalTheme(), theme,
}); });
let fitAddon=null; let fitAddon=null;
if(window.FitAddon&&typeof window.FitAddon.FitAddon==='function'){ if(window.FitAddon&&typeof window.FitAddon.FitAddon==='function'){
@@ -162,7 +239,10 @@ function _ensureXterm(){
}); });
TERMINAL_UI.term=term; TERMINAL_UI.term=term;
TERMINAL_UI.fitAddon=fitAddon; TERMINAL_UI.fitAddon=fitAddon;
TERMINAL_UI.lastAppliedTheme={...theme};
TERMINAL_UI.lastAppliedFontFamily=fontFamily;
_fitTerminal(); _fitTerminal();
_watchTerminalFontLoad(term,fontFamily);
return term; return term;
} }
@@ -571,12 +651,20 @@ function expandComposerTerminal(opts){
} }
function _disposeXterm(){ function _disposeXterm(){
TERMINAL_UI.fontLoadGeneration+=1;
TERMINAL_UI.fontLoadRequest=null;
if(TERMINAL_UI.fontFitFrame!==null){
if(typeof cancelAnimationFrame==='function')cancelAnimationFrame(TERMINAL_UI.fontFitFrame);
TERMINAL_UI.fontFitFrame=null;
}
if(TERMINAL_UI.term){ if(TERMINAL_UI.term){
try{TERMINAL_UI.term.dispose();}catch(_){} try{TERMINAL_UI.term.dispose();}catch(_){}
} }
TERMINAL_UI.term=null; TERMINAL_UI.term=null;
TERMINAL_UI.fitAddon=null; TERMINAL_UI.fitAddon=null;
TERMINAL_UI.typedLine=''; TERMINAL_UI.typedLine='';
TERMINAL_UI.lastAppliedTheme=null;
TERMINAL_UI.lastAppliedFontFamily=null;
const {surface}= _terminalEls(); const {surface}= _terminalEls();
if(surface)surface.textContent=''; if(surface)surface.textContent='';
} }
@@ -696,8 +784,29 @@ window.addEventListener('resize',()=>{
}); });
if(window.MutationObserver){ if(window.MutationObserver){
new MutationObserver(syncComposerTerminalTheme).observe(document.documentElement,{ new MutationObserver(()=>syncComposerTerminalAppearance()).observe(document.documentElement,{
attributes:true, attributes:true,
attributeFilter:['class','data-skin'], attributeFilter:['class','data-skin','style'],
}); });
const terminalHead=document.head;
if(terminalHead){
const terminalHeadStylesheetLoadListener=(event)=>{
const target=event && event.target;
if(!target||typeof target.tagName!=='string'||target.tagName.toLowerCase()!=='link')return;
const rel=typeof target.getAttribute==='function'
? target.getAttribute('rel')
: target.rel;
if(!String(rel||'').toLowerCase().split(/\s+/).includes('stylesheet'))return;
syncComposerTerminalAppearance(true);
};
terminalHead.addEventListener('load',terminalHeadStylesheetLoadListener,true);
new MutationObserver(()=>syncComposerTerminalAppearance(true)).observe(terminalHead,{
attributes:true,
attributeFilter:['href','media','disabled'],
childList:true,
subtree:true,
characterData:true,
});
}
} }
+1 -1
View File
@@ -10140,7 +10140,7 @@ function _renderLockManualInstruction(target, res){
code.style.background='rgba(0,0,0,0.05)'; code.style.background='rgba(0,0,0,0.05)';
code.style.padding='6px'; code.style.padding='6px';
code.style.margin='4px 0'; code.style.margin='4px 0';
code.style.fontFamily='ui-monospace,monospace'; code.style.fontFamily='var(--font-mono)';
code.style.borderRadius='4px'; code.style.borderRadius='4px';
code.style.whiteSpace='pre-wrap'; code.style.whiteSpace='pre-wrap';
code.style.wordBreak='break-all'; code.style.wordBreak='break-all';
+2 -2
View File
@@ -247,8 +247,8 @@ def test_terminal_xterm_theme_follows_appearance_tokens():
assert "function _terminalTheme" in terminal_js assert "function _terminalTheme" in terminal_js
assert "_terminalCssVar('--code-bg'" in terminal_js assert "_terminalCssVar('--code-bg'" in terminal_js
assert "_terminalCssVar('--pre-text'" in terminal_js assert "_terminalCssVar('--pre-text'" in terminal_js
assert "syncComposerTerminalTheme" in terminal_js assert "syncComposerTerminalAppearance" in terminal_js
assert "attributeFilter:['class','data-skin']" in terminal_js assert "attributeFilter:['class','data-skin','style']" in terminal_js
assert "background:var(--code-bg)" in style_css assert "background:var(--code-bg)" in style_css
assert "color:var(--pre-text)" in style_css assert "color:var(--pre-text)" in style_css
+1 -1
View File
@@ -67,6 +67,6 @@ def test_graphite_skin_uses_native_ui_and_mono_font_stacks():
assert "-webkit-font-smoothing:antialiased" in CSS assert "-webkit-font-smoothing:antialiased" in CSS
assert ':root[data-skin="graphite"] textarea#msg' in CSS assert ':root[data-skin="graphite"] textarea#msg' in CSS
assert ':root[data-skin="graphite"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;' in CSS assert ':root[data-skin="graphite"] textarea#msg{font-family:var(--font-ui)!important;font-size:14px;' in CSS
assert ':root[data-skin="graphite"] .msg-body{font-family:var(--font-ui);font-size:13px;font-weight:430;' in CSS assert ':root[data-skin="graphite"] .msg-body{font-family:var(--font-conversation);font-size:13px;font-weight:430;' in CSS
assert ':root[data-skin="graphite"][data-font-size="large"] .msg-body{font-size:15px;' in CSS assert ':root[data-skin="graphite"][data-font-size="large"] .msg-body{font-size:15px;' in CSS
assert ':root[data-skin="graphite"] .tool-card-name' in CSS assert ':root[data-skin="graphite"] .tool-card-name' in CSS
+856
View File
@@ -0,0 +1,856 @@
from pathlib import Path
import json
import re
import shutil
import subprocess
import pytest
try:
from playwright.sync_api import Error as PlaywrightError, sync_playwright
except Exception: # pragma: no cover - dependency optional
PlaywrightError = None
sync_playwright = None
REPO = Path(__file__).parent.parent
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
TERMINAL_JS = (REPO / "static" / "terminal.js").read_text(encoding="utf-8")
PANEL_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
NODE = shutil.which("node")
def _iter_root_skin_blocks(css):
selector_re = re.compile(r'(:root(?:\.dark)?\[data-skin="[^"]+"\][^{]*?)\{')
for selector_match in selector_re.finditer(css):
selector = selector_match.group(1).strip()
idx = selector_match.end()
depth = 1
end = idx
while end < len(css) and depth:
ch = css[end]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
end += 1
if depth:
continue
yield selector, css[idx : end - 1]
def _get_root_skin_block(css, skin):
target = f':root[data-skin="{skin}"]'
for selector, block in _iter_root_skin_blocks(css):
if selector == target:
return block
return ""
def _font_stack_offenders(css):
cleaned = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
pattern = re.compile(r"(?i)\b(font-family|font)\s*:\s*([^;]+);")
offenders = []
for _, declaration in pattern.findall(cleaned):
value = declaration.strip().lower()
if ("monospace" not in value and "ui-monospace" not in value):
continue
if value == "inherit":
continue
offenders.append(declaration)
return offenders
def test_typography_root_tokens_are_defined():
assert '--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;' in CSS
assert '--font-conversation:var(--font-ui);' in CSS
assert '--font-mono:ui-monospace,"SFMono-Regular","SF Mono",Menlo,Consolas,"Liberation Mono",monospace;' in CSS
def test_msg_body_uses_conversation_font():
assert '.msg-body{font-family:var(--font-conversation);font-size:var(--message-body-font-size);line-height:var(--message-body-line-height);' in CSS
def test_builtin_skin_msg_body_rules_use_conversation_font():
for skin in ("graphite", "codex", "terracotta", "github"):
selector = (
f':root[data-skin="{skin}"] .msg-body'
'{font-family:var(--font-conversation);font-size:13px;font-weight:430;letter-spacing:0;line-height:1.6;}'
)
assert selector in CSS
def test_no_skin_msg_body_rules_use_root_font_ui_token():
assert not re.search(
r':root(?:\.dark)?\[data-skin="[^"]+"\]\s*\.msg-body\s*\{[^{}]*\bfont-family\s*:\s*var\(--font-ui\)\s*;[^{}]*\}',
CSS,
re.S,
)
def test_no_skin_redefines_font_conversation():
for selector, block in _iter_root_skin_blocks(CSS):
if '--font-conversation:' in block:
raise AssertionError(f'Unexpected skin-level --font-conversation: {selector}')
def test_nous_skin_keeps_monospace_ui_default():
assert _get_root_skin_block(CSS, "nous")
assert re.search(
r'--font-ui:"SF Mono","Roboto Mono","Courier New",monospace;',
_get_root_skin_block(CSS, "nous"),
re.S,
)
def test_geist_and_neon_skins_set_expected_font_ui_tokens():
geist_block = _get_root_skin_block(CSS, "geist-contrast")
assert geist_block
assert "--font-ui:\"Geist\",\"Geist Sans\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;" in geist_block
for skin in ("neon", "neon-soft", "neon-paint"):
block = _get_root_skin_block(CSS, skin)
assert block, f"Missing {skin} root skin block"
assert "--font-ui:system-ui,-apple-system,sans-serif;" in block
def test_no_stale_var_mono_usage_or_literal_monospace_font_family_stacks():
assert 'var(--mono' not in CSS
offending_lines = _font_stack_offenders(CSS)
assert not offending_lines, f"Unexpected literal monospace font stacks: {offending_lines[:4]}"
def test_edit_surfaces_use_font_contract_tokens_structurally():
assert re.search(
r"\.msg-edit-area\{[^}]*font-family:var\(--font-conversation\)",
CSS,
re.S,
), ".msg-edit-area must use var(--font-conversation)"
assert re.search(
r'<textarea id="previewEditArea"[^>]*style="[^"]*font-family:var\(--font-mono\)',
INDEX_HTML,
), "#previewEditArea must use var(--font-mono)"
def test_playwright_regression_ensures_font_contract_for_syntax_and_edit_surfaces():
preview_style_match = re.search(
r'<textarea id="previewEditArea"[^>]*style="([^"]*)"',
INDEX_HTML,
)
assert preview_style_match, "Could not locate #previewEditArea inline style in index.html"
fixture_html = f"""
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>{CSS}</style>
<style>
:root{{
--font-ui:"UiFontSentinel";
--font-conversation:"ConvoFontSentinel";
--font-mono:"MonoFontSentinel";
}}
</style>
<style>
code[class*="language-"],pre[class*="language-"]{{font-family: "PrismIntruder", sans-serif;}}
</style>
</head>
<body>
<div class="msg-body">
<p>inline <code id="inlineCode" class="language-js">token</code> sample.</p>
<pre id="fencedPre" class="language-js"><code id="fencedCode" class="language-js">fenced token</code></pre>
</div>
<textarea class="msg-edit-area" id="msgEditArea"></textarea>
<textarea id="previewEditArea" style="{preview_style_match.group(1)}"></textarea>
</body>
</html>
"""
if sync_playwright is None:
pytest.skip("playwright is unavailable; run `playwright install chromium`")
with sync_playwright() as playwright:
browser = None
try:
try:
browser = playwright.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage"],
)
except PlaywrightError as exc:
if "Executable doesn't exist at" in str(exc):
pytest.skip("playwright chromium executable is unavailable; run `playwright install chromium`")
raise
page = browser.new_page(viewport={"width": 1024, "height": 768})
page.set_content(fixture_html)
inline_code_font = page.eval_on_selector(
"#inlineCode",
"el => getComputedStyle(el).fontFamily",
)
fenced_pre_font = page.eval_on_selector(
"#fencedPre",
"el => getComputedStyle(el).fontFamily",
)
fenced_code_font = page.eval_on_selector(
"#fencedCode",
"el => getComputedStyle(el).fontFamily",
)
message_edit_font = page.eval_on_selector(
"#msgEditArea",
"el => getComputedStyle(el).fontFamily",
)
preview_edit_font = page.eval_on_selector(
"#previewEditArea",
"el => getComputedStyle(el).fontFamily",
)
runtime_token_font = page.evaluate("""
() => {
const style = document.createElement('style');
style.id = 'runtime-font-sentinel';
style.textContent = ':root { --font-mono: "RuntimeMono", monospace; }';
document.head.appendChild(style);
return getComputedStyle(document.documentElement).getPropertyValue('--font-mono');
}
""")
edited_runtime_token_font = page.evaluate("""
() => {
const style = document.getElementById('runtime-font-sentinel');
style.textContent = ':root { --font-mono: "EditedRuntimeMono", monospace; }';
return getComputedStyle(document.documentElement).getPropertyValue('--font-mono');
}
""")
linked_stylesheet_font = page.evaluate("""
async () => {
const link = document.createElement('link');
const encodedCss = encodeURIComponent(':root { --font-mono: "LinkedRuntimeMono", monospace; }');
link.rel = 'stylesheet';
link.href = 'data:text/css,' + encodedCss;
return await new Promise((resolve) => {
link.onload = () => {
resolve(getComputedStyle(document.documentElement).getPropertyValue('--font-mono'));
};
link.onerror = () => {
resolve(getComputedStyle(document.documentElement).getPropertyValue('--font-mono'));
};
document.head.appendChild(link);
});
}
""")
finally:
if browser is not None:
browser.close()
assert "MonoFontSentinel" in inline_code_font
assert "MonoFontSentinel" in fenced_pre_font
assert "MonoFontSentinel" in fenced_code_font
assert "ConvoFontSentinel" in message_edit_font
assert "MonoFontSentinel" in preview_edit_font
assert "RuntimeMono" in runtime_token_font
assert "EditedRuntimeMono" in edited_runtime_token_font
assert "LinkedRuntimeMono" in linked_stylesheet_font
def test_terminal_sync_tracks_applied_appearance_and_observes_relevant_root_attributes():
block_match = re.search(
r"function\s+syncComposerTerminalAppearance\([^)]*\)\s*\{.*?\n\}\s*\n\s*function\s+_xtermReady\(\)",
TERMINAL_JS,
re.S,
)
assert block_match
sync_block = block_match.group(0)
assert "function _terminalThemesEqual" in TERMINAL_JS
assert "lastAppliedTheme" in sync_block
assert "lastAppliedFontFamily" in sync_block
assert re.search(
r"new\s+MutationObserver\(\(\)\s*=>\s*syncComposerTerminalAppearance\(\)\)\.observe\(document\.documentElement,\{.*?attributeFilter:\s*\[\s*['\"]class['\"],\s*['\"]data-skin['\"],\s*['\"]style['\"],?\s*\]",
TERMINAL_JS,
re.S,
)
assert "const terminalHead=document.head" in TERMINAL_JS
assert "if(terminalHead){" in TERMINAL_JS
assert "terminalHeadStylesheetLoadListener" in TERMINAL_JS
assert re.search(
r"new\s+MutationObserver\(\(\)\s*=>\s*syncComposerTerminalAppearance\(true\)\)\.observe\(terminalHead,\{\s*attributes:\s*true,\s*attributeFilter:\s*\[\s*['\"]href['\"],\s*['\"]media['\"],\s*['\"]disabled['\"]\s*\],\s*childList:\s*true,\s*subtree:\s*true,\s*characterData:\s*true,\s*\}\)",
TERMINAL_JS,
re.S,
)
head_decl=TERMINAL_JS.index("const terminalHead=document.head")
head_guard=TERMINAL_JS.index("if(terminalHead){")
head_load_listener=TERMINAL_JS.index("terminalHead.addEventListener('load',terminalHeadStylesheetLoadListener,true)")
head_observer=TERMINAL_JS.index("new MutationObserver(()=>syncComposerTerminalAppearance(true)).observe(terminalHead,{")
assert head_decl < head_guard < head_load_listener < head_observer
assert re.search(
r"terminalHead\.addEventListener\(\s*['\"]load['\"],\s*terminalHeadStylesheetLoadListener,\s*true\s*\)",
TERMINAL_JS,
re.S,
)
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_terminal_appearance_sync_is_behaviorally_idempotent_and_lifecycle_safe():
script = (
"""
const styles={
'--font-mono':'"Active Mono",monospace',
'--code-bg':'#111111',
};
const rootInline={};
let dark=false;
let observerCallbacks={};
let observedTargets=[];
let observedOptions={};
let fitCalls=0;
let resizeSchedules=0;
let terminalHeadLoadListener=null;
let terminalHeadLoadListenerOptions=null;
let unhandledFontLoadRejections=0;
let nextAnimationFrameId=1;
const animationFrames=new Map();
const writes=[];
const createdAppearances=[];
const fontLoadCalls=[];
const pendingFontLoads=[];
const surface={textContent:''};
process.on('unhandledRejection',()=>{unhandledFontLoadRejections+=1;});
const terminalFonts={
load(shorthand){
let resolve;
let reject;
const promise=new Promise((resolvePromise,rejectPromise)=>{
resolve=resolvePromise;
reject=rejectPromise;
});
const request={shorthand,resolve,reject};
fontLoadCalls.push(shorthand);
pendingFontLoads.push(request);
return promise;
},
};
function pendingAnimationFrames(){
return animationFrames.size;
}
function flushAnimationFrames(){
const pending=[...animationFrames.values()];
animationFrames.clear();
pending.forEach(callback=>callback());
}
class FakeMutationObserver {
constructor(callback){ this.callback=callback; }
observe(target,options){
const targetName = target===document.documentElement ? 'documentElement' : target===document.head ? 'head' : 'other';
observedTargets.push(targetName);
observedOptions[targetName]=options;
observerCallbacks[targetName]=this.callback;
}
}
class FakeTerminal {
constructor(options){
createdAppearances.push({
fontFamily:options.fontFamily,
fontSize:options.fontSize,
theme:{...options.theme},
});
const current={
fontFamily:options.fontFamily,
fontSize:options.fontSize,
theme:options.theme,
};
this.options=new Proxy(current,{
set(target,key,value){
writes.push(String(key));
target[key]=value;
return true;
},
});
this.cols=80;
this.rows=24;
}
loadAddon(){}
open(){}
onData(){}
dispose(){}
}
class FakeFitAddon {
fit(){ fitCalls+=1; }
}
const root={
classList:{contains(name){ return name==='dark'&&dark; }},
style:{setProperty(name,value){ rootInline[name]=value; }},
};
global.window={
addEventListener(){},
MutationObserver:FakeMutationObserver,
Terminal:FakeTerminal,
FitAddon:{FitAddon:FakeFitAddon},
};
global.MutationObserver=FakeMutationObserver;
global.document={
documentElement:root,
fonts:terminalFonts,
head:{
addEventListener(type,listener,options){
if(type==='load'){
terminalHeadLoadListener=listener;
terminalHeadLoadListenerOptions=options;
}
},
removeEventListener(){
},
},
getElementById(){ return null; },
};
global.getComputedStyle=()=>({
getPropertyValue(name){ return styles[name]||rootInline[name]||''; },
});
global.$=(id)=>id==='terminalSurface'?surface:null;
global.requestAnimationFrame=(callback)=>{
const id=nextAnimationFrameId++;
animationFrames.set(id,callback);
return id;
};
global.cancelAnimationFrame=(id)=>animationFrames.delete(id);
global.setTimeout=(_callback,delay)=>{
if(delay===120)resizeSchedules+=1;
return 1;
};
global.clearTimeout=()=>{};
"""
+ TERMINAL_JS
+ """
async function settleFontLoad(){
await Promise.resolve();
await Promise.resolve();
}
(async()=>{
const first=_ensureXterm();
const initialFontLoad=pendingFontLoads[0];
TERMINAL_UI.open=true;
TERMINAL_UI.sessionId='session-1';
const initialFitCalls=fitCalls;
resizeSchedules=0;
initialFontLoad.resolve([]);
await settleFontLoad();
const initialDelayedPendingFrames=pendingAnimationFrames();
flushAnimationFrames();
const initialDelayedFitDelta=fitCalls-initialFitCalls;
const initialDelayedResizeSchedules=resizeSchedules;
writes.length=0;
resizeSchedules=0;
const fitsBeforeLateSameFamily=fitCalls;
const fontLoadsBeforeLateSameFamily=fontLoadCalls.length;
observerCallbacks.head([{type:'childList'},{type:'characterData'}]);
const lateSameFamilyFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const lateSameFamilyWrites=[...writes];
const lateSameFamilyLoadCount=fontLoadCalls.length-fontLoadsBeforeLateSameFamily;
const lateSameFamilyPendingFramesBeforeCompletion=pendingAnimationFrames();
flushAnimationFrames();
const lateSameFamilyFitDeltaBeforeCompletion=fitCalls-fitsBeforeLateSameFamily;
lateSameFamilyFontLoad.resolve([{}]);
await settleFontLoad();
const lateSameFamilyPendingFramesAfterLoad=pendingAnimationFrames();
const fitsBeforeLateSameFamilyFit=fitCalls;
flushAnimationFrames();
const lateSameFamilyFitDelta=fitCalls-fitsBeforeLateSameFamilyFit;
const lateSameFamilyResizeSchedules=resizeSchedules;
writes.length=0;
const fitsBeforeLateStylesheetLoad=fitCalls;
const fontLoadsBeforeLateStylesheetLoad=fontLoadCalls.length;
terminalHeadLoadListener&&terminalHeadLoadListener({target:{
tagName:'LINK',
getAttribute(name){ return name==='rel'?'stylesheet':null; },
}});
const lateStylesheetFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const lateStylesheetLoadWrites=[...writes];
const lateStylesheetLoadCount=fontLoadCalls.length-fontLoadsBeforeLateStylesheetLoad;
const lateStylesheetPendingFrames=pendingAnimationFrames();
flushAnimationFrames();
lateStylesheetFontLoad.resolve([]);
await settleFontLoad();
const lateStylesheetFitDelta=fitCalls-fitsBeforeLateStylesheetLoad;
const lateStylesheetPendingFramesAfterLoad=pendingAnimationFrames();
writes.length=0;
const fitsBeforeUnchanged=fitCalls;
syncComposerTerminalAppearance();
const unchangedWrites=[...writes];
const unchangedFitDelta=fitCalls-fitsBeforeUnchanged;
const unchangedPendingFrames=pendingAnimationFrames();
writes.length=0;
const fitsBeforeUnrelated=fitCalls;
document.documentElement.style.setProperty('--unrelated-outline-offset','3px');
observerCallbacks.documentElement([{attributeName:'style'}]);
const unrelatedStyleWrites=[...writes];
const unrelatedFitDelta=fitCalls-fitsBeforeUnrelated;
const unrelatedPendingFrames=pendingAnimationFrames();
writes.length=0;
const fitsBeforeTheme=fitCalls;
styles['--code-bg']='#222222';
observerCallbacks.documentElement([{attributeName:'style'}]);
const themeOnlyWrites=[...writes];
const backgroundAfterChange=first.options.theme.background;
const themeOnlyFitDelta=fitCalls-fitsBeforeTheme;
const themeOnlyPendingFrames=pendingAnimationFrames();
writes.length=0;
resizeSchedules=0;
const fitsBeforeFont=fitCalls;
const fontLoadsBeforeExtension=fontLoadCalls.length;
styles['--font-mono']='"Extension Mono",monospace';
observerCallbacks.head([{type:'childList'},{type:'characterData'}]);
const extensionFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const extensionFontLoadCount=fontLoadCalls.length-fontLoadsBeforeExtension;
const fontOnlyWrites=[...writes];
const fontAfterChange=first.options.fontFamily;
const fontFitDeltaBeforeFrame=fitCalls-fitsBeforeFont;
const fontPendingFramesBeforeFlush=pendingAnimationFrames();
flushAnimationFrames();
const fontFitDeltaAfterFrame=fitCalls-fitsBeforeFont;
const fontResizeSchedules=resizeSchedules;
extensionFontLoad.resolve([{}]);
await settleFontLoad();
const fontPendingFramesAfterLoad=pendingAnimationFrames();
const fitsBeforeDelayedFontFit=fitCalls;
flushAnimationFrames();
const fontFitDeltaAfterLoad=fitCalls-fitsBeforeDelayedFontFit;
const fontResizeSchedulesAfterLoad=resizeSchedules;
writes.length=0;
resizeSchedules=0;
const fitsBeforeNonStylesheetLoad=fitCalls;
styles['--font-mono']='\"Non Stylesheet Mono\",monospace';
terminalHeadLoadListener&&terminalHeadLoadListener({target:{
tagName:'SCRIPT',
getAttribute:()=>null,
}});
const nonStylesheetLoadWrites=[...writes];
const nonStylesheetLoadFitDelta=fitCalls-fitsBeforeNonStylesheetLoad;
const nonStylesheetLoadPendingFrames=pendingAnimationFrames();
writes.length=0;
resizeSchedules=0;
const fitsBeforeEmptyFont=fitCalls;
styles['--font-mono']='ui-monospace,monospace';
observerCallbacks.head([{type:'childList'}]);
const emptyFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const emptyFontWrites=[...writes];
const emptyFontPendingFramesBeforeFlush=pendingAnimationFrames();
flushAnimationFrames();
const emptyFontImmediateFitDelta=fitCalls-fitsBeforeEmptyFont;
const emptyFontResizeSchedules=resizeSchedules;
const fitsBeforeEmptyFontCompletion=fitCalls;
emptyFontLoad.resolve([]);
await settleFontLoad();
const emptyFontPendingFramesAfterLoad=pendingAnimationFrames();
const emptyFontDelayedFitDelta=fitCalls-fitsBeforeEmptyFontCompletion;
writes.length=0;
resizeSchedules=0;
const fitsBeforeStylesheetLoad=fitCalls;
styles['--font-mono']='\"Stylesheet Load Mono\",monospace';
terminalHeadLoadListener&&terminalHeadLoadListener({target:{
tagName:'LINK',
getAttribute(name){ return name==='rel'?'stylesheet':null; },
}});
const stylesheetFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const stylesheetLoadWrites=[...writes];
const stylesheetLoadFontAfterChange=first.options.fontFamily;
const stylesheetLoadFitDeltaBeforeFrame=fitCalls-fitsBeforeStylesheetLoad;
const stylesheetLoadPendingFramesBeforeFlush=pendingAnimationFrames();
flushAnimationFrames();
const stylesheetLoadFitDeltaAfterFrame=fitCalls-fitsBeforeStylesheetLoad;
const stylesheetLoadResizeSchedules=resizeSchedules;
stylesheetFontLoad.resolve([]);
await settleFontLoad();
resizeSchedules=0;
styles['--font-mono']='"Stale Prior Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const staleFontLoad=pendingFontLoads[pendingFontLoads.length-1];
flushAnimationFrames();
styles['--font-mono']='"Replacement Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const replacementFontLoad=pendingFontLoads[pendingFontLoads.length-1];
flushAnimationFrames();
const fitsBeforeStaleCompletion=fitCalls;
staleFontLoad.resolve([{}]);
await settleFontLoad();
const staleCompletionFitDelta=fitCalls-fitsBeforeStaleCompletion;
const staleCompletionPendingFrames=pendingAnimationFrames();
replacementFontLoad.resolve([]);
await settleFontLoad();
styles['--font-mono']='"Rejecting Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const rejectedFontLoad=pendingFontLoads[pendingFontLoads.length-1];
flushAnimationFrames();
const fitsBeforeRejectedCompletion=fitCalls;
rejectedFontLoad.reject(new Error('optional font failed'));
await settleFontLoad();
const rejectedCompletionFitDelta=fitCalls-fitsBeforeRejectedCompletion;
const rejectedCompletionPendingFrames=pendingAnimationFrames();
styles['--font-mono']='"Disposed Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const disposedFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const oldTerm=TERMINAL_UI.term;
const generationBeforeDispose=TERMINAL_UI.fontLoadGeneration;
const pendingFitBeforeDispose=pendingAnimationFrames();
_disposeXterm();
const pendingFitAfterDispose=pendingAnimationFrames();
const cacheReset=TERMINAL_UI.lastAppliedTheme===null
&&TERMINAL_UI.lastAppliedFontFamily===null
&&TERMINAL_UI.fontFitFrame===null
&&TERMINAL_UI.fontLoadRequest===null
&&TERMINAL_UI.fontLoadGeneration===generationBeforeDispose+1;
styles['--font-mono']='"Recreated Mono",monospace';
styles['--code-bg']='#333333';
const fitsBeforeRecreate=fitCalls;
const recreated=_ensureXterm();
const recreatedFontLoad=pendingFontLoads[pendingFontLoads.length-1];
const recreateFitDelta=fitCalls-fitsBeforeRecreate;
const recreatePendingFrames=pendingAnimationFrames();
const fitsBeforeDisposedCompletion=fitCalls;
disposedFontLoad.resolve([{}]);
await settleFontLoad();
const disposedCompletionFitDelta=fitCalls-fitsBeforeDisposedCompletion;
const disposedCompletionPendingFrames=pendingAnimationFrames();
recreatedFontLoad.resolve([]);
await settleFontLoad();
TERMINAL_UI.open=true;
styles['--font-mono']='"Closed Terminal Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const closedFontLoad=pendingFontLoads[pendingFontLoads.length-1];
flushAnimationFrames();
const fitsBeforeClosedCompletion=fitCalls;
TERMINAL_UI.open=false;
closedFontLoad.resolve([{}]);
await settleFontLoad();
const closedCompletionFitDelta=fitCalls-fitsBeforeClosedCompletion;
const closedCompletionPendingFrames=pendingAnimationFrames();
TERMINAL_UI.open=true;
styles['--font-mono']='"Collapsed Terminal Mono",monospace';
observerCallbacks.head([{type:'childList'}]);
const collapsedFontLoad=pendingFontLoads[pendingFontLoads.length-1];
flushAnimationFrames();
const fitsBeforeCollapsedCompletion=fitCalls;
TERMINAL_UI.collapsed=true;
collapsedFontLoad.resolve([{}]);
await settleFontLoad();
const collapsedCompletionFitDelta=fitCalls-fitsBeforeCollapsedCompletion;
const collapsedCompletionPendingFrames=pendingAnimationFrames();
TERMINAL_UI.collapsed=false;
process.stdout.write(JSON.stringify({
initial:createdAppearances[0],
initialFontLoadShorthand:fontLoadCalls[0],
initialFitCalls,
initialDelayedPendingFrames,
initialDelayedFitDelta,
initialDelayedResizeSchedules,
lateSameFamilyWrites,
lateSameFamilyLoadCount,
lateSameFamilyPendingFramesBeforeCompletion,
lateSameFamilyFitDeltaBeforeCompletion,
lateSameFamilyPendingFramesAfterLoad,
lateSameFamilyFitDelta,
lateSameFamilyResizeSchedules,
lateStylesheetLoadWrites,
lateStylesheetLoadCount,
lateStylesheetPendingFrames,
lateStylesheetFitDelta,
lateStylesheetPendingFramesAfterLoad,
unchangedWrites,
unchangedFitDelta,
unchangedPendingFrames,
unrelatedStyleWrites,
unrelatedFitDelta,
unrelatedPendingFrames,
themeOnlyWrites,
backgroundAfterChange,
themeOnlyFitDelta,
themeOnlyPendingFrames,
fontOnlyWrites,
fontAfterChange,
fontFitDeltaBeforeFrame,
fontPendingFramesBeforeFlush,
fontFitDeltaAfterFrame,
fontResizeSchedules,
extensionFontLoadCount,
fontPendingFramesAfterLoad,
fontFitDeltaAfterLoad,
fontResizeSchedulesAfterLoad,
nonStylesheetLoadWrites,
nonStylesheetLoadFitDelta,
nonStylesheetLoadPendingFrames,
emptyFontWrites,
emptyFontPendingFramesBeforeFlush,
emptyFontImmediateFitDelta,
emptyFontResizeSchedules,
emptyFontPendingFramesAfterLoad,
emptyFontDelayedFitDelta,
stylesheetLoadWrites,
stylesheetLoadFontAfterChange,
stylesheetLoadFitDeltaBeforeFrame,
stylesheetLoadPendingFramesBeforeFlush,
stylesheetLoadFitDeltaAfterFrame,
stylesheetLoadResizeSchedules,
staleCompletionFitDelta,
staleCompletionPendingFrames,
rejectedCompletionFitDelta,
rejectedCompletionPendingFrames,
unhandledFontLoadRejections,
terminalHeadLoadListenerRegistered: typeof terminalHeadLoadListener === 'function',
terminalHeadLoadListenerOptions,
observerTargets:observedTargets,
observerCallbacks:Object.keys(observerCallbacks),
observedOptions,
pendingFitBeforeDispose,
pendingFitAfterDispose,
cacheReset,
oldTermIsReplaced:oldTerm!==recreated,
recreated:createdAppearances[1],
recreateFitDelta,
recreatePendingFrames,
disposedCompletionFitDelta,
disposedCompletionPendingFrames,
closedCompletionFitDelta,
closedCompletionPendingFrames,
collapsedCompletionFitDelta,
collapsedCompletionPendingFrames,
}));
})().catch((error)=>{
process.exitCode=1;
process.stderr.write((error&&error.stack?error.stack:String(error))+"\\n");
});
""")
result = subprocess.run(
[NODE, "-e", script],
cwd=REPO,
capture_output=True,
text=True,
timeout=20,
)
assert result.returncode == 0, result.stderr
observed = json.loads(result.stdout)
assert observed["initial"]["fontFamily"] == '"Active Mono",monospace'
assert observed["initial"]["fontSize"] == 13
assert observed["initial"]["theme"]["background"] == "#111111"
assert observed["initialFontLoadShorthand"] == '13px "Active Mono",monospace'
assert observed["initialFitCalls"] == 1
assert observed["initialDelayedPendingFrames"] == 0
assert observed["initialDelayedFitDelta"] == 0
assert observed["initialDelayedResizeSchedules"] == 0
assert observed["lateSameFamilyWrites"] == []
assert observed["lateSameFamilyLoadCount"] == 1
assert observed["lateSameFamilyPendingFramesBeforeCompletion"] == 0
assert observed["lateSameFamilyFitDeltaBeforeCompletion"] == 0
assert observed["lateSameFamilyPendingFramesAfterLoad"] == 1
assert observed["lateSameFamilyFitDelta"] == 1
assert observed["lateSameFamilyResizeSchedules"] == 1
assert observed["lateStylesheetLoadWrites"] == []
assert observed["lateStylesheetLoadCount"] == 1
assert observed["lateStylesheetPendingFrames"] == 0
assert observed["lateStylesheetFitDelta"] == 0
assert observed["lateStylesheetPendingFramesAfterLoad"] == 0
assert observed["unchangedWrites"] == []
assert observed["unchangedFitDelta"] == 0
assert observed["unchangedPendingFrames"] == 0
assert observed["unrelatedStyleWrites"] == []
assert observed["unrelatedFitDelta"] == 0
assert observed["unrelatedPendingFrames"] == 0
assert observed["themeOnlyWrites"] == ["theme"]
assert observed["backgroundAfterChange"] == "#222222"
assert observed["themeOnlyFitDelta"] == 0
assert observed["themeOnlyPendingFrames"] == 0
assert observed["fontOnlyWrites"] == ["fontFamily"]
assert observed["fontAfterChange"] == '"Extension Mono",monospace'
assert observed["fontFitDeltaBeforeFrame"] == 0
assert observed["fontPendingFramesBeforeFlush"] == 1
assert observed["fontFitDeltaAfterFrame"] == 1
assert observed["fontResizeSchedules"] == 1
assert observed["extensionFontLoadCount"] == 1
assert observed["fontPendingFramesAfterLoad"] == 1
assert observed["fontFitDeltaAfterLoad"] == 1
assert observed["fontResizeSchedulesAfterLoad"] == 2
assert observed["terminalHeadLoadListenerRegistered"] is True
assert observed["terminalHeadLoadListenerOptions"] is True
assert observed["nonStylesheetLoadWrites"] == []
assert observed["nonStylesheetLoadFitDelta"] == 0
assert observed["nonStylesheetLoadPendingFrames"] == 0
assert observed["emptyFontWrites"] == ["fontFamily"]
assert observed["emptyFontPendingFramesBeforeFlush"] == 1
assert observed["emptyFontImmediateFitDelta"] == 1
assert observed["emptyFontResizeSchedules"] == 1
assert observed["emptyFontPendingFramesAfterLoad"] == 0
assert observed["emptyFontDelayedFitDelta"] == 0
assert observed["stylesheetLoadWrites"] == ["fontFamily"]
assert observed["stylesheetLoadFontAfterChange"] == '"Stylesheet Load Mono",monospace'
assert observed["stylesheetLoadFitDeltaBeforeFrame"] == 0
assert observed["stylesheetLoadPendingFramesBeforeFlush"] == 1
assert observed["stylesheetLoadFitDeltaAfterFrame"] == 1
assert observed["stylesheetLoadResizeSchedules"] == 1
assert observed["staleCompletionFitDelta"] == 0
assert observed["staleCompletionPendingFrames"] == 0
assert observed["rejectedCompletionFitDelta"] == 0
assert observed["rejectedCompletionPendingFrames"] == 0
assert observed["unhandledFontLoadRejections"] == 0
assert observed["observerTargets"] == ["documentElement", "head"]
assert observed["observerCallbacks"] == ["documentElement", "head"]
assert observed["observedOptions"]["documentElement"] == {
"attributes": True,
"attributeFilter": ["class", "data-skin", "style"],
}
assert observed["observedOptions"]["head"] == {
"attributes": True,
"attributeFilter": ["href", "media", "disabled"],
"childList": True,
"subtree": True,
"characterData": True,
}
assert observed["pendingFitBeforeDispose"] == 1
assert observed["pendingFitAfterDispose"] == 0
assert observed["cacheReset"] is True
assert observed["oldTermIsReplaced"] is True
assert observed["recreated"]["fontFamily"] == '"Recreated Mono",monospace'
assert observed["recreated"]["theme"]["background"] == "#333333"
assert observed["recreateFitDelta"] == 1
assert observed["recreatePendingFrames"] == 0
assert observed["disposedCompletionFitDelta"] == 0
assert observed["disposedCompletionPendingFrames"] == 0
assert observed["closedCompletionFitDelta"] == 0
assert observed["closedCompletionPendingFrames"] == 0
assert observed["collapsedCompletionFitDelta"] == 0
assert observed["collapsedCompletionPendingFrames"] == 0
def test_first_party_technical_js_uses_font_mono_contract():
assert 'function _terminalMonoFont()' in TERMINAL_JS
assert "'ui-monospace,\"SFMono-Regular\",\"SF Mono\",Menlo,Consolas,\"Liberation Mono\",monospace'" in TERMINAL_JS
assert 'font-family:var(--font-mono)' in PANEL_JS
assert "fontFamily='var(--font-mono)'" in UI_JS
assert "font-family:var(--font-ui)" in BOOT_JS
def test_terminal_contract_updates_on_root_style_changes():
assert "attributeFilter:['class','data-skin','style']" in TERMINAL_JS
assert 'function syncComposerTerminalAppearance' in TERMINAL_JS
assert 'function _terminalMonoFont' in TERMINAL_JS