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
+1082 -74

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
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
Use split typography intentionally:
Use three explicit font tokens:
- assistant prose uses the same system sans stack as the rest of the UI by
default (`--font-ui` in `static/style.css`),
- editorial serif assistant prose is historical/proposal or skin-scoped only;
do not reintroduce a global assistant serif without explicit design approval
plus code and test evidence,
- user bubbles and functional UI also use the system sans stack unless a scoped
skin intentionally overrides typography,
- monospace is for code, file paths, commands, tool names, and compact metadata.
- `--font-ui`: shell chrome, controls, composer, labels, and ordinary UI text
- `--font-conversation`: user/assistant message prose; by default this is
`var(--font-ui)` in `static/style.css`
- `--font-mono`: code, file paths, command lines, tool payloads, technical logs,
and terminal output
Use semantic tokens for typography. Keep prose on `--font-conversation` by
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,
radius values, or spacing values when an existing token works.
+1 -1
View File
@@ -3871,5 +3871,5 @@ async function shutdownServer() {
function _showServerStopped() {
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)">
<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>
<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>
</aside>
</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"')+
serviceTierField+
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')+
`<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>`;
}
+56 -52
View File
@@ -1,4 +1,9 @@
*, *::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 ── */
:root {
--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;
--file-tree-toggle-width:10px;
--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);
--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;
@@ -141,7 +148,6 @@
--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;
--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);
}
: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[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"] .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="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;}
@@ -268,7 +274,6 @@
--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;
--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);
}
: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[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"] .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="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;}
@@ -395,7 +400,6 @@
--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;
--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);
}
: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[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"] .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="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;}
@@ -522,7 +526,6 @@
--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;
--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);
}
: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[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"] .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="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;}
@@ -1551,7 +1554,7 @@
.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-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-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;}
@@ -1991,7 +1994,7 @@
.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-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-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;}
@@ -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.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);}
.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 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;}
@@ -2317,7 +2320,7 @@
.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 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 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;}
@@ -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;}
/* JSON/YAML tree viewer */
.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-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);}
@@ -2818,7 +2821,7 @@
.workspace-panel-heading.workspace-panel-heading--enabled{cursor:pointer;}
.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;}
.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;}
.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;}
@@ -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.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-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
styles BOTH pre[class*="language-"] and code[class*="language-"] with its own
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 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 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 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 */
@@ -3398,7 +3401,7 @@
.cmd-item-name{font-size:13px;color:var(--text);font-weight:500;}
.cmd-item-parent{color:var(--muted);font-weight:400;}
.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-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);}
@@ -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}}
/* ── 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-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;}
@@ -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{
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-body{
padding-left:0;
@@ -3971,7 +3974,7 @@ body.resizing .sidebar{transition:none!important;}
.tool-worklog-tool-group-label,
.tg-sum{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.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-group-body{overflow:hidden;max-height:0;transition:max-height .3s ease;}
.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-args{margin-bottom:6px;}
.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-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-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-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: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: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 ── */
/* Inline rhythm: transparent stream events are transcript metadata inside an
@@ -4623,7 +4626,7 @@ body.resizing .sidebar{transition:none!important;}
color:var(--strong);
}
.handoff-summary-body code{
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
font-size:.92em;
background:var(--code-inline-bg);
color:var(--code-text);
@@ -4678,7 +4681,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-card-label{
font-size:11px;
font-weight:700;
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
letter-spacing:.03em;
color:var(--muted);
flex-shrink:0;
@@ -4686,7 +4689,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-card-command{
font-size:11px;
color:var(--text);
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
@@ -4724,7 +4727,7 @@ body.resizing .sidebar{transition:none!important;}
}
.compression-card-focus .compression-card-focus-label{
color:var(--muted);
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
}
.compression-card-error-body{
color:#fecaca;
@@ -4760,7 +4763,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-reference-kind{
font-size:11px;
font-weight:700;
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
letter-spacing:.03em;
flex-shrink:0;
white-space:nowrap;
@@ -4812,7 +4815,7 @@ body.resizing .sidebar{transition:none!important;}
}
.compression-reference-body pre{
margin:0;
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
font-size:11px;
line-height:1.55;
white-space:pre-wrap;
@@ -4850,7 +4853,7 @@ body.resizing .sidebar{transition:none!important;}
.compression-complete-label{
font-size:11px;
font-weight:700;
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
letter-spacing:.03em;
flex-shrink:0;
}
@@ -4861,7 +4864,7 @@ body.resizing .sidebar{transition:none!important;}
text-overflow:ellipsis;
white-space:nowrap;
color:var(--text);
font-family:'SF Mono',ui-monospace,monospace;
font-family:var(--font-mono);
font-size:11px;
}
.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-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-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 .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);}
@@ -5382,7 +5385,7 @@ main.main > #mainPlugin{display:none;}
border:1px solid var(--border);
border-radius:8px;
font-size:12px;
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
font-family:var(--font-mono);
outline:none;
transition:border-color .15s ease, box-shadow .15s ease;
}
@@ -5569,7 +5572,7 @@ main.main > #mainPlugin{display:none;}
.provider-card-model-tag{
display:inline-block;
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);
background:var(--surface);
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-job-banner{margin-bottom:12px;}
.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;}
.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);}
@@ -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 code,
.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; }
.assistant-turn { width: 100%; }
.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;
}
.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-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-back{font-size:12px;color:var(--accent-text);text-decoration:none;font-weight:500;}
.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;}
@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-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 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.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);}
@@ -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-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-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-run-item{border-top:1px solid var(--border);padding:8px 0;}
.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-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-filename{font-family:'SF Mono',ui-monospace,monospace;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-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-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-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: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-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-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"] .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="number"]{width:64px;}
.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;}
.detail-form-row textarea{resize:vertical;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;}
.cron-schedule-preview{font-family:var(--font-mono);color:var(--text);opacity:.85;}
.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-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;}
@@ -6893,7 +6897,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
font-size:11px;
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-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}
.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-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-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.priority{color:var(--accent)}
.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-red{border-color:rgba(255,95,95,.65)}
.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){
.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:hover{background:var(--hover-bg);border-color:var(--border2);}
.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-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;}
.log-line{padding:0 14px;min-height:1.55em;border-left:3px solid transparent;}
.log-line:hover{background:rgba(255,255,255,.04);}
+115 -6
View File
@@ -15,6 +15,11 @@ const TERMINAL_UI={
resizing:false,
resizeStartY:0,
resizeStartHeight:0,
lastAppliedTheme:null,
lastAppliedFontFamily:null,
fontFitFrame:null,
fontLoadGeneration:0,
fontLoadRequest:null,
};
const TERMINAL_HEIGHT_DEFAULT=260;
@@ -114,8 +119,78 @@ function _terminalTheme(){
};
}
function syncComposerTerminalTheme(){
if(TERMINAL_UI.term)TERMINAL_UI.term.options.theme=_terminalTheme();
function _terminalMonoFont(){
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(){
@@ -130,13 +205,15 @@ function _ensureXterm(){
surface.textContent='Terminal library failed to load. Check network access to cdn.jsdelivr.net.';
return null;
}
const theme=_terminalTheme();
const fontFamily=_terminalMonoFont();
const term=new window.Terminal({
cursorBlink:true,
fontSize:13,
fontFamily:'Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontFamily,
scrollback:1000,
convertEol:false,
theme:_terminalTheme(),
theme,
});
let fitAddon=null;
if(window.FitAddon&&typeof window.FitAddon.FitAddon==='function'){
@@ -162,7 +239,10 @@ function _ensureXterm(){
});
TERMINAL_UI.term=term;
TERMINAL_UI.fitAddon=fitAddon;
TERMINAL_UI.lastAppliedTheme={...theme};
TERMINAL_UI.lastAppliedFontFamily=fontFamily;
_fitTerminal();
_watchTerminalFontLoad(term,fontFamily);
return term;
}
@@ -571,12 +651,20 @@ function expandComposerTerminal(opts){
}
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){
try{TERMINAL_UI.term.dispose();}catch(_){}
}
TERMINAL_UI.term=null;
TERMINAL_UI.fitAddon=null;
TERMINAL_UI.typedLine='';
TERMINAL_UI.lastAppliedTheme=null;
TERMINAL_UI.lastAppliedFontFamily=null;
const {surface}= _terminalEls();
if(surface)surface.textContent='';
}
@@ -696,8 +784,29 @@ window.addEventListener('resize',()=>{
});
if(window.MutationObserver){
new MutationObserver(syncComposerTerminalTheme).observe(document.documentElement,{
new MutationObserver(()=>syncComposerTerminalAppearance()).observe(document.documentElement,{
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.padding='6px';
code.style.margin='4px 0';
code.style.fontFamily='ui-monospace,monospace';
code.style.fontFamily='var(--font-mono)';
code.style.borderRadius='4px';
code.style.whiteSpace='pre-wrap';
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 "_terminalCssVar('--code-bg'" in terminal_js
assert "_terminalCssVar('--pre-text'" in terminal_js
assert "syncComposerTerminalTheme" in terminal_js
assert "attributeFilter:['class','data-skin']" in terminal_js
assert "syncComposerTerminalAppearance" in terminal_js
assert "attributeFilter:['class','data-skin','style']" in terminal_js
assert "background:var(--code-bg)" 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 ':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"] .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"] .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