diff --git a/THEMES.md b/THEMES.md index 1fa8c9c8..e3191f71 100644 --- a/THEMES.md +++ b/THEMES.md @@ -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)` — diff --git a/docs/UIUX-GUIDE.md b/docs/UIUX-GUIDE.md index 5791fe3b..0fe44958 100644 --- a/docs/UIUX-GUIDE.md +++ b/docs/UIUX-GUIDE.md @@ -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. diff --git a/static/boot.js b/static/boot.js index e13d35de..c0d50011 100644 --- a/static/boot.js +++ b/static/boot.js @@ -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 = '

' + stoppedMsg + '

'; + document.body.innerHTML = '

' + stoppedMsg + '

'; } diff --git a/static/index.html b/static/index.html index 8e9568de..10e5dc9c 100644 --- a/static/index.html +++ b/static/index.html @@ -1718,7 +1718,7 @@ - + diff --git a/static/panels.js b/static/panels.js index f625103c..3024b725 100644 --- a/static/panels.js +++ b/static/panels.js @@ -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+ - ``+ + ``+ _auxAdvancedInputHtml('auxAdvancedApiKey',t('settings_aux_advanced_api_key')||'API key override','',apiKeyHint,'text','autocomplete="one-time-code" inputmode="text" readonly onfocus="this.removeAttribute("readonly")"',';-webkit-text-security:disc')+ ``; } diff --git a/static/style.css b/static/style.css index eb2e228f..dd3c22fd 100644 --- a/static/style.css +++ b/static/style.css @@ -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;} @@ -251,7 +257,7 @@ :root[data-skin="graphite"] .btn.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;} - /* ── Skin: Codex (OpenAI terminal IDE) ── + /* ── Skin: Codex (OpenAI terminal IDE) ── Full palette rewrite with minimal editor surfaces, crisp contrast, 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. */ @@ -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);} diff --git a/static/terminal.js b/static/terminal.js index 82def255..45855932 100644 --- a/static/terminal.js +++ b/static/terminal.js @@ -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, + }); + } } diff --git a/static/ui.js b/static/ui.js index 7483622e..3639c6c1 100644 --- a/static/ui.js +++ b/static/ui.js @@ -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'; diff --git a/tests/test_embedded_workspace_terminal.py b/tests/test_embedded_workspace_terminal.py index 2f4a6fe6..a48b836e 100644 --- a/tests/test_embedded_workspace_terminal.py +++ b/tests/test_embedded_workspace_terminal.py @@ -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 diff --git a/tests/test_graphite_skin.py b/tests/test_graphite_skin.py index 2691b5b3..c4c8c470 100644 --- a/tests/test_graphite_skin.py +++ b/tests/test_graphite_skin.py @@ -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 diff --git a/tests/test_typography_contract.py b/tests/test_typography_contract.py new file mode 100644 index 00000000..adc24237 --- /dev/null +++ b/tests/test_typography_contract.py @@ -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' + + + +""" + + 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