History

Rework research page around a tabbed context-window viewer

Rhys Sullivan committed 2 months ago

Commit cd450ba1 parent d0646921 file · +267 −321Browse files

Unified · Split

apps/marketing/src/pages/research/mcp-tool-visibility.astro+267−321
@@ -8,7 +8,7 @@ import opencodeLogo from "../../assets/logos/opencode.svg?url"; · 7 unmodified lines
88 const GH = "https://github.com/UsefulSoftwareCo/executor";
99 const title = "How agents actually see your MCP tools";
1010 const description =
11− "The same MCP server was connected to five coding agents and the model-bound traffic was measured. Each harness shows the model something different: full definitions, bare names, or nothing at all.";
11+ "The same MCP server, connected to five coding agents, with the model-bound traffic measured. Each harness shows the model something different: full definitions, bare names, or nothing at all.";
1212
1313 // Δ prompt tokens for the 31 per-integration search tools, provider-reported.
1414 const costs = [
@@ -63,229 +63,141 @@ const max = Math.max(...costs.map((c) => c.delta)); · 48 unmodified lines
6363 {title}
6464 </h1>
6565 <p class="mt-5 max-w-[62ch] text-[17px] leading-[1.6] text-ink-2 text-pretty">
66− {description} We connected one Executor workspace — 31 integrations — to Claude Code,
67− Cursor's CLI, Codex, and two versions of opencode, twice each: once plain, once with
68− Executor's <code class="mono-inline">?search_tools=true</code> option, which adds one
69− minimal <code class="mono-inline">search_&lt;integration&gt;</code> tool per connected
70− integration. The difference between those two sessions is exactly what 31 extra tool
71− definitions cost, and where they end up.
66+ One Executor workspace — 31 integrations — connected to each harness twice: plain, and
67+ with <code class="mono-inline">?search_tools=true</code>, which adds one minimal
68+ <code class="mono-inline">search_&lt;integration&gt;</code> tool per integration. The
69+ difference between the two captured sessions is what 31 tool definitions cost, and
70+ where they end up.
7271 </p>
7372 </div>
7473 </section>
7574
76− {/* ─── THE THREE REALITIES ─── */}
75+ {/* ─── CONTEXT VIEWER ─── */}
7776 <section class="border-t border-rule px-6 py-14 sm:px-10 sm:py-20">
7877 <div class="mx-auto max-w-[1100px]">
79− <span class="eyebrow">What each harness shows the model</span>
80− <h2 class="section-title mt-4 max-w-[24ch]">Five harnesses, three different realities</h2>
81− <p class="section-sub mt-4 max-w-[62ch]">
82− "The model sees your tools" hides three different mechanisms. Every card below is one
83− harness's view of the same server, drawn from the captured request that actually went
84− to the model. Solid blocks are in the context window on every turn; dashed blocks are
85− fetched on demand; crossed-out blocks never reach the model at all.
86− </p>
78+ <span class="eyebrow">The model's context window, per harness</span>
79+ <h2 class="section-title mt-4 max-w-[26ch]">Same server, four context windows</h2>
8780
88− <div class="mt-10 grid gap-6">
89− {/* Inliners */}
90− <div class="persp surface-card">
91− <div class="persp-bar">
92− <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span>
93− <span class="persp-icons">
94− <img src={claudeLogo} alt="Claude" width="18" height="18" />
95− <img src={opencodeLogo} alt="opencode" width="18" height="18" />
96− </span>
97− <span class="persp-name">Claude Code · opencode</span>
98− <span class="persp-ver">2.1.247 · 1.18.15</span>
99− <span class="persp-cost">+3,399–3,464 tokens</span>
81+ <div class="viewer surface-card mt-10">
82+ <div class="viewer-tabs" role="tablist" aria-label="Harness">
83+ <button class="vtab is-active" data-tab="claude" role="tab" aria-selected="true">
84+ <img src={claudeLogo} alt="" width="16" height="16" /> Claude Code
85+ </button>
86+ <button class="vtab" data-tab="opencode" role="tab" aria-selected="false">
87+ <img src={opencodeLogo} alt="" width="16" height="16" /> opencode
88+ </button>
89+ <button class="vtab" data-tab="cursor" role="tab" aria-selected="false">
90+ <img src={cursorLogo} alt="" width="16" height="16" /> Cursor CLI
91+ </button>
92+ <button class="vtab" data-tab="codex" role="tab" aria-selected="false">
93+ <img src={codexLogo} alt="" width="16" height="16" /> Codex CLI
94+ </button>
95+ </div>
96+
97+ <div class="viewer-sub">
98+ <span class="viewer-meta" data-meta="claude">v2.1.247</span>
99+ <span class="viewer-meta" data-meta="opencode" hidden>v1.18.15 — beta behaves identically</span>
100+ <span class="viewer-meta" data-meta="cursor" hidden>v2026.07.23</span>
101+ <span class="viewer-meta" data-meta="codex" hidden>v0.147.0 — with or without the defer flag</span>
102+ <div class="seg" data-seg="opencode" hidden>
103+ <span class="seg-label">code mode</span>
104+ <button class="seg-btn is-active" data-cm="off">Off</button>
105+ <button class="seg-btn" data-cm="on">On</button>
100106 </div>
101− <p class="persp-lede">
102− Every definition is pasted into the prompt, every turn — name, description, and
103− schema for all 38 tools.
104− </p>
105− <div class="persp-body">
106− <div class="ctx">
107− <div class="ctx-label">the model's context window</div>
108− <div class="ctx-block ctx-quiet">system prompt</div>
109− <div class="ctx-block ctx-loud">
110− <b>38 tool definitions — full JSON schema each</b>
111− <span>name + description + parameters for every tool, inlined as API function tools</span>
112− </div>
113− <div class="ctx-block ctx-quiet">conversation</div>
114− </div>
115− <div class="persp-detail">
116− <div class="code-window">
117− <div class="code-titlebar">
118− <span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span>
119− <span class="code-filename">tools[7] of 38 — as sent to the model</span>
120− </div>
121− <pre
122− class="code-body">{`{
107+ <span class="viewer-cost" data-cost>+3,464 tokens for the 31 tools</span>
108+ </div>
109+
110+ {/* Claude Code */}
111+ <div class="pane" data-pane="claude">
112+ <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div>
113+ <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 12 — Bash, Read, Edit, …</div>
114+ <div class="blk blk-loud">
115+ <span class="blk-tag">your MCP tools · 38 of 38 in context</span>
116+ <pre>{`{
123117 "name": "mcp__executor__search_mcp_linear_app",
124− "description": "Search this integration's tools;
125− empty query lists all. Run results with execute.",
126− "input_schema": {
127− "type": "object",
128− "properties": { "query": { "type": "string" } }
129− }
118+ "description": "Search this integration's tools; empty query lists all.
119+ Run results with execute.",
120+ "input_schema": { "type": "object",
121+ "properties": { "query": { "type": "string" } } }
130122 }`}</pre>
131− </div>
132− <p class="persp-path">
133− <span class="persp-path-label">To search Linear’s tools</span>
134− one call — <code class="mono-inline">search_mcp_linear_app({"{ query }"})</code>,
135− the definition is already in context.
136− </p>
137− </div>
123+ <span class="blk-note">full name + description + schema, × 38, resent every turn</span>
138124 </div>
125+ <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div>
126+ <div class="pane-measure">prompt 36,013 → 39,477 tokens · Δ +3,464</div>
139127 </div>
140128
141− {/* opencode code mode */}
142− <div class="persp surface-card">
143− <div class="persp-bar">
144− <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span>
145− <span class="persp-icons">
146− <img src={opencodeLogo} alt="opencode" width="18" height="18" />
147− </span>
148− <span class="persp-name">opencode · code mode</span>
149− <span class="persp-ver">OPENCODE_EXPERIMENTAL_CODE_MODE</span>
150− <span class="persp-cost">+1,280 tokens</span>
129+ {/* opencode, code mode off */}
130+ <div class="pane" data-pane="opencode-off" hidden>
131+ <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div>
132+ <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 13</div>
133+ <div class="blk blk-loud">
134+ <span class="blk-tag">your MCP tools · 38 of 38 in context</span>
135+ <pre>{`{
136+ "name": "executor_search_mcp_linear_app",
137+ "description": "Search this integration's tools; empty query lists all.
138+ Run results with execute.",
139+ "inputSchema": { "type": "object",
140+ "properties": { "query": { "type": "string" } } }
141+}`}</pre>
142+ <span class="blk-note">full name + description + schema, × 38, resent every turn</span>
151143 </div>
152− <p class="persp-lede">
153− Your tools collapse into one <code class="mono-inline">execute</code> tool and
154− reappear inside its description as typed signatures — still readable, 2.7× denser.
155− </p>
156− <div class="persp-body">
157− <div class="ctx">
158− <div class="ctx-label">the model's context window</div>
159− <div class="ctx-block ctx-quiet">system prompt</div>
160− <div class="ctx-block ctx-quiet">13 built-in tools</div>
161− <div class="ctx-block ctx-loud">
162− <b>one <code class="mono-inline">execute</code> tool</b>
163− <span>its description lists every MCP tool as a TypeScript signature — 2.7× denser than JSON definitions</span>
164− </div>
165− <div class="ctx-block ctx-quiet">conversation</div>
166− </div>
167− <div class="persp-detail">
168− <div class="code-window">
169− <div class="code-titlebar">
170− <span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span>
171− <span class="code-filename">inside execute's description</span>
172− </div>
173− <pre
174− class="code-body">{`- tools.executor.search_google_gmail(input: {
144+ <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div>
145+ <div class="pane-measure">prompt 17,458 → 20,857 tokens · Δ +3,399</div>
146+ </div>
147+
148+ {/* opencode, code mode on */}
149+ <div class="pane" data-pane="opencode-on" hidden>
150+ <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div>
151+ <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 13</div>
152+ <div class="blk blk-loud">
153+ <span class="blk-tag">one execute tool — your 38 MCP tools inside its description</span>
154+ <pre>{`- tools.executor.search_mcp_linear_app(input: {
175155 query?: string,
176− }): Promise<unknown> // Search this integration's
177− tools; empty query lists all. Run results
178− with execute.`}</pre>
179− </div>
180− <p class="persp-path">
181− <span class="persp-path-label">To search Linear’s tools</span>
182− one call through code —
183− <code class="mono-inline">execute("await tools.executor.search_mcp_linear_app(…)")</code>.
184− </p>
185− </div>
156+ }): Promise<unknown> // Search this integration's tools;
157+ empty query lists all. Run results with execute.`}</pre>
158+ <span class="blk-note">TypeScript signatures — 2.7× denser than JSON definitions</span>
186159 </div>
160+ <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div>
161+ <div class="pane-measure">prompt 16,126 → 17,406 tokens · Δ +1,280</div>
187162 </div>
188163
189164 {/* Cursor */}
190− <div class="persp surface-card">
191− <div class="persp-bar">
192− <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span>
193− <span class="persp-icons">
194− <img src={cursorLogo} alt="Cursor" width="18" height="18" />
195− </span>
196− <span class="persp-name">Cursor CLI</span>
197− <span class="persp-ver">2026.07.23</span>
198− <span class="persp-cost">+170 tokens</span>
165+ <div class="pane" data-pane="cursor" hidden>
166+ <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div>
167+ <div class="blk blk-quiet">
168+ <span class="blk-tag">built-in tools</span> 19 — incl. <code class="mono-inline">GetDynamicTools</code> / <code class="mono-inline">CallDynamicTool</code>
199169 </div>
200− <p class="persp-lede">
201− The model gets a names-only catalog. Descriptions and schemas exist on Cursor's
202− side, but the model has to ask for them.
203− </p>
204− <div class="persp-body">
205− <div class="ctx">
206− <div class="ctx-label">the model's context window</div>
207− <div class="ctx-block ctx-quiet">system prompt</div>
208− <div class="ctx-block ctx-quiet">19 first-party tools, incl. <code class="mono-inline">GetDynamicTools</code> / <code class="mono-inline">CallDynamicTool</code></div>
209− <div class="ctx-block ctx-loud">
210− <b>catalog: bare tool names, grouped by server</b>
211− <span>≈5.5 tokens per tool — the name is all the model gets</span>
212− </div>
213− <div class="ctx-block ctx-dashed">
214− descriptions &amp; schemas — fetched on demand via <code class="mono-inline">GetDynamicTools</code>
215− </div>
216− <div class="ctx-block ctx-quiet">conversation</div>
217− </div>
218− <div class="persp-detail">
219− <div class="code-window">
220− <div class="code-titlebar">
221− <span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span>
222− <span class="code-filename">the model's catalog view</span>
223− </div>
224− <pre
225− class="code-body">{`executor: execute · skills · resume ·
226− search_axiom_mcp · search_github_rest ·
227− search_google_gmail · search_mcp_linear_app · …
228−
229−(full definition available via GetDynamicTools —
230− not fetched unless the model asks)`}</pre>
231− </div>
232− <p class="persp-path">
233− <span class="persp-path-label">To search Linear’s tools</span>
234− the name is visible; call it through
235− <code class="mono-inline">CallDynamicTool("search_mcp_linear_app", …)</code>,
236− optionally fetching the schema first.
237− </p>
238− </div>
170+ <div class="blk blk-loud">
171+ <span class="blk-tag">your MCP tools · names only</span>
172+ <pre>{`executor: execute · skills · resume ·
173+ search_axiom_mcp · search_github_rest · search_google_gmail ·
174+ search_mcp_linear_app · …`}</pre>
175+ <span class="blk-note">≈5.5 tokens per tool</span>
176+ </div>
177+ <div class="blk blk-dashed">
178+ descriptions &amp; schemas — sent only when the model calls
179+ <code class="mono-inline">GetDynamicTools</code>
239180 </div>
181+ <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div>
182+ <div class="pane-measure">prompt 16,140 → 16,310 tokens · Δ +170 (Cursor's usage meter)</div>
240183 </div>
241184
242185 {/* Codex */}
243− <div class="persp surface-card">
244− <div class="persp-bar">
245− <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span>
246− <span class="persp-icons">
247− <img src={codexLogo} alt="Codex" width="18" height="18" />
248− </span>
249− <span class="persp-name">Codex CLI</span>
250− <span class="persp-ver">0.147.0</span>
251− <span class="persp-cost">+0 tokens</span>
186+ <div class="pane" data-pane="codex" hidden>
187+ <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div>
188+ <div class="blk blk-quiet">
189+ <span class="blk-tag">native tools</span> 9 — <code class="mono-inline">exec</code>, collaboration, …
252190 </div>
253− <p class="persp-lede">
254− Nothing about your tools reaches the prompt. They exist only as callables inside
255− Codex's code sandbox — the model discovers them by writing code.
256− </p>
257− <div class="persp-body">
258− <div class="ctx">
259− <div class="ctx-label">the model's context window</div>
260− <div class="ctx-block ctx-quiet">system prompt</div>
261− <div class="ctx-block ctx-quiet">9 native tools — <code class="mono-inline">exec</code>, collaboration, …</div>
262− <div class="ctx-block ctx-never">
263− <b>your MCP tools</b>
264− <span>carried only in request metadata the model never reads; prompts were byte-identical with 7 vs 38 tools</span>
265− </div>
266− <div class="ctx-block ctx-quiet">conversation</div>
267− </div>
268− <div class="persp-detail">
269− <div class="code-window">
270− <div class="code-titlebar">
271− <span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span>
272− <span class="code-filename">request metadata — not model-visible</span>
273− </div>
274− <pre
275− class="code-body">{`"code_mode_tool_names": {
276− "search_google_gmail":
277− { "namespace": "mcp__executor" },
191+ <div class="blk blk-never">
192+ <span class="blk-tag"><s>your MCP tools</s></span>
193+ <pre>{`"code_mode_tool_names": {
194+ "search_mcp_linear_app": { "namespace": "mcp__executor" },
278195 … 37 more
279196 }`}</pre>
280− </div>
281− <p class="persp-path">
282− <span class="persp-path-label">To search Linear’s tools</span>
283− the model must call <code class="mono-inline">exec</code> and write code —
284− <code class="mono-inline">await tools.mcp__executor__search_mcp_linear_app(…)</code> —
285− discovering what exists only from inside the sandbox.
286− </p>
287− </div>
197+ <span class="blk-note">request metadata — never rendered into the prompt; callable only from code inside <code class="mono-inline">exec</code></span>
288198 </div>
199+ <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div>
200+ <div class="pane-measure">prompt 15,776 → 15,776 tokens · Δ 0, byte-identical</div>
289201 </div>
290202 </div>
291203 </div>
@@ -410,6 +322,57 @@ const max = Math.max(...costs.map((c) => c.delta)); · 118 unmodified lines
410322 </main>
411323 </Layout>
412324
325+<script>
326+ const viewer = document.querySelector(".viewer");
327+ if (viewer) {
328+ const tabs = [...viewer.querySelectorAll(".vtab")];
329+ const panes = [...viewer.querySelectorAll(".pane")];
330+ const metas = [...viewer.querySelectorAll(".viewer-meta")];
331+ const seg = viewer.querySelector("[data-seg='opencode']");
332+ const segBtns = [...viewer.querySelectorAll(".seg-btn")];
333+ const cost = viewer.querySelector("[data-cost]");
334+
335+ const COSTS = {
336+ claude: "+3,464 tokens for the 31 tools",
337+ "opencode-off": "+3,399 tokens for the 31 tools",
338+ "opencode-on": "+1,280 tokens for the 31 tools",
339+ cursor: "+170 tokens for the 31 tools",
340+ codex: "0 tokens — not in the prompt",
341+ };
342+
343+ let tab = "claude";
344+ let codemode = false;
345+
346+ const paneKey = () => (tab === "opencode" ? (codemode ? "opencode-on" : "opencode-off") : tab);
347+
348+ const update = () => {
349+ const key = paneKey();
350+ for (const t of tabs) {
351+ const active = t.dataset.tab === tab;
352+ t.classList.toggle("is-active", active);
353+ t.setAttribute("aria-selected", String(active));
354+ }
355+ for (const p of panes) p.hidden = p.dataset.pane !== key;
356+ for (const m of metas) m.hidden = m.dataset.meta !== tab;
357+ if (seg) seg.hidden = tab !== "opencode";
358+ for (const b of segBtns) b.classList.toggle("is-active", (b.dataset.cm === "on") === codemode);
359+ if (cost) cost.textContent = COSTS[key];
360+ };
361+
362+ for (const t of tabs)
363+ t.addEventListener("click", () => {
364+ tab = t.dataset.tab ?? "claude";
365+ update();
366+ });
367+ for (const b of segBtns)
368+ b.addEventListener("click", () => {
369+ codemode = b.dataset.cm === "on";
370+ update();
371+ });
372+ update();
373+ }
374+</script>
375+
413376 <style>
414377 .mono-inline {
415378 font-family: var(--font-mono);
@@ -421,192 +384,175 @@ const max = Math.max(...costs.map((c) => c.delta)); · 5 unmodified lines
421384 white-space: nowrap;
422385 }
423386
424− /* ── perspective cards ── */
425− .persp {
387+ /* ── viewer ── */
388+ .viewer {
426389 padding: 0;
427390 overflow: hidden;
428391 }
429− /* window titlebar — the card IS the harness's window */
430− .persp-bar {
392+ .viewer-tabs {
431393 display: flex;
432− align-items: center;
433− gap: 10px;
434− padding: 12px 18px;
394+ align-items: stretch;
435395 border-bottom: 1px solid var(--color-rule);
436− background: var(--color-surface-2);
437− }
438− .persp-dot {
439− width: 9px;
440− height: 9px;
441− border-radius: 9999px;
442− background: var(--color-rule-strong);
443− flex-shrink: 0;
444− }
445− .persp-dot + .persp-dot {
446− margin-left: -3px;
396+ overflow-x: auto;
447397 }
448− .persp-icons {
398+ .vtab {
449399 display: flex;
450400 align-items: center;
451− gap: 7px;
452− flex-shrink: 0;
453− margin-left: 8px;
401+ gap: 8px;
402+ padding: 13px 18px;
403+ font-size: 14px;
404+ font-weight: 500;
405+ color: var(--color-ink-2);
406+ border: 0;
407+ border-bottom: 2px solid transparent;
408+ background: none;
409+ cursor: pointer;
410+ white-space: nowrap;
411+ transition: color 0.15s;
454412 }
455− .persp-icons img {
413+ .vtab img {
456414 display: block;
457− height: 18px;
458− width: 18px;
415+ height: 16px;
416+ width: 16px;
417+ opacity: 0.75;
459418 }
460− .persp-name {
461− font-weight: 600;
462− font-size: 15px;
463− letter-spacing: -0.01em;
419+ .vtab:hover {
464420 color: var(--color-ink);
465− white-space: nowrap;
466421 }
467− .persp-ver {
422+ .vtab.is-active {
423+ color: var(--color-ink);
424+ border-bottom-color: var(--color-ink);
425+ }
426+ .vtab.is-active img {
427+ opacity: 1;
428+ }
429+
430+ .viewer-sub {
431+ display: flex;
432+ align-items: center;
433+ gap: 14px;
434+ padding: 10px 18px;
435+ border-bottom: 1px solid var(--color-rule);
436+ background: var(--color-surface-2);
437+ }
438+ .viewer-meta {
468439 font-family: var(--font-mono);
469− font-size: 11px;
440+ font-size: 11.5px;
470441 color: var(--color-ink-3);
471442 overflow: hidden;
472443 text-overflow: ellipsis;
473444 white-space: nowrap;
474− min-width: 0;
475445 }
476− .persp-cost {
446+ .viewer-cost {
447+ margin-left: auto;
477448 font-family: var(--font-mono);
478− font-size: 12.5px;
449+ font-size: 12px;
479450 color: var(--color-ink);
480451 white-space: nowrap;
481− margin-left: auto;
482452 }
483− @media (max-width: 640px) {
484− .persp-ver {
485− display: none;
486− }
453+ .seg {
454+ display: flex;
455+ align-items: center;
456+ gap: 4px;
487457 }
488− .persp-lede {
489− padding: 16px 24px 0;
490− font-size: 14.5px;
491− line-height: 1.6;
458+ .seg-label {
459+ font-family: var(--font-mono);
460+ font-size: 11px;
461+ letter-spacing: 0.08em;
462+ text-transform: uppercase;
463+ color: var(--color-ink-3);
464+ margin-right: 4px;
465+ }
466+ .seg-btn {
467+ font-family: var(--font-mono);
468+ font-size: 11.5px;
469+ padding: 3px 10px;
470+ border: 1px solid var(--color-rule-strong);
471+ background: var(--color-surface);
492472 color: var(--color-ink-2);
493− max-width: 72ch;
473+ cursor: pointer;
494474 }
495− /* one window chrome per card: inner excerpts keep a filename bar, no dots */
496− .persp-detail .code-dot {
497− display: none;
475+ .seg-btn:first-of-type {
476+ border-radius: 5px 0 0 5px;
498477 }
499− .persp-body {
500− display: grid;
501− grid-template-columns: 340px 1fr;
502− gap: 0;
478+ .seg-btn:last-of-type {
479+ border-radius: 0 5px 5px 0;
480+ margin-left: -1px;
503481 }
504− @media (max-width: 820px) {
505− .persp-body {
506− grid-template-columns: 1fr;
507− }
482+ .seg-btn.is-active {
483+ background: var(--color-accent);
484+ border-color: var(--color-accent);
485+ color: var(--color-surface);
508486 }
509487
510− /* context-window schematic */
511− .ctx {
512− padding: 20px 24px;
513− border-right: 1px solid var(--color-rule);
488+ .pane {
514489 display: flex;
515490 flex-direction: column;
516− gap: 6px;
491+ gap: 8px;
492+ padding: 20px;
517493 }
518− @media (max-width: 820px) {
519− .ctx {
520− border-right: 0;
521− border-bottom: 1px solid var(--color-rule);
522− }
494+ .blk {
495+ border-radius: 6px;
496+ padding: 10px 14px;
497+ font-size: 13px;
498+ line-height: 1.5;
499+ color: var(--color-ink-2);
523500 }
524− .ctx-label {
501+ .blk-tag {
525502 font-family: var(--font-mono);
526− font-size: 10.5px;
503+ font-size: 11px;
527504 letter-spacing: 0.08em;
528505 text-transform: uppercase;
529506 color: var(--color-ink-3);
530− margin-bottom: 4px;
531− }
532− .ctx-block {
533− border-radius: 6px;
534− padding: 9px 12px;
535− font-size: 12.5px;
536− line-height: 1.45;
507+ margin-right: 8px;
537508 }
538− .ctx-quiet {
509+ .blk-quiet {
539510 border: 1px solid var(--color-rule);
540511 background: var(--color-surface-2);
541− color: var(--color-ink-2);
542512 }
543− .ctx-loud {
513+ .blk-loud {
544514 border: 1px solid var(--color-rule-strong);
545515 background: var(--color-surface);
546516 color: var(--color-ink);
547517 }
548− .ctx-loud b {
549− display: block;
550− font-weight: 600;
551− font-size: 13px;
552− }
553− .ctx-loud span {
554− display: block;
555− margin-top: 2px;
556− color: var(--color-ink-2);
557− font-size: 12px;
518+ .blk-loud .blk-tag {
519+ color: var(--color-ink);
558520 }
559− .ctx-dashed {
521+ .blk-dashed {
560522 border: 1px dashed var(--color-rule-strong);
561523 color: var(--color-ink-3);
562− background: transparent;
563524 }
564− .ctx-never {
525+ .blk-never {
565526 border: 1px dashed var(--color-rule);
566− background: transparent;
567527 color: var(--color-ink-3);
568528 }
569− .ctx-never b {
570− display: block;
571− font-weight: 500;
572− font-size: 13px;
573− text-decoration: line-through;
574− color: var(--color-ink-3);
575− }
576− .ctx-never span {
577− display: block;
578− margin-top: 2px;
579− font-size: 12px;
580− }
581−
582− .persp-detail {
583− padding: 20px 24px;
584− display: flex;
585− flex-direction: column;
586− gap: 14px;
587− min-width: 0;
588− }
589− .persp-detail .code-window {
590− margin: 0;
591− }
592− .persp-detail .code-body {
529+ .blk pre {
530+ margin: 10px 0 6px;
531+ font-family: var(--font-mono);
593532 font-size: 12px;
594533 line-height: 1.6;
534+ color: var(--color-ink-2);
535+ background: var(--color-surface-2);
536+ border: 1px solid var(--color-rule);
537+ border-radius: 6px;
538+ padding: 12px 14px;
595539 overflow-x: auto;
540+ white-space: pre;
596541 }
597− .persp-path {
598− font-size: 13.5px;
599− line-height: 1.6;
600− color: var(--color-ink-2);
542+ .blk-never pre {
543+ background: transparent;
601544 }
602− .persp-path-label {
545+ .blk-note {
603546 display: block;
547+ font-size: 12px;
548+ color: var(--color-ink-3);
549+ }
550+ .pane-measure {
551+ margin-top: 8px;
604552 font-family: var(--font-mono);
605− font-size: 10.5px;
606− letter-spacing: 0.08em;
607− text-transform: uppercase;
553+ font-size: 12px;
608554 color: var(--color-ink-3);
609− margin-bottom: 3px;
555+ text-align: right;
610556 }
611557
612558 /* ── cost chart ── */