Unified · Split
| @@ -8,7 +8,7 @@ import opencodeLogo from "../../assets/logos/opencode.svg?url"; · 7 unmodified lines | |||
| 8 | const GH = "https://github.com/UsefulSoftwareCo/executor"; | 8 | const GH = "https://github.com/UsefulSoftwareCo/executor"; |
| 9 | const title = "How agents actually see your MCP tools"; | 9 | const title = "How agents actually see your MCP tools"; |
| 10 | const description = | 10 | 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."; |
| 12 | 12 | ||
| 13 | // Δ prompt tokens for the 31 per-integration search tools, provider-reported. | 13 | // Δ prompt tokens for the 31 per-integration search tools, provider-reported. |
| 14 | const costs = [ | 14 | const costs = [ |
| @@ -63,229 +63,141 @@ const max = Math.max(...costs.map((c) => c.delta)); · 48 unmodified lines | |||
| 63 | {title} | 63 | {title} |
| 64 | </h1> | 64 | </h1> |
| 65 | <p class="mt-5 max-w-[62ch] text-[17px] leading-[1.6] text-ink-2 text-pretty"> | 65 | <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, | 66 | One Executor workspace — 31 integrations — connected to each harness twice: plain, and |
| 67 | Cursor's CLI, Codex, and two versions of opencode, twice each: once plain, once with | 67 | with <code class="mono-inline">?search_tools=true</code>, which adds one minimal |
| 68 | Executor's <code class="mono-inline">?search_tools=true</code> option, which adds one | 68 | <code class="mono-inline">search_<integration></code> tool per integration. The |
| 69 | minimal <code class="mono-inline">search_<integration></code> tool per connected | 69 | difference between the two captured sessions is what 31 tool definitions cost, and |
| 70 | integration. The difference between those two sessions is exactly what 31 extra tool | 70 | where they end up. |
| 71 | definitions cost, and where they end up. | ||
| 72 | </p> | 71 | </p> |
| 73 | </div> | 72 | </div> |
| 74 | </section> | 73 | </section> |
| 75 | 74 | ||
| 76 | {/* ─── THE THREE REALITIES ─── */} | 75 | {/* ─── CONTEXT VIEWER ─── */} |
| 77 | <section class="border-t border-rule px-6 py-14 sm:px-10 sm:py-20"> | 76 | <section class="border-t border-rule px-6 py-14 sm:px-10 sm:py-20"> |
| 78 | <div class="mx-auto max-w-[1100px]"> | 77 | <div class="mx-auto max-w-[1100px]"> |
| 79 | <span class="eyebrow">What each harness shows the model</span> | 78 | <span class="eyebrow">The model's context window, per harness</span> |
| 80 | <h2 class="section-title mt-4 max-w-[24ch]">Five harnesses, three different realities</h2> | 79 | <h2 class="section-title mt-4 max-w-[26ch]">Same server, four context windows</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> | ||
| 87 | 80 | ||
| 88 | <div class="mt-10 grid gap-6"> | 81 | <div class="viewer surface-card mt-10"> |
| 89 | {/* Inliners */} | 82 | <div class="viewer-tabs" role="tablist" aria-label="Harness"> |
| 90 | <div class="persp surface-card"> | 83 | <button class="vtab is-active" data-tab="claude" role="tab" aria-selected="true"> |
| 91 | <div class="persp-bar"> | 84 | <img src={claudeLogo} alt="" width="16" height="16" /> Claude Code |
| 92 | <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span> | 85 | </button> |
| 93 | <span class="persp-icons"> | 86 | <button class="vtab" data-tab="opencode" role="tab" aria-selected="false"> |
| 94 | <img src={claudeLogo} alt="Claude" width="18" height="18" /> | 87 | <img src={opencodeLogo} alt="" width="16" height="16" /> opencode |
| 95 | <img src={opencodeLogo} alt="opencode" width="18" height="18" /> | 88 | </button> |
| 96 | </span> | 89 | <button class="vtab" data-tab="cursor" role="tab" aria-selected="false"> |
| 97 | <span class="persp-name">Claude Code · opencode</span> | 90 | <img src={cursorLogo} alt="" width="16" height="16" /> Cursor CLI |
| 98 | <span class="persp-ver">2.1.247 · 1.18.15</span> | 91 | </button> |
| 99 | <span class="persp-cost">+3,399–3,464 tokens</span> | 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> | ||
| 100 | </div> | 106 | </div> |
| 101 | <p class="persp-lede"> | 107 | <span class="viewer-cost" data-cost>+3,464 tokens for the 31 tools</span> |
| 102 | Every definition is pasted into the prompt, every turn — name, description, and | 108 | </div> |
| 103 | schema for all 38 tools. | 109 | |
| 104 | </p> | 110 | {/* Claude Code */} |
| 105 | <div class="persp-body"> | 111 | <div class="pane" data-pane="claude"> |
| 106 | <div class="ctx"> | 112 | <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div> |
| 107 | <div class="ctx-label">the model's context window</div> | 113 | <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 12 — Bash, Read, Edit, …</div> |
| 108 | <div class="ctx-block ctx-quiet">system prompt</div> | 114 | <div class="blk blk-loud"> |
| 109 | <div class="ctx-block ctx-loud"> | 115 | <span class="blk-tag">your MCP tools · 38 of 38 in context</span> |
| 110 | <b>38 tool definitions — full JSON schema each</b> | 116 | <pre>{`{ |
| 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">{`{ | ||
| 123 | "name": "mcp__executor__search_mcp_linear_app", | 117 | "name": "mcp__executor__search_mcp_linear_app", |
| 124 | "description": "Search this integration's tools; | 118 | "description": "Search this integration's tools; empty query lists all. |
| 125 | empty query lists all. Run results with execute.", | 119 | Run results with execute.", |
| 126 | "input_schema": { | 120 | "input_schema": { "type": "object", |
| 127 | "type": "object", | 121 | "properties": { "query": { "type": "string" } } } |
| 128 | "properties": { "query": { "type": "string" } } | ||
| 129 | } | ||
| 130 | }`}</pre> | 122 | }`}</pre> |
| 131 | </div> | 123 | <span class="blk-note">full name + description + schema, × 38, resent every turn</span> |
| 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> | ||
| 138 | </div> | 124 | </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> | ||
| 139 | </div> | 127 | </div> |
| 140 | 128 | ||
| 141 | {/* opencode code mode */} | 129 | {/* opencode, code mode off */} |
| 142 | <div class="persp surface-card"> | 130 | <div class="pane" data-pane="opencode-off" hidden> |
| 143 | <div class="persp-bar"> | 131 | <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div> |
| 144 | <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span> | 132 | <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 13</div> |
| 145 | <span class="persp-icons"> | 133 | <div class="blk blk-loud"> |
| 146 | <img src={opencodeLogo} alt="opencode" width="18" height="18" /> | 134 | <span class="blk-tag">your MCP tools · 38 of 38 in context</span> |
| 147 | </span> | 135 | <pre>{`{ |
| 148 | <span class="persp-name">opencode · code mode</span> | 136 | "name": "executor_search_mcp_linear_app", |
| 149 | <span class="persp-ver">OPENCODE_EXPERIMENTAL_CODE_MODE</span> | 137 | "description": "Search this integration's tools; empty query lists all. |
| 150 | <span class="persp-cost">+1,280 tokens</span> | 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> | ||
| 151 | </div> | 143 | </div> |
| 152 | <p class="persp-lede"> | 144 | <div class="blk blk-quiet"><span class="blk-tag">conversation</span></div> |
| 153 | Your tools collapse into one <code class="mono-inline">execute</code> tool and | 145 | <div class="pane-measure">prompt 17,458 → 20,857 tokens · Δ +3,399</div> |
| 154 | reappear inside its description as typed signatures — still readable, 2.7× denser. | 146 | </div> |
| 155 | </p> | 147 | |
| 156 | <div class="persp-body"> | 148 | {/* opencode, code mode on */} |
| 157 | <div class="ctx"> | 149 | <div class="pane" data-pane="opencode-on" hidden> |
| 158 | <div class="ctx-label">the model's context window</div> | 150 | <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div> |
| 159 | <div class="ctx-block ctx-quiet">system prompt</div> | 151 | <div class="blk blk-quiet"><span class="blk-tag">built-in tools</span> 13</div> |
| 160 | <div class="ctx-block ctx-quiet">13 built-in tools</div> | 152 | <div class="blk blk-loud"> |
| 161 | <div class="ctx-block ctx-loud"> | 153 | <span class="blk-tag">one execute tool — your 38 MCP tools inside its description</span> |
| 162 | <b>one <code class="mono-inline">execute</code> tool</b> | 154 | <pre>{`- tools.executor.search_mcp_linear_app(input: { |
| 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: { | ||
| 175 | query?: string, | 155 | query?: string, |
| 176 | }): Promise<unknown> // Search this integration's | 156 | }): Promise<unknown> // Search this integration's tools; |
| 177 | tools; empty query lists all. Run results | 157 | empty query lists all. Run results with execute.`}</pre> |
| 178 | with execute.`}</pre> | 158 | <span class="blk-note">TypeScript signatures — 2.7× denser than JSON definitions</span> |
| 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> | ||
| 186 | </div> | 159 | </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> | ||
| 187 | </div> | 162 | </div> |
| 188 | 163 | ||
| 189 | {/* Cursor */} | 164 | {/* Cursor */} |
| 190 | <div class="persp surface-card"> | 165 | <div class="pane" data-pane="cursor" hidden> |
| 191 | <div class="persp-bar"> | 166 | <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div> |
| 192 | <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span> | 167 | <div class="blk blk-quiet"> |
| 193 | <span class="persp-icons"> | 168 | <span class="blk-tag">built-in tools</span> 19 — incl. <code class="mono-inline">GetDynamicTools</code> / <code class="mono-inline">CallDynamicTool</code> |
| 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> | ||
| 199 | </div> | 169 | </div> |
| 200 | <p class="persp-lede"> | 170 | <div class="blk blk-loud"> |
| 201 | The model gets a names-only catalog. Descriptions and schemas exist on Cursor's | 171 | <span class="blk-tag">your MCP tools · names only</span> |
| 202 | side, but the model has to ask for them. | 172 | <pre>{`executor: execute · skills · resume · |
| 203 | </p> | 173 | search_axiom_mcp · search_github_rest · search_google_gmail · |
| 204 | <div class="persp-body"> | 174 | search_mcp_linear_app · …`}</pre> |
| 205 | <div class="ctx"> | 175 | <span class="blk-note">≈5.5 tokens per tool</span> |
| 206 | <div class="ctx-label">the model's context window</div> | 176 | </div> |
| 207 | <div class="ctx-block ctx-quiet">system prompt</div> | 177 | <div class="blk blk-dashed"> |
| 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> | 178 | descriptions & schemas — sent only when the model calls |
| 209 | <div class="ctx-block ctx-loud"> | 179 | <code class="mono-inline">GetDynamicTools</code> |
| 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 & 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> | ||
| 239 | </div> | 180 | </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> | ||
| 240 | </div> | 183 | </div> |
| 241 | 184 | ||
| 242 | {/* Codex */} | 185 | {/* Codex */} |
| 243 | <div class="persp surface-card"> | 186 | <div class="pane" data-pane="codex" hidden> |
| 244 | <div class="persp-bar"> | 187 | <div class="blk blk-quiet"><span class="blk-tag">system prompt</span></div> |
| 245 | <span class="persp-dot"></span><span class="persp-dot"></span><span class="persp-dot"></span> | 188 | <div class="blk blk-quiet"> |
| 246 | <span class="persp-icons"> | 189 | <span class="blk-tag">native tools</span> 9 — <code class="mono-inline">exec</code>, collaboration, … |
| 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> | ||
| 252 | </div> | 190 | </div> |
| 253 | <p class="persp-lede"> | 191 | <div class="blk blk-never"> |
| 254 | Nothing about your tools reaches the prompt. They exist only as callables inside | 192 | <span class="blk-tag"><s>your MCP tools</s></span> |
| 255 | Codex's code sandbox — the model discovers them by writing code. | 193 | <pre>{`"code_mode_tool_names": { |
| 256 | </p> | 194 | "search_mcp_linear_app": { "namespace": "mcp__executor" }, |
| 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" }, | ||
| 278 | … 37 more | 195 | … 37 more |
| 279 | }`}</pre> | 196 | }`}</pre> |
| 280 | </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> |
| 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> | ||
| 288 | </div> | 198 | </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> | ||
| 289 | </div> | 201 | </div> |
| 290 | </div> | 202 | </div> |
| 291 | </div> | 203 | </div> |
| @@ -410,6 +322,57 @@ const max = Math.max(...costs.map((c) => c.delta)); · 118 unmodified lines | |||
| 410 | </main> | 322 | </main> |
| 411 | </Layout> | 323 | </Layout> |
| 412 | 324 | ||
| 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 | |||
| 413 | <style> | 376 | <style> |
| 414 | .mono-inline { | 377 | .mono-inline { |
| 415 | font-family: var(--font-mono); | 378 | font-family: var(--font-mono); |
| @@ -421,192 +384,175 @@ const max = Math.max(...costs.map((c) => c.delta)); · 5 unmodified lines | |||
| 421 | white-space: nowrap; | 384 | white-space: nowrap; |
| 422 | } | 385 | } |
| 423 | 386 | ||
| 424 | /* ── perspective cards ── */ | 387 | /* ── viewer ── */ |
| 425 | .persp { | 388 | .viewer { |
| 426 | padding: 0; | 389 | padding: 0; |
| 427 | overflow: hidden; | 390 | overflow: hidden; |
| 428 | } | 391 | } |
| 429 | /* window titlebar — the card IS the harness's window */ | 392 | .viewer-tabs { |
| 430 | .persp-bar { | ||
| 431 | display: flex; | 393 | display: flex; |
| 432 | align-items: center; | 394 | align-items: stretch; |
| 433 | gap: 10px; | ||
| 434 | padding: 12px 18px; | ||
| 435 | border-bottom: 1px solid var(--color-rule); | 395 | border-bottom: 1px solid var(--color-rule); |
| 436 | background: var(--color-surface-2); | 396 | overflow-x: auto; |
| 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; | ||
| 447 | } | 397 | } |
| 448 | .persp-icons { | 398 | .vtab { |
| 449 | display: flex; | 399 | display: flex; |
| 450 | align-items: center; | 400 | align-items: center; |
| 451 | gap: 7px; | 401 | gap: 8px; |
| 452 | flex-shrink: 0; | 402 | padding: 13px 18px; |
| 453 | margin-left: 8px; | 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; | ||
| 454 | } | 412 | } |
| 455 | .persp-icons img { | 413 | .vtab img { |
| 456 | display: block; | 414 | display: block; |
| 457 | height: 18px; | 415 | height: 16px; |
| 458 | width: 18px; | 416 | width: 16px; |
| 417 | opacity: 0.75; | ||
| 459 | } | 418 | } |
| 460 | .persp-name { | 419 | .vtab:hover { |
| 461 | font-weight: 600; | ||
| 462 | font-size: 15px; | ||
| 463 | letter-spacing: -0.01em; | ||
| 464 | color: var(--color-ink); | 420 | color: var(--color-ink); |
| 465 | white-space: nowrap; | ||
| 466 | } | 421 | } |
| 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 { | ||
| 468 | font-family: var(--font-mono); | 439 | font-family: var(--font-mono); |
| 469 | font-size: 11px; | 440 | font-size: 11.5px; |
| 470 | color: var(--color-ink-3); | 441 | color: var(--color-ink-3); |
| 471 | overflow: hidden; | 442 | overflow: hidden; |
| 472 | text-overflow: ellipsis; | 443 | text-overflow: ellipsis; |
| 473 | white-space: nowrap; | 444 | white-space: nowrap; |
| 474 | min-width: 0; | ||
| 475 | } | 445 | } |
| 476 | .persp-cost { | 446 | .viewer-cost { |
| 447 | margin-left: auto; | ||
| 477 | font-family: var(--font-mono); | 448 | font-family: var(--font-mono); |
| 478 | font-size: 12.5px; | 449 | font-size: 12px; |
| 479 | color: var(--color-ink); | 450 | color: var(--color-ink); |
| 480 | white-space: nowrap; | 451 | white-space: nowrap; |
| 481 | margin-left: auto; | ||
| 482 | } | 452 | } |
| 483 | @media (max-width: 640px) { | 453 | .seg { |
| 484 | .persp-ver { | 454 | display: flex; |
| 485 | display: none; | 455 | align-items: center; |
| 486 | } | 456 | gap: 4px; |
| 487 | } | 457 | } |
| 488 | .persp-lede { | 458 | .seg-label { |
| 489 | padding: 16px 24px 0; | 459 | font-family: var(--font-mono); |
| 490 | font-size: 14.5px; | 460 | font-size: 11px; |
| 491 | line-height: 1.6; | 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); | ||
| 492 | color: var(--color-ink-2); | 472 | color: var(--color-ink-2); |
| 493 | max-width: 72ch; | 473 | cursor: pointer; |
| 494 | } | 474 | } |
| 495 | /* one window chrome per card: inner excerpts keep a filename bar, no dots */ | 475 | .seg-btn:first-of-type { |
| 496 | .persp-detail .code-dot { | 476 | border-radius: 5px 0 0 5px; |
| 497 | display: none; | ||
| 498 | } | 477 | } |
| 499 | .persp-body { | 478 | .seg-btn:last-of-type { |
| 500 | display: grid; | 479 | border-radius: 0 5px 5px 0; |
| 501 | grid-template-columns: 340px 1fr; | 480 | margin-left: -1px; |
| 502 | gap: 0; | ||
| 503 | } | 481 | } |
| 504 | @media (max-width: 820px) { | 482 | .seg-btn.is-active { |
| 505 | .persp-body { | 483 | background: var(--color-accent); |
| 506 | grid-template-columns: 1fr; | 484 | border-color: var(--color-accent); |
| 507 | } | 485 | color: var(--color-surface); |
| 508 | } | 486 | } |
| 509 | 487 | ||
| 510 | /* context-window schematic */ | 488 | .pane { |
| 511 | .ctx { | ||
| 512 | padding: 20px 24px; | ||
| 513 | border-right: 1px solid var(--color-rule); | ||
| 514 | display: flex; | 489 | display: flex; |
| 515 | flex-direction: column; | 490 | flex-direction: column; |
| 516 | gap: 6px; | 491 | gap: 8px; |
| 492 | padding: 20px; | ||
| 517 | } | 493 | } |
| 518 | @media (max-width: 820px) { | 494 | .blk { |
| 519 | .ctx { | 495 | border-radius: 6px; |
| 520 | border-right: 0; | 496 | padding: 10px 14px; |
| 521 | border-bottom: 1px solid var(--color-rule); | 497 | font-size: 13px; |
| 522 | } | 498 | line-height: 1.5; |
| 499 | color: var(--color-ink-2); | ||
| 523 | } | 500 | } |
| 524 | .ctx-label { | 501 | .blk-tag { |
| 525 | font-family: var(--font-mono); | 502 | font-family: var(--font-mono); |
| 526 | font-size: 10.5px; | 503 | font-size: 11px; |
| 527 | letter-spacing: 0.08em; | 504 | letter-spacing: 0.08em; |
| 528 | text-transform: uppercase; | 505 | text-transform: uppercase; |
| 529 | color: var(--color-ink-3); | 506 | color: var(--color-ink-3); |
| 530 | margin-bottom: 4px; | 507 | margin-right: 8px; |
| 531 | } | ||
| 532 | .ctx-block { | ||
| 533 | border-radius: 6px; | ||
| 534 | padding: 9px 12px; | ||
| 535 | font-size: 12.5px; | ||
| 536 | line-height: 1.45; | ||
| 537 | } | 508 | } |
| 538 | .ctx-quiet { | 509 | .blk-quiet { |
| 539 | border: 1px solid var(--color-rule); | 510 | border: 1px solid var(--color-rule); |
| 540 | background: var(--color-surface-2); | 511 | background: var(--color-surface-2); |
| 541 | color: var(--color-ink-2); | ||
| 542 | } | 512 | } |
| 543 | .ctx-loud { | 513 | .blk-loud { |
| 544 | border: 1px solid var(--color-rule-strong); | 514 | border: 1px solid var(--color-rule-strong); |
| 545 | background: var(--color-surface); | 515 | background: var(--color-surface); |
| 546 | color: var(--color-ink); | 516 | color: var(--color-ink); |
| 547 | } | 517 | } |
| 548 | .ctx-loud b { | 518 | .blk-loud .blk-tag { |
| 549 | display: block; | 519 | color: var(--color-ink); |
| 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; | ||
| 558 | } | 520 | } |
| 559 | .ctx-dashed { | 521 | .blk-dashed { |
| 560 | border: 1px dashed var(--color-rule-strong); | 522 | border: 1px dashed var(--color-rule-strong); |
| 561 | color: var(--color-ink-3); | 523 | color: var(--color-ink-3); |
| 562 | background: transparent; | ||
| 563 | } | 524 | } |
| 564 | .ctx-never { | 525 | .blk-never { |
| 565 | border: 1px dashed var(--color-rule); | 526 | border: 1px dashed var(--color-rule); |
| 566 | background: transparent; | ||
| 567 | color: var(--color-ink-3); | 527 | color: var(--color-ink-3); |
| 568 | } | 528 | } |
| 569 | .ctx-never b { | 529 | .blk pre { |
| 570 | display: block; | 530 | margin: 10px 0 6px; |
| 571 | font-weight: 500; | 531 | font-family: var(--font-mono); |
| 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 { | ||
| 593 | font-size: 12px; | 532 | font-size: 12px; |
| 594 | line-height: 1.6; | 533 | 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; | ||
| 595 | overflow-x: auto; | 539 | overflow-x: auto; |
| 540 | white-space: pre; | ||
| 596 | } | 541 | } |
| 597 | .persp-path { | 542 | .blk-never pre { |
| 598 | font-size: 13.5px; | 543 | background: transparent; |
| 599 | line-height: 1.6; | ||
| 600 | color: var(--color-ink-2); | ||
| 601 | } | 544 | } |
| 602 | .persp-path-label { | 545 | .blk-note { |
| 603 | display: block; | 546 | display: block; |
| 547 | font-size: 12px; | ||
| 548 | color: var(--color-ink-3); | ||
| 549 | } | ||
| 550 | .pane-measure { | ||
| 551 | margin-top: 8px; | ||
| 604 | font-family: var(--font-mono); | 552 | font-family: var(--font-mono); |
| 605 | font-size: 10.5px; | 553 | font-size: 12px; |
| 606 | letter-spacing: 0.08em; | ||
| 607 | text-transform: uppercase; | ||
| 608 | color: var(--color-ink-3); | 554 | color: var(--color-ink-3); |
| 609 | margin-bottom: 3px; | 555 | text-align: right; |
| 610 | } | 556 | } |
| 611 | 557 | ||
| 612 | /* ── cost chart ── */ | 558 | /* ── cost chart ── */ |