From 9f2eb9f5976ba5828462640f40cdf6340ae991e9 Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Thu, 8 Oct 2026 20:33:34 +0200 Subject: [PATCH 1/2] Reformat JS, CSS, JSON and YAML with oxfmt The output of `just fmt-js` (oxfmt 0.69.0 with .oxfmtrc.json, plus scripts/fmt_templates.py for the component files holding Jinja), and nothing else: no hand edits. Meaning is unchanged: the JS parses to equal ASTs, the CSS is equal once whitespace, quote style, hex case and leading zeros are normalized, and the YAML and JSON load to equal data. Co-Authored-By: Claude Opus 5.5 --- .claude/settings.json | 2 +- .github/workflows/ci.yml | 180 +- .github/workflows/docs.yml | 4 +- .vscode/settings.json | 2 +- .../templates/components/filter_styles.css | 8 +- .../templates/components/global_styles.css | 41 +- .../html/templates/components/image_zoom.js | 60 +- .../html/templates/components/live_update.js | 33 +- .../templates/components/message_styles.css | 183 +- .../components/minimal/components.css | 144 +- .../html/templates/components/minimal/dag.css | 142 +- .../templates/components/minimal/layout.css | 67 +- .../templates/components/minimal/minimal.js | 826 ++-- .../components/minimal/minimal_dag.js | 4097 +++++++++-------- .../templates/components/minimal/pages.css | 24 +- .../templates/components/minimal/pages.js | 106 +- .../templates/components/minimal/scheme.js | 80 +- .../components/minimal/scheme_init.js | 26 +- .../components/minimal/search_rows.js | 185 +- .../components/minimal/theme_init.js | 30 +- .../templates/components/page_nav_styles.css | 8 +- .../components/project_card_styles.css | 12 +- .../templates/components/search_styles.css | 30 +- .../components/session_nav_styles.css | 6 +- .../templates/components/timeline_styles.css | 8 +- .../components/timezone_converter.js | 47 +- .../html/templates/components/todo_styles.css | 4 +- 27 files changed, 3613 insertions(+), 2742 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 298a4e2b..09ac7ace 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -12,4 +12,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d143a6..b495635c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: test: @@ -31,91 +31,91 @@ jobs: COV_APPEND: ${{ matrix.is-primary && '--cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term' || '' }} steps: - - uses: actions/checkout@v4 - with: - # Full history so test/test_commit_linkifier.py's - # TestIntegrationLocalRepo can resolve SHAs older than the - # current tip (`git branch -r --contains` reads local refs - # only and needs the commits in the object DB). - fetch-depth: 0 - - - name: Move TEMP to the workspace disk (Windows) - # On windows runners TMP/TEMP default to the C: OS disk, whose - # throttled I/O dominates this tmp-heavy suite; RUNNER_TEMP lives on - # the same disk as the workspace (D:). Measured via A/B experiment - # (run 28715118956): this redirect alone took the unit selection from - # 555s mean to 150s — parity with ubuntu (~161s). Defender is NOT the - # culprit: real-time protection is already off on current images - # (exclusions/disable measured as no-ops), and a ReFS dev drive - # (~296s) was dominated by this redirect. Details: work/win-io.md - if: runner.os == 'Windows' - shell: pwsh - run: | - "TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV - "TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --all-extras --dev && uv run playwright install chromium - - - name: Run unit tests (coverage on primary only) - # -p no:playwright: unit tests don't use the browser fixtures; skipping the - # plugin avoids a ~1s playwright import per xdist worker, which Windows pays - # per spawned worker. See work/xdist-import-cost.md. - run: uv run pytest -p no:playwright -m "not (tui or browser or benchmark)" ${{ env.COV }} - - - name: Run TUI tests (coverage append on primary only) - run: uv run pytest -m tui ${{ env.COV_APPEND }} - - - name: Run browser tests (coverage append on primary only) - run: uv run pytest -m browser ${{ env.COV_APPEND }} - - - name: Run benchmark tests with coverage append (primary only) - if: matrix.is-primary - env: - CLAUDE_CODE_LOG_DEBUG_TIMING: "1" - run: uv run pytest -m benchmark ${{ env.COV_APPEND }} -v - - - name: Upload coverage HTML report as artifact - uses: actions/upload-artifact@v4 - if: matrix.is-primary - with: - name: coverage-report-html - path: htmlcov/ - retention-days: 30 - - - name: Upload coverage XML as artifact - uses: actions/upload-artifact@v4 - if: matrix.is-primary - with: - name: coverage-report-xml - path: coverage.xml - retention-days: 30 - - - name: Coverage comment (PR only) - if: github.event_name == 'pull_request' && matrix.is-primary - uses: py-cov-action/python-coverage-comment-action@v3 - with: - GITHUB_TOKEN: ${{ github.token }} - MINIMUM_GREEN: 75 - MINIMUM_ORANGE: 60 - - - name: Run linting - run: uv run ruff check - - - name: Run formatting check - run: uv run ruff format --check - - - name: Run type checking - run: uv run pyright - - - name: Lint the templates' JavaScript (primary only) - if: matrix.is-primary - run: uv run --group js python scripts/lint_js.py + - uses: actions/checkout@v4 + with: + # Full history so test/test_commit_linkifier.py's + # TestIntegrationLocalRepo can resolve SHAs older than the + # current tip (`git branch -r --contains` reads local refs + # only and needs the commits in the object DB). + fetch-depth: 0 + + - name: Move TEMP to the workspace disk (Windows) + # On windows runners TMP/TEMP default to the C: OS disk, whose + # throttled I/O dominates this tmp-heavy suite; RUNNER_TEMP lives on + # the same disk as the workspace (D:). Measured via A/B experiment + # (run 28715118956): this redirect alone took the unit selection from + # 555s mean to 150s — parity with ubuntu (~161s). Defender is NOT the + # culprit: real-time protection is already off on current images + # (exclusions/disable measured as no-ops), and a ReFS dev drive + # (~296s) was dominated by this redirect. Details: work/win-io.md + if: runner.os == 'Windows' + shell: pwsh + run: | + "TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV + "TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev && uv run playwright install chromium + + - name: Run unit tests (coverage on primary only) + # -p no:playwright: unit tests don't use the browser fixtures; skipping the + # plugin avoids a ~1s playwright import per xdist worker, which Windows pays + # per spawned worker. See work/xdist-import-cost.md. + run: uv run pytest -p no:playwright -m "not (tui or browser or benchmark)" ${{ env.COV }} + + - name: Run TUI tests (coverage append on primary only) + run: uv run pytest -m tui ${{ env.COV_APPEND }} + + - name: Run browser tests (coverage append on primary only) + run: uv run pytest -m browser ${{ env.COV_APPEND }} + + - name: Run benchmark tests with coverage append (primary only) + if: matrix.is-primary + env: + CLAUDE_CODE_LOG_DEBUG_TIMING: "1" + run: uv run pytest -m benchmark ${{ env.COV_APPEND }} -v + + - name: Upload coverage HTML report as artifact + uses: actions/upload-artifact@v4 + if: matrix.is-primary + with: + name: coverage-report-html + path: htmlcov/ + retention-days: 30 + + - name: Upload coverage XML as artifact + uses: actions/upload-artifact@v4 + if: matrix.is-primary + with: + name: coverage-report-xml + path: coverage.xml + retention-days: 30 + + - name: Coverage comment (PR only) + if: github.event_name == 'pull_request' && matrix.is-primary + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + MINIMUM_GREEN: 75 + MINIMUM_ORANGE: 60 + + - name: Run linting + run: uv run ruff check + + - name: Run formatting check + run: uv run ruff format --check + + - name: Run type checking + run: uv run pyright + + - name: Lint the templates' JavaScript (primary only) + if: matrix.is-primary + run: uv run --group js python scripts/lint_js.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3e5ce90e..ada49d83 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,9 +2,9 @@ name: Docs on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] # Allow the deploy job to publish to GitHub Pages. permissions: diff --git a/.vscode/settings.json b/.vscode/settings.json index e05bc6a7..ce5288c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ ".claude-trace/**": true, "docs/cache/**": true, "docs/claude-code-log-transcript.html": true, - "test/test_data/cache/**": true, + "test/test_data/cache/**": true }, "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python-envs.defaultEnvManager": "ms-python.python:venv", diff --git a/claude_code_log/html/templates/components/filter_styles.css b/claude_code_log/html/templates/components/filter_styles.css index 43b2616c..a3504416 100644 --- a/claude_code_log/html/templates/components/filter_styles.css +++ b/claude_code_log/html/templates/components/filter_styles.css @@ -4,7 +4,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -68,7 +70,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -77,7 +78,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -169,4 +169,4 @@ .filter-messages.active { background-color: #fff3cd; -} \ No newline at end of file +} diff --git a/claude_code_log/html/templates/components/global_styles.css b/claude_code_log/html/templates/components/global_styles.css index 0d6ac498..e3327d92 100644 --- a/claude_code_log/html/templates/components/global_styles.css +++ b/claude_code_log/html/templates/components/global_styles.css @@ -118,8 +118,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -128,7 +130,19 @@ body { max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -149,7 +163,6 @@ code { line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -171,7 +184,9 @@ pre { background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -179,7 +194,9 @@ pre { } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -235,7 +252,9 @@ pre { align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -271,7 +290,7 @@ pre { .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -343,7 +362,7 @@ pre { right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -382,7 +401,7 @@ body.show-raw-user .user-content:not([data-user-view="md"]) .user-raw { .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -412,7 +431,7 @@ body.show-raw-user .user-content:not([data-user-view="md"]) .user-raw { * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); diff --git a/claude_code_log/html/templates/components/image_zoom.js b/claude_code_log/html/templates/components/image_zoom.js index 975564e3..ead1a86c 100644 --- a/claude_code_log/html/templates/components/image_zoom.js +++ b/claude_code_log/html/templates/components/image_zoom.js @@ -22,17 +22,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -48,7 +48,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -76,15 +76,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -98,8 +96,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -131,7 +129,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -153,19 +153,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { diff --git a/claude_code_log/html/templates/components/live_update.js b/claude_code_log/html/templates/components/live_update.js index 30f5af66..9560e69b 100644 --- a/claude_code_log/html/templates/components/live_update.js +++ b/claude_code_log/html/templates/components/live_update.js @@ -224,10 +224,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -282,8 +292,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -405,9 +414,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -438,7 +447,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -525,7 +534,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -560,7 +569,9 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); diff --git a/claude_code_log/html/templates/components/message_styles.css b/claude_code_log/html/templates/components/message_styles.css index 905cb09d..9da8c28d 100644 --- a/claude_code_log/html/templates/components/message_styles.css +++ b/claude_code_log/html/templates/components/message_styles.css @@ -16,7 +16,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -655,7 +657,7 @@ div.system-hook .header { } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -754,7 +756,8 @@ div.system-hook .header { background-color: var(--error-semi); } -.message.tool_use pre, .message.tool_result pre { +.message.tool_use pre, +.message.tool_result pre { font-size: 80%; } @@ -791,7 +794,9 @@ div.system-hook .header { border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -887,7 +892,7 @@ div.system-hook .header { overflow-wrap: break-word; } -.content>pre { +.content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -910,12 +915,12 @@ div.system-hook .header { } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -931,7 +936,9 @@ pre > code { padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -1131,8 +1138,8 @@ button > .tool-param-fold-glyph { list-style-type: disclosure-closed; } -.tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, -[data-state='expanded'] > .tool-param-fold-glyph { +.tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, +[data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -1221,7 +1228,9 @@ button > .tool-param-fold-glyph { padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -1236,7 +1245,9 @@ button > .tool-param-fold-glyph { margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -1340,39 +1351,87 @@ details summary { /* ANSI color classes */ /* Standard colors */ -.ansi-black { color: #000000; } -.ansi-red { color: #cd3131; } -.ansi-green { color: #0dbc79; } -.ansi-yellow { color: #e5e510; } -.ansi-blue { color: #2472c8; } -.ansi-magenta { color: #bc3fbc; } -.ansi-cyan { color: #11a8cd; } -.ansi-white { color: #e5e5e5; } +.ansi-black { + color: #000000; +} +.ansi-red { + color: #cd3131; +} +.ansi-green { + color: #0dbc79; +} +.ansi-yellow { + color: #e5e510; +} +.ansi-blue { + color: #2472c8; +} +.ansi-magenta { + color: #bc3fbc; +} +.ansi-cyan { + color: #11a8cd; +} +.ansi-white { + color: #e5e5e5; +} /* Bright colors */ -.ansi-bright-black { color: #666666; } -.ansi-bright-red { color: #f14c4c; } -.ansi-bright-green { color: #23d18b; } -.ansi-bright-yellow { color: #f5f543; } -.ansi-bright-blue { color: #3b8eea; } -.ansi-bright-magenta { color: #d670d6; } -.ansi-bright-cyan { color: #29b8db; } -.ansi-bright-white { color: #ffffff; } +.ansi-bright-black { + color: #666666; +} +.ansi-bright-red { + color: #f14c4c; +} +.ansi-bright-green { + color: #23d18b; +} +.ansi-bright-yellow { + color: #f5f543; +} +.ansi-bright-blue { + color: #3b8eea; +} +.ansi-bright-magenta { + color: #d670d6; +} +.ansi-bright-cyan { + color: #29b8db; +} +.ansi-bright-white { + color: #ffffff; +} /* Background colors */ -.ansi-bg-black { background-color: #000000; } -.ansi-bg-red { background-color: #cd3131; } -.ansi-bg-green { background-color: #0dbc79; } -.ansi-bg-yellow { background-color: #e5e510; } -.ansi-bg-blue { background-color: #2472c8; } -.ansi-bg-magenta { background-color: #bc3fbc; } -.ansi-bg-cyan { background-color: #11a8cd; } -.ansi-bg-white { background-color: #e5e5e5; } +.ansi-bg-black { + background-color: #000000; +} +.ansi-bg-red { + background-color: #cd3131; +} +.ansi-bg-green { + background-color: #0dbc79; +} +.ansi-bg-yellow { + background-color: #e5e510; +} +.ansi-bg-blue { + background-color: #2472c8; +} +.ansi-bg-magenta { + background-color: #bc3fbc; +} +.ansi-bg-cyan { + background-color: #11a8cd; +} +.ansi-bg-white { + background-color: #e5e5e5; +} /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -1384,20 +1443,44 @@ details summary { } /* Bright background colors */ -.ansi-bg-bright-black { background-color: #666666; } -.ansi-bg-bright-red { background-color: #f14c4c; } -.ansi-bg-bright-green { background-color: #23d18b; } -.ansi-bg-bright-yellow { background-color: #f5f543; } -.ansi-bg-bright-blue { background-color: #3b8eea; } -.ansi-bg-bright-magenta { background-color: #d670d6; } -.ansi-bg-bright-cyan { background-color: #29b8db; } -.ansi-bg-bright-white { background-color: #ffffff; } +.ansi-bg-bright-black { + background-color: #666666; +} +.ansi-bg-bright-red { + background-color: #f14c4c; +} +.ansi-bg-bright-green { + background-color: #23d18b; +} +.ansi-bg-bright-yellow { + background-color: #f5f543; +} +.ansi-bg-bright-blue { + background-color: #3b8eea; +} +.ansi-bg-bright-magenta { + background-color: #d670d6; +} +.ansi-bg-bright-cyan { + background-color: #29b8db; +} +.ansi-bg-bright-white { + background-color: #ffffff; +} /* Text styles */ -.ansi-bold { font-weight: bold; } -.ansi-dim { opacity: 0.7; } -.ansi-italic { font-style: italic; } -.ansi-underline { text-decoration: underline; } +.ansi-bold { + font-weight: bold; +} +.ansi-dim { + opacity: 0.7; +} +.ansi-italic { + font-style: italic; +} +.ansi-underline { + text-decoration: underline; +} /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -1911,7 +1994,7 @@ img.artifact-favicon { border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; diff --git a/claude_code_log/html/templates/components/minimal/components.css b/claude_code_log/html/templates/components/minimal/components.css index b53b04ed..bf8657f4 100644 --- a/claude_code_log/html/templates/components/minimal/components.css +++ b/claude_code_log/html/templates/components/minimal/components.css @@ -460,14 +460,23 @@ body.theme-minimal { display: flex; } -.theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last) > .header > .header-info > :not(.mn-role) { +.theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last) + > .header + > .header-info + > :not(.mn-role) { display: none; } /* …one gutter line down, below the call's role, which hangs into this row (layout.css; P7c — the pill used to cover it). */ @media (min-width: 641px) { - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { padding-top: 1.85em; } } @@ -493,7 +502,10 @@ body.theme-minimal { --pv: 2.8em; } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary { +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary { display: flex; flex-direction: column; align-items: stretch; @@ -506,7 +518,10 @@ body.theme-minimal { cursor: pointer; } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::-webkit-details-marker { +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::-webkit-details-marker { display: none; } @@ -527,7 +542,10 @@ body.theme-minimal { } /* The "+N lines" / "− less" line (mockup .more). */ -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after, +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after, .theme-minimal #transcript details.collapsible-code > summary > .line-count, .theme-minimal #transcript .mn-less { order: 2; @@ -542,11 +560,17 @@ body.theme-minimal { color: var(--muted); } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after { +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after { content: "+ more"; } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary[data-more]::after { +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary[data-more]::after { content: attr(data-more); } @@ -568,11 +592,17 @@ body.theme-minimal { display: none; } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] > summary::after { +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] + > summary::after { content: "− less"; } -.theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary:hover::after, +.theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary:hover::after, .theme-minimal #transcript details.collapsible-code > summary:hover > .line-count, .theme-minimal #transcript .mn-less:hover { color: var(--fg); @@ -677,7 +707,7 @@ body.theme-minimal { table), so the size is one length resolved on #transcript: a registered computes where it is declared and inherits as that value. */ @property --mn-ctl { - syntax: ''; + syntax: ""; inherits: true; initial-value: 10px; } @@ -1234,14 +1264,30 @@ body.theme-minimal { transform: none; } -.theme-minimal .filter-toggle[data-type="user"] { --rc: var(--user); } -.theme-minimal .filter-toggle[data-type="system"] { --rc: var(--sys); } -.theme-minimal .filter-toggle[data-type="assistant"] { --rc: var(--asst); } -.theme-minimal .filter-toggle[data-type="thinking"] { --rc: var(--muted); } -.theme-minimal .filter-toggle[data-type="tool"] { --rc: var(--tool); } -.theme-minimal .filter-toggle[data-type="sidechain"] { --rc: var(--ring); } -.theme-minimal .filter-toggle[data-type="memory"] { --rc: var(--asst); } -.theme-minimal .filter-toggle[data-type="image"] { --rc: var(--note); } +.theme-minimal .filter-toggle[data-type="user"] { + --rc: var(--user); +} +.theme-minimal .filter-toggle[data-type="system"] { + --rc: var(--sys); +} +.theme-minimal .filter-toggle[data-type="assistant"] { + --rc: var(--asst); +} +.theme-minimal .filter-toggle[data-type="thinking"] { + --rc: var(--muted); +} +.theme-minimal .filter-toggle[data-type="tool"] { + --rc: var(--tool); +} +.theme-minimal .filter-toggle[data-type="sidechain"] { + --rc: var(--ring); +} +.theme-minimal .filter-toggle[data-type="memory"] { + --rc: var(--asst); +} +.theme-minimal .filter-toggle[data-type="image"] { + --rc: var(--note); +} .theme-minimal .filter-action-btn, .theme-minimal .search-nav-btn { @@ -1365,30 +1411,56 @@ body.theme-minimal { font-size: 0.94em; } -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary { +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary { flex-direction: row; align-items: flex-end; column-gap: 10px; } -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .preview-content { +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .preview-content { flex: 1 1 0; min-width: 0; } -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary::after, -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .line-count { +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary::after, +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .line-count { flex: none; align-self: flex-end; } -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(p, ul, ol, pre, blockquote, table) { +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(p, ul, ol, pre, blockquote, table) { margin: 0; } /* A heading at the top of an agent's answer (`## Summary`) reads as a bold line of the preview, not a title twice the size that fills the clip. */ -.theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(h1, h2, h3, h4, h5, h6) { +.theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(h1, h2, h3, h4, h5, h6) { margin: 0; font-size: 1em; line-height: inherit; @@ -1398,7 +1470,12 @@ body.theme-minimal { metadata (agent id, worktree, branch, usage) and an async notification's task fields (id, status, tokens, tool uses, duration) as one dim wrapped line of key value pairs instead of a card of rows. */ -.theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) { +.theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) { display: flex; flex-wrap: wrap; align-items: baseline; @@ -1410,10 +1487,23 @@ body.theme-minimal { color: var(--muted); } -.theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dt { +.theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dt { font-size: 1em; } -.theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dd + dt { +.theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dd + + dt { margin-left: 8px; } diff --git a/claude_code_log/html/templates/components/minimal/dag.css b/claude_code_log/html/templates/components/minimal/dag.css index 77da1150..a7beec0e 100644 --- a/claude_code_log/html/templates/components/minimal/dag.css +++ b/claude_code_log/html/templates/components/minimal/dag.css @@ -14,18 +14,40 @@ carry at least two classes (layout.css "Specificity, two tiers"). */ /* Lane colours (rail slot → colour; forks are always --lF). */ -.theme-minimal .dag-lc-a { --lc: var(--lA); } -.theme-minimal .dag-lc-b { --lc: var(--lB); } -.theme-minimal .dag-lc-f { --lc: var(--lF); } -.theme-minimal .dag-lc-s { --lc: var(--sys); } -.theme-minimal .dag-lc-p { --lc: var(--asst); } - -.theme-minimal .dag-s1 { --dag-slot: 1; } -.theme-minimal .dag-s2 { --dag-slot: 2; } -.theme-minimal .dag-s3 { --dag-slot: 3; } -.theme-minimal .dag-s4 { --dag-slot: 4; } -.theme-minimal .dag-s5 { --dag-slot: 5; } -.theme-minimal .dag-s6 { --dag-slot: 6; } +.theme-minimal .dag-lc-a { + --lc: var(--lA); +} +.theme-minimal .dag-lc-b { + --lc: var(--lB); +} +.theme-minimal .dag-lc-f { + --lc: var(--lF); +} +.theme-minimal .dag-lc-s { + --lc: var(--sys); +} +.theme-minimal .dag-lc-p { + --lc: var(--asst); +} + +.theme-minimal .dag-s1 { + --dag-slot: 1; +} +.theme-minimal .dag-s2 { + --dag-slot: 2; +} +.theme-minimal .dag-s3 { + --dag-slot: 3; +} +.theme-minimal .dag-s4 { + --dag-slot: 4; +} +.theme-minimal .dag-s5 { + --dag-slot: 5; +} +.theme-minimal .dag-s6 { + --dag-slot: 6; +} /* ------------------------------------------------------------- stage */ @@ -94,7 +116,9 @@ } @keyframes dag-run { - to { stroke-dashoffset: -7; } + to { + stroke-dashoffset: -7; + } } /* A column lane's pointers (`col-in`, `col-out`): a short curve towards the @@ -113,11 +137,21 @@ stop-opacity: 0; } -.theme-minimal #dag-rail path.dag-fade.dag-lc-a { stroke: url(#dag-fade-a); } -.theme-minimal #dag-rail path.dag-fade.dag-lc-b { stroke: url(#dag-fade-b); } -.theme-minimal #dag-rail path.dag-fade.dag-lc-f { stroke: url(#dag-fade-f); } -.theme-minimal #dag-rail path.dag-fade.dag-lc-s { stroke: url(#dag-fade-s); } -.theme-minimal #dag-rail path.dag-fade.dag-lc-p { stroke: url(#dag-fade-p); } +.theme-minimal #dag-rail path.dag-fade.dag-lc-a { + stroke: url(#dag-fade-a); +} +.theme-minimal #dag-rail path.dag-fade.dag-lc-b { + stroke: url(#dag-fade-b); +} +.theme-minimal #dag-rail path.dag-fade.dag-lc-f { + stroke: url(#dag-fade-f); +} +.theme-minimal #dag-rail path.dag-fade.dag-lc-s { + stroke: url(#dag-fade-s); +} +.theme-minimal #dag-rail path.dag-fade.dag-lc-p { + stroke: url(#dag-fade-p); +} @media (prefers-reduced-motion: reduce) { .theme-minimal #dag-rail path.dag-running.dag-dash, @@ -175,9 +209,23 @@ /* Workflow phase and agent cards, and the agents' transcripts (lanes), are rows of the grid too: the nested workflow gutter (components.css) only applies without the engine, or inside a block. */ -.theme-minimal .mn-stage.dag-on #transcript .children:has(> .message-node > .message.workflow_phase) > .message-node:not(.dag-block *), -.theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_phase) > .children > .message-node:not(.dag-block *), -.theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_agent) > .children > .message-node:not(.dag-block *) { +.theme-minimal + .mn-stage.dag-on + #transcript + .children:has(> .message-node > .message.workflow_phase) + > .message-node:not(.dag-block *), +.theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_phase) + > .children + > .message-node:not(.dag-block *), +.theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_agent) + > .children + > .message-node:not(.dag-block *) { --gut: var(--dag-gut); --rail: var(--dag-rail); } @@ -213,10 +261,12 @@ --dag-indent: calc(var(--dag-ind0) + var(--dag-ind-step) * var(--dag-slot, 1)); --dag-c3: calc(var(--gut) + var(--rail)); grid-template-columns: var(--gut) calc(var(--rail) + var(--dag-indent)) minmax(0, 1fr); - background: linear-gradient(90deg, + background: linear-gradient( + 90deg, transparent var(--dag-c3), color-mix(in srgb, var(--lc) 9%, transparent) var(--dag-c3), - transparent 75%); + transparent 75% + ); } .theme-minimal .mn-stage.dag-on #transcript .message.dag-in > .header > .header-info::after { @@ -241,17 +291,39 @@ the call, then the tag (and an error pill) from the result, which grows its row if it must rather than overlap the next. Wide layout only. */ @media (min-width: 641px) { - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.pair_first:not(.dag-split) > .header > .header-info::after { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.pair_first:not(.dag-split) + > .header + > .header-info::after { content: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { display: flex; padding-top: 1.85em; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) > .header > .header-info > *, - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info > :not(.mn-role) { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) + > .header + > .header-info + > *, + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info + > :not(.mn-role) { display: none; } } @@ -451,7 +523,9 @@ } @keyframes dag-pulse { - 50% { opacity: 0.25; } + 50% { + opacity: 0.25; + } } .theme-minimal #transcript .mn-bmini { @@ -521,7 +595,7 @@ display: none; } -.theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded='false']) { +.theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded="false"]) { padding-bottom: 3px; } @@ -585,7 +659,8 @@ body.theme-minimal.dag-wide { min-width: calc(var(--dag-min, 0px) + 32px); } -body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { +body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { position: sticky; left: 16px; max-width: calc(var(--dag-view, 100vw) - 32px); @@ -626,7 +701,9 @@ body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline overflow: hidden; white-space: nowrap; border-bottom: 1px solid var(--rule); - background: linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), var(--bg); + background: + linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), + var(--bg); } /* A column's head: the name and two compact actions (`⇤`, `−`; their @@ -855,7 +932,8 @@ body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline min-width: calc(var(--dag-min, 0px) + 20px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { left: 10px; max-width: calc(var(--dag-view, 100vw) - 20px); } diff --git a/claude_code_log/html/templates/components/minimal/layout.css b/claude_code_log/html/templates/components/minimal/layout.css index a0391eb7..5ce44e50 100644 --- a/claude_code_log/html/templates/components/minimal/layout.css +++ b/claude_code_log/html/templates/components/minimal/layout.css @@ -300,7 +300,10 @@ color: var(--fg); } -.theme-minimal #transcript .mn-title :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { +.theme-minimal + #transcript + .mn-title + :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { font: inherit; font-weight: 400; color: var(--muted); @@ -467,13 +470,29 @@ file, say — or the output) starts right under it: no top margin on it, nor on the code box at the head of a preview inside it. */ .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child, -.theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child > details:first-child > summary > .preview-content > .highlight:first-child { +.theme-minimal + #transcript + .message:is(.tool_use, .pair_middle, .pair_last) + > .content + > :first-child + > details:first-child + > summary + > .preview-content + > .highlight:first-child { margin-top: 0; } /* Turns: one rule above each main-lane prompt, except a session's first. A slash command's output (the pair's last half) belongs to its turn. */ -.theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last) { +.theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last) { margin-top: 4px; padding-top: 7px; border-top: 1px solid var(--rule2); @@ -502,8 +521,21 @@ .theme-minimal #transcript .message:is(.pair_middle, .pair_last):not(.dag-split)::after, .theme-minimal #transcript .message.dag-cfirst::after, -.theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node:first-child > .message::after, -.theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last)::after { +.theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node:first-child + > .message::after, +.theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last)::after { content: none; } @@ -557,7 +589,11 @@ transition: color 0.12s; } -.theme-minimal #transcript .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) > .fold-bar > .fold-bar-section, +.theme-minimal + #transcript + .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) + > .fold-bar + > .fold-bar-section, .theme-minimal #transcript .message:hover > .fold-bar > .fold-bar-section, .theme-minimal #transcript .message > .fold-bar:focus-within > .fold-bar-section { color: var(--muted); @@ -748,7 +784,9 @@ 2px line; the classic per-depth ring colours still apply. Same selector shape as the classic rule, one class more specific. */ .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), -.theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { +.theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin: 4px 0 4px calc(var(--gut) + var(--rail)); padding-left: 8px; border-left-width: 2px; @@ -756,7 +794,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), -.theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { +.theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.agent-deep) { margin-left: 12px; } @@ -832,7 +872,12 @@ } /* User prompt: a warm tint (mockup .turn > .k-user .main). */ -.theme-minimal #transcript .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not(.sidechain) > .content { +.theme-minimal + #transcript + .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not( + .sidechain + ) + > .content { margin-left: 4px; padding: 2px 8px; border-radius: 4px; @@ -1049,7 +1094,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin-left: 16px; padding-left: 6px; } diff --git a/claude_code_log/html/templates/components/minimal/minimal.js b/claude_code_log/html/templates/components/minimal/minimal.js index 7a4d1960..44bf2028 100644 --- a/claude_code_log/html/templates/components/minimal/minimal.js +++ b/claude_code_log/html/templates/components/minimal/minimal.js @@ -1,422 +1,442 @@ - // Minimal theme (--theme minimal) runtime: colour-scheme toggle, - // gutter times, session-summary more/less, toolbar offsets, the - // overflow menu, collapse previews ("+N lines" / "− less") and the - // fold-depth control (Prompts / Steps / All). See - // work/minimal-theme-dag.md § 1.4–1.5 and § 3.4. - // Everything bound here is either on the toolbar or delegated on - // `document`, so a live update's #transcript swap leaves it intact; - // only per-card decoration (gutter times, session summaries, - // collapse labels, fold depth for new cards) registers with the - // rehydrate hook. - (function () { - 'use strict'; +// Minimal theme (--theme minimal) runtime: colour-scheme toggle, +// gutter times, session-summary more/less, toolbar offsets, the +// overflow menu, collapse previews ("+N lines" / "− less") and the +// fold-depth control (Prompts / Steps / All). See +// work/minimal-theme-dag.md § 1.4–1.5 and § 3.4. +// Everything bound here is either on the toolbar or delegated on +// `document`, so a live update's #transcript swap leaves it intact; +// only per-card decoration (gutter times, session summaries, +// collapse labels, fold depth for new cards) registers with the +// rehydrate hook. +(function () { + 'use strict'; {% include 'components/minimal/scheme.js' %} - // ---- gutter time: short local HH:MM:SS ------------------------- - // The server renders the UTC time-of-day (correct without JS); - // this localises it, like timezone_converter.js does for the full - // timestamp, which stays in the DOM and becomes the tooltip. - const timeFormatter = new Intl.DateTimeFormat(undefined, { - hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false - }); - const fullFormatter = new Intl.DateTimeFormat(undefined, { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false, timeZoneName: 'short' - }); - function localiseGutterTimes(scope) { - (scope || document).querySelectorAll('.mn-time').forEach(function (el) { - const stamp = el.parentElement && - el.parentElement.querySelector('.timestamp[data-timestamp]'); - if (!stamp) return; - const date = new Date(stamp.getAttribute('data-timestamp')); - if (isNaN(date.getTime())) return; - el.textContent = timeFormatter.format(date); - const duration = stamp.getAttribute('data-duration'); - el.title = fullFormatter.format(date) + (duration ? ' · ' + duration : ''); - }); - } - localiseGutterTimes(document); - if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(localiseGutterTimes); + // ---- gutter time: short local HH:MM:SS ------------------------- + // The server renders the UTC time-of-day (correct without JS); + // this localises it, like timezone_converter.js does for the full + // timestamp, which stays in the DOM and becomes the tooltip. + const timeFormatter = new Intl.DateTimeFormat(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + const fullFormatter = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short' + }); + function localiseGutterTimes(scope) { + (scope || document).querySelectorAll('.mn-time').forEach(function (el) { + const stamp = el.parentElement && el.parentElement.querySelector('.timestamp[data-timestamp]'); + if (!stamp) return; + const date = new Date(stamp.getAttribute('data-timestamp')); + if (isNaN(date.getTime())) return; + el.textContent = timeFormatter.format(date); + const duration = stamp.getAttribute('data-duration'); + el.title = fullFormatter.format(date) + (duration ? ' · ' + duration : ''); + }); + } + localiseGutterTimes(document); + if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(localiseGutterTimes); - // ---- session summary: two lines, more / less ------------------- - // CSS clamps a session header's summary to two lines; when that - // hides text, a small toggle (placed on the meta line after it) - // expands it. The full text is also the summary's `title`. A live - // update can replace a header, so this runs on rehydrate too. - function decorateSessionSummaries(scope) { - (scope || document).querySelectorAll('.mn-sh-sum').forEach(function (sum) { - const next = sum.nextElementSibling; - const toggle = next && next.classList.contains('mn-sh-more') ? next : null; - const open = sum.classList.contains('mn-open'); - const clamped = open || sum.scrollHeight > sum.clientHeight + 1; - if (!clamped) { - if (toggle) toggle.remove(); - return; - } - if (toggle) return; - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'mn-sh-more'; - btn.textContent = open ? '− less' : '+ more'; - btn.setAttribute('aria-expanded', open ? 'true' : 'false'); - sum.after(btn); - }); + // ---- session summary: two lines, more / less ------------------- + // CSS clamps a session header's summary to two lines; when that + // hides text, a small toggle (placed on the meta line after it) + // expands it. The full text is also the summary's `title`. A live + // update can replace a header, so this runs on rehydrate too. + function decorateSessionSummaries(scope) { + (scope || document).querySelectorAll('.mn-sh-sum').forEach(function (sum) { + const next = sum.nextElementSibling; + const toggle = next && next.classList.contains('mn-sh-more') ? next : null; + const open = sum.classList.contains('mn-open'); + const clamped = open || sum.scrollHeight > sum.clientHeight + 1; + if (!clamped) { + if (toggle) toggle.remove(); + return; } - document.addEventListener('click', function (event) { - const btn = event.target.closest('.mn-sh-more'); - if (!btn) return; - const sum = btn.previousElementSibling; - if (!sum || !sum.classList.contains('mn-sh-sum')) return; - const open = sum.classList.toggle('mn-open'); - btn.textContent = open ? '− less' : '+ more'; - btn.setAttribute('aria-expanded', open ? 'true' : 'false'); - }); - // Measuring a clamp forces a layout. At parse time that would be a - // whole extra layout of a large page (≈ 0.6s on a 5MB one, P8), - // thrown away when the DAG engine turns the grid on; in the first - // frame after DOMContentLoaded it reuses the layout the page needs - // anyway. (Not the first frame: during a long parse the stage is - // not rendered yet — theme_init.js — and nothing would measure.) - function decorateFirstFrame() { - requestAnimationFrame(function () { decorateSessionSummaries(document); }); - } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', decorateFirstFrame); - } else { - decorateFirstFrame(); - } - if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(decorateSessionSummaries); - window.addEventListener('resize', function () { decorateSessionSummaries(document); }); + if (toggle) return; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'mn-sh-more'; + btn.textContent = open ? '− less' : '+ more'; + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + sum.after(btn); + }); + } + document.addEventListener('click', function (event) { + const btn = event.target.closest('.mn-sh-more'); + if (!btn) return; + const sum = btn.previousElementSibling; + if (!sum || !sum.classList.contains('mn-sh-sum')) return; + const open = sum.classList.toggle('mn-open'); + btn.textContent = open ? '− less' : '+ more'; + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + }); + // Measuring a clamp forces a layout. At parse time that would be a + // whole extra layout of a large page (≈ 0.6s on a 5MB one, P8), + // thrown away when the DAG engine turns the grid on; in the first + // frame after DOMContentLoaded it reuses the layout the page needs + // anyway. (Not the first frame: during a long parse the stage is + // not rendered yet — theme_init.js — and nothing would measure.) + function decorateFirstFrame() { + requestAnimationFrame(function () { + decorateSessionSummaries(document); + }); + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', decorateFirstFrame); + } else { + decorateFirstFrame(); + } + if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(decorateSessionSummaries); + window.addEventListener('resize', function () { + decorateSessionSummaries(document); + }); - // ---- truncated role labels: the full label on hover ------------ - // Only the rendered width tells whether a gutter label is - // ellipsized, so its tooltip is set here, on first hover. A label - // whose hidden title already gave it one (role_tooltip) keeps it. - document.addEventListener('mouseover', function (event) { - const role = event.target.closest && event.target.closest('#transcript .mn-role'); - if (!role || role.hasAttribute('title')) return; - if (role.scrollWidth > role.clientWidth) role.title = role.textContent.trim(); - }); + // ---- truncated role labels: the full label on hover ------------ + // Only the rendered width tells whether a gutter label is + // ellipsized, so its tooltip is set here, on first hover. A label + // whose hidden title already gave it one (role_tooltip) keeps it. + document.addEventListener('mouseover', function (event) { + const role = event.target.closest && event.target.closest('#transcript .mn-role'); + if (!role || role.hasAttribute('title')) return; + if (role.scrollWidth > role.clientWidth) role.title = role.textContent.trim(); + }); - // ---- sticky offsets ------------------------------------------ - // The search & filter panel and the timeline are sticky too; they - // sit under the toolbar, whose height changes when it wraps. - const toolbar = document.querySelector('.mn-toolbar'); - const filterPanel = document.querySelector('.filter-toolbar'); - // Changing a custom property on the root restyles every element - // of the page (≈ 0.15–0.3s on a 5MB one, P8), so write only a - // real change — tokens.css declares the usual values — and - // measure both heights before writing either. - const offsets = {}; - function setProp(name, value) { - if (!(name in offsets)) offsets[name] = getComputedStyle(root).getPropertyValue(name).trim(); - if (offsets[name] === value) return; - offsets[name] = value; - root.style.setProperty(name, value); - } - function updateOffsets() { - const barHeight = toolbar ? toolbar.offsetHeight : 0; - const filterHeight = filterPanel && getComputedStyle(filterPanel).display !== 'none' - ? filterPanel.offsetHeight : 0; - if (toolbar) setProp('--mn-bar-h', barHeight + 'px'); - setProp('--mn-filter-h', filterHeight + 'px'); - } - // Not measured at parse time either: reading a height there forces - // a layout of the whole page. A ResizeObserver reports every - // observed element once after the first layout, before the first - // paint, so the offsets are right when the page first shows. - if (window.ResizeObserver) { - const observer = new ResizeObserver(updateOffsets); - if (toolbar) observer.observe(toolbar); - if (filterPanel) observer.observe(filterPanel); - } else { - updateOffsets(); - window.addEventListener('resize', updateOffsets); - } + // ---- sticky offsets ------------------------------------------ + // The search & filter panel and the timeline are sticky too; they + // sit under the toolbar, whose height changes when it wraps. + const toolbar = document.querySelector('.mn-toolbar'); + const filterPanel = document.querySelector('.filter-toolbar'); + // Changing a custom property on the root restyles every element + // of the page (≈ 0.15–0.3s on a 5MB one, P8), so write only a + // real change — tokens.css declares the usual values — and + // measure both heights before writing either. + const offsets = {}; + function setProp(name, value) { + if (!(name in offsets)) offsets[name] = getComputedStyle(root).getPropertyValue(name).trim(); + if (offsets[name] === value) return; + offsets[name] = value; + root.style.setProperty(name, value); + } + function updateOffsets() { + const barHeight = toolbar ? toolbar.offsetHeight : 0; + const filterHeight = + filterPanel && getComputedStyle(filterPanel).display !== 'none' ? filterPanel.offsetHeight : 0; + if (toolbar) setProp('--mn-bar-h', barHeight + 'px'); + setProp('--mn-filter-h', filterHeight + 'px'); + } + // Not measured at parse time either: reading a height there forces + // a layout of the whole page. A ResizeObserver reports every + // observed element once after the first layout, before the first + // paint, so the offsets are right when the page first shows. + if (window.ResizeObserver) { + const observer = new ResizeObserver(updateOffsets); + if (toolbar) observer.observe(toolbar); + if (filterPanel) observer.observe(filterPanel); + } else { + updateOffsets(); + window.addEventListener('resize', updateOffsets); + } - // The resume button's copy toast is fixed-position; drop it just - // below the toolbar wherever the toolbar is when it's clicked - // (it moves from under the header to the top as the page scrolls). - document.addEventListener('click', function (event) { - if (!toolbar || !event.target.closest('#resumeSession')) return; - root.style.setProperty('--mn-toast-top', - Math.max(0, toolbar.getBoundingClientRect().bottom) + 8 + 'px'); - }); + // The resume button's copy toast is fixed-position; drop it just + // below the toolbar wherever the toolbar is when it's clicked + // (it moves from under the header to the top as the page scrolls). + document.addEventListener('click', function (event) { + if (!toolbar || !event.target.closest('#resumeSession')) return; + root.style.setProperty('--mn-toast-top', Math.max(0, toolbar.getBoundingClientRect().bottom) + 8 + 'px'); + }); - // ---- collapse previews: "+N lines" / "− less" ----------------- - // The formatters'
already carry a preview in their - // ; CSS clips it (with a fade) while closed and shows - // the summary as a "− less" line while open. The label comes - // from the `.line-count` the formatter wrote, else from the - // body's text (\n count) or a params table's row count, and is - // written to `data-more` on the — CSS renders it with - // `attr()`, which only reads the pseudo-element's own element. - // Generated content stays out of textContent, so search and the - // timeline never index the labels. Long bodies also get a - // trailing "− less" button (label in CSS too). - const COLLAPSIBLES = 'details.collapsible-code, details.collapsible-details, details.tool-param-collapsible'; - const LONG_BODY = 12; + // ---- collapse previews: "+N lines" / "− less" ----------------- + // The formatters'
already carry a preview in their + // ; CSS clips it (with a fade) while closed and shows + // the summary as a "− less" line while open. The label comes + // from the `.line-count` the formatter wrote, else from the + // body's text (\n count) or a params table's row count, and is + // written to `data-more` on the — CSS renders it with + // `attr()`, which only reads the pseudo-element's own element. + // Generated content stays out of textContent, so search and the + // timeline never index the labels. Long bodies also get a + // trailing "− less" button (label in CSS too). + const COLLAPSIBLES = 'details.collapsible-code, details.collapsible-details, details.tool-param-collapsible'; + const LONG_BODY = 12; - function countLines(text) { - const trimmed = text.trim(); - return trimmed ? trimmed.split('\n').length : 0; - } - function measureBody(details, summary) { - const lineCount = summary.querySelector(':scope > .line-count'); - const counted = lineCount ? parseInt(lineCount.textContent, 10) : NaN; - if (counted > 0) return { n: counted, unit: 'line' }; - const table = details.querySelector(':scope > table'); - if (table) { - const rows = table.querySelectorAll(':scope > tbody > tr, :scope > tr').length; - return { n: rows, unit: 'item' }; - } - let text = ''; - Array.from(details.children).forEach(function (child) { - if (child === summary || child.classList.contains('mn-less') - || child.classList.contains('tool-param-fold-controls')) return; - text += child.textContent; - }); - return { n: countLines(text), unit: 'line' }; + function countLines(text) { + const trimmed = text.trim(); + return trimmed ? trimmed.split('\n').length : 0; + } + function measureBody(details, summary) { + const lineCount = summary.querySelector(':scope > .line-count'); + const counted = lineCount ? parseInt(lineCount.textContent, 10) : NaN; + if (counted > 0) return { n: counted, unit: 'line' }; + const table = details.querySelector(':scope > table'); + if (table) { + const rows = table.querySelectorAll(':scope > tbody > tr, :scope > tr').length; + return { n: rows, unit: 'item' }; + } + let text = ''; + Array.from(details.children).forEach(function (child) { + if ( + child === summary || + child.classList.contains('mn-less') || + child.classList.contains('tool-param-fold-controls') + ) + return; + text += child.textContent; + }); + return { n: countLines(text), unit: 'line' }; + } + function moreLabel(size) { + if (size.unit === 'item' && size.n > 0) { + return '+ ' + size.n + (size.n === 1 ? ' item' : ' items'); + } + // One or two lines are (mostly) in the preview already: what + // is hidden is the rest of a long line, not more lines. + if (size.n >= 3) return '+ ' + size.n + ' lines'; + return '+ more'; + } + function decorateCollapsibles(scope) { + const found = Array.from((scope || document).querySelectorAll(COLLAPSIBLES)); + if (scope && scope.matches && scope.matches(COLLAPSIBLES)) found.unshift(scope); + found.forEach(function (details) { + const summary = details.querySelector(':scope > summary'); + if (!summary || summary.hasAttribute('data-more')) return; + const size = measureBody(details, summary); + summary.setAttribute('data-more', moreLabel(size)); + if (size.n >= LONG_BODY && !details.querySelector(':scope > .mn-less')) { + const less = document.createElement('button'); + less.type = 'button'; + less.className = 'mn-less'; + less.setAttribute('aria-label', 'Show less'); + details.appendChild(less); } - function moreLabel(size) { - if (size.unit === 'item' && size.n > 0) { - return '+ ' + size.n + (size.n === 1 ? ' item' : ' items'); - } - // One or two lines are (mostly) in the preview already: what - // is hidden is the rest of a long line, not more lines. - if (size.n >= 3) return '+ ' + size.n + ' lines'; - return '+ more'; - } - function decorateCollapsibles(scope) { - const found = Array.from((scope || document).querySelectorAll(COLLAPSIBLES)); - if (scope && scope.matches && scope.matches(COLLAPSIBLES)) found.unshift(scope); - found.forEach(function (details) { - const summary = details.querySelector(':scope > summary'); - if (!summary || summary.hasAttribute('data-more')) return; - const size = measureBody(details, summary); - summary.setAttribute('data-more', moreLabel(size)); - if (size.n >= LONG_BODY && !details.querySelector(':scope > .mn-less')) { - const less = document.createElement('button'); - less.type = 'button'; - less.className = 'mn-less'; - less.setAttribute('aria-label', 'Show less'); - details.appendChild(less); - } - }); - } - // A trailing "− less" closes its block and, when the block's top - // has scrolled away under the sticky toolbar, brings it back. - document.addEventListener('click', function (event) { - const btn = event.target.closest('.mn-less'); - if (!btn) return; - const details = btn.parentElement; - if (!details || details.tagName !== 'DETAILS') return; - details.open = false; - const bar = document.querySelector('.mn-toolbar'); - const barBottom = bar ? Math.max(0, bar.getBoundingClientRect().bottom) : 0; - const top = details.getBoundingClientRect().top; - if (top < barBottom) window.scrollBy(0, top - barBottom - 8); - }); - decorateCollapsibles(document); - if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(decorateCollapsibles); + }); + } + // A trailing "− less" closes its block and, when the block's top + // has scrolled away under the sticky toolbar, brings it back. + document.addEventListener('click', function (event) { + const btn = event.target.closest('.mn-less'); + if (!btn) return; + const details = btn.parentElement; + if (!details || details.tagName !== 'DETAILS') return; + details.open = false; + const bar = document.querySelector('.mn-toolbar'); + const barBottom = bar ? Math.max(0, bar.getBoundingClientRect().bottom) : 0; + const top = details.getBoundingClientRect().top; + if (top < barBottom) window.scrollBy(0, top - barBottom - 8); + }); + decorateCollapsibles(document); + if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(decorateCollapsibles); - // ---- fold depth: Prompts / Steps / All ------------------------- - // Maps onto the classic fold state machine (transcript.html, - // dev-docs/message-hierarchy.md) through the hooks the minimal - // template branch exports: every card decides only its OWN - // children container, then each fold bar is re-synced from what - // the containers show. - // prompts — session and branch headers open, everything else - // folded: each turn is its prompt + fold-bar line; - // steps — everything open except sub-agent subtrees (cards - // that open a deeper agent's transcript); long blocks - // as previews (every collapsible closed); - // all — everything open, every collapsible open. - // A fold-bar click or the 📋 open/close-all overrides the depth - // locally: the segment then shows no choice until the next one. - // Opening one preview doesn't (reading a block isn't a new depth). - const DEPTH_KEY = 'claude-code-log:fold-depth'; - const DEPTHS = ['prompts', 'steps', 'all']; - function readDepth() { - try { - const stored = window.localStorage.getItem(DEPTH_KEY); - return DEPTHS.indexOf(stored) >= 0 ? stored : 'steps'; - } catch (err) { - return 'steps'; - } - } - function writeDepth(value) { - try { - window.localStorage.setItem(DEPTH_KEY, value); - } catch (err) { /* storage unavailable: depth lasts this page */ } - } - let depth = readDepth(); + // ---- fold depth: Prompts / Steps / All ------------------------- + // Maps onto the classic fold state machine (transcript.html, + // dev-docs/message-hierarchy.md) through the hooks the minimal + // template branch exports: every card decides only its OWN + // children container, then each fold bar is re-synced from what + // the containers show. + // prompts — session and branch headers open, everything else + // folded: each turn is its prompt + fold-bar line; + // steps — everything open except sub-agent subtrees (cards + // that open a deeper agent's transcript); long blocks + // as previews (every collapsible closed); + // all — everything open, every collapsible open. + // A fold-bar click or the 📋 open/close-all overrides the depth + // locally: the segment then shows no choice until the next one. + // Opening one preview doesn't (reading a block isn't a new depth). + const DEPTH_KEY = 'claude-code-log:fold-depth'; + const DEPTHS = ['prompts', 'steps', 'all']; + function readDepth() { + try { + const stored = window.localStorage.getItem(DEPTH_KEY); + return DEPTHS.indexOf(stored) >= 0 ? stored : 'steps'; + } catch (err) { + return 'steps'; + } + } + function writeDepth(value) { + try { + window.localStorage.setItem(DEPTH_KEY, value); + } catch (err) { + /* storage unavailable: depth lasts this page */ + } + } + let depth = readDepth(); - function markDepth(active) { - document.querySelectorAll('[data-mn-depth]').forEach(function (btn) { - const on = active && btn.getAttribute('data-mn-depth') === depth; - btn.classList.toggle('on', on); - btn.setAttribute('aria-pressed', on ? 'true' : 'false'); - }); - } - function childrenOf(card) { - const node = card.parentElement; - return node && node.classList.contains('message-node') - ? node.querySelector(':scope > .children') : null; - } - function agentDepth(card) { - const match = /(?:^|\s)agent-depth-(\d+)(?:\s|$)/.exec(card.className); - if (match) return parseInt(match[1], 10); - return card.classList.contains('sidechain') ? 1 : 0; - } - // A card whose children start a deeper agent's transcript: the - // spawning tool_result (or a workflow agent card). Steps keeps - // these folded; for a sub-agent LANE the DAG engine - // (minimal_dag.js) then shows or hides the container by the - // Branches mode regardless (dag-entry / dag-hidden; a workflow - // agent's transcript is a lane too), so only teammate threads - // follow the depth here. - function opensBranch(card, children) { - const own = agentDepth(card); - return Array.from(children.querySelectorAll(':scope > .message-node > .message.sidechain')) - .some(function (kid) { return agentDepth(kid) > own; }); - } - function wantsOpen(card, children, value) { - if (value === 'all') return true; - if (card.classList.contains('session-header')) return true; - if (value === 'prompts') return false; - return !opensBranch(card, children); - } - const seenContainers = new WeakSet(); - const depthApplied = new WeakSet(); - function applyDepthTo(cards, value) { - const fold = window.claudeLogApplyFoldState; - const sync = window.claudeLogSyncFoldBar; - if (!fold || !sync) return; - const touched = []; - cards.forEach(function (card) { - depthApplied.add(card); - const children = childrenOf(card); - if (!children) return; - seenContainers.add(children); - if (wantsOpen(card, children, value)) children.style.display = ''; - else fold(card, 'folded'); - touched.push(card); - }); - touched.forEach(sync); - } - function setCollapsibles(scope, open) { - scope.querySelectorAll(COLLAPSIBLES).forEach(function (details) { - if (details.open !== open) details.open = open; - }); + function markDepth(active) { + document.querySelectorAll('[data-mn-depth]').forEach(function (btn) { + const on = active && btn.getAttribute('data-mn-depth') === depth; + btn.classList.toggle('on', on); + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + }); + } + function childrenOf(card) { + const node = card.parentElement; + return node && node.classList.contains('message-node') ? node.querySelector(':scope > .children') : null; + } + function agentDepth(card) { + const match = /(?:^|\s)agent-depth-(\d+)(?:\s|$)/.exec(card.className); + if (match) return parseInt(match[1], 10); + return card.classList.contains('sidechain') ? 1 : 0; + } + // A card whose children start a deeper agent's transcript: the + // spawning tool_result (or a workflow agent card). Steps keeps + // these folded; for a sub-agent LANE the DAG engine + // (minimal_dag.js) then shows or hides the container by the + // Branches mode regardless (dag-entry / dag-hidden; a workflow + // agent's transcript is a lane too), so only teammate threads + // follow the depth here. + function opensBranch(card, children) { + const own = agentDepth(card); + return Array.from(children.querySelectorAll(':scope > .message-node > .message.sidechain')).some( + function (kid) { + return agentDepth(kid) > own; } - function transcriptCards() { - const transcript = document.getElementById('transcript'); - return transcript ? Array.from(transcript.querySelectorAll('.message')) : []; - } - function chooseDepth(value) { - depth = value; - writeDepth(value); - applyDepthTo(transcriptCards(), value); - const transcript = document.getElementById('transcript'); - if (transcript) setCollapsibles(transcript, value === 'all'); - markDepth(true); - if (window.claudeLogUpdateDetailsToggle) window.claudeLogUpdateDetailsToggle(); - } - document.addEventListener('click', function (event) { - const btn = event.target.closest('[data-mn-depth]'); - if (btn) { - chooseDepth(btn.getAttribute('data-mn-depth')); - return; - } - if (event.target.closest('#transcript .fold-bar-section, #toggleDetails')) { - markDepth(false); - } - }); - // Called by transcript.html right after the classic initial fold - // state, before deep links and the search component reveal - // anything. Previews start closed in the markup, so only All has - // collapsibles to open. - window.claudeLogMinimalInitFolds = function () { - applyDepthTo(transcriptCards(), depth); - if (depth === 'all') { - const transcript = document.getElementById('transcript'); - if (transcript) setCollapsibles(transcript, true); - } - markDepth(true); - }; - markDepth(true); + ); + } + function wantsOpen(card, children, value) { + if (value === 'all') return true; + if (card.classList.contains('session-header')) return true; + if (value === 'prompts') return false; + return !opensBranch(card, children); + } + const seenContainers = new WeakSet(); + const depthApplied = new WeakSet(); + function applyDepthTo(cards, value) { + const fold = window.claudeLogApplyFoldState; + const sync = window.claudeLogSyncFoldBar; + if (!fold || !sync) return; + const touched = []; + cards.forEach(function (card) { + depthApplied.add(card); + const children = childrenOf(card); + if (!children) return; + seenContainers.add(children); + if (wantsOpen(card, children, value)) children.style.display = ''; + else fold(card, 'folded'); + touched.push(card); + }); + touched.forEach(sync); + } + function setCollapsibles(scope, open) { + scope.querySelectorAll(COLLAPSIBLES).forEach(function (details) { + if (details.open !== open) details.open = open; + }); + } + function transcriptCards() { + const transcript = document.getElementById('transcript'); + return transcript ? Array.from(transcript.querySelectorAll('.message')) : []; + } + function chooseDepth(value) { + depth = value; + writeDepth(value); + applyDepthTo(transcriptCards(), value); + const transcript = document.getElementById('transcript'); + if (transcript) setCollapsibles(transcript, value === 'all'); + markDepth(true); + if (window.claudeLogUpdateDetailsToggle) window.claudeLogUpdateDetailsToggle(); + } + document.addEventListener('click', function (event) { + const btn = event.target.closest('[data-mn-depth]'); + if (btn) { + chooseDepth(btn.getAttribute('data-mn-depth')); + return; + } + if (event.target.closest('#transcript .fold-bar-section, #toggleDetails')) { + markDepth(false); + } + }); + // Called by transcript.html right after the classic initial fold + // state, before deep links and the search component reveal + // anything. Previews start closed in the markup, so only All has + // collapsibles to open. + window.claudeLogMinimalInitFolds = function () { + applyDepthTo(transcriptCards(), depth); + if (depth === 'all') { + const transcript = document.getElementById('transcript'); + if (transcript) setCollapsibles(transcript, true); + } + markDepth(true); + }; + markDepth(true); - // Live updates (serve --watch): the swap restores the fold state - // of every card that was already on screen and tags the new ones - // `live-new`; the patch keeps existing cards' containers and also - // tags its additions. New cards — and, on a patch, a card that - // just gained its first children (its container arrives - // wholesale) — get the current depth; nothing the reader folded - // or unfolded by hand is touched. - if (window.claudeLogOnRehydrate) { - window.claudeLogOnRehydrate(function (scope) { - if (!scope || !scope.querySelectorAll) return; - const swap = scope.id === 'transcript'; - const cards = Array.from(scope.querySelectorAll('.message')); - if (scope.matches && scope.matches('.message')) cards.unshift(scope); - const fresh = cards.filter(function (card) { - if (card.classList.contains('live-new') && !depthApplied.has(card)) return true; - if (swap) return false; - const children = childrenOf(card); - return !!children && !seenContainers.has(children); - }); - cards.forEach(function (card) { - const children = childrenOf(card); - if (children) seenContainers.add(children); - if (!card.classList.contains('live-new')) depthApplied.add(card); - }); - applyDepthTo(fresh, depth); - if (depth === 'all') { - fresh.forEach(function (card) { setCollapsibles(card, true); }); - } + // Live updates (serve --watch): the swap restores the fold state + // of every card that was already on screen and tags the new ones + // `live-new`; the patch keeps existing cards' containers and also + // tags its additions. New cards — and, on a patch, a card that + // just gained its first children (its container arrives + // wholesale) — get the current depth; nothing the reader folded + // or unfolded by hand is touched. + if (window.claudeLogOnRehydrate) { + window.claudeLogOnRehydrate(function (scope) { + if (!scope || !scope.querySelectorAll) return; + const swap = scope.id === 'transcript'; + const cards = Array.from(scope.querySelectorAll('.message')); + if (scope.matches && scope.matches('.message')) cards.unshift(scope); + const fresh = cards.filter(function (card) { + if (card.classList.contains('live-new') && !depthApplied.has(card)) return true; + if (swap) return false; + const children = childrenOf(card); + return !!children && !seenContainers.has(children); + }); + cards.forEach(function (card) { + const children = childrenOf(card); + if (children) seenContainers.add(children); + if (!card.classList.contains('live-new')) depthApplied.add(card); + }); + applyDepthTo(fresh, depth); + if (depth === 'all') { + fresh.forEach(function (card) { + setCollapsibles(card, true); }); } + }); + } - // ---- fold bars: keyboard (P7c) ---------------------------------- - // The classic fold-bar sections are click-only
s. Here they - // become focusable buttons (Enter / Space fold like a click), so - // the dimmed-at-rest bars (layout.css) come up on keyboard focus - // as they do on hover. Re-applied to patched / swapped cards. - function focusableFoldBars(scope) { - const root = scope && scope.querySelectorAll ? scope : document; - root.querySelectorAll('.fold-bar-section').forEach(function (section) { - if (section.hasAttribute('tabindex')) return; - section.setAttribute('tabindex', '0'); - section.setAttribute('role', 'button'); - }); - } - document.addEventListener('keydown', function (event) { - if (event.key !== 'Enter' && event.key !== ' ') return; - const section = event.target && event.target.closest - ? event.target.closest('#transcript .fold-bar-section') : null; - if (!section || section !== event.target) return; - event.preventDefault(); - section.click(); - }); - focusableFoldBars(document.getElementById('transcript')); - if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(focusableFoldBars); + // ---- fold bars: keyboard (P7c) ---------------------------------- + // The classic fold-bar sections are click-only
s. Here they + // become focusable buttons (Enter / Space fold like a click), so + // the dimmed-at-rest bars (layout.css) come up on keyboard focus + // as they do on hover. Re-applied to patched / swapped cards. + function focusableFoldBars(scope) { + const root = scope && scope.querySelectorAll ? scope : document; + root.querySelectorAll('.fold-bar-section').forEach(function (section) { + if (section.hasAttribute('tabindex')) return; + section.setAttribute('tabindex', '0'); + section.setAttribute('role', 'button'); + }); + } + document.addEventListener('keydown', function (event) { + if (event.key !== 'Enter' && event.key !== ' ') return; + const section = + event.target && event.target.closest ? event.target.closest('#transcript .fold-bar-section') : null; + if (!section || section !== event.target) return; + event.preventDefault(); + section.click(); + }); + focusableFoldBars(document.getElementById('transcript')); + if (window.claudeLogOnRehydrate) window.claudeLogOnRehydrate(focusableFoldBars); - // ---- overflow menu --------------------------------------------- - // A
: closes on a click outside it or on Escape. Clicks - // on its items keep it open, so a state change stays visible. - const more = document.querySelector('.mn-more'); - if (more) { - document.addEventListener('click', function (event) { - if (more.open && !more.contains(event.target)) more.open = false; - }); - document.addEventListener('keydown', function (event) { - if (event.key === 'Escape' && more.open) { - more.open = false; - const summary = more.querySelector('summary'); - if (summary) summary.focus(); - } - }); + // ---- overflow menu --------------------------------------------- + // A
: closes on a click outside it or on Escape. Clicks + // on its items keep it open, so a state change stays visible. + const more = document.querySelector('.mn-more'); + if (more) { + document.addEventListener('click', function (event) { + if (more.open && !more.contains(event.target)) more.open = false; + }); + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape' && more.open) { + more.open = false; + const summary = more.querySelector('summary'); + if (summary) summary.focus(); } - })(); + }); + } +})(); diff --git a/claude_code_log/html/templates/components/minimal/minimal_dag.js b/claude_code_log/html/templates/components/minimal/minimal_dag.js index 78211ebd..9408498e 100644 --- a/claude_code_log/html/templates/components/minimal/minimal_dag.js +++ b/claude_code_log/html/templates/components/minimal/minimal_dag.js @@ -1,1907 +1,2320 @@ - // Minimal theme (--theme minimal): the DAG engine — branch lanes - // (sub-agents at any depth, workflow agents, rewind forks) drawn as a git-graph rail - // beside the main line, each folded ("Main only", the default), - // interleaved with the main line at its real arrival times, or in a - // column of its own (a swimlane, time-aligned with the main line; - // collapsible to a narrow strip). - // work/minimal-theme-dag.md § 1.6 and § 3.5; as built in - // dev-docs/minimal-theme.md. - // - // The server renders every card where it always has (nested - // `.message-node > .children`, P5's `data-lane*` attributes on the - // cards). Nothing here moves a DOM node: `dag-on` on the stage turns - // the wrappers into `display: contents` and #transcript into a - // one-column grid in which each card gets an inline `grid-row`. Rows - // are packed as if every lane were interleaved; a folded lane is - // hidden and its rows are empty, so opening or folding one lane - // rewrites only that lane's cards. The fold state machine, the - // filter, search and live updates all keep working on the nested DOM. - // - // Ownership (nothing else writes these, this writes nothing else): - // the `dag-*` classes, inline `grid-row` / `grid-column` and - // `--dag-tag` on cards, the `.mn-bctls` branch controls, the column - // chrome (`.dag-chrome` elements at the head of #transcript), - // `--dag-slots` / `--dag-cols` and the `dag-on` / `dag-cols` - // classes on the stage, `dag-wide` / `--dag-min` / `--dag-view` on - // , and everything in #dag-rail. - // - // A relayout is a pure function of (DOM, lane modes): it walks the - // tree once, orders the visible lanes' cards by time, packs rows, - // assigns rail slots, writes what changed, then measures the few - // rows the rail needs and redraws the SVG. Triggers: one - // MutationObserver on #transcript (folds, filter, search, live - // patches), the rehydrate hook (live swaps), a ResizeObserver - // (redraw only), the toolbar and branch controls. - (function () { - 'use strict'; - const stage = document.querySelector('.mn-stage'); - const railHost = document.getElementById('dag-rail'); - if (!stage || !railHost) return; +// Minimal theme (--theme minimal): the DAG engine — branch lanes +// (sub-agents at any depth, workflow agents, rewind forks) drawn as a git-graph rail +// beside the main line, each folded ("Main only", the default), +// interleaved with the main line at its real arrival times, or in a +// column of its own (a swimlane, time-aligned with the main line; +// collapsible to a narrow strip). +// work/minimal-theme-dag.md § 1.6 and § 3.5; as built in +// dev-docs/minimal-theme.md. +// +// The server renders every card where it always has (nested +// `.message-node > .children`, P5's `data-lane*` attributes on the +// cards). Nothing here moves a DOM node: `dag-on` on the stage turns +// the wrappers into `display: contents` and #transcript into a +// one-column grid in which each card gets an inline `grid-row`. Rows +// are packed as if every lane were interleaved; a folded lane is +// hidden and its rows are empty, so opening or folding one lane +// rewrites only that lane's cards. The fold state machine, the +// filter, search and live updates all keep working on the nested DOM. +// +// Ownership (nothing else writes these, this writes nothing else): +// the `dag-*` classes, inline `grid-row` / `grid-column` and +// `--dag-tag` on cards, the `.mn-bctls` branch controls, the column +// chrome (`.dag-chrome` elements at the head of #transcript), +// `--dag-slots` / `--dag-cols` and the `dag-on` / `dag-cols` +// classes on the stage, `dag-wide` / `--dag-min` / `--dag-view` on +// , and everything in #dag-rail. +// +// A relayout is a pure function of (DOM, lane modes): it walks the +// tree once, orders the visible lanes' cards by time, packs rows, +// assigns rail slots, writes what changed, then measures the few +// rows the rail needs and redraws the SVG. Triggers: one +// MutationObserver on #transcript (folds, filter, search, live +// patches), the rehydrate hook (live swaps), a ResizeObserver +// (redraw only), the toolbar and branch controls. +(function () { + 'use strict'; + const stage = document.querySelector('.mn-stage'); + const railHost = document.getElementById('dag-rail'); + if (!stage || !railHost) return; - const BRANCHES_KEY = 'claude-code-log:branches'; - // Global modes; per lane: 'folded' | 'interleaved' | 'column' | - // 'strip' (a column collapsed to a narrow strip). - const GLOBAL_MODES = ['main', 'interleaved', 'columns']; - const CAP = 3; // interleaved lanes (and controls shown) per user turn / workflow group - // Column mode (P7c): the main column takes twice a branch column's - // share of the spare width, and never less than MAIN_MIN; on a - // phone every column (main too) is the viewport's width, one at a - // time, with horizontal scroll snapping (dag.css). - const MAIN_MIN = 520; // px: the main column's minimum in column mode - const MAIN_FR = 2; // the main column's share of the spare width - const COL_MIN = 300; // px: a column's minimum - const PHONE_PAD = 20; // px: the body's side padding on a phone (2 × 10px) - const STRIP = 34; // px: a collapsed column - const ROW0 = 2; // grid row of the first card (row 1: column heads) - const MAX_SLOTS = 6; // rail slots beside the main line - const RADIUS = 8; // fork/merge connector corner - const STUB = 12; // a folded fork's stub below its row - const END_R = 3.5; // a running lane's open end marker - // A column lane's pointers: a few px of lane past the end dot - // (clear of the dot and its ring), the RADIUS curve, a short tail. - const POINTER_LEAD = 6; - const POINTER_TAIL = 3; - const STRIP_X = 6; // px: a strip's lane, from its left edge (clear of its name) - // An open lane (an agent with no result yet, P7b) reads as - // *running* on a page served live (`serve`, live_update.js) — - // however long it has been quiet (P7c: agents wait on background - // processes, watchers and Monitor tasks for hours); its label - // says how long instead ("running · quiet 42m"). Only a session - // silent for a week is taken to have stopped: `serve` runs over - // whole archives, and an agent that died with a session closed - // months ago is not running. A static page (file://, an export) - // never shows one running. dev-docs/minimal-theme.md § 8. - const RUNNING_MAX_QUIET_MS = 7 * 24 * 60 * 60 * 1000; - const QUIET_LABEL_MS = 60 * 1000; // "quiet …" from a minute - const RUNNING_RECHECK_MS = 30 * 1000; - // Forks are always --lF; agents cycle through the other lane colours. - const AGENT_COLOURS = ['a', 'b', 's', 'p']; - const DEBUG = /[?&]debug-dag(?:[=&]|$)/.test(window.location.search); - const phone = window.matchMedia ? window.matchMedia('(max-width: 640px)') : null; + const BRANCHES_KEY = 'claude-code-log:branches'; + // Global modes; per lane: 'folded' | 'interleaved' | 'column' | + // 'strip' (a column collapsed to a narrow strip). + const GLOBAL_MODES = ['main', 'interleaved', 'columns']; + const CAP = 3; // interleaved lanes (and controls shown) per user turn / workflow group + // Column mode (P7c): the main column takes twice a branch column's + // share of the spare width, and never less than MAIN_MIN; on a + // phone every column (main too) is the viewport's width, one at a + // time, with horizontal scroll snapping (dag.css). + const MAIN_MIN = 520; // px: the main column's minimum in column mode + const MAIN_FR = 2; // the main column's share of the spare width + const COL_MIN = 300; // px: a column's minimum + const PHONE_PAD = 20; // px: the body's side padding on a phone (2 × 10px) + const STRIP = 34; // px: a collapsed column + const ROW0 = 2; // grid row of the first card (row 1: column heads) + const MAX_SLOTS = 6; // rail slots beside the main line + const RADIUS = 8; // fork/merge connector corner + const STUB = 12; // a folded fork's stub below its row + const END_R = 3.5; // a running lane's open end marker + // A column lane's pointers: a few px of lane past the end dot + // (clear of the dot and its ring), the RADIUS curve, a short tail. + const POINTER_LEAD = 6; + const POINTER_TAIL = 3; + const STRIP_X = 6; // px: a strip's lane, from its left edge (clear of its name) + // An open lane (an agent with no result yet, P7b) reads as + // *running* on a page served live (`serve`, live_update.js) — + // however long it has been quiet (P7c: agents wait on background + // processes, watchers and Monitor tasks for hours); its label + // says how long instead ("running · quiet 42m"). Only a session + // silent for a week is taken to have stopped: `serve` runs over + // whole archives, and an agent that died with a session closed + // months ago is not running. A static page (file://, an export) + // never shows one running. dev-docs/minimal-theme.md § 8. + const RUNNING_MAX_QUIET_MS = 7 * 24 * 60 * 60 * 1000; + const QUIET_LABEL_MS = 60 * 1000; // "quiet …" from a minute + const RUNNING_RECHECK_MS = 30 * 1000; + // Forks are always --lF; agents cycle through the other lane colours. + const AGENT_COLOURS = ['a', 'b', 's', 'p']; + const DEBUG = /[?&]debug-dag(?:[=&]|$)/.test(window.location.search); + const phone = window.matchMedia ? window.matchMedia('(max-width: 640px)') : null; - // ---- state --------------------------------------------------- - let globalMode = readGlobal(); - const modes = new Map(); // lane id -> 'folded' | 'interleaved' | 'column' | 'strip' - // Turns are keyed by their card's data-uuid (lane.turnKey), not - // its positional d-N: a live swap renumbers the ids, and the - // per-turn cap and overflow must follow the turn, not the slot. - // A workflow's agents are capped per group instead (their phase, - // or a run without phases: data-lane-group, `g:/`), so - // a fan-out of many agents never pushes the turn's other - // branches out, and gets its own "+N more agents". - const recency = new Map(); // turn / group key -> interleaved lane ids, least recent first - const expanded = new Set(); // turn / group keys whose "+N more" are shown - const acksShown = new Set(); // lane ids whose launch acknowledgement is shown (P7c) - let lanes = new Map(); // lane id -> record (rebuilt each relayout) - let started = false; - let layout = null; // last relayout's result, for redraws - let lastTiming = null; - const pendingReveals = []; + // ---- state --------------------------------------------------- + let globalMode = readGlobal(); + const modes = new Map(); // lane id -> 'folded' | 'interleaved' | 'column' | 'strip' + // Turns are keyed by their card's data-uuid (lane.turnKey), not + // its positional d-N: a live swap renumbers the ids, and the + // per-turn cap and overflow must follow the turn, not the slot. + // A workflow's agents are capped per group instead (their phase, + // or a run without phases: data-lane-group, `g:/`), so + // a fan-out of many agents never pushes the turn's other + // branches out, and gets its own "+N more agents". + const recency = new Map(); // turn / group key -> interleaved lane ids, least recent first + const expanded = new Set(); // turn / group keys whose "+N more" are shown + const acksShown = new Set(); // lane ids whose launch acknowledgement is shown (P7c) + let lanes = new Map(); // lane id -> record (rebuilt each relayout) + let started = false; + let layout = null; // last relayout's result, for redraws + let lastTiming = null; + const pendingReveals = []; - function readGlobal() { - try { - const stored = window.localStorage.getItem(BRANCHES_KEY); - return GLOBAL_MODES.indexOf(stored) >= 0 ? stored : 'main'; - } catch (err) { - return 'main'; - } - } - function writeGlobal(value) { - try { - window.localStorage.setItem(BRANCHES_KEY, value); - } catch (err) { /* storage unavailable: the choice lasts this page */ } - } + function readGlobal() { + try { + const stored = window.localStorage.getItem(BRANCHES_KEY); + return GLOBAL_MODES.indexOf(stored) >= 0 ? stored : 'main'; + } catch (err) { + return 'main'; + } + } + function writeGlobal(value) { + try { + window.localStorage.setItem(BRANCHES_KEY, value); + } catch (err) { + /* storage unavailable: the choice lasts this page */ + } + } - // ---- lanes --------------------------------------------------- - function readLanes() { - const transcript = document.getElementById('transcript'); - const found = new Map(); - if (!transcript) return found; - transcript.querySelectorAll('[data-lane-id]').forEach(function (head) { - const id = head.getAttribute('data-lane-id'); - if (found.has(id)) return; - const ts = (head.getAttribute('data-lane-ts') || '').split(' '); - const turn = head.getAttribute('data-lane-turn') || ''; - found.set(id, { - id: id, - head: head, - kind: head.getAttribute('data-lane-kind') || 'agent', - name: head.getAttribute('data-lane-name') || id, - tag: head.getAttribute('data-lane-tag') || '', - meta: head.getAttribute('data-lane-meta') || '', - parent: head.getAttribute('data-lane-parent') || 'main', - from: head.getAttribute('data-lane-from') || '', - to: head.getAttribute('data-lane-to') || '', - turn: turn, - turnKey: turnKeyOf(turn), - // Workflow agents: the phase (run) they belong to. - group: head.getAttribute('data-lane-group') || '', - rank: parseInt(head.getAttribute('data-lane-rank') || '1', 10) || 1, - stats: head.getAttribute('data-lane-stats') || '', - // Agents without a result: 'open' (may be running) - // or 'ended' (the page proves it is over); '' when - // merged, and for forks (lanes.py, P7b). - state: head.getAttribute('data-lane-state') || '', - firstTs: ts[0] ? Date.parse(ts[0]) : NaN, - }); - }); - return found; - } - // A stable key for a turn: its card's data-uuid (or session id), - // falling back to the positional id. - function turnKeyOf(turn) { - if (!turn) return ''; - const card = document.getElementById('msg-' + turn); - const stable = card && (card.getAttribute('data-uuid') || card.getAttribute('data-session-id')); - return stable ? 'u:' + stable : turn; - } - function parentOf(id) { - const lane = lanes.get(id); - return lane && lane.parent !== 'main' && lanes.has(lane.parent) ? lane.parent : null; - } - function ancestorsOf(id) { - const chain = []; - let up = parentOf(id); - while (up && chain.indexOf(up) < 0) { - chain.unshift(up); - up = parentOf(up); - } - return chain; - } - // The cap's unit: the user turn, or a workflow agent's group. - function turnOf(id) { - const lane = lanes.get(id); - if (lane && lane.group) return 'g:' + lane.group; - return lane ? lane.turnKey || ('lane:' + id) : 'lane:' + id; - } - function defaultMode(lane, value) { - if (value === 'columns') return 'column'; - return value === 'interleaved' && lane.rank <= CAP ? 'interleaved' : 'folded'; - } - function modeOf(id) { - return modes.get(id) || 'folded'; - } - function inColumn(mode) { - return mode === 'column' || mode === 'strip'; - } - function forget(id) { - const turn = turnOf(id); - recency.set(turn, (recency.get(turn) || []).filter(function (x) { return x !== id; })); - } + // ---- lanes --------------------------------------------------- + function readLanes() { + const transcript = document.getElementById('transcript'); + const found = new Map(); + if (!transcript) return found; + transcript.querySelectorAll('[data-lane-id]').forEach(function (head) { + const id = head.getAttribute('data-lane-id'); + if (found.has(id)) return; + const ts = (head.getAttribute('data-lane-ts') || '').split(' '); + const turn = head.getAttribute('data-lane-turn') || ''; + found.set(id, { + id: id, + head: head, + kind: head.getAttribute('data-lane-kind') || 'agent', + name: head.getAttribute('data-lane-name') || id, + tag: head.getAttribute('data-lane-tag') || '', + meta: head.getAttribute('data-lane-meta') || '', + parent: head.getAttribute('data-lane-parent') || 'main', + from: head.getAttribute('data-lane-from') || '', + to: head.getAttribute('data-lane-to') || '', + turn: turn, + turnKey: turnKeyOf(turn), + // Workflow agents: the phase (run) they belong to. + group: head.getAttribute('data-lane-group') || '', + rank: parseInt(head.getAttribute('data-lane-rank') || '1', 10) || 1, + stats: head.getAttribute('data-lane-stats') || '', + // Agents without a result: 'open' (may be running) + // or 'ended' (the page proves it is over); '' when + // merged, and for forks (lanes.py, P7b). + state: head.getAttribute('data-lane-state') || '', + firstTs: ts[0] ? Date.parse(ts[0]) : NaN + }); + }); + return found; + } + // A stable key for a turn: its card's data-uuid (or session id), + // falling back to the positional id. + function turnKeyOf(turn) { + if (!turn) return ''; + const card = document.getElementById('msg-' + turn); + const stable = card && (card.getAttribute('data-uuid') || card.getAttribute('data-session-id')); + return stable ? 'u:' + stable : turn; + } + function parentOf(id) { + const lane = lanes.get(id); + return lane && lane.parent !== 'main' && lanes.has(lane.parent) ? lane.parent : null; + } + function ancestorsOf(id) { + const chain = []; + let up = parentOf(id); + while (up && chain.indexOf(up) < 0) { + chain.unshift(up); + up = parentOf(up); + } + return chain; + } + // The cap's unit: the user turn, or a workflow agent's group. + function turnOf(id) { + const lane = lanes.get(id); + if (lane && lane.group) return 'g:' + lane.group; + return lane ? lane.turnKey || 'lane:' + id : 'lane:' + id; + } + function defaultMode(lane, value) { + if (value === 'columns') return 'column'; + return value === 'interleaved' && lane.rank <= CAP ? 'interleaved' : 'folded'; + } + function modeOf(id) { + return modes.get(id) || 'folded'; + } + function inColumn(mode) { + return mode === 'column' || mode === 'strip'; + } + function forget(id) { + const turn = turnOf(id); + recency.set( + turn, + (recency.get(turn) || []).filter(function (x) { + return x !== id; + }) + ); + } - // Interleave a lane (and, first, the folded lanes it is nested - // in), most recent last; past the cap the least recently selected - // lane of the same turn folds — never one the new lane needs to be - // visible. A reveal (search, a link, the timeline) passes - // `evict === false`: it never hides what the reader has open, so a - // turn can go past the cap until the next manual selection. - // An enclosing lane in a column stays there (the lane then shows - // interleaved in that column); a strip expands back to a column. - function select(id, evict) { - if (!lanes.has(id)) return; - const keep = ancestorsOf(id).concat([id]); - const turn = turnOf(id); - let list = recency.get(turn) || []; - keep.forEach(function (lane) { - const mode = modeOf(lane); - if (mode === 'strip') modes.set(lane, 'column'); - if (inColumn(modeOf(lane)) && lane !== id) return; - modes.set(lane, 'interleaved'); - list = list.filter(function (x) { return x !== lane; }); - list.push(lane); - }); - recency.set(turn, list); - if (evict === false) return; - while (list.length > CAP) { - const victim = list.find(function (x) { return keep.indexOf(x) < 0; }); - if (!victim) break; - fold(victim); - list = recency.get(turn) || []; - } - } - // Put a lane in a column of its own (and the folded lanes it is - // nested in: a column needs its parent shown). Columns are not - // capped. - function column(id) { - if (!lanes.has(id)) return; - ancestorsOf(id).forEach(function (lane) { - const mode = modeOf(lane); - if (mode === 'folded' || mode === 'strip') { - modes.set(lane, 'column'); - forget(lane); - } - }); + // Interleave a lane (and, first, the folded lanes it is nested + // in), most recent last; past the cap the least recently selected + // lane of the same turn folds — never one the new lane needs to be + // visible. A reveal (search, a link, the timeline) passes + // `evict === false`: it never hides what the reader has open, so a + // turn can go past the cap until the next manual selection. + // An enclosing lane in a column stays there (the lane then shows + // interleaved in that column); a strip expands back to a column. + function select(id, evict) { + if (!lanes.has(id)) return; + const keep = ancestorsOf(id).concat([id]); + const turn = turnOf(id); + let list = recency.get(turn) || []; + keep.forEach(function (lane) { + const mode = modeOf(lane); + if (mode === 'strip') modes.set(lane, 'column'); + if (inColumn(modeOf(lane)) && lane !== id) return; + modes.set(lane, 'interleaved'); + list = list.filter(function (x) { + return x !== lane; + }); + list.push(lane); + }); + recency.set(turn, list); + if (evict === false) return; + while (list.length > CAP) { + const victim = list.find(function (x) { + return keep.indexOf(x) < 0; + }); + if (!victim) break; + fold(victim); + list = recency.get(turn) || []; + } + } + // Put a lane in a column of its own (and the folded lanes it is + // nested in: a column needs its parent shown). Columns are not + // capped. + function column(id) { + if (!lanes.has(id)) return; + ancestorsOf(id).forEach(function (lane) { + const mode = modeOf(lane); + if (mode === 'folded' || mode === 'strip') { + modes.set(lane, 'column'); + forget(lane); + } + }); + modes.set(id, 'column'); + forget(id); + } + function strip(id) { + if (!lanes.has(id)) return; + modes.set(id, 'strip'); + forget(id); + } + // Fold a lane and every lane nested in it. + function fold(id) { + modes.set(id, 'folded'); + forget(id); + lanes.forEach(function (lane, other) { + if (other !== id && ancestorsOf(other).indexOf(id) >= 0 && modeOf(other) !== 'folded') { + fold(other); + } + }); + } + function applyGlobal(value) { + recency.clear(); + const byTurn = new Map(); + lanes.forEach(function (lane) { + modes.set(lane.id, value === 'columns' ? 'column' : 'folded'); + const list = byTurn.get(turnOf(lane.id)) || []; + list.push(lane); + byTurn.set(turnOf(lane.id), list); + }); + if (value !== 'interleaved') return; + byTurn.forEach(function (list) { + list.sort(function (a, b) { + return a.rank - b.rank; + }); + list.forEach(function (lane) { + if (defaultMode(lane, value) === 'interleaved') select(lane.id); + }); + }); + } + // Lanes seen for the first time (page load, a live update) take the + // global choice — but never push out a lane the reader picked. + function adoptNewLanes() { + lanes.forEach(function (lane, id) { + if (modes.has(id)) return; + modes.set(id, 'folded'); + const mode = defaultMode(lane, globalMode); + if (mode === 'column') { modes.set(id, 'column'); - forget(id); - } - function strip(id) { - if (!lanes.has(id)) return; - modes.set(id, 'strip'); - forget(id); - } - // Fold a lane and every lane nested in it. - function fold(id) { - modes.set(id, 'folded'); - forget(id); - lanes.forEach(function (lane, other) { - if (other !== id && ancestorsOf(other).indexOf(id) >= 0 && modeOf(other) !== 'folded') { - fold(other); - } - }); - } - function applyGlobal(value) { - recency.clear(); - const byTurn = new Map(); - lanes.forEach(function (lane) { - modes.set(lane.id, value === 'columns' ? 'column' : 'folded'); - const list = byTurn.get(turnOf(lane.id)) || []; - list.push(lane); - byTurn.set(turnOf(lane.id), list); - }); - if (value !== 'interleaved') return; - byTurn.forEach(function (list) { - list.sort(function (a, b) { return a.rank - b.rank; }); - list.forEach(function (lane) { - if (defaultMode(lane, value) === 'interleaved') select(lane.id); - }); - }); - } - // Lanes seen for the first time (page load, a live update) take the - // global choice — but never push out a lane the reader picked. - function adoptNewLanes() { - lanes.forEach(function (lane, id) { - if (modes.has(id)) return; - modes.set(id, 'folded'); - const mode = defaultMode(lane, globalMode); - if (mode === 'column') { - modes.set(id, 'column'); - return; - } - if (mode !== 'interleaved') return; - const list = recency.get(turnOf(id)) || []; - if (list.length < CAP) select(id); - }); - } - // Open = its cards are shown (interleaved, or in an expanded - // column), and so are its parent's. - function isOpen(id, memo) { - if (id === 'main') return true; - if (memo.has(id)) return memo.get(id); - memo.set(id, false); // cycle guard - const mode = modeOf(id); - const open = (mode === 'interleaved' || mode === 'column') && parentOpen(id, memo); - memo.set(id, open); - return open; - } - function parentOpen(id, memo) { - const parent = lanes.has(id) ? lanes.get(id).parent : 'main'; - return parent === 'main' || !lanes.has(parent) || isOpen(parent, memo); - } - // The column a lane's cards are laid out in: its own when it is in - // a column (or a strip), else its parent's — so a lane interleaved - // inside a column lane shows in that column — else 'main'. - function columnKey(id, memo) { - if (!id || id === 'main' || !lanes.has(id)) return 'main'; - const hit = memo.get(id); - if (hit !== undefined) return hit; - memo.set(id, 'main'); // cycle guard - const key = inColumn(modeOf(id)) ? id : columnKey(lanes.get(id).parent, memo); - memo.set(id, key); - return key; - } - // Which segment of the global control describes the current modes? - function matchingGlobal() { - let match = null; - GLOBAL_MODES.forEach(function (value) { - let all = true; - lanes.forEach(function (lane, id) { - if ((modes.get(id) || 'folded') !== defaultMode(lane, value)) all = false; - }); - if (all && match === null) match = value; - }); - return match; - } + return; + } + if (mode !== 'interleaved') return; + const list = recency.get(turnOf(id)) || []; + if (list.length < CAP) select(id); + }); + } + // Open = its cards are shown (interleaved, or in an expanded + // column), and so are its parent's. + function isOpen(id, memo) { + if (id === 'main') return true; + if (memo.has(id)) return memo.get(id); + memo.set(id, false); // cycle guard + const mode = modeOf(id); + const open = (mode === 'interleaved' || mode === 'column') && parentOpen(id, memo); + memo.set(id, open); + return open; + } + function parentOpen(id, memo) { + const parent = lanes.has(id) ? lanes.get(id).parent : 'main'; + return parent === 'main' || !lanes.has(parent) || isOpen(parent, memo); + } + // The column a lane's cards are laid out in: its own when it is in + // a column (or a strip), else its parent's — so a lane interleaved + // inside a column lane shows in that column — else 'main'. + function columnKey(id, memo) { + if (!id || id === 'main' || !lanes.has(id)) return 'main'; + const hit = memo.get(id); + if (hit !== undefined) return hit; + memo.set(id, 'main'); // cycle guard + const key = inColumn(modeOf(id)) ? id : columnKey(lanes.get(id).parent, memo); + memo.set(id, key); + return key; + } + // Which segment of the global control describes the current modes? + function matchingGlobal() { + let match = null; + GLOBAL_MODES.forEach(function (value) { + let all = true; + lanes.forEach(function (lane, id) { + if ((modes.get(id) || 'folded') !== defaultMode(lane, value)) all = false; + }); + if (all && match === null) match = value; + }); + return match; + } - // ---- running lanes (P7b, rule revised in P7c) ------------------ - // The server marks an agent lane without a result 'open' unless - // the page proves it is over ('ended': a parent that moved on, a - // TaskStop, a finished parent lane); it cannot know whether the - // session is still being written, so the page decides: an open - // lane is *running* while the page is served live — unless its - // session (any card, in any lane) has been silent for - // RUNNING_MAX_QUIET_MS. Elapsed time alone never ends it sooner: - // the label shows how long the lane has been quiet instead. A - // static page never shows one running. - // Newest activity per session (top-level node): on a combined - // page a live session must not make another session's crashed - // agent read as running. Lane id -> its session's newest time. - function newestTimes(model, sequence) { - const bySession = new Map(); - sequence.forEach(function (item) { - if (isNaN(item.ts)) return; - const seen = bySession.get(item.session); - if (seen === undefined || item.ts > seen) bySession.set(item.session, item.ts); - }); - const byLane = new Map(); - lanes.forEach(function (lane, id) { - if (lane.state !== 'open') return; - const anchor = model.spawns.get(id) || (model.seqs.get(id) || [])[0]; - const newest = anchor ? bySession.get(anchor.session) : undefined; - if (newest !== undefined) byLane.set(id, newest); - }); - return byLane; - } - // A lane's last activity: its newest card, or a nested lane's - // (a parent waiting on a busy child is not quiet), else its - // spawn's time. - function laneActivity(model, sequence) { - const out = new Map(); - sequence.forEach(function (item) { - if (isNaN(item.ts) || item.lane === 'main') return; - let id = item.lane; - let guard = 0; - while (id && guard++ < 64) { - const seen = out.get(id); - if (seen === undefined || item.ts > seen) out.set(id, item.ts); - id = parentOf(id); - } - }); - lanes.forEach(function (lane, id) { - if (out.has(id)) return; - const spawn = model.spawns.get(id); - if (spawn && !isNaN(spawn.ts)) out.set(id, spawn.ts); - }); - return out; - } - // Running lanes: lane id -> how long it has been quiet (ms). - function runningLanes(newest, activity) { - const out = new Map(); - if (!window.claudeLogLiveUpdate) return out; - const now = Date.now(); - newest.forEach(function (ts, id) { - if (now - ts > RUNNING_MAX_QUIET_MS) return; - if (!lanes.has(id) || lanes.get(id).state !== 'open') return; - const last = activity.get(id); - out.set(id, last === undefined || isNaN(last) ? 0 : Math.max(0, now - last)); - }); - return out; - } - function quietText(ms) { - if (!(ms >= QUIET_LABEL_MS)) return ''; - const m = Math.floor(ms / 60000); - if (m < 60) return m + 'm'; - const h = Math.floor(m / 60); - if (h < 24) return h + 'h' + (m % 60 ? ' ' + (m % 60) + 'm' : ''); - const d = Math.floor(h / 24); - return d + 'd' + (h % 24 ? ' ' + (h % 24) + 'h' : ''); - } - function runningText(running, id) { - const quiet = quietText(running.get(id)); - return 'running' + (quiet ? ' · quiet ' + quiet : ''); - } - function sameSet(a, b) { - if (a.size !== b.size) return false; - let same = true; - a.forEach(function (_value, key) { if (!b.has(key)) same = false; }); - return same; - } + // ---- running lanes (P7b, rule revised in P7c) ------------------ + // The server marks an agent lane without a result 'open' unless + // the page proves it is over ('ended': a parent that moved on, a + // TaskStop, a finished parent lane); it cannot know whether the + // session is still being written, so the page decides: an open + // lane is *running* while the page is served live — unless its + // session (any card, in any lane) has been silent for + // RUNNING_MAX_QUIET_MS. Elapsed time alone never ends it sooner: + // the label shows how long the lane has been quiet instead. A + // static page never shows one running. + // Newest activity per session (top-level node): on a combined + // page a live session must not make another session's crashed + // agent read as running. Lane id -> its session's newest time. + function newestTimes(model, sequence) { + const bySession = new Map(); + sequence.forEach(function (item) { + if (isNaN(item.ts)) return; + const seen = bySession.get(item.session); + if (seen === undefined || item.ts > seen) bySession.set(item.session, item.ts); + }); + const byLane = new Map(); + lanes.forEach(function (lane, id) { + if (lane.state !== 'open') return; + const anchor = model.spawns.get(id) || (model.seqs.get(id) || [])[0]; + const newest = anchor ? bySession.get(anchor.session) : undefined; + if (newest !== undefined) byLane.set(id, newest); + }); + return byLane; + } + // A lane's last activity: its newest card, or a nested lane's + // (a parent waiting on a busy child is not quiet), else its + // spawn's time. + function laneActivity(model, sequence) { + const out = new Map(); + sequence.forEach(function (item) { + if (isNaN(item.ts) || item.lane === 'main') return; + let id = item.lane; + let guard = 0; + while (id && guard++ < 64) { + const seen = out.get(id); + if (seen === undefined || item.ts > seen) out.set(id, item.ts); + id = parentOf(id); + } + }); + lanes.forEach(function (lane, id) { + if (out.has(id)) return; + const spawn = model.spawns.get(id); + if (spawn && !isNaN(spawn.ts)) out.set(id, spawn.ts); + }); + return out; + } + // Running lanes: lane id -> how long it has been quiet (ms). + function runningLanes(newest, activity) { + const out = new Map(); + if (!window.claudeLogLiveUpdate) return out; + const now = Date.now(); + newest.forEach(function (ts, id) { + if (now - ts > RUNNING_MAX_QUIET_MS) return; + if (!lanes.has(id) || lanes.get(id).state !== 'open') return; + const last = activity.get(id); + out.set(id, last === undefined || isNaN(last) ? 0 : Math.max(0, now - last)); + }); + return out; + } + function quietText(ms) { + if (!(ms >= QUIET_LABEL_MS)) return ''; + const m = Math.floor(ms / 60000); + if (m < 60) return m + 'm'; + const h = Math.floor(m / 60); + if (h < 24) return h + 'h' + (m % 60 ? ' ' + (m % 60) + 'm' : ''); + const d = Math.floor(h / 24); + return d + 'd' + (h % 24 ? ' ' + (h % 24) + 'h' : ''); + } + function runningText(running, id) { + const quiet = quietText(running.get(id)); + return 'running' + (quiet ? ' · quiet ' + quiet : ''); + } + function sameSet(a, b) { + if (a.size !== b.size) return false; + let same = true; + a.forEach(function (_value, key) { + if (!b.has(key)) same = false; + }); + return same; + } - // ---- reveal: links, deep links, search, timeline open lanes ---- - function laneOfElement(el) { - if (!el || !el.closest) return null; - const card = el.closest('[data-lane]'); - const lane = card ? card.getAttribute('data-lane') : null; - return lane && lane !== 'main' ? lane : null; - } - // Returns true when a lane had to be opened. - function revealLanes(el) { - const lane = laneOfElement(el); - if (!lane) return false; - if (!started) { - pendingReveals.push(lane); - return false; + // ---- reveal: links, deep links, search, timeline open lanes ---- + function laneOfElement(el) { + if (!el || !el.closest) return null; + const card = el.closest('[data-lane]'); + const lane = card ? card.getAttribute('data-lane') : null; + return lane && lane !== 'main' ? lane : null; + } + // Returns true when a lane had to be opened. + function revealLanes(el) { + const lane = laneOfElement(el); + if (!lane) return false; + if (!started) { + pendingReveals.push(lane); + return false; + } + if (!lanes.has(lane)) lanes = readLanes(); + if (!lanes.has(lane) || isOpen(lane, new Map())) return false; + openForReveal(lane); + return true; + } + // A reveal opens the lane the way the page is being read: as a + // column under the global Columns choice (or when it, or a lane + // it is nested in, is a strip), interleaved otherwise — without + // evicting anything (select's `evict === false`): a search that + // hits more than three lanes of one turn shows every hit. + function openForReveal(id) { + const chain = ancestorsOf(id).concat([id]); + const stripped = chain.some(function (lane) { + return modeOf(lane) === 'strip'; + }); + if (globalMode === 'columns' || stripped || inColumn(modeOf(id))) column(id); + else select(id, false); + } + // The classic page assigns these hooks inside its DOMContentLoaded + // handler; intercepting the assignment wraps them whenever that + // happens, so search (`?q=`) and archive deep links (`?uuid=`) open + // the lane holding their target before revealing it. + function wrapHook(name, wrap) { + let inner = window[name]; + let wrapped = inner ? wrap(inner) : undefined; + try { + Object.defineProperty(window, name, { + configurable: true, + get: function () { + return wrapped; + }, + set: function (fn) { + inner = fn; + wrapped = fn ? wrap(fn) : fn; } - if (!lanes.has(lane)) lanes = readLanes(); - if (!lanes.has(lane) || isOpen(lane, new Map())) return false; - openForReveal(lane); - return true; - } - // A reveal opens the lane the way the page is being read: as a - // column under the global Columns choice (or when it, or a lane - // it is nested in, is a strip), interleaved otherwise — without - // evicting anything (select's `evict === false`): a search that - // hits more than three lanes of one turn shows every hit. - function openForReveal(id) { - const chain = ancestorsOf(id).concat([id]); - const stripped = chain.some(function (lane) { return modeOf(lane) === 'strip'; }); - if (globalMode === 'columns' || stripped || inColumn(modeOf(id))) column(id); - else select(id, false); - } - // The classic page assigns these hooks inside its DOMContentLoaded - // handler; intercepting the assignment wraps them whenever that - // happens, so search (`?q=`) and archive deep links (`?uuid=`) open - // the lane holding their target before revealing it. - function wrapHook(name, wrap) { - let inner = window[name]; - let wrapped = inner ? wrap(inner) : undefined; - try { - Object.defineProperty(window, name, { - configurable: true, - get: function () { return wrapped; }, - set: function (fn) { inner = fn; wrapped = fn ? wrap(fn) : fn; }, - }); - } catch (err) { /* leave the hook unwrapped */ } - } - wrapHook('claudeLogRevealMessage', function (inner) { - return function (target) { - if (revealLanes(target)) relayout(); - return inner.apply(this, arguments); - }; }); - wrapHook('claudeLogRevealMessageByUuid', function (inner) { - return function (uuid) { - if (uuid) { - const target = document.querySelector('[data-uuid="' + CSS.escape(uuid) + '"]'); - if (target && revealLanes(target)) relayout(); - } - return inner.apply(this, arguments); - }; - }); - function revealHash(scroll) { - const hash = window.location.hash; - if (!hash || hash.indexOf('#msg-') !== 0) return; - const target = document.getElementById(hash.slice(1)); - if (!target) return; - if (revealLanes(target)) relayout(); - if (scroll && target.scrollIntoView) target.scrollIntoView({ block: 'start' }); - } + } catch (err) { + /* leave the hook unwrapped */ + } + } + wrapHook('claudeLogRevealMessage', function (inner) { + return function (target) { + if (revealLanes(target)) relayout(); + return inner.apply(this, arguments); + }; + }); + wrapHook('claudeLogRevealMessageByUuid', function (inner) { + return function (uuid) { + if (uuid) { + const target = document.querySelector('[data-uuid="' + CSS.escape(uuid) + '"]'); + if (target && revealLanes(target)) relayout(); + } + return inner.apply(this, arguments); + }; + }); + function revealHash(scroll) { + const hash = window.location.hash; + if (!hash || hash.indexOf('#msg-') !== 0) return; + const target = document.getElementById(hash.slice(1)); + if (!target) return; + if (revealLanes(target)) relayout(); + if (scroll && target.scrollIntoView) target.scrollIntoView({ block: 'start' }); + } - // ---- model: one walk over the nested DOM ---------------------- - function cardTs(el, cache) { - const hit = cache.get(el); - if (hit !== undefined) return hit; - const stamp = el.querySelector('.timestamp[data-timestamp]'); - const value = stamp ? Date.parse(stamp.getAttribute('data-timestamp')) : NaN; - cache.set(el, value); - return value; - } - const tsCache = new WeakMap(); + // ---- model: one walk over the nested DOM ---------------------- + function cardTs(el, cache) { + const hit = cache.get(el); + if (hit !== undefined) return hit; + const stamp = el.querySelector('.timestamp[data-timestamp]'); + const value = stamp ? Date.parse(stamp.getAttribute('data-timestamp')) : NaN; + cache.set(el, value); + return value; + } + const tsCache = new WeakMap(); - function agentDepth(card) { - const match = /(?:^|\s)agent-depth-(\d+)(?:\s|$)/.exec(card.className); - if (match) return parseInt(match[1], 10); - return card.classList.contains('sidechain') ? 1 : 0; - } - // A nested group that is not a lane keeps its nested look as one - // block: teammate threads (spec § 1.6.5), old-style sidechains - // without an agent transcript. (Workflow phase and agent cards - // are rows: each agent's transcript is a lane of its own.) - function isBlockGroup(owner, kid) { - return !!owner && kid.classList.contains('sidechain') && agentDepth(kid) > agentDepth(owner); - } - // A sub-agent's or a workflow agent's transcript (not a fork). - const AGENT_LANE = /^(?:agent|wfagent)-/; + function agentDepth(card) { + const match = /(?:^|\s)agent-depth-(\d+)(?:\s|$)/.exec(card.className); + if (match) return parseInt(match[1], 10); + return card.classList.contains('sidechain') ? 1 : 0; + } + // A nested group that is not a lane keeps its nested look as one + // block: teammate threads (spec § 1.6.5), old-style sidechains + // without an agent transcript. (Workflow phase and agent cards + // are rows: each agent's transcript is a lane of its own.) + function isBlockGroup(owner, kid) { + return !!owner && kid.classList.contains('sidechain') && agentDepth(kid) > agentDepth(owner); + } + // A sub-agent's or a workflow agent's transcript (not a fork). + const AGENT_LANE = /^(?:agent|wfagent)-/; - function buildModel() { - const transcript = document.getElementById('transcript'); - const memo = new Map(); - const model = { - items: [], - seqs: new Map(), - spawns: new Map(), // lane -> spawn item - merges: new Map(), // lane -> merge item - cls: new Map(), // element -> engine classes wanted - spawnCards: [], - }; - if (!transcript) return model; - function want(el, cls) { - const prev = model.cls.get(el); - model.cls.set(el, prev ? prev + ' ' + cls : cls); - } - function addItem(el, lane, kind, hidden) { - const item = { - el: el, lane: lane, kind: kind, - hidden: hidden || el.classList.contains('filtered-hidden') || el.classList.contains('search-hidden'), - row: -1, ts: NaN, session: session, - }; - model.items.push(item); - let seq = model.seqs.get(lane); - if (!seq) model.seqs.set(lane, seq = []); - seq.push(item); - if (kind === 'block') return item; - const spawns = el.getAttribute('data-spawns'); - if (spawns) { - model.spawnCards.push(item); - spawns.split(' ').forEach(function (id) { if (id) model.spawns.set(id, item); }); - } - const merges = el.getAttribute('data-merges'); - if (merges) merges.split(' ').forEach(function (id) { if (id) model.merges.set(id, item); }); - return item; - } - function walkContainer(container, lane, hidden) { - const kids = container.children; - for (let i = 0; i < kids.length; i++) { - const child = kids[i]; - const list = child.classList; - if (list.contains('message-node')) walkNode(child, lane, hidden); - else if (list.contains('fork-point')) addItem(child, child.getAttribute('data-lane') || lane, 'box', hidden); + function buildModel() { + const transcript = document.getElementById('transcript'); + const memo = new Map(); + const model = { + items: [], + seqs: new Map(), + spawns: new Map(), // lane -> spawn item + merges: new Map(), // lane -> merge item + cls: new Map(), // element -> engine classes wanted + spawnCards: [] + }; + if (!transcript) return model; + function want(el, cls) { + const prev = model.cls.get(el); + model.cls.set(el, prev ? prev + ' ' + cls : cls); + } + function addItem(el, lane, kind, hidden) { + const item = { + el: el, + lane: lane, + kind: kind, + hidden: hidden || el.classList.contains('filtered-hidden') || el.classList.contains('search-hidden'), + row: -1, + ts: NaN, + session: session + }; + model.items.push(item); + let seq = model.seqs.get(lane); + if (!seq) model.seqs.set(lane, (seq = [])); + seq.push(item); + if (kind === 'block') return item; + const spawns = el.getAttribute('data-spawns'); + if (spawns) { + model.spawnCards.push(item); + spawns.split(' ').forEach(function (id) { + if (id) model.spawns.set(id, item); + }); + } + const merges = el.getAttribute('data-merges'); + if (merges) + merges.split(' ').forEach(function (id) { + if (id) model.merges.set(id, item); + }); + return item; + } + function walkContainer(container, lane, hidden) { + const kids = container.children; + for (let i = 0; i < kids.length; i++) { + const child = kids[i]; + const list = child.classList; + if (list.contains('message-node')) walkNode(child, lane, hidden); + else if (list.contains('fork-point')) + addItem(child, child.getAttribute('data-lane') || lane, 'box', hidden); + } + } + function walkNode(node, lane, hidden) { + let card = null; + let cardLane = lane; + const kids = node.children; + for (let i = 0; i < kids.length; i++) { + const el = kids[i]; + const list = el.classList; + if (list.contains('message')) { + card = el; + cardLane = el.getAttribute('data-lane') || lane; + // A fork lane's head is its branch header: the + // whole node is the lane. + const head = el.getAttribute('data-lane-id'); + if (head && list.contains('session-header') && lanes.has(head) && !isOpen(head, memo)) { + want(node, 'dag-hidden'); + hidden = true; } - } - function walkNode(node, lane, hidden) { - let card = null; - let cardLane = lane; - const kids = node.children; - for (let i = 0; i < kids.length; i++) { - const el = kids[i]; - const list = el.classList; - if (list.contains('message')) { - card = el; - cardLane = el.getAttribute('data-lane') || lane; - // A fork lane's head is its branch header: the - // whole node is the lane. - const head = el.getAttribute('data-lane-id'); - if (head && list.contains('session-header') && lanes.has(head) && !isOpen(head, memo)) { - want(node, 'dag-hidden'); - hidden = true; - } - addItem(el, cardLane, 'card', hidden); - } else if (list.contains('children')) { - const folded = el.style.display === 'none'; - const first = el.querySelector(':scope > .message-node > .message'); - const kidLane = first ? (first.getAttribute('data-lane') || cardLane) : cardLane; - if (first && kidLane !== cardLane && AGENT_LANE.test(kidLane)) { - // A sub-agent's transcript: the lane decides. - if (card) want(card, 'dag-owner'); - // Folded, it is still walked (hidden): rows are - // packed as if every lane were open, so opening - // or folding one never renumbers the others. - const shut = lanes.has(kidLane) && !isOpen(kidLane, memo); - want(el, shut ? 'dag-hidden' : 'dag-entry'); - walkContainer(el, kidLane, hidden || shut); - } else if (first && isBlockGroup(card, first)) { - want(el, 'dag-block'); - addItem(el, cardLane, 'block', hidden || folded); - } else { - walkContainer(el, cardLane, hidden || folded); - } - } + addItem(el, cardLane, 'card', hidden); + } else if (list.contains('children')) { + const folded = el.style.display === 'none'; + const first = el.querySelector(':scope > .message-node > .message'); + const kidLane = first ? first.getAttribute('data-lane') || cardLane : cardLane; + if (first && kidLane !== cardLane && AGENT_LANE.test(kidLane)) { + // A sub-agent's transcript: the lane decides. + if (card) want(card, 'dag-owner'); + // Folded, it is still walked (hidden): rows are + // packed as if every lane were open, so opening + // or folding one never renumbers the others. + const shut = lanes.has(kidLane) && !isOpen(kidLane, memo); + want(el, shut ? 'dag-hidden' : 'dag-entry'); + walkContainer(el, kidLane, hidden || shut); + } else if (first && isBlockGroup(card, first)) { + want(el, 'dag-block'); + addItem(el, cardLane, 'block', hidden || folded); + } else { + walkContainer(el, cardLane, hidden || folded); } } - // Top-level nodes are sessions (a combined page holds many): - // each item records which, for the running-lane rule. - let session = 0; - const tops = transcript.children; - for (let i = 0; i < tops.length; i++) { - session = i; - const child = tops[i]; - if (child.classList.contains('message-node')) walkNode(child, 'main', false); - else if (child.classList.contains('fork-point')) addItem(child, child.getAttribute('data-lane') || 'main', 'box', false); - } - model.memo = memo; - return model; } + } + // Top-level nodes are sessions (a combined page holds many): + // each item records which, for the running-lane rule. + let session = 0; + const tops = transcript.children; + for (let i = 0; i < tops.length; i++) { + session = i; + const child = tops[i]; + if (child.classList.contains('message-node')) walkNode(child, 'main', false); + else if (child.classList.contains('fork-point')) + addItem(child, child.getAttribute('data-lane') || 'main', 'box', false); + } + model.memo = memo; + return model; + } - // ---- order: each lane keeps its DOM order; lanes merge by time -- - // Every lane takes part, folded or not (a folded lane's rows are - // empty and take no height), so the order — and every card's row — - // depends only on the DOM: toggling a lane rewrites nothing else. - function order(model) { - const main = model.seqs.get('main') || []; - const others = []; - model.seqs.forEach(function (seq, id) { if (id !== 'main') others.push(id); }); - if (!others.length) return main.slice(); - const seqs = [main].concat(others.map(function (id) { return model.seqs.get(id); })); - const ids = ['main'].concat(others); - // Timestamps: a card's own, else the previous one in its lane - // (fork heads: the lane's first time; else its spawn's). - seqs.forEach(function (seq, k) { - let prev = NaN; - if (k > 0) { - const lane = lanes.get(ids[k]); - const spawn = model.spawns.get(ids[k]); - if (lane && !isNaN(lane.firstTs)) prev = lane.firstTs; - else if (spawn) prev = cardTs(spawn.el, tsCache); - } - seq.forEach(function (item) { - const own = item.kind === 'card' ? cardTs(item.el, tsCache) : NaN; - item.ts = isNaN(own) ? prev : own; - if (!isNaN(item.ts)) prev = item.ts; - }); - }); - const pos = seqs.map(function () { return 0; }); - const index = new Map(); - ids.forEach(function (id, k) { index.set(id, k); }); - const emitted = new Set(); - const active = [0]; - const waiting = new Map(); // spawn item -> [lane slots] - for (let k = 1; k < ids.length; k++) { - const spawn = model.spawns.get(ids[k]); - if (spawn && spawn.lane !== ids[k]) { - const list = waiting.get(spawn) || []; - list.push(k); - waiting.set(spawn, list); - } else { - active.push(k); + // ---- order: each lane keeps its DOM order; lanes merge by time -- + // Every lane takes part, folded or not (a folded lane's rows are + // empty and take no height), so the order — and every card's row — + // depends only on the DOM: toggling a lane rewrites nothing else. + function order(model) { + const main = model.seqs.get('main') || []; + const others = []; + model.seqs.forEach(function (seq, id) { + if (id !== 'main') others.push(id); + }); + if (!others.length) return main.slice(); + const seqs = [main].concat( + others.map(function (id) { + return model.seqs.get(id); + }) + ); + const ids = ['main'].concat(others); + // Timestamps: a card's own, else the previous one in its lane + // (fork heads: the lane's first time; else its spawn's). + seqs.forEach(function (seq, k) { + let prev = NaN; + if (k > 0) { + const lane = lanes.get(ids[k]); + const spawn = model.spawns.get(ids[k]); + if (lane && !isNaN(lane.firstTs)) prev = lane.firstTs; + else if (spawn) prev = cardTs(spawn.el, tsCache); + } + seq.forEach(function (item) { + const own = item.kind === 'card' ? cardTs(item.el, tsCache) : NaN; + item.ts = isNaN(own) ? prev : own; + if (!isNaN(item.ts)) prev = item.ts; + }); + }); + const pos = seqs.map(function () { + return 0; + }); + const index = new Map(); + ids.forEach(function (id, k) { + index.set(id, k); + }); + const emitted = new Set(); + const active = [0]; + const waiting = new Map(); // spawn item -> [lane slots] + for (let k = 1; k < ids.length; k++) { + const spawn = model.spawns.get(ids[k]); + if (spawn && spawn.lane !== ids[k]) { + const list = waiting.get(spawn) || []; + list.push(k); + waiting.set(spawn, list); + } else { + active.push(k); + } + } + const out = []; + function blocked(item) { + const merges = item.kind === 'card' ? item.el.getAttribute('data-merges') : null; + if (!merges) return false; + return merges.split(' ').some(function (id) { + const k = index.get(id); + return k !== undefined && pos[k] < seqs[k].length; + }); + } + while (true) { + let best = -1; + let bestTs = Infinity; + for (let a = 0; a < active.length; a++) { + const k = active[a]; + if (pos[k] >= seqs[k].length) continue; + const item = seqs[k][pos[k]]; + if (blocked(item)) continue; + const ts = isNaN(item.ts) ? -Infinity : item.ts; + if (best < 0 || ts < bestTs) { + best = k; + bestTs = ts; + } + } + if (best < 0) { + // Everything left is waiting on something that cannot + // come first (clock skew, a lane without its spawn): + // take the earliest remaining lane in lane order. + for (let k = 0; k < seqs.length && best < 0; k++) { + if (pos[k] < seqs[k].length) { + best = k; + if (active.indexOf(k) < 0) active.push(k); } } - const out = []; - function blocked(item) { - const merges = item.kind === 'card' ? item.el.getAttribute('data-merges') : null; - if (!merges) return false; - return merges.split(' ').some(function (id) { - const k = index.get(id); - return k !== undefined && pos[k] < seqs[k].length; + if (best < 0) break; + } + // A tool call and its result are one step: the result + // half follows its call at once, unless it closes a lane + // whose rows must come first (a synchronous agent). + let item = null; + do { + item = seqs[best][pos[best]++]; + out.push(item); + emitted.add(item); + const opened = waiting.get(item); + if (opened) { + opened.forEach(function (k) { + if (active.indexOf(k) < 0) active.push(k); }); + waiting.delete(item); } - while (true) { - let best = -1; - let bestTs = Infinity; - for (let a = 0; a < active.length; a++) { - const k = active[a]; - if (pos[k] >= seqs[k].length) continue; - const item = seqs[k][pos[k]]; - if (blocked(item)) continue; - const ts = isNaN(item.ts) ? -Infinity : item.ts; - if (best < 0 || ts < bestTs) { best = k; bestTs = ts; } - } - if (best < 0) { - // Everything left is waiting on something that cannot - // come first (clock skew, a lane without its spawn): - // take the earliest remaining lane in lane order. - for (let k = 0; k < seqs.length && best < 0; k++) { - if (pos[k] < seqs[k].length) { - best = k; - if (active.indexOf(k) < 0) active.push(k); - } - } - if (best < 0) break; - } - // A tool call and its result are one step: the result - // half follows its call at once, unless it closes a lane - // whose rows must come first (a synchronous agent). - let item = null; - do { - item = seqs[best][pos[best]++]; - out.push(item); - emitted.add(item); - const opened = waiting.get(item); - if (opened) { - opened.forEach(function (k) { if (active.indexOf(k) < 0) active.push(k); }); - waiting.delete(item); - } - } while (opensPair(item) && pos[best] < seqs[best].length - && closesPair(seqs[best][pos[best]]) && !blocked(seqs[best][pos[best]])); - if (best > 0 && pos[best] >= seqs[best].length) { - const at = active.indexOf(best); - if (at >= 0) active.splice(at, 1); - } - } - return out; - } + } while ( + opensPair(item) && + pos[best] < seqs[best].length && + closesPair(seqs[best][pos[best]]) && + !blocked(seqs[best][pos[best]]) + ); + if (best > 0 && pos[best] >= seqs[best].length) { + const at = active.indexOf(best); + if (at >= 0) active.splice(at, 1); + } + } + return out; + } - function opensPair(item) { - const list = item.el.classList; - return item.kind === 'card' && (list.contains('pair_first') || list.contains('pair_middle')); - } - function closesPair(item) { - const list = item.el.classList; - return item.kind === 'card' && (list.contains('pair_middle') || list.contains('pair_last')); - } + function opensPair(item) { + const list = item.el.classList; + return item.kind === 'card' && (list.contains('pair_first') || list.contains('pair_middle')); + } + function closesPair(item) { + const list = item.el.classList; + return item.kind === 'card' && (list.contains('pair_middle') || list.contains('pair_last')); + } - // Rows — ported from DagRail.dc.html / DagLanes.dc.html - // renderVals() "Pack rows": time order is kept top to bottom; a - // lane's first row is below its spawn, a merge row below the - // merged lane's last row. `nextFree` is kept per column (`keyOf`: - // a column lane's id, else 'main'), so cards in different columns - // may share a row; without columns it is one item per row. - function pack(sequence, model, keyOf) { - const nextFree = new Map(); - const lastRowOf = new Map(); - let prev = 0; - sequence.forEach(function (item) { - const key = keyOf(item.lane); - item.key = key; - let r = Math.max(prev, nextFree.get(key) || 0); - if (item.lane !== 'main') { - const spawn = model.spawns.get(item.lane); - if (spawn && spawn.row >= 0 && !lastRowOf.has(item.lane)) r = Math.max(r, spawn.row + 1); - } - const merges = item.kind === 'card' ? item.el.getAttribute('data-merges') : null; - if (merges) { - merges.split(' ').forEach(function (id) { - if (lastRowOf.has(id)) r = Math.max(r, lastRowOf.get(id) + 1); - }); - } - item.row = r; - lastRowOf.set(item.lane, r); - nextFree.set(key, r + 1); - prev = r; + // Rows — ported from DagRail.dc.html / DagLanes.dc.html + // renderVals() "Pack rows": time order is kept top to bottom; a + // lane's first row is below its spawn, a merge row below the + // merged lane's last row. `nextFree` is kept per column (`keyOf`: + // a column lane's id, else 'main'), so cards in different columns + // may share a row; without columns it is one item per row. + function pack(sequence, model, keyOf) { + const nextFree = new Map(); + const lastRowOf = new Map(); + let prev = 0; + sequence.forEach(function (item) { + const key = keyOf(item.lane); + item.key = key; + let r = Math.max(prev, nextFree.get(key) || 0); + if (item.lane !== 'main') { + const spawn = model.spawns.get(item.lane); + if (spawn && spawn.row >= 0 && !lastRowOf.has(item.lane)) r = Math.max(r, spawn.row + 1); + } + const merges = item.kind === 'card' ? item.el.getAttribute('data-merges') : null; + if (merges) { + merges.split(' ').forEach(function (id) { + if (lastRowOf.has(id)) r = Math.max(r, lastRowOf.get(id) + 1); }); - return { rows: prev + 1, lastRowOf: lastRowOf }; } + item.row = r; + lastRowOf.set(item.lane, r); + nextFree.set(key, r + 1); + prev = r; + }); + return { rows: prev + 1, lastRowOf: lastRowOf }; + } - // Rail — the "Rail" part of renderVals(): slot 0 is the main line; - // every lane with something to draw gets the lowest slot free over - // its span [spawn row, end row] (greedy interval colouring; the - // mockup hard-coded its slots). End: the merge row; else, when - // interleaved, the lane's last row; else its spawn row (a stub). - // Interleaved lanes are placed first — their cards sit in their - // slot — then folded ones while slots last. - function railLanes(model, open, packed, skip, running) { - const memo = model.memo; - const plans = []; - lanes.forEach(function (lane, id) { - // Columns have no rail (the mockup's `railed` = folded + - // interleaved), nor do lanes inside one, nor the lanes - // behind a turn's "+N more branches". - if (skip(id)) return; - const opened = open.indexOf(id) >= 0; - const spawn = model.spawns.get(id); - const parentOpen = lane.parent === 'main' || !lanes.has(lane.parent) || isOpen(lane.parent, memo); - if (!parentOpen) return; - const seq = opened ? (model.seqs.get(id) || []) : []; - if (!spawn && !seq.length) return; - const merge = model.merges.get(id); - const first = seq.length ? seq[0].row : -1; - const last = seq.length ? packed.lastRowOf.get(id) : -1; - const start = spawn && spawn.row >= 0 ? spawn.row : first; - let end = start; - const live = running.has(id); - if (merge && merge.row >= 0) end = merge.row; - else if (live) end = packed.rows - 1; // runs on to the newest row - else if (opened && last >= 0) end = last; - plans.push({ id: id, lane: lane, open: opened, running: live, spawn: spawn, merge: merge, seq: seq, start: start, end: Math.max(start, end) }); - }); - plans.sort(function (a, b) { - if (a.open !== b.open) return a.open ? -1 : 1; - return a.start - b.start; - }); - const slots = []; // slot -> [ [start, end], ... ] - const slotOf = new Map(); - plans.forEach(function (plan) { - const parentSlot = slotOf.get(plan.lane.parent) || 0; - let s = parentSlot + 1; - for (; s <= MAX_SLOTS; s++) { - const taken = (slots[s] || []).some(function (span) { - return plan.start <= span[1] && span[0] <= plan.end; - }); - if (!taken) break; - } - if (s > MAX_SLOTS) { - if (!plan.open) return; // no room: control only - s = MAX_SLOTS; - } - (slots[s] = slots[s] || []).push([plan.start, plan.end]); - slotOf.set(plan.id, s); - plan.slot = s; - plan.colour = plan.lane.kind === 'fork' ? 'f' : AGENT_COLOURS[(s - 1) % AGENT_COLOURS.length]; + // Rail — the "Rail" part of renderVals(): slot 0 is the main line; + // every lane with something to draw gets the lowest slot free over + // its span [spawn row, end row] (greedy interval colouring; the + // mockup hard-coded its slots). End: the merge row; else, when + // interleaved, the lane's last row; else its spawn row (a stub). + // Interleaved lanes are placed first — their cards sit in their + // slot — then folded ones while slots last. + function railLanes(model, open, packed, skip, running) { + const memo = model.memo; + const plans = []; + lanes.forEach(function (lane, id) { + // Columns have no rail (the mockup's `railed` = folded + + // interleaved), nor do lanes inside one, nor the lanes + // behind a turn's "+N more branches". + if (skip(id)) return; + const opened = open.indexOf(id) >= 0; + const spawn = model.spawns.get(id); + const parentOpen = lane.parent === 'main' || !lanes.has(lane.parent) || isOpen(lane.parent, memo); + if (!parentOpen) return; + const seq = opened ? model.seqs.get(id) || [] : []; + if (!spawn && !seq.length) return; + const merge = model.merges.get(id); + const first = seq.length ? seq[0].row : -1; + const last = seq.length ? packed.lastRowOf.get(id) : -1; + const start = spawn && spawn.row >= 0 ? spawn.row : first; + let end = start; + const live = running.has(id); + if (merge && merge.row >= 0) end = merge.row; + else if (live) end = packed.rows - 1; // runs on to the newest row + else if (opened && last >= 0) end = last; + plans.push({ + id: id, + lane: lane, + open: opened, + running: live, + spawn: spawn, + merge: merge, + seq: seq, + start: start, + end: Math.max(start, end) + }); + }); + plans.sort(function (a, b) { + if (a.open !== b.open) return a.open ? -1 : 1; + return a.start - b.start; + }); + const slots = []; // slot -> [ [start, end], ... ] + const slotOf = new Map(); + plans.forEach(function (plan) { + const parentSlot = slotOf.get(plan.lane.parent) || 0; + let s = parentSlot + 1; + for (; s <= MAX_SLOTS; s++) { + const taken = (slots[s] || []).some(function (span) { + return plan.start <= span[1] && span[0] <= plan.end; }); - return { plans: plans.filter(function (p) { return p.slot; }), slotOf: slotOf, slots: slots.length ? slots.length - 1 : 0 }; - } + if (!taken) break; + } + if (s > MAX_SLOTS) { + if (!plan.open) return; // no room: control only + s = MAX_SLOTS; + } + (slots[s] = slots[s] || []).push([plan.start, plan.end]); + slotOf.set(plan.id, s); + plan.slot = s; + plan.colour = plan.lane.kind === 'fork' ? 'f' : AGENT_COLOURS[(s - 1) % AGENT_COLOURS.length]; + }); + return { + plans: plans.filter(function (p) { + return p.slot; + }), + slotOf: slotOf, + slots: slots.length ? slots.length - 1 : 0 + }; + } - // ---- writes ---------------------------------------------------- - const ENGINE_CLASSES = /(?:^|\s)(dag-[a-z0-9-]+)(?=\s|$)/g; - let classed = new Set(); - const rowWritten = new WeakMap(); - const colWritten = new WeakMap(); - const tagWritten = new WeakMap(); + // ---- writes ---------------------------------------------------- + const ENGINE_CLASSES = /(?:^|\s)(dag-[a-z0-9-]+)(?=\s|$)/g; + let classed = new Set(); + const rowWritten = new WeakMap(); + const colWritten = new WeakMap(); + const tagWritten = new WeakMap(); - function setEngineClasses(el, wanted) { - const current = []; - el.className.replace(ENGINE_CLASSES, function (m, cls) { current.push(cls); return m; }); - const next = wanted ? wanted.split(' ') : []; - current.forEach(function (cls) { if (next.indexOf(cls) < 0) el.classList.remove(cls); }); - next.forEach(function (cls) { if (cls && current.indexOf(cls) < 0) el.classList.add(cls); }); - } + function setEngineClasses(el, wanted) { + const current = []; + el.className.replace(ENGINE_CLASSES, function (m, cls) { + current.push(cls); + return m; + }); + const next = wanted ? wanted.split(' ') : []; + current.forEach(function (cls) { + if (next.indexOf(cls) < 0) el.classList.remove(cls); + }); + next.forEach(function (cls) { + if (cls && current.indexOf(cls) < 0) el.classList.add(cls); + }); + } - function cssString(text) { - return '"' + String(text).replace(/["\\\n]/g, function (c) { return c === '\n' ? ' ' : '\\' + c; }) + '"'; - } + function cssString(text) { + return ( + '"' + + String(text).replace(/["\\\n]/g, function (c) { + return c === '\n' ? ' ' : '\\' + c; + }) + + '"' + ); + } - const CHEVRON = ""; + const CHEVRON = + ""; - const forkTimes = new Map(); - function localTime(cardId) { - const card = cardId ? document.getElementById('msg-' + cardId) : null; - const stamp = card ? card.querySelector('.timestamp[data-timestamp]') : null; - const date = stamp ? new Date(stamp.getAttribute('data-timestamp')) : null; - if (!date || isNaN(date.getTime())) return ''; - return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); - } + const forkTimes = new Map(); + function localTime(cardId) { + const card = cardId ? document.getElementById('msg-' + cardId) : null; + const stamp = card ? card.querySelector('.timestamp[data-timestamp]') : null; + const date = stamp ? new Date(stamp.getAttribute('data-timestamp')) : null; + if (!date || isNaN(date.getTime())) return ''; + return date.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + } - // One control per lane, in a container the engine owns at the end - // of the spawn card (grid row 4 of the card). Labels are generated - // content (`data-label`), so search and the timeline never index - // them. A live patch replaces the card and drops the container; - // the next relayout puts it back. `overflow` hides the controls of - // a turn's lanes past the third (until "+N more branches") and - // says which control carries that toggle. - function setAttr(el, name, value) { - if (el.getAttribute(name) !== value) el.setAttribute(name, value); - } - // An async spawn's result card that holds nothing but the launch - // acknowledgement (`.mn-ack`, html/renderer.py) and its - // `Result ↓` line (P7c compact spawn rows): the engine hides it - // (`dag-ack`) and the spawn's control carries both — a - // `Result ↓` link and a `launched` toggle that shows the card - // again. A result card with anything else on it stays. - function ackCardOf(card) { - const node = card.parentElement; - const next = node && node.classList.contains('message-node') ? node.nextElementSibling : null; - const result = next && next.classList.contains('message-node') - ? next.querySelector(':scope > .message.tool_result.pair_last') : null; - const content = result ? result.querySelector(':scope > .content') : null; - if (!content || !content.querySelector(':scope > .mn-ack')) return null; - const kids = content.children; - for (let i = 0; i < kids.length; i++) { - if (!kids[i].classList.contains('mn-ack') && !kids[i].classList.contains('mn-async-jump')) return null; - } - return result; - } - function ensureExtra(ctl, cls, tag, before) { - let el = ctl.querySelector(':scope > .' + cls); - if (!el) { - el = document.createElement(tag); - el.className = cls + (tag === 'button' ? ' mn-bmini' : ''); - if (tag === 'button') el.type = 'button'; - ctl.insertBefore(el, before); - } - return el; - } - function ensureControls(item, colours, memo, overflow, running, acks) { - const ids = (item.el.getAttribute('data-spawns') || '').split(' ').filter(function (id) { return lanes.has(id); }); - let box = item.el.querySelector(':scope > .mn-bctls'); - if (!ids.length) { - if (box) box.remove(); - return; - } - if (!box) { - box = document.createElement('div'); - box.className = 'mn-bctls'; - item.el.appendChild(box); + // One control per lane, in a container the engine owns at the end + // of the spawn card (grid row 4 of the card). Labels are generated + // content (`data-label`), so search and the timeline never index + // them. A live patch replaces the card and drops the container; + // the next relayout puts it back. `overflow` hides the controls of + // a turn's lanes past the third (until "+N more branches") and + // says which control carries that toggle. + function setAttr(el, name, value) { + if (el.getAttribute(name) !== value) el.setAttribute(name, value); + } + // An async spawn's result card that holds nothing but the launch + // acknowledgement (`.mn-ack`, html/renderer.py) and its + // `Result ↓` line (P7c compact spawn rows): the engine hides it + // (`dag-ack`) and the spawn's control carries both — a + // `Result ↓` link and a `launched` toggle that shows the card + // again. A result card with anything else on it stays. + function ackCardOf(card) { + const node = card.parentElement; + const next = node && node.classList.contains('message-node') ? node.nextElementSibling : null; + const result = + next && next.classList.contains('message-node') + ? next.querySelector(':scope > .message.tool_result.pair_last') + : null; + const content = result ? result.querySelector(':scope > .content') : null; + if (!content || !content.querySelector(':scope > .mn-ack')) return null; + const kids = content.children; + for (let i = 0; i < kids.length; i++) { + if (!kids[i].classList.contains('mn-ack') && !kids[i].classList.contains('mn-async-jump')) return null; + } + return result; + } + function ensureExtra(ctl, cls, tag, before) { + let el = ctl.querySelector(':scope > .' + cls); + if (!el) { + el = document.createElement(tag); + el.className = cls + (tag === 'button' ? ' mn-bmini' : ''); + if (tag === 'button') el.type = 'button'; + ctl.insertBefore(el, before); + } + return el; + } + function ensureControls(item, colours, memo, overflow, running, acks) { + const ids = (item.el.getAttribute('data-spawns') || '').split(' ').filter(function (id) { + return lanes.has(id); + }); + let box = item.el.querySelector(':scope > .mn-bctls'); + if (!ids.length) { + if (box) box.remove(); + return; + } + if (!box) { + box = document.createElement('div'); + box.className = 'mn-bctls'; + item.el.appendChild(box); + } + ids.sort(function (a, b) { + return lanes.get(a).rank - lanes.get(b).rank; + }); + const existing = Array.from(box.children); + ids.forEach(function (id, i) { + const lane = lanes.get(id); + let ctl = existing.find(function (el) { + return el.getAttribute('data-lane-ref') === id; + }); + if (!ctl) { + ctl = document.createElement('div'); + ctl.setAttribute('data-lane-ref', id); + ctl.innerHTML = + "' + + "" + + ""; + } + if (box.children[i] !== ctl) box.insertBefore(ctl, box.children[i] || null); + const mode = modeOf(id); + const opened = isOpen(id, memo); + const columned = inColumn(mode) && parentOpen(id, memo); + const interleaved = opened && !columned; + const colour = colours.get(id) || (lane.kind === 'fork' ? 'f' : 'a'); + const live = running.has(id); + const cls = + 'mn-bctl dag-lc-' + + colour + + (interleaved ? ' is-open' : '') + + (columned ? ' is-col' : '') + + (live ? ' is-running' : ''); + if (ctl.className !== cls) ctl.className = cls; + const hide = overflow.hidden.has(id); + if (ctl.hidden !== hide) ctl.hidden = hide; + const fork = lane.kind === 'fork'; + // A phase spawns several workflow agents: each control + // names its agent (a Task's spawn row is its own name). + const named = fork || lane.kind === 'workflow-agent'; + // An agent without a result that is not running ended + // without one (or the page is a static copy). + const unfinished = !live && (lane.state === 'open' || lane.state === 'ended'); + const label = + (fork ? '⑂ ' : '') + + (named ? lane.name + (lane.stats ? ' · ' : '') : '') + + (named ? lane.stats : lane.stats || lane.name) + + (unfinished ? ' · no result' : ''); + // The mode is its own part (P8): on a narrow line the + // label gives way (ellipsis), the mode stays readable. + const modeText = interleaved ? ' · interleaved' : columned ? ' · in column →' : ''; + const btn = ctl.querySelector('.mn-bfold'); + setAttr(btn, 'data-label', label); + if (modeText) setAttr(btn, 'data-mode', modeText); + else if (btn.hasAttribute('data-mode')) btn.removeAttribute('data-mode'); + setAttr(btn, 'aria-expanded', opened ? 'true' : 'false'); + const runLabel = live ? runningText(running, id) : 'running'; + const pill = ctl.querySelector('.mn-brun'); + if (pill) setAttr(pill, 'data-label', runLabel); + setAttr( + btn, + 'aria-label', + (mode === 'folded' ? 'Interleave branch: ' : 'Fold branch: ') + + lane.name + + (lane.stats + ? ' (' + lane.stats + (live ? ', ' + runLabel : unfinished ? ', no result' : '') + ')' + : '') + ); + // The stats too: a narrow control truncates its label (P8). + let title = + lane.name + + (lane.meta ? ' · ' + lane.meta : '') + + (lane.stats ? ' · ' + lane.stats : '') + + (live ? ' · still ' + runLabel : unfinished ? ' · ended without a result' : ''); + if (fork) { + // Keyed by lane and card: a live swap can renumber d-N. + const key = id + '|' + lane.from; + if (!forkTimes.has(key)) forkTimes.set(key, localTime(lane.from)); + const at = forkTimes.get(key); + title = lane.name + ' · rewound' + (at ? ' to ' + at : ''); + } + if (btn.title !== title) btn.title = title; + const col = ctl.querySelector('.mn-bcol'); + if (col) { + setAttr(col, 'data-label', inColumn(mode) ? '⇤ Interleave' : 'Column ⇥'); + setAttr( + col, + 'aria-label', + (inColumn(mode) ? 'Interleave branch: ' : 'Show in its own column: ') + lane.name + ); + const colTitle = inColumn(mode) + ? 'Merge this branch back into the main line' + : 'Show this branch in its own column'; + if (col.title !== colTitle) col.title = colTitle; + } + // P7c: an async agent's `Result ↓` (its answer is on the + // notification, the merge row further down) and the + // folded launch acknowledgement, between the stats and + // the Column button. + const resultHref = lane.kind === 'async-agent' && lane.to ? '#msg-' + lane.to : ''; + let res = ctl.querySelector(':scope > .mn-bres'); + if (resultHref) { + res = ensureExtra(ctl, 'mn-bres', 'a', col); + setAttr(res, 'href', resultHref); + setAttr(res, 'data-label', 'Result ↓'); + setAttr(res, 'aria-label', 'Result of ' + lane.name + ' (with the async notification)'); + if (res.title !== 'Jump to the result, with the async notification') + res.title = 'Jump to the result, with the async notification'; + } else if (res) { + res.remove(); + } + let ack = ctl.querySelector(':scope > .mn-back'); + if (acks.has(id)) { + ack = ensureExtra(ctl, 'mn-back', 'button', col); + const shown = acksShown.has(id); + setAttr(ack, 'data-label', shown ? 'launched ▾' : 'launched ▸'); + setAttr(ack, 'aria-expanded', shown ? 'true' : 'false'); + setAttr(ack, 'aria-label', (shown ? 'Hide' : 'Show') + ' the launch acknowledgement of ' + lane.name); + if (ack.title !== 'The launch acknowledgement (the tool result)') + ack.title = 'The launch acknowledgement (the tool result)'; + } else if (ack) { + ack.remove(); + } + // "+N more branches" / "− fewer branches" on the turn's + // third control. + const more = overflow.anchors.get(id); + let toggle = ctl.querySelector('.mn-bmore'); + if (more) { + if (!toggle) { + toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'mn-bmini mn-bmore'; + ctl.appendChild(toggle); } - ids.sort(function (a, b) { return lanes.get(a).rank - lanes.get(b).rank; }); - const existing = Array.from(box.children); - ids.forEach(function (id, i) { - const lane = lanes.get(id); - let ctl = existing.find(function (el) { return el.getAttribute('data-lane-ref') === id; }); - if (!ctl) { - ctl = document.createElement('div'); - ctl.setAttribute('data-lane-ref', id); - ctl.innerHTML = "" - + "" - + ""; - } - if (box.children[i] !== ctl) box.insertBefore(ctl, box.children[i] || null); - const mode = modeOf(id); - const opened = isOpen(id, memo); - const columned = inColumn(mode) && parentOpen(id, memo); - const interleaved = opened && !columned; - const colour = colours.get(id) || (lane.kind === 'fork' ? 'f' : 'a'); - const live = running.has(id); - const cls = 'mn-bctl dag-lc-' + colour + (interleaved ? ' is-open' : '') + (columned ? ' is-col' : '') - + (live ? ' is-running' : ''); - if (ctl.className !== cls) ctl.className = cls; - const hide = overflow.hidden.has(id); - if (ctl.hidden !== hide) ctl.hidden = hide; - const fork = lane.kind === 'fork'; - // A phase spawns several workflow agents: each control - // names its agent (a Task's spawn row is its own name). - const named = fork || lane.kind === 'workflow-agent'; - // An agent without a result that is not running ended - // without one (or the page is a static copy). - const unfinished = !live && (lane.state === 'open' || lane.state === 'ended'); - const label = (fork ? '⑂ ' : '') + (named ? lane.name + (lane.stats ? ' · ' : '') : '') - + (named ? lane.stats : (lane.stats || lane.name)) - + (unfinished ? ' · no result' : ''); - // The mode is its own part (P8): on a narrow line the - // label gives way (ellipsis), the mode stays readable. - const modeText = interleaved ? ' · interleaved' : columned ? ' · in column →' : ''; - const btn = ctl.querySelector('.mn-bfold'); - setAttr(btn, 'data-label', label); - if (modeText) setAttr(btn, 'data-mode', modeText); - else if (btn.hasAttribute('data-mode')) btn.removeAttribute('data-mode'); - setAttr(btn, 'aria-expanded', opened ? 'true' : 'false'); - const runLabel = live ? runningText(running, id) : 'running'; - const pill = ctl.querySelector('.mn-brun'); - if (pill) setAttr(pill, 'data-label', runLabel); - setAttr(btn, 'aria-label', (mode === 'folded' ? 'Interleave branch: ' : 'Fold branch: ') + lane.name - + (lane.stats ? ' (' + lane.stats + (live ? ', ' + runLabel : unfinished ? ', no result' : '') + ')' : '')); - // The stats too: a narrow control truncates its label (P8). - let title = lane.name + (lane.meta ? ' · ' + lane.meta : '') + (lane.stats ? ' · ' + lane.stats : '') - + (live ? ' · still ' + runLabel : unfinished ? ' · ended without a result' : ''); - if (fork) { - // Keyed by lane and card: a live swap can renumber d-N. - const key = id + '|' + lane.from; - if (!forkTimes.has(key)) forkTimes.set(key, localTime(lane.from)); - const at = forkTimes.get(key); - title = lane.name + ' · rewound' + (at ? ' to ' + at : ''); - } - if (btn.title !== title) btn.title = title; - const col = ctl.querySelector('.mn-bcol'); - if (col) { - setAttr(col, 'data-label', inColumn(mode) ? '⇤ Interleave' : 'Column ⇥'); - setAttr(col, 'aria-label', (inColumn(mode) ? 'Interleave branch: ' : 'Show in its own column: ') + lane.name); - const colTitle = inColumn(mode) ? 'Merge this branch back into the main line' : 'Show this branch in its own column'; - if (col.title !== colTitle) col.title = colTitle; - } - // P7c: an async agent's `Result ↓` (its answer is on the - // notification, the merge row further down) and the - // folded launch acknowledgement, between the stats and - // the Column button. - const resultHref = lane.kind === 'async-agent' && lane.to ? '#msg-' + lane.to : ''; - let res = ctl.querySelector(':scope > .mn-bres'); - if (resultHref) { - res = ensureExtra(ctl, 'mn-bres', 'a', col); - setAttr(res, 'href', resultHref); - setAttr(res, 'data-label', 'Result ↓'); - setAttr(res, 'aria-label', 'Result of ' + lane.name + ' (with the async notification)'); - if (res.title !== 'Jump to the result, with the async notification') res.title = 'Jump to the result, with the async notification'; - } else if (res) { - res.remove(); - } - let ack = ctl.querySelector(':scope > .mn-back'); - if (acks.has(id)) { - ack = ensureExtra(ctl, 'mn-back', 'button', col); - const shown = acksShown.has(id); - setAttr(ack, 'data-label', shown ? 'launched ▾' : 'launched ▸'); - setAttr(ack, 'aria-expanded', shown ? 'true' : 'false'); - setAttr(ack, 'aria-label', (shown ? 'Hide' : 'Show') + ' the launch acknowledgement of ' + lane.name); - if (ack.title !== 'The launch acknowledgement (the tool result)') ack.title = 'The launch acknowledgement (the tool result)'; - } else if (ack) { - ack.remove(); - } - // "+N more branches" / "− fewer branches" on the turn's - // third control. - const more = overflow.anchors.get(id); - let toggle = ctl.querySelector('.mn-bmore'); - if (more) { - if (!toggle) { - toggle = document.createElement('button'); - toggle.type = 'button'; - toggle.className = 'mn-bmini mn-bmore'; - ctl.appendChild(toggle); - } - setAttr(toggle, 'data-turn', more.turn); - const agents = more.noun === 'agent'; - const plural = agents ? 'agents' : 'branches'; - const counted = more.count + ' more ' + (more.count === 1 ? more.noun : plural); - const scope = agents ? 'this workflow phase' : 'this turn'; - setAttr(toggle, 'data-label', more.expanded ? '− fewer ' + plural : '+' + counted); - setAttr(toggle, 'aria-label', more.expanded ? 'Hide the extra ' + plural + ' of ' + scope : 'Show ' + counted + ' of ' + scope); - setAttr(toggle, 'aria-expanded', more.expanded ? 'true' : 'false'); - } else if (toggle) { - toggle.remove(); - } - }); - existing.forEach(function (el) { - if (ids.indexOf(el.getAttribute('data-lane-ref')) < 0) el.remove(); - }); - } + setAttr(toggle, 'data-turn', more.turn); + const agents = more.noun === 'agent'; + const plural = agents ? 'agents' : 'branches'; + const counted = more.count + ' more ' + (more.count === 1 ? more.noun : plural); + const scope = agents ? 'this workflow phase' : 'this turn'; + setAttr(toggle, 'data-label', more.expanded ? '− fewer ' + plural : '+' + counted); + setAttr( + toggle, + 'aria-label', + more.expanded ? 'Hide the extra ' + plural + ' of ' + scope : 'Show ' + counted + ' of ' + scope + ); + setAttr(toggle, 'aria-expanded', more.expanded ? 'true' : 'false'); + } else if (toggle) { + toggle.remove(); + } + }); + existing.forEach(function (el) { + if (ids.indexOf(el.getAttribute('data-lane-ref')) < 0) el.remove(); + }); + } - // Branch overflow (spec § 1.6.3, per user turn): of a turn's - // top-level lanes (spawned from the main line), the first three by - // rank keep a control (and a rail lane); the rest stay folded - // behind a "+N more branches" toggle on the third control, unless - // the turn is expanded or the lane is not folded (a lane the - // reader opened, or a reveal did, always shows its control). - // Nested lanes are left out: they only show once the reader opens - // their parent, and counting them would make opening one lane hide - // another's control. A workflow group ("+N more agents") counts - // at any depth: its lanes share one spawn row, so one parent. - function computeOverflow(model) { - const hidden = new Set(); - const anchors = new Map(); - const byTurn = new Map(); - lanes.forEach(function (lane, id) { - if ((parentOf(id) && !lane.group) || !model.spawns.has(id)) return; - const turn = turnOf(id); - const list = byTurn.get(turn) || []; - list.push(lane); - byTurn.set(turn, list); - }); - byTurn.forEach(function (list, turn) { - if (list.length <= CAP) return; - list.sort(function (a, b) { return a.rank - b.rank; }); - const isExpanded = expanded.has(turn); - let count = 0; - list.slice(CAP).forEach(function (lane) { - if (modeOf(lane.id) !== 'folded') return; - count++; - if (!isExpanded) hidden.add(lane.id); - }); - const noun = list[CAP - 1].group ? 'agent' : 'branch'; - if (isExpanded || count) anchors.set(list[CAP - 1].id, { turn: turn, count: count, expanded: isExpanded, noun: noun }); - }); - return { hidden: hidden, anchors: anchors }; - } + // Branch overflow (spec § 1.6.3, per user turn): of a turn's + // top-level lanes (spawned from the main line), the first three by + // rank keep a control (and a rail lane); the rest stay folded + // behind a "+N more branches" toggle on the third control, unless + // the turn is expanded or the lane is not folded (a lane the + // reader opened, or a reveal did, always shows its control). + // Nested lanes are left out: they only show once the reader opens + // their parent, and counting them would make opening one lane hide + // another's control. A workflow group ("+N more agents") counts + // at any depth: its lanes share one spawn row, so one parent. + function computeOverflow(model) { + const hidden = new Set(); + const anchors = new Map(); + const byTurn = new Map(); + lanes.forEach(function (lane, id) { + if ((parentOf(id) && !lane.group) || !model.spawns.has(id)) return; + const turn = turnOf(id); + const list = byTurn.get(turn) || []; + list.push(lane); + byTurn.set(turn, list); + }); + byTurn.forEach(function (list, turn) { + if (list.length <= CAP) return; + list.sort(function (a, b) { + return a.rank - b.rank; + }); + const isExpanded = expanded.has(turn); + let count = 0; + list.slice(CAP).forEach(function (lane) { + if (modeOf(lane.id) !== 'folded') return; + count++; + if (!isExpanded) hidden.add(lane.id); + }); + const noun = list[CAP - 1].group ? 'agent' : 'branch'; + if (isExpanded || count) + anchors.set(list[CAP - 1].id, { turn: turn, count: count, expanded: isExpanded, noun: noun }); + }); + return { hidden: hidden, anchors: anchors }; + } - function markToolbar() { - const seg = document.querySelector('.mn-branches'); - if (!seg) return; - const has = lanes.size > 0; - if (seg.hidden === has) seg.hidden = !has; - const match = matchingGlobal(); - seg.querySelectorAll('[data-mn-branches]').forEach(function (btn) { - const on = btn.getAttribute('data-mn-branches') === match; - btn.classList.toggle('on', on); - btn.setAttribute('aria-pressed', on ? 'true' : 'false'); - }); - } + function markToolbar() { + const seg = document.querySelector('.mn-branches'); + if (!seg) return; + const has = lanes.size > 0; + if (seg.hidden === has) seg.hidden = !has; + const match = matchingGlobal(); + seg.querySelectorAll('[data-mn-branches]').forEach(function (btn) { + const on = btn.getAttribute('data-mn-branches') === match; + btn.classList.toggle('on', on); + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + }); + } - // ---- relayout -------------------------------------------------- - let observer = null; - let observed = null; - let frame = 0; + // ---- relayout -------------------------------------------------- + let observer = null; + let observed = null; + let frame = 0; - // Column tracks: lanes in a column (or strip) whose parent is - // shown, ordered by spawn row, each nested lane right after its - // parent column's subtree (DagLanes.dc.html, generalised). - function columnTracks(model, memo) { - const tracks = []; - lanes.forEach(function (lane, id) { - if (inColumn(modeOf(id)) && parentOpen(id, memo)) tracks.push(id); - }); - if (!tracks.length) return tracks; - const set = new Set(tracks); - function spawnRow(id) { - const spawn = model.spawns.get(id); - if (spawn && spawn.row >= 0) return spawn.row; - const seq = model.seqs.get(id); - return seq && seq.length ? seq[0].row : Infinity; + // Column tracks: lanes in a column (or strip) whose parent is + // shown, ordered by spawn row, each nested lane right after its + // parent column's subtree (DagLanes.dc.html, generalised). + function columnTracks(model, memo) { + const tracks = []; + lanes.forEach(function (lane, id) { + if (inColumn(modeOf(id)) && parentOpen(id, memo)) tracks.push(id); + }); + if (!tracks.length) return tracks; + const set = new Set(tracks); + function spawnRow(id) { + const spawn = model.spawns.get(id); + if (spawn && spawn.row >= 0) return spawn.row; + const seq = model.seqs.get(id); + return seq && seq.length ? seq[0].row : Infinity; + } + const kids = new Map(); + const roots = []; + tracks.forEach(function (id) { + let up = parentOf(id); + while (up && !set.has(up)) up = parentOf(up); + if (up) { + if (!kids.has(up)) kids.set(up, []); + kids.get(up).push(id); + } else { + roots.push(id); + } + }); + const byRow = function (a, b) { + return spawnRow(a) - spawnRow(b) || lanes.get(a).rank - lanes.get(b).rank; + }; + const out = []; + (function visit(list) { + list.sort(byRow).forEach(function (id) { + out.push(id); + if (kids.has(id)) visit(kids.get(id)); + }); + })(roots); + return out; + } + + // Column chrome: per column a background spanning every row (the + // mockup's `colbg`: lane-colour edge + tint) holding its head — a + // name / meta line with "⇤ Interleave" and "Collapse", sticky under + // the toolbar — or, collapsed, a narrow strip with the name set + // vertically (click to expand); plus the main column's "Main + // session" head. They are engine-owned grid items at the head of + // #transcript: first in DOM order, so they paint under the cards. + // Nothing else reads them (live updates, search, the filter and + // the timeline all work on `.message-node` / `.message`), and a + // wholesale swap drops them until the next relayout. + const chrome = new Map(); // column key -> element + function syncChrome(transcript, tracks, colours, rows, running) { + const wanted = tracks.length ? ['main'].concat(tracks) : []; + chrome.forEach(function (el, key) { + if (wanted.indexOf(key) < 0 || el.parentNode !== transcript) { + el.remove(); + if (wanted.indexOf(key) < 0) chrome.delete(key); + } + }); + transcript.querySelectorAll(':scope > .dag-chrome').forEach(function (el) { + if (wanted.indexOf(el.getAttribute('data-col')) < 0) el.remove(); + }); + wanted.forEach(function (key, i) { + const main = key === 'main'; + const lane = main ? null : lanes.get(key); + const isStrip = !main && modeOf(key) === 'strip'; + let el = chrome.get(key); + const shape = main ? 'main' : isStrip ? 'strip' : 'col'; + if (!el || el.getAttribute('data-shape') !== shape) { + if (el) el.remove(); + el = document.createElement('div'); + el.setAttribute('data-col', key); + el.setAttribute('data-shape', shape); + if (main) { + el.innerHTML = "
"; + } else if (isStrip) { + el.innerHTML = + ""; + } else { + el.innerHTML = + "
" + + "" + + "
"; + } + chrome.set(key, el); + } + const cls = + 'dag-chrome dag-colbg' + + (main ? ' dag-mainbg' : ' dag-lc-' + (colours.get(key) || 'a')) + + (isStrip ? ' dag-stripbg' : ''); + if (el.className !== cls) el.className = cls; + el.style.gridColumn = String(i + 1); + el.style.gridRow = '1 / span ' + Math.max(1, rows + ROW0 - 1); + if (lane) { + // Stats first (P8): they are what a narrow head keeps. + const meta = [lane.stats, running.has(key) ? runningText(running, key) : '', lane.meta] + .filter(Boolean) + .join(' · '); + if (isStrip) { + const btn = el.querySelector('.dag-strip'); + setAttr(btn, 'aria-label', 'Expand column: ' + lane.name); + btn.title = lane.name + (meta ? ' · ' + meta : ''); + setAttr(btn.firstChild, 'data-label', lane.name); + } else { + setAttr( + el.querySelector('.dag-colname'), + 'data-label', + (lane.kind === 'fork' ? '⑂ ' : '') + lane.name + ); + setAttr(el.querySelector('.dag-colmeta'), 'data-label', meta); + const head = el.querySelector('.dag-colhead'); + const title = lane.name + (meta ? ' · ' + meta : ''); + if (head.title !== title) head.title = title; + setAttr( + el.querySelector("[data-col-act='interleave']"), + 'aria-label', + 'Interleave branch: ' + lane.name + ); + setAttr( + el.querySelector("[data-col-act='collapse']"), + 'aria-label', + 'Collapse column: ' + lane.name + ); } - const kids = new Map(); - const roots = []; - tracks.forEach(function (id) { - let up = parentOf(id); - while (up && !set.has(up)) up = parentOf(up); - if (up) { - if (!kids.has(up)) kids.set(up, []); - kids.get(up).push(id); - } else { - roots.push(id); - } - }); - const byRow = function (a, b) { - return (spawnRow(a) - spawnRow(b)) || (lanes.get(a).rank - lanes.get(b).rank); - }; - const out = []; - (function visit(list) { - list.sort(byRow).forEach(function (id) { - out.push(id); - if (kids.has(id)) visit(kids.get(id)); - }); - })(roots); - return out; } - - // Column chrome: per column a background spanning every row (the - // mockup's `colbg`: lane-colour edge + tint) holding its head — a - // name / meta line with "⇤ Interleave" and "Collapse", sticky under - // the toolbar — or, collapsed, a narrow strip with the name set - // vertically (click to expand); plus the main column's "Main - // session" head. They are engine-owned grid items at the head of - // #transcript: first in DOM order, so they paint under the cards. - // Nothing else reads them (live updates, search, the filter and - // the timeline all work on `.message-node` / `.message`), and a - // wholesale swap drops them until the next relayout. - const chrome = new Map(); // column key -> element - function syncChrome(transcript, tracks, colours, rows, running) { - const wanted = tracks.length ? ['main'].concat(tracks) : []; - chrome.forEach(function (el, key) { - if (wanted.indexOf(key) < 0 || el.parentNode !== transcript) { - el.remove(); - if (wanted.indexOf(key) < 0) chrome.delete(key); - } - }); - transcript.querySelectorAll(':scope > .dag-chrome').forEach(function (el) { - if (wanted.indexOf(el.getAttribute('data-col')) < 0) el.remove(); - }); - wanted.forEach(function (key, i) { - const main = key === 'main'; - const lane = main ? null : lanes.get(key); - const isStrip = !main && modeOf(key) === 'strip'; - let el = chrome.get(key); - const shape = main ? 'main' : isStrip ? 'strip' : 'col'; - if (!el || el.getAttribute('data-shape') !== shape) { - if (el) el.remove(); - el = document.createElement('div'); - el.setAttribute('data-col', key); - el.setAttribute('data-shape', shape); - if (main) { - el.innerHTML = "
"; - } else if (isStrip) { - el.innerHTML = ""; - } else { - el.innerHTML = "
" - + "" - + "
"; - } - chrome.set(key, el); - } - const cls = 'dag-chrome dag-colbg' + (main ? ' dag-mainbg' : ' dag-lc-' + (colours.get(key) || 'a')) + (isStrip ? ' dag-stripbg' : ''); - if (el.className !== cls) el.className = cls; - el.style.gridColumn = String(i + 1); - el.style.gridRow = '1 / span ' + Math.max(1, rows + ROW0 - 1); - if (lane) { - // Stats first (P8): they are what a narrow head keeps. - const meta = [lane.stats, running.has(key) ? runningText(running, key) : '', lane.meta].filter(Boolean).join(' · '); - if (isStrip) { - const btn = el.querySelector('.dag-strip'); - setAttr(btn, 'aria-label', 'Expand column: ' + lane.name); - btn.title = lane.name + (meta ? ' · ' + meta : ''); - setAttr(btn.firstChild, 'data-label', lane.name); - } else { - setAttr(el.querySelector('.dag-colname'), 'data-label', (lane.kind === 'fork' ? '⑂ ' : '') + lane.name); - setAttr(el.querySelector('.dag-colmeta'), 'data-label', meta); - const head = el.querySelector('.dag-colhead'); - const title = lane.name + (meta ? ' · ' + meta : ''); - if (head.title !== title) head.title = title; - setAttr(el.querySelector("[data-col-act='interleave']"), 'aria-label', 'Interleave branch: ' + lane.name); - setAttr(el.querySelector("[data-col-act='collapse']"), 'aria-label', 'Collapse column: ' + lane.name); - } - } - if (el.parentNode !== transcript || el.previousSibling !== (i ? chrome.get(wanted[i - 1]) : null)) { - transcript.insertBefore(el, i ? chrome.get(wanted[i - 1]).nextSibling : transcript.firstChild); - } - }); + if (el.parentNode !== transcript || el.previousSibling !== (i ? chrome.get(wanted[i - 1]) : null)) { + transcript.insertBefore(el, i ? chrome.get(wanted[i - 1]).nextSibling : transcript.firstChild); } + }); + } - function relayout() { - if (!started) return; - const t0 = performance.now(); - lanes = readLanes(); - adoptNewLanes(); - const model = buildModel(); - const memo = model.memo; - const keyMemo = new Map(); - const keyOf = function (id) { return columnKey(id, keyMemo); }; - // Interleaved into the main column: rail slots, tint, tags. - const open = []; - lanes.forEach(function (lane, id) { - if (isOpen(id, memo) && keyOf(id) === 'main' && (model.seqs.get(id) || []).length) open.push(id); - }); - const t1 = performance.now(); - const sequence = order(model); - const packed = pack(sequence, model, keyOf); - const newest = newestTimes(model, sequence); - const activity = laneActivity(model, sequence); - const running = runningLanes(newest, activity); - const tracks = columnTracks(model, memo); - const colIndex = new Map(); - const colours = new Map(); - tracks.forEach(function (id, i) { - colIndex.set(id, i + 2); - colours.set(id, lanes.get(id).kind === 'fork' ? 'f' : AGENT_COLOURS[i % AGENT_COLOURS.length]); - }); - const overflow = computeOverflow(model); - const openSet = new Set(open); - const rail = railLanes(model, open, packed, function (id) { - return keyOf(id) !== 'main' || overflow.hidden.has(id); - }, running); - rail.plans.forEach(function (plan) { colours.set(plan.id, plan.colour); }); - // A lane inside a column takes its column's colour. - lanes.forEach(function (lane, id) { - const key = keyOf(id); - if (key !== 'main' && key !== id && colours.has(key)) colours.set(id, colours.get(key)); - }); - const t2 = performance.now(); + function relayout() { + if (!started) return; + const t0 = performance.now(); + lanes = readLanes(); + adoptNewLanes(); + const model = buildModel(); + const memo = model.memo; + const keyMemo = new Map(); + const keyOf = function (id) { + return columnKey(id, keyMemo); + }; + // Interleaved into the main column: rail slots, tint, tags. + const open = []; + lanes.forEach(function (lane, id) { + if (isOpen(id, memo) && keyOf(id) === 'main' && (model.seqs.get(id) || []).length) open.push(id); + }); + const t1 = performance.now(); + const sequence = order(model); + const packed = pack(sequence, model, keyOf); + const newest = newestTimes(model, sequence); + const activity = laneActivity(model, sequence); + const running = runningLanes(newest, activity); + const tracks = columnTracks(model, memo); + const colIndex = new Map(); + const colours = new Map(); + tracks.forEach(function (id, i) { + colIndex.set(id, i + 2); + colours.set(id, lanes.get(id).kind === 'fork' ? 'f' : AGENT_COLOURS[i % AGENT_COLOURS.length]); + }); + const overflow = computeOverflow(model); + const openSet = new Set(open); + const rail = railLanes( + model, + open, + packed, + function (id) { + return keyOf(id) !== 'main' || overflow.hidden.has(id); + }, + running + ); + rail.plans.forEach(function (plan) { + colours.set(plan.id, plan.colour); + }); + // A lane inside a column takes its column's colour. + lanes.forEach(function (lane, id) { + const key = keyOf(id); + if (key !== 'main' && key !== id && colours.has(key)) colours.set(id, colours.get(key)); + }); + const t2 = performance.now(); - // Per-card engine state: interleaved rows carry their lane's - // slot, colour and gutter tag; column rows their column's - // colour (and, for a lane interleaved inside a column, its - // tag); a pair split by other rows shows both halves in full. - const tags = new Map(); - const want = function (el, cls) { - const prev = model.cls.get(el); - model.cls.set(el, prev ? prev + ' ' + cls : cls); - }; - open.forEach(function (id) { - const slot = rail.slotOf.get(id) || 1; - const lane = lanes.get(id); - const cls = 'dag-in dag-s' + Math.min(slot, MAX_SLOTS) + ' dag-lc-' + (colours.get(id) || 'a'); - (model.seqs.get(id) || []).forEach(function (item) { - want(item.el, cls); - if (item.kind === 'card') tags.set(item.el, lane.tag || lane.kind); - }); - }); - const acks = new Map(); - model.spawnCards.forEach(function (item) { - if (item.el.classList.contains('fork-point')) return; - const card = ackCardOf(item.el); - if (!card) return; - (item.el.getAttribute('data-spawns') || '').split(' ').forEach(function (id) { - if (!id || !lanes.has(id)) return; - acks.set(id, card); - if (!acksShown.has(id)) want(card, 'dag-ack'); - }); + // Per-card engine state: interleaved rows carry their lane's + // slot, colour and gutter tag; column rows their column's + // colour (and, for a lane interleaved inside a column, its + // tag); a pair split by other rows shows both halves in full. + const tags = new Map(); + const want = function (el, cls) { + const prev = model.cls.get(el); + model.cls.set(el, prev ? prev + ' ' + cls : cls); + }; + open.forEach(function (id) { + const slot = rail.slotOf.get(id) || 1; + const lane = lanes.get(id); + const cls = 'dag-in dag-s' + Math.min(slot, MAX_SLOTS) + ' dag-lc-' + (colours.get(id) || 'a'); + (model.seqs.get(id) || []).forEach(function (item) { + want(item.el, cls); + if (item.kind === 'card') tags.set(item.el, lane.tag || lane.kind); + }); + }); + const acks = new Map(); + model.spawnCards.forEach(function (item) { + if (item.el.classList.contains('fork-point')) return; + const card = ackCardOf(item.el); + if (!card) return; + (item.el.getAttribute('data-spawns') || '').split(' ').forEach(function (id) { + if (!id || !lanes.has(id)) return; + acks.set(id, card); + if (!acksShown.has(id)) want(card, 'dag-ack'); + }); + }); + const shownLanes = new Set(openSet); + tracks.forEach(function (key) { + if (modeOf(key) !== 'column') return; + lanes.forEach(function (lane, id) { + if (keyOf(id) !== key || !isOpen(id, memo)) return; + shownLanes.add(id); + const cls = 'dag-col dag-lc-' + (colours.get(key) || 'a') + (id !== key ? ' dag-colin' : ''); + (model.seqs.get(id) || []).forEach(function (item) { + want(item.el, cls); + if (id !== key && item.kind === 'card') tags.set(item.el, lane.tag || lane.kind); }); - const shownLanes = new Set(openSet); - tracks.forEach(function (key) { - if (modeOf(key) !== 'column') return; - lanes.forEach(function (lane, id) { - if (keyOf(id) !== key || !isOpen(id, memo)) return; - shownLanes.add(id); - const cls = 'dag-col dag-lc-' + (colours.get(key) || 'a') + (id !== key ? ' dag-colin' : ''); - (model.seqs.get(id) || []).forEach(function (item) { - want(item.el, cls); - if (id !== key && item.kind === 'card') tags.set(item.el, lane.tag || lane.kind); - }); - }); + }); + }); + if (tracks.length) { + // The first shown card under each column head (main's + // included) draws no hairline: the head's edge is there. + const firstSeen = new Set(); + sequence.forEach(function (item) { + if (item.kind !== 'card' || item.hidden || firstSeen.has(item.key)) return; + if (item.key !== 'main' && !colIndex.has(item.key)) return; + firstSeen.add(item.key); + want(item.el, 'dag-cfirst'); + }); + } + { + // Only rows that are shown count: a folded lane between + // two halves leaves empty rows, not a split. + const lastOf = new Map(); + const isHalf = function (el, a, b) { + return el.classList.contains(a) || el.classList.contains(b); + }; + let shown = 0; + let lastRow = -1; + sequence.forEach(function (item) { + if (item.lane !== 'main' && !shownLanes.has(item.lane)) return; + // Rows shared across columns count once. + if (item.row !== lastRow) shown++; + lastRow = item.row; + item.shown = shown; + const previous = lastOf.get(item.lane); + lastOf.set(item.lane, item); + if (!previous || item.shown === previous.shown + 1) return; + if (!isHalf(item.el, 'pair_middle', 'pair_last')) return; + if (!isHalf(previous.el, 'pair_first', 'pair_middle')) return; + [previous.el, item.el].forEach(function (half) { + want(half, 'dag-split'); }); - if (tracks.length) { - // The first shown card under each column head (main's - // included) draws no hairline: the head's edge is there. - const firstSeen = new Set(); - sequence.forEach(function (item) { - if (item.kind !== 'card' || item.hidden || firstSeen.has(item.key)) return; - if (item.key !== 'main' && !colIndex.has(item.key)) return; - firstSeen.add(item.key); - want(item.el, 'dag-cfirst'); - }); - } - { - // Only rows that are shown count: a folded lane between - // two halves leaves empty rows, not a split. - const lastOf = new Map(); - const isHalf = function (el, a, b) { - return el.classList.contains(a) || el.classList.contains(b); - }; - let shown = 0; - let lastRow = -1; - sequence.forEach(function (item) { - if (item.lane !== 'main' && !shownLanes.has(item.lane)) return; - // Rows shared across columns count once. - if (item.row !== lastRow) shown++; - lastRow = item.row; - item.shown = shown; - const previous = lastOf.get(item.lane); - lastOf.set(item.lane, item); - if (!previous || item.shown === previous.shown + 1) return; - if (!isHalf(item.el, 'pair_middle', 'pair_last')) return; - if (!isHalf(previous.el, 'pair_first', 'pair_middle')) return; - [previous.el, item.el].forEach(function (half) { want(half, 'dag-split'); }); - }); - } + }); + } - // The one layout read before the writes (setView's width). - const view = tracks.length ? document.documentElement.clientWidth : 0; - if (observer) observer.disconnect(); - if (!stage.classList.contains('dag-on')) stage.classList.add('dag-on'); - const slotsValue = String(rail.slots); - if (stage.style.getPropertyValue('--dag-slots') !== slotsValue) stage.style.setProperty('--dag-slots', slotsValue); - // Columns: the stage grid gains one track per column and the - // page widens past the 960px column (body `dag-wide`), to at - // least the columns' minimum widths — the page then scrolls - // horizontally, and the toolbar stays put (dag.css). - const cols = tracks.length > 0; - stage.classList.toggle('dag-cols', cols); - document.body.classList.toggle('dag-wide', cols); - if (cols) { - const narrow = !!(phone && phone.matches); - const pane = Math.max(240, view - PHONE_PAD); - let min = narrow ? pane : MAIN_MIN; - const gtc = [narrow ? pane + 'px' : 'minmax(' + MAIN_MIN + 'px, ' + MAIN_FR + 'fr)']; - tracks.forEach(function (id) { - const isStrip = modeOf(id) === 'strip'; - gtc.push(isStrip ? STRIP + 'px' : narrow ? pane + 'px' : 'minmax(' + COL_MIN + 'px, 1fr)'); - min += isStrip ? STRIP : narrow ? pane : COL_MIN; - }); - stage.classList.toggle('dag-panes', narrow); - const value = gtc.join(' '); - if (stage.style.getPropertyValue('--dag-cols') !== value) stage.style.setProperty('--dag-cols', value); - if (document.body.style.getPropertyValue('--dag-min') !== min + 'px') document.body.style.setProperty('--dag-min', min + 'px'); - setView(view); - } else { - stage.style.removeProperty('--dag-cols'); - stage.classList.remove('dag-panes'); - document.body.style.removeProperty('--dag-min'); - } - const nextClassed = new Set(); - model.cls.forEach(function (wanted, el) { - setEngineClasses(el, wanted); - nextClassed.add(el); - }); - classed.forEach(function (el) { - if (!nextClassed.has(el)) setEngineClasses(el, ''); - }); - classed = nextClassed; - sequence.forEach(function (item) { - const value = String(item.row + ROW0); - if (rowWritten.get(item.el) !== value) { - item.el.style.gridRow = value; - rowWritten.set(item.el, value); - } - // With columns every item needs its track: auto-placement - // would put a definite-row item after the last placed one. - const col = colIndex.get(item.key); - const column = col ? String(col) : cols ? '1' : ''; - if ((colWritten.get(item.el) || '') !== column) { - item.el.style.gridColumn = column; - colWritten.set(item.el, column); - } - }); - model.items.forEach(function (item) { - if (item.kind !== 'card') return; - const tag = tags.get(item.el); - const value = tag ? cssString(tag) : ''; - if ((tagWritten.get(item.el) || '') === value) return; - if (value) item.el.style.setProperty('--dag-tag', value); - else item.el.style.removeProperty('--dag-tag'); - tagWritten.set(item.el, value); - }); - const transcript = document.getElementById('transcript'); - if (transcript) syncChrome(transcript, tracks, colours, packed.rows, running); - model.spawnCards.forEach(function (item) { ensureControls(item, colours, memo, overflow, running, acks); }); - markToolbar(); - if (observer && observed) observe(observed); - const t3 = performance.now(); + // The one layout read before the writes (setView's width). + const view = tracks.length ? document.documentElement.clientWidth : 0; + if (observer) observer.disconnect(); + if (!stage.classList.contains('dag-on')) stage.classList.add('dag-on'); + const slotsValue = String(rail.slots); + if (stage.style.getPropertyValue('--dag-slots') !== slotsValue) + stage.style.setProperty('--dag-slots', slotsValue); + // Columns: the stage grid gains one track per column and the + // page widens past the 960px column (body `dag-wide`), to at + // least the columns' minimum widths — the page then scrolls + // horizontally, and the toolbar stays put (dag.css). + const cols = tracks.length > 0; + stage.classList.toggle('dag-cols', cols); + document.body.classList.toggle('dag-wide', cols); + if (cols) { + const narrow = !!(phone && phone.matches); + const pane = Math.max(240, view - PHONE_PAD); + let min = narrow ? pane : MAIN_MIN; + const gtc = [narrow ? pane + 'px' : 'minmax(' + MAIN_MIN + 'px, ' + MAIN_FR + 'fr)']; + tracks.forEach(function (id) { + const isStrip = modeOf(id) === 'strip'; + gtc.push(isStrip ? STRIP + 'px' : narrow ? pane + 'px' : 'minmax(' + COL_MIN + 'px, 1fr)'); + min += isStrip ? STRIP : narrow ? pane : COL_MIN; + }); + stage.classList.toggle('dag-panes', narrow); + const value = gtc.join(' '); + if (stage.style.getPropertyValue('--dag-cols') !== value) stage.style.setProperty('--dag-cols', value); + if (document.body.style.getPropertyValue('--dag-min') !== min + 'px') + document.body.style.setProperty('--dag-min', min + 'px'); + setView(view); + } else { + stage.style.removeProperty('--dag-cols'); + stage.classList.remove('dag-panes'); + document.body.style.removeProperty('--dag-min'); + } + const nextClassed = new Set(); + model.cls.forEach(function (wanted, el) { + setEngineClasses(el, wanted); + nextClassed.add(el); + }); + classed.forEach(function (el) { + if (!nextClassed.has(el)) setEngineClasses(el, ''); + }); + classed = nextClassed; + sequence.forEach(function (item) { + const value = String(item.row + ROW0); + if (rowWritten.get(item.el) !== value) { + item.el.style.gridRow = value; + rowWritten.set(item.el, value); + } + // With columns every item needs its track: auto-placement + // would put a definite-row item after the last placed one. + const col = colIndex.get(item.key); + const column = col ? String(col) : cols ? '1' : ''; + if ((colWritten.get(item.el) || '') !== column) { + item.el.style.gridColumn = column; + colWritten.set(item.el, column); + } + }); + model.items.forEach(function (item) { + if (item.kind !== 'card') return; + const tag = tags.get(item.el); + const value = tag ? cssString(tag) : ''; + if ((tagWritten.get(item.el) || '') === value) return; + if (value) item.el.style.setProperty('--dag-tag', value); + else item.el.style.removeProperty('--dag-tag'); + tagWritten.set(item.el, value); + }); + const transcript = document.getElementById('transcript'); + if (transcript) syncChrome(transcript, tracks, colours, packed.rows, running); + model.spawnCards.forEach(function (item) { + ensureControls(item, colours, memo, overflow, running, acks); + }); + markToolbar(); + if (observer && observed) observe(observed); + const t3 = performance.now(); - layout = { - model: model, rail: rail, packed: packed, sequence: sequence, newest: newest, activity: activity, running: running, - columns: columnLanes(sequence, tracks, colours, model, running), - }; - draw(); - const t4 = performance.now(); - lastTiming = { - total: t4 - t0, model: t1 - t0, order: t2 - t1, write: t3 - t2, draw: t4 - t3, - items: model.items.length, rows: packed.rows, lanes: lanes.size, open: open.length, - columns: tracks.length, - }; - if (DEBUG) console.info('[dag] relayout ' + lastTiming.total.toFixed(1) + 'ms', lastTiming); - } - // Column lanes (drawn by draw()): per column or strip, its - // colour, its shown items in row order (a strip: the row range of - // its hidden ones), its merge row and whether it runs. - function columnLanes(sequence, tracks, colours, model, running) { - if (!tracks.length) return []; - const byKey = new Map(); - const out = tracks.map(function (key) { - const col = { - key: key, colour: colours.get(key) || 'a', strip: modeOf(key) === 'strip', - running: running.has(key), merge: model.merges.get(key) || null, - items: [], first: -1, last: -1, - }; - byKey.set(key, col); - return col; - }); - sequence.forEach(function (item) { - const col = byKey.get(item.key); - if (!col) return; - if (col.first < 0) col.first = item.row; - col.last = item.row; - if (!item.hidden) col.items.push(item); - }); - return out; - } - // The viewport's width without its scrollbar: the sticky toolbar - // keeps to it while a wide (columns) page scrolls sideways. - function setView(width) { - const view = (width || document.documentElement.clientWidth) + 'px'; - if (document.body.style.getPropertyValue('--dag-view') !== view) document.body.style.setProperty('--dag-view', view); - } + layout = { + model: model, + rail: rail, + packed: packed, + sequence: sequence, + newest: newest, + activity: activity, + running: running, + columns: columnLanes(sequence, tracks, colours, model, running) + }; + draw(); + const t4 = performance.now(); + lastTiming = { + total: t4 - t0, + model: t1 - t0, + order: t2 - t1, + write: t3 - t2, + draw: t4 - t3, + items: model.items.length, + rows: packed.rows, + lanes: lanes.size, + open: open.length, + columns: tracks.length + }; + if (DEBUG) console.info('[dag] relayout ' + lastTiming.total.toFixed(1) + 'ms', lastTiming); + } + // Column lanes (drawn by draw()): per column or strip, its + // colour, its shown items in row order (a strip: the row range of + // its hidden ones), its merge row and whether it runs. + function columnLanes(sequence, tracks, colours, model, running) { + if (!tracks.length) return []; + const byKey = new Map(); + const out = tracks.map(function (key) { + const col = { + key: key, + colour: colours.get(key) || 'a', + strip: modeOf(key) === 'strip', + running: running.has(key), + merge: model.merges.get(key) || null, + items: [], + first: -1, + last: -1 + }; + byKey.set(key, col); + return col; + }); + sequence.forEach(function (item) { + const col = byKey.get(item.key); + if (!col) return; + if (col.first < 0) col.first = item.row; + col.last = item.row; + if (!item.hidden) col.items.push(item); + }); + return out; + } + // The viewport's width without its scrollbar: the sticky toolbar + // keeps to it while a wide (columns) page scrolls sideways. + function setView(width) { + const view = (width || document.documentElement.clientWidth) + 'px'; + if (document.body.style.getPropertyValue('--dag-view') !== view) + document.body.style.setProperty('--dag-view', view); + } - function schedule() { - if (frame) return; - frame = window.requestAnimationFrame(function () { - frame = 0; - if (document.hidden) { - hiddenDirty = true; - return; - } - relayout(); - }); - } - let hiddenDirty = false; - document.addEventListener('visibilitychange', function () { - if (!document.hidden && hiddenDirty) { - hiddenDirty = false; - relayout(); - } - }); + function schedule() { + if (frame) return; + frame = window.requestAnimationFrame(function () { + frame = 0; + if (document.hidden) { + hiddenDirty = true; + return; + } + relayout(); + }); + } + let hiddenDirty = false; + document.addEventListener('visibilitychange', function () { + if (!document.hidden && hiddenDirty) { + hiddenDirty = false; + relayout(); + } + }); - // ---- rail: SVG from measured rows ------------------------------ - let drawFrame = 0; - function scheduleDraw() { - if (drawFrame) return; - drawFrame = window.requestAnimationFrame(function () { - drawFrame = 0; - if (!document.hidden) draw(); - }); - } + // ---- rail: SVG from measured rows ------------------------------ + let drawFrame = 0; + function scheduleDraw() { + if (drawFrame) return; + drawFrame = window.requestAnimationFrame(function () { + drawFrame = 0; + if (!document.hidden) draw(); + }); + } - function visible(item) { - if (!item || item.hidden) return false; - return item.el.getClientRects().length > 0; - } + function visible(item) { + if (!item || item.hidden) return false; + return item.el.getClientRects().length > 0; + } - function draw() { - if (!layout) return; - if (stage.classList.contains('dag-cols')) setView(); - const transcript = document.getElementById('transcript'); - if (!transcript) return; - const stageBox = stage.getBoundingClientRect(); - const transcriptBox = transcript.getBoundingClientRect(); - const style = getComputedStyle(stage); - const step = parseFloat(style.getPropertyValue('--dag-step')) || 16; - const x0 = parseFloat(style.getPropertyValue('--dag-x0')) || 9; - const gut = phone && phone.matches ? 0 : (parseFloat(style.getPropertyValue('--gut')) || 72); - const base = transcriptBox.left - stageBox.left + gut + x0; - const slotOf = layout.rail.slotOf; - function x(slot) { return base + step * slot; } - const dyCache = new Map(); - function rowY(item) { - const el = item.el; - const box = el.getBoundingClientRect(); - // The dot's centre (layout.css): padding-top + .7em. - let dy = dyCache.get(el.className); - if (dy === undefined) { - const cs = getComputedStyle(el); - dy = parseFloat(cs.paddingTop) + 0.7 * parseFloat(cs.fontSize); - dyCache.set(el.className, dy); - } - return box.top - stageBox.top + dy; - } - function round(v) { return Math.round(v * 10) / 10; } - const paths = []; - function path(lane, part, colour, d, dashed, extra) { - paths.push(""); + function draw() { + if (!layout) return; + if (stage.classList.contains('dag-cols')) setView(); + const transcript = document.getElementById('transcript'); + if (!transcript) return; + const stageBox = stage.getBoundingClientRect(); + const transcriptBox = transcript.getBoundingClientRect(); + const style = getComputedStyle(stage); + const step = parseFloat(style.getPropertyValue('--dag-step')) || 16; + const x0 = parseFloat(style.getPropertyValue('--dag-x0')) || 9; + const gut = phone && phone.matches ? 0 : parseFloat(style.getPropertyValue('--gut')) || 72; + const base = transcriptBox.left - stageBox.left + gut + x0; + const slotOf = layout.rail.slotOf; + function x(slot) { + return base + step * slot; + } + const dyCache = new Map(); + function rowY(item) { + const el = item.el; + const box = el.getBoundingClientRect(); + // The dot's centre (layout.css): padding-top + .7em. + let dy = dyCache.get(el.className); + if (dy === undefined) { + const cs = getComputedStyle(el); + dy = parseFloat(cs.paddingTop) + 0.7 * parseFloat(cs.fontSize); + dyCache.set(el.className, dy); + } + return box.top - stageBox.top + dy; + } + function round(v) { + return Math.round(v * 10) / 10; + } + const paths = []; + function path(lane, part, colour, d, dashed, extra) { + paths.push( + "" + ); + } + // The bottom of the newest shown row: where a running lane + // ends (it carries on beside everything that happens). + let newestBottom = null; + function lastShownBottom() { + if (newestBottom !== null) return newestBottom; + newestBottom = NaN; + for (let i = layout.sequence.length - 1; i >= 0; i--) { + const item = layout.sequence[i]; + if (!visible(item)) continue; + newestBottom = item.el.getBoundingClientRect().bottom - stageBox.top; + break; + } + return newestBottom; + } + layout.rail.plans.forEach(function (plan) { + const xs = x(slotOf.get(plan.lane.parent) || 0); + const xk = x(plan.slot); + // First and last shown rows of the lane only. + const seen = []; + for (let i = 0; i < plan.seq.length; i++) { + if (visible(plan.seq[i])) { + seen.push(plan.seq[i]); + break; } - // The bottom of the newest shown row: where a running lane - // ends (it carries on beside everything that happens). - let newestBottom = null; - function lastShownBottom() { - if (newestBottom !== null) return newestBottom; - newestBottom = NaN; - for (let i = layout.sequence.length - 1; i >= 0; i--) { - const item = layout.sequence[i]; - if (!visible(item)) continue; - newestBottom = item.el.getBoundingClientRect().bottom - stageBox.top; - break; - } - return newestBottom; + } + for (let i = plan.seq.length - 1; i >= 0 && seen.length; i--) { + if (visible(plan.seq[i])) { + seen.push(plan.seq[i]); + break; } - layout.rail.plans.forEach(function (plan) { - const xs = x(slotOf.get(plan.lane.parent) || 0); - const xk = x(plan.slot); - // First and last shown rows of the lane only. - const seen = []; - for (let i = 0; i < plan.seq.length; i++) { - if (visible(plan.seq[i])) { seen.push(plan.seq[i]); break; } - } - for (let i = plan.seq.length - 1; i >= 0 && seen.length; i--) { - if (visible(plan.seq[i])) { seen.push(plan.seq[i]); break; } - } - const spawnShown = visible(plan.spawn); - if (!spawnShown && !(plan.open && seen.length)) return; - const ys = spawnShown ? rowY(plan.spawn) : rowY(seen[0]); - if (plan.running) { - // Running (P7b): no merge yet and not a fork's stub — - // the lane carries on to the bottom of the newest row - // and ends in an open marker; when its result arrives - // the next relayout draws the ordinary merge. - const r = RADIUS; - let bottom = lastShownBottom() - 2 * END_R; - if (plan.open && seen.length) bottom = Math.max(bottom, rowY(seen[seen.length - 1])); - const tail = (spawnShown ? ys + r : ys) + 2 * r; - if (!(bottom >= tail)) bottom = tail; - let top = ys; - if (spawnShown) { - const dir = xk >= xs ? 1 : -1; - path(plan.id, 'fork', plan.colour, - 'M' + round(xs) + ' ' + round(ys) + 'H' + round(xk - dir * r) - + 'A' + r + ' ' + r + ' 0 0 ' + (dir > 0 ? 1 : 0) + ' ' + round(xk) + ' ' + round(ys + r), false); - top = ys + r; - } - path(plan.id, 'lane', plan.colour, 'M' + round(xk) + ' ' + round(top) + 'V' + round(bottom - END_R), !plan.open, 'dag-running'); - path(plan.id, 'end', plan.colour, - 'M' + round(xk - END_R) + ' ' + round(bottom) - + 'a' + END_R + ' ' + END_R + ' 0 1 0 ' + (2 * END_R) + ' 0' - + 'a' + END_R + ' ' + END_R + ' 0 1 0 ' + (-2 * END_R) + ' 0', false, 'dag-end'); - return; - } - const mergeShown = plan.merge && visible(plan.merge) && plan.merge.row > (plan.spawn ? plan.spawn.row : -1); - let ye; - if (mergeShown) ye = rowY(plan.merge); - else if (plan.open && seen.length) ye = rowY(seen[seen.length - 1]); - else ye = ys; - const dir = xk >= xs ? 1 : -1; - if (ye - ys < 2 * RADIUS + 1) { - // Nothing to span: a stub hanging off the spawn row. - if (!spawnShown) return; - const r = RADIUS; - path(plan.id, 'stub', plan.colour, - 'M' + round(xs) + ' ' + round(ys) + 'H' + round(xk - dir * r) - + 'A' + r + ' ' + r + ' 0 0 ' + (dir > 0 ? 1 : 0) + ' ' + round(xk) + ' ' + round(ys + r) - + 'V' + round(ys + STUB), false); - return; - } - const r = Math.min(RADIUS, (ye - ys) / 2); - let top = ys; - if (spawnShown) { - path(plan.id, 'fork', plan.colour, - 'M' + round(xs) + ' ' + round(ys) + 'H' + round(xk - dir * r) - + 'A' + r + ' ' + r + ' 0 0 ' + (dir > 0 ? 1 : 0) + ' ' + round(xk) + ' ' + round(ys + r), false); - top = ys + r; - } - const bottom = mergeShown ? ye - r : ye; - if (bottom > top) { - path(plan.id, 'lane', plan.colour, 'M' + round(xk) + ' ' + round(top) + 'V' + round(bottom), !plan.open); - } - if (mergeShown) { - const xm = x(slotOf.get(plan.merge.lane) || 0); - const dm = xk >= xm ? 1 : -1; - path(plan.id, 'merge', plan.colour, - 'M' + round(xk) + ' ' + round(ye - r) - + 'A' + r + ' ' + r + ' 0 0 ' + (dm > 0 ? 1 : 0) + ' ' + round(xk - dm * r) + ' ' + round(ye) - + 'H' + round(xm), false); - } - }); - const fades = drawColumns(transcript, stageBox, style, rowY, round, path, lastShownBottom); - // Measure without the previous drawing: an absolute SVG taller - // than the stage counts in its scrollHeight, so a rail drawn - // for a taller layout (a lane in a column) would never shrink - // and would leave empty scroll space under the transcript. - railHost.textContent = ''; - const width = Math.ceil(stage.clientWidth); - const height = Math.ceil(stage.scrollHeight); - // One fading stroke per lane colour a pointer uses (the - // colour is the gradient's own --lc, so it follows the scheme). - let defs = ''; - fades.forEach(function (colour) { - defs += "" - + ""; - }); - railHost.innerHTML = paths.length - ? "" - + (defs ? '' + defs + '' : '') + paths.join('') + '' - : ''; } - - // Column lanes: in Columns mode each column (or strip) draws its - // lane where the main line has its rail — between the column - // cards' gutter and content (`--dag-col-*`, dag.css), whose dots - // sit on it — over the lane's active span only: from its first - // shown row to its last, or down to its merge row (a sync result, - // an async notification, a workflow agent's row). No join is - // drawn across to the parent column: a short curve at the top - // bends left towards where the branch came from and fades out - // (`col-in`), and a merging lane ends in a matching one - // (`col-out`); a fork never merges, and a running lane carries on - // to the newest row and ends in the open marker, as on the rail. - // A strip (its cards hidden) draws the line alone over the rows - // its lane spans, at its left edge. Returns the colours whose - // fade the pointers use. - function drawColumns(transcript, stageBox, style, rowY, round, path, lastShownBottom) { - const fades = new Set(); - const columns = layout.columns || []; - if (!columns.length) return fades; - const pad = parseFloat(style.getPropertyValue('--dag-col-pad')) || 0; - const gut = parseFloat(style.getPropertyValue('--dag-col-gut')) || 0; - const rail = parseFloat(style.getPropertyValue('--dag-col-rail')) || 16; - const offset = pad + gut + rail / 2; + const spawnShown = visible(plan.spawn); + if (!spawnShown && !(plan.open && seen.length)) return; + const ys = spawnShown ? rowY(plan.spawn) : rowY(seen[0]); + if (plan.running) { + // Running (P7b): no merge yet and not a fork's stub — + // the lane carries on to the bottom of the newest row + // and ends in an open marker; when its result arrives + // the next relayout draws the ordinary merge. const r = RADIUS; - const sequence = layout.sequence; - const lastRow = sequence.length ? sequence[sequence.length - 1].row : -1; - // The grid's row heights, read once per draw and only for a - // strip: the resolved grid-template-rows lists every track, - // implicit ones included, and the layout is already clean. - let heights = null; - function rowHeights() { - if (!heights) heights = getComputedStyle(transcript).gridTemplateRows.split(' ').map(parseFloat); - return heights; - } - // The dot y of the first shown item in a row (NaN: none). - function shownIn(row) { - let lo = 0; - let hi = sequence.length; - while (lo < hi) { - const mid = (lo + hi) >> 1; - if (sequence[mid].row < row) lo = mid + 1; - else hi = mid; - } - for (let i = lo; i < sequence.length && sequence[i].row === row; i++) { - if (visible(sequence[i])) return rowY(sequence[i]); - } - return NaN; - } - // A strip's rows are empty: the dot y of the nearest shown - // item at or after (dir 1) / at or before (dir -1) a row. An - // empty row has no height, so the walk passes over those on - // the measured heights alone, however many there are (no - // per-item visibility read): only a row with a height is - // looked into (rows are packed in sequence order). - function nearRowY(row, dir) { - const sizes = rowHeights(); - for (let k = row; k >= 0 && k <= lastRow; k += dir) { - if (sizes[k + ROW0 - 1] === 0) continue; - const y = shownIn(k); - if (!isNaN(y)) return y; - } - return NaN; - } - function pointer(id, part, colour, d) { - fades.add(colour); - path(id, part, colour, d, false, 'dag-fade'); + let bottom = lastShownBottom() - 2 * END_R; + if (plan.open && seen.length) bottom = Math.max(bottom, rowY(seen[seen.length - 1])); + const tail = (spawnShown ? ys + r : ys) + 2 * r; + if (!(bottom >= tail)) bottom = tail; + let top = ys; + if (spawnShown) { + const dir = xk >= xs ? 1 : -1; + path( + plan.id, + 'fork', + plan.colour, + 'M' + + round(xs) + + ' ' + + round(ys) + + 'H' + + round(xk - dir * r) + + 'A' + + r + + ' ' + + r + + ' 0 0 ' + + (dir > 0 ? 1 : 0) + + ' ' + + round(xk) + + ' ' + + round(ys + r), + false + ); + top = ys + r; } - columns.forEach(function (col) { - const el = chrome.get(col.key); - if (!el || !el.isConnected) return; - const xk = el.getBoundingClientRect().left - stageBox.left + (col.strip ? STRIP_X : offset); - let ys = NaN; - let ye = NaN; - let lastRow = col.last; - if (col.strip) { - if (col.first < 0) return; - ys = nearRowY(col.first, 1); - // Its rows are the last shown (a session that ended - // mid-branch, or a running one): it starts level with - // the shown row they follow. - if (isNaN(ys)) ys = nearRowY(col.first, -1); - ye = nearRowY(col.last, -1); - } else { - let first = null; - let last = null; - for (let i = 0; i < col.items.length && !first; i++) if (visible(col.items[i])) first = col.items[i]; - for (let i = col.items.length - 1; i >= 0 && first && !last; i--) if (visible(col.items[i])) last = col.items[i]; - if (!first) return; - ys = rowY(first); - ye = rowY(last); - lastRow = last.row; - } - if (isNaN(ys)) return; - if (!(ye >= ys)) ye = ys; - const id = col.key; - const colour = col.colour; - const lead = POINTER_LEAD; - pointer(id, 'col-in', colour, - 'M' + round(xk) + ' ' + round(ys) + 'V' + round(ys - lead) - + 'A' + r + ' ' + r + ' 0 0 0 ' + round(xk - r) + ' ' + round(ys - lead - r) - + 'H' + round(xk - r - POINTER_TAIL)); - if (col.running) { - // The open marker clears the last dot even when that - // row is the newest (a short card). - let bottom = lastShownBottom() - 2 * END_R; - if (!(bottom >= ye + 2 * END_R + 4)) bottom = ye + 2 * END_R + 4; - path(id, 'col', colour, 'M' + round(xk) + ' ' + round(ys) + 'V' + round(bottom - END_R), false, 'dag-running'); - path(id, 'end', colour, - 'M' + round(xk - END_R) + ' ' + round(bottom) - + 'a' + END_R + ' ' + END_R + ' 0 1 0 ' + (2 * END_R) + ' 0' - + 'a' + END_R + ' ' + END_R + ' 0 1 0 ' + (-2 * END_R) + ' 0', false, 'dag-end'); - return; - } - const merge = col.merge; - const ym = merge && visible(merge) && merge.row > lastRow ? rowY(merge) : NaN; - if (ym > ye) { - // The merge row: the lane runs down to it and bends - // back towards the parent column there. - const rm = Math.min(r, Math.max(2, ym - ys)); - const split = Math.max(ys, ym - rm - lead); - path(id, 'col', colour, 'M' + round(xk) + ' ' + round(ys) + 'V' + round(split), false); - pointer(id, 'col-out', colour, - 'M' + round(xk) + ' ' + round(split) + 'V' + round(ym - rm) - + 'A' + rm + ' ' + rm + ' 0 0 1 ' + round(xk - rm) + ' ' + round(ym) - + 'H' + round(xk - rm - POINTER_TAIL)); - return; - } - path(id, 'col', colour, 'M' + round(xk) + ' ' + round(ys) + 'V' + round(ye), false); - }); - return fades; - } + path( + plan.id, + 'lane', + plan.colour, + 'M' + round(xk) + ' ' + round(top) + 'V' + round(bottom - END_R), + !plan.open, + 'dag-running' + ); + path( + plan.id, + 'end', + plan.colour, + 'M' + + round(xk - END_R) + + ' ' + + round(bottom) + + 'a' + + END_R + + ' ' + + END_R + + ' 0 1 0 ' + + 2 * END_R + + ' 0' + + 'a' + + END_R + + ' ' + + END_R + + ' 0 1 0 ' + + -2 * END_R + + ' 0', + false, + 'dag-end' + ); + return; + } + const mergeShown = plan.merge && visible(plan.merge) && plan.merge.row > (plan.spawn ? plan.spawn.row : -1); + let ye; + if (mergeShown) ye = rowY(plan.merge); + else if (plan.open && seen.length) ye = rowY(seen[seen.length - 1]); + else ye = ys; + const dir = xk >= xs ? 1 : -1; + if (ye - ys < 2 * RADIUS + 1) { + // Nothing to span: a stub hanging off the spawn row. + if (!spawnShown) return; + const r = RADIUS; + path( + plan.id, + 'stub', + plan.colour, + 'M' + + round(xs) + + ' ' + + round(ys) + + 'H' + + round(xk - dir * r) + + 'A' + + r + + ' ' + + r + + ' 0 0 ' + + (dir > 0 ? 1 : 0) + + ' ' + + round(xk) + + ' ' + + round(ys + r) + + 'V' + + round(ys + STUB), + false + ); + return; + } + const r = Math.min(RADIUS, (ye - ys) / 2); + let top = ys; + if (spawnShown) { + path( + plan.id, + 'fork', + plan.colour, + 'M' + + round(xs) + + ' ' + + round(ys) + + 'H' + + round(xk - dir * r) + + 'A' + + r + + ' ' + + r + + ' 0 0 ' + + (dir > 0 ? 1 : 0) + + ' ' + + round(xk) + + ' ' + + round(ys + r), + false + ); + top = ys + r; + } + const bottom = mergeShown ? ye - r : ye; + if (bottom > top) { + path( + plan.id, + 'lane', + plan.colour, + 'M' + round(xk) + ' ' + round(top) + 'V' + round(bottom), + !plan.open + ); + } + if (mergeShown) { + const xm = x(slotOf.get(plan.merge.lane) || 0); + const dm = xk >= xm ? 1 : -1; + path( + plan.id, + 'merge', + plan.colour, + 'M' + + round(xk) + + ' ' + + round(ye - r) + + 'A' + + r + + ' ' + + r + + ' 0 0 ' + + (dm > 0 ? 1 : 0) + + ' ' + + round(xk - dm * r) + + ' ' + + round(ye) + + 'H' + + round(xm), + false + ); + } + }); + const fades = drawColumns(transcript, stageBox, style, rowY, round, path, lastShownBottom); + // Measure without the previous drawing: an absolute SVG taller + // than the stage counts in its scrollHeight, so a rail drawn + // for a taller layout (a lane in a column) would never shrink + // and would leave empty scroll space under the transcript. + railHost.textContent = ''; + const width = Math.ceil(stage.clientWidth); + const height = Math.ceil(stage.scrollHeight); + // One fading stroke per lane colour a pointer uses (the + // colour is the gradient's own --lc, so it follows the scheme). + let defs = ''; + fades.forEach(function (colour) { + defs += + "" + + ""; + }); + railHost.innerHTML = paths.length + ? "" + + (defs ? '' + defs + '' : '') + + paths.join('') + + '' + : ''; + } - // ---- triggers ---------------------------------------------------- - const STRUCTURE = ['message-node', 'message', 'children', 'fork-point']; - function isStructural(node) { - if (!node || node.nodeType !== 1) return false; - return STRUCTURE.some(function (cls) { return node.classList.contains(cls); }); - } - const HIDING = /(?:^|\s)(?:filtered-hidden|search-hidden)(?=\s|$)/; - function relevant(records) { - for (let i = 0; i < records.length; i++) { - const record = records[i]; - if (record.type === 'childList') { - for (let j = 0; j < record.addedNodes.length; j++) if (isStructural(record.addedNodes[j])) return true; - for (let j = 0; j < record.removedNodes.length; j++) if (isStructural(record.removedNodes[j])) return true; - } else if (record.attributeName === 'style') { - if (record.target.classList && record.target.classList.contains('children')) return true; - } else if (record.attributeName === 'class') { - const target = record.target; - if (!target.classList || !(target.classList.contains('message') || target.classList.contains('fork-point'))) continue; - const was = HIDING.test(record.oldValue || ''); - const now = HIDING.test(target.className); - if (was !== now) return true; - } - } - return false; - } - function observe(transcript) { - observed = transcript; - if (!observer) { - observer = new MutationObserver(function (records) { - if (relevant(records)) schedule(); - }); - } - observer.observe(transcript, { - subtree: true, childList: true, attributes: true, - attributeFilter: ['class', 'style'], attributeOldValue: true, - }); - if (resizer) { - resizer.disconnect(); - resizer.observe(transcript); - } - } - const resizer = window.ResizeObserver ? new ResizeObserver(scheduleDraw) : null; - // Phone columns are sized to the viewport: a resize re-lays them. - window.addEventListener('resize', function () { - if (stage.classList.contains('dag-panes')) schedule(); - else scheduleDraw(); - }); - // A card a live update adds fades in from a few px below - // (`live-new-in`, a transform): the rail measured it mid-way, and - // a transform resizes nothing, so redraw once it has landed. - document.addEventListener('animationend', function (event) { - if (event.animationName === 'live-new-in') scheduleDraw(); - }); - if (phone) { - const onPhone = function () { - if (stage.classList.contains('dag-cols')) schedule(); - else scheduleDraw(); - }; - if (phone.addEventListener) phone.addEventListener('change', onPhone); - else if (phone.addListener) phone.addListener(onPhone); - } + // Column lanes: in Columns mode each column (or strip) draws its + // lane where the main line has its rail — between the column + // cards' gutter and content (`--dag-col-*`, dag.css), whose dots + // sit on it — over the lane's active span only: from its first + // shown row to its last, or down to its merge row (a sync result, + // an async notification, a workflow agent's row). No join is + // drawn across to the parent column: a short curve at the top + // bends left towards where the branch came from and fades out + // (`col-in`), and a merging lane ends in a matching one + // (`col-out`); a fork never merges, and a running lane carries on + // to the newest row and ends in the open marker, as on the rail. + // A strip (its cards hidden) draws the line alone over the rows + // its lane spans, at its left edge. Returns the colours whose + // fade the pointers use. + function drawColumns(transcript, stageBox, style, rowY, round, path, lastShownBottom) { + const fades = new Set(); + const columns = layout.columns || []; + if (!columns.length) return fades; + const pad = parseFloat(style.getPropertyValue('--dag-col-pad')) || 0; + const gut = parseFloat(style.getPropertyValue('--dag-col-gut')) || 0; + const rail = parseFloat(style.getPropertyValue('--dag-col-rail')) || 16; + const offset = pad + gut + rail / 2; + const r = RADIUS; + const sequence = layout.sequence; + const lastRow = sequence.length ? sequence[sequence.length - 1].row : -1; + // The grid's row heights, read once per draw and only for a + // strip: the resolved grid-template-rows lists every track, + // implicit ones included, and the layout is already clean. + let heights = null; + function rowHeights() { + if (!heights) heights = getComputedStyle(transcript).gridTemplateRows.split(' ').map(parseFloat); + return heights; + } + // The dot y of the first shown item in a row (NaN: none). + function shownIn(row) { + let lo = 0; + let hi = sequence.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (sequence[mid].row < row) lo = mid + 1; + else hi = mid; + } + for (let i = lo; i < sequence.length && sequence[i].row === row; i++) { + if (visible(sequence[i])) return rowY(sequence[i]); + } + return NaN; + } + // A strip's rows are empty: the dot y of the nearest shown + // item at or after (dir 1) / at or before (dir -1) a row. An + // empty row has no height, so the walk passes over those on + // the measured heights alone, however many there are (no + // per-item visibility read): only a row with a height is + // looked into (rows are packed in sequence order). + function nearRowY(row, dir) { + const sizes = rowHeights(); + for (let k = row; k >= 0 && k <= lastRow; k += dir) { + if (sizes[k + ROW0 - 1] === 0) continue; + const y = shownIn(k); + if (!isNaN(y)) return y; + } + return NaN; + } + function pointer(id, part, colour, d) { + fades.add(colour); + path(id, part, colour, d, false, 'dag-fade'); + } + columns.forEach(function (col) { + const el = chrome.get(col.key); + if (!el || !el.isConnected) return; + const xk = el.getBoundingClientRect().left - stageBox.left + (col.strip ? STRIP_X : offset); + let ys = NaN; + let ye = NaN; + let lastRow = col.last; + if (col.strip) { + if (col.first < 0) return; + ys = nearRowY(col.first, 1); + // Its rows are the last shown (a session that ended + // mid-branch, or a running one): it starts level with + // the shown row they follow. + if (isNaN(ys)) ys = nearRowY(col.first, -1); + ye = nearRowY(col.last, -1); + } else { + let first = null; + let last = null; + for (let i = 0; i < col.items.length && !first; i++) if (visible(col.items[i])) first = col.items[i]; + for (let i = col.items.length - 1; i >= 0 && first && !last; i--) + if (visible(col.items[i])) last = col.items[i]; + if (!first) return; + ys = rowY(first); + ye = rowY(last); + lastRow = last.row; + } + if (isNaN(ys)) return; + if (!(ye >= ys)) ye = ys; + const id = col.key; + const colour = col.colour; + const lead = POINTER_LEAD; + pointer( + id, + 'col-in', + colour, + 'M' + + round(xk) + + ' ' + + round(ys) + + 'V' + + round(ys - lead) + + 'A' + + r + + ' ' + + r + + ' 0 0 0 ' + + round(xk - r) + + ' ' + + round(ys - lead - r) + + 'H' + + round(xk - r - POINTER_TAIL) + ); + if (col.running) { + // The open marker clears the last dot even when that + // row is the newest (a short card). + let bottom = lastShownBottom() - 2 * END_R; + if (!(bottom >= ye + 2 * END_R + 4)) bottom = ye + 2 * END_R + 4; + path( + id, + 'col', + colour, + 'M' + round(xk) + ' ' + round(ys) + 'V' + round(bottom - END_R), + false, + 'dag-running' + ); + path( + id, + 'end', + colour, + 'M' + + round(xk - END_R) + + ' ' + + round(bottom) + + 'a' + + END_R + + ' ' + + END_R + + ' 0 1 0 ' + + 2 * END_R + + ' 0' + + 'a' + + END_R + + ' ' + + END_R + + ' 0 1 0 ' + + -2 * END_R + + ' 0', + false, + 'dag-end' + ); + return; + } + const merge = col.merge; + const ym = merge && visible(merge) && merge.row > lastRow ? rowY(merge) : NaN; + if (ym > ye) { + // The merge row: the lane runs down to it and bends + // back towards the parent column there. + const rm = Math.min(r, Math.max(2, ym - ys)); + const split = Math.max(ys, ym - rm - lead); + path(id, 'col', colour, 'M' + round(xk) + ' ' + round(ys) + 'V' + round(split), false); + pointer( + id, + 'col-out', + colour, + 'M' + + round(xk) + + ' ' + + round(split) + + 'V' + + round(ym - rm) + + 'A' + + rm + + ' ' + + rm + + ' 0 0 1 ' + + round(xk - rm) + + ' ' + + round(ym) + + 'H' + + round(xk - rm - POINTER_TAIL) + ); + return; + } + path(id, 'col', colour, 'M' + round(xk) + ' ' + round(ys) + 'V' + round(ye), false); + }); + return fades; + } - document.addEventListener('click', function (event) { - const target = event.target; - if (!target || !target.closest) return; - const toggle = target.closest('.mn-bfold, .mn-bcol, .mn-bmore, .mn-back'); - if (toggle) { - if (toggle.classList.contains('mn-bmore')) { - const turn = toggle.getAttribute('data-turn'); - if (expanded.has(turn)) expanded.delete(turn); - else expanded.add(turn); - relayout(); - return; - } - const ctl = toggle.closest('[data-lane-ref]'); - const id = ctl ? ctl.getAttribute('data-lane-ref') : null; - if (!id || !lanes.has(id)) return; - if (toggle.classList.contains('mn-back')) { - if (acksShown.has(id)) acksShown.delete(id); - else acksShown.add(id); - } else if (toggle.classList.contains('mn-bcol')) { - if (inColumn(modeOf(id))) select(id); - else column(id); - } else if (modeOf(id) !== 'folded' && parentOpen(id, new Map())) { - fold(id); - } else { - select(id); - } - relayout(); - return; - } - const act = target.closest('.dag-chrome [data-col-act]'); - if (act) { - const id = act.closest('.dag-chrome').getAttribute('data-col'); - if (!id || !lanes.has(id)) return; - const what = act.getAttribute('data-col-act'); - if (what === 'interleave') select(id); - else if (what === 'collapse') strip(id); - else column(id); - relayout(); - return; - } - const btn = event.target.closest('[data-mn-branches]'); - if (btn) { - const value = btn.getAttribute('data-mn-branches'); - if (GLOBAL_MODES.indexOf(value) < 0) return; - globalMode = value; - writeGlobal(value); - lanes = readLanes(); - applyGlobal(value); - relayout(); - } + // ---- triggers ---------------------------------------------------- + const STRUCTURE = ['message-node', 'message', 'children', 'fork-point']; + function isStructural(node) { + if (!node || node.nodeType !== 1) return false; + return STRUCTURE.some(function (cls) { + return node.classList.contains(cls); + }); + } + const HIDING = /(?:^|\s)(?:filtered-hidden|search-hidden)(?=\s|$)/; + function relevant(records) { + for (let i = 0; i < records.length; i++) { + const record = records[i]; + if (record.type === 'childList') { + for (let j = 0; j < record.addedNodes.length; j++) if (isStructural(record.addedNodes[j])) return true; + for (let j = 0; j < record.removedNodes.length; j++) + if (isStructural(record.removedNodes[j])) return true; + } else if (record.attributeName === 'style') { + if (record.target.classList && record.target.classList.contains('children')) return true; + } else if (record.attributeName === 'class') { + const target = record.target; + if ( + !target.classList || + !(target.classList.contains('message') || target.classList.contains('fork-point')) + ) + continue; + const was = HIDING.test(record.oldValue || ''); + const now = HIDING.test(target.className); + if (was !== now) return true; + } + } + return false; + } + function observe(transcript) { + observed = transcript; + if (!observer) { + observer = new MutationObserver(function (records) { + if (relevant(records)) schedule(); }); - window.addEventListener('hashchange', function () { revealHash(true); }); - - // A live update (patch or swap) relayouts before the browser - // gets to lay out or paint the new markup — a swapped-in - // #transcript has none of the engine's rows or classes, and an - // intermediate layout with every lane unfolded would move the - // reader's scroll position (P7b). Rehydrate is called once per - // changed element, so the relayout is coalesced to one - // microtask after the update's synchronous work. - let rehydrateQueued = false; - if (window.claudeLogOnRehydrate) { - window.claudeLogOnRehydrate(function (scope) { - if (scope && scope.id === 'transcript') observe(scope); - if (rehydrateQueued) return; - rehydrateQueued = true; - const run = function () { - rehydrateQueued = false; - if (document.hidden) { - hiddenDirty = true; - return; - } - relayout(); - }; - if (window.queueMicrotask) window.queueMicrotask(run); - else Promise.resolve().then(run); - }); - } - // While a lane runs, its "quiet …" label is brought up to date - // (and a session silent for RUNNING_MAX_QUIET_MS stops showing - // one), with or without an update. - setInterval(function () { - if (!started || !layout || document.hidden || !window.claudeLogLiveUpdate) return; - const next = runningLanes(layout.newest, layout.activity); - if (next.size || !sameSet(next, layout.running)) relayout(); - }, RUNNING_RECHECK_MS); + } + observer.observe(transcript, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['class', 'style'], + attributeOldValue: true + }); + if (resizer) { + resizer.disconnect(); + resizer.observe(transcript); + } + } + const resizer = window.ResizeObserver ? new ResizeObserver(scheduleDraw) : null; + // Phone columns are sized to the viewport: a resize re-lays them. + window.addEventListener('resize', function () { + if (stage.classList.contains('dag-panes')) schedule(); + else scheduleDraw(); + }); + // A card a live update adds fades in from a few px below + // (`live-new-in`, a transform): the rail measured it mid-way, and + // a transform resizes nothing, so redraw once it has landed. + document.addEventListener('animationend', function (event) { + if (event.animationName === 'live-new-in') scheduleDraw(); + }); + if (phone) { + const onPhone = function () { + if (stage.classList.contains('dag-cols')) schedule(); + else scheduleDraw(); + }; + if (phone.addEventListener) phone.addEventListener('change', onPhone); + else if (phone.addListener) phone.addListener(onPhone); + } - function start() { - const transcript = document.getElementById('transcript'); - if (!transcript) return; - lanes = readLanes(); - applyGlobal(globalMode); - lanes.forEach(function (lane, id) { if (!modes.has(id)) modes.set(id, 'folded'); }); - started = true; - pendingReveals.splice(0).forEach(function (lane) { - if (lanes.has(lane) && !isOpen(lane, new Map())) openForReveal(lane); - }); - const hash = window.location.hash; - const hashTarget = hash && hash.indexOf('#msg-') === 0 ? document.getElementById(hash.slice(1)) : null; - if (hashTarget) revealLanes(hashTarget); - observe(transcript); + document.addEventListener('click', function (event) { + const target = event.target; + if (!target || !target.closest) return; + const toggle = target.closest('.mn-bfold, .mn-bcol, .mn-bmore, .mn-back'); + if (toggle) { + if (toggle.classList.contains('mn-bmore')) { + const turn = toggle.getAttribute('data-turn'); + if (expanded.has(turn)) expanded.delete(turn); + else expanded.add(turn); relayout(); - if (hashTarget && hashTarget.scrollIntoView) hashTarget.scrollIntoView({ block: 'start' }); - } + return; + } + const ctl = toggle.closest('[data-lane-ref]'); + const id = ctl ? ctl.getAttribute('data-lane-ref') : null; + if (!id || !lanes.has(id)) return; + if (toggle.classList.contains('mn-back')) { + if (acksShown.has(id)) acksShown.delete(id); + else acksShown.add(id); + } else if (toggle.classList.contains('mn-bcol')) { + if (inColumn(modeOf(id))) select(id); + else column(id); + } else if (modeOf(id) !== 'folded' && parentOpen(id, new Map())) { + fold(id); + } else { + select(id); + } + relayout(); + return; + } + const act = target.closest('.dag-chrome [data-col-act]'); + if (act) { + const id = act.closest('.dag-chrome').getAttribute('data-col'); + if (!id || !lanes.has(id)) return; + const what = act.getAttribute('data-col-act'); + if (what === 'interleave') select(id); + else if (what === 'collapse') strip(id); + else column(id); + relayout(); + return; + } + const btn = event.target.closest('[data-mn-branches]'); + if (btn) { + const value = btn.getAttribute('data-mn-branches'); + if (GLOBAL_MODES.indexOf(value) < 0) return; + globalMode = value; + writeGlobal(value); + lanes = readLanes(); + applyGlobal(value); + relayout(); + } + }); + window.addEventListener('hashchange', function () { + revealHash(true); + }); - // For tests and the ?debug-dag timing log. - window.claudeLogDag = { - relayout: function () { relayout(); return lastTiming; }, - timing: function () { return lastTiming; }, - // 'folded' | 'interleaved' | 'column' | 'strip', as shown (a - // lane inside a folded one reads 'folded'). - mode: function (id) { - const memo = new Map(); - const mode = modeOf(id); - if (!parentOpen(id, memo)) return 'folded'; - return mode; - }, - // True while the lane is drawn as running (P7b). - running: function (id) { - return !!(layout && layout.running.has(id)); - }, - // How long a running lane has been quiet (ms), else null (P7c). - quietMs: function (id) { - return layout && layout.running.has(id) ? layout.running.get(id) : null; - }, - runningMaxQuietMs: RUNNING_MAX_QUIET_MS, + // A live update (patch or swap) relayouts before the browser + // gets to lay out or paint the new markup — a swapped-in + // #transcript has none of the engine's rows or classes, and an + // intermediate layout with every lane unfolded would move the + // reader's scroll position (P7b). Rehydrate is called once per + // changed element, so the relayout is coalesced to one + // microtask after the update's synchronous work. + let rehydrateQueued = false; + if (window.claudeLogOnRehydrate) { + window.claudeLogOnRehydrate(function (scope) { + if (scope && scope.id === 'transcript') observe(scope); + if (rehydrateQueued) return; + rehydrateQueued = true; + const run = function () { + rehydrateQueued = false; + if (document.hidden) { + hiddenDirty = true; + return; + } + relayout(); }; + if (window.queueMicrotask) window.queueMicrotask(run); + else Promise.resolve().then(run); + }); + } + // While a lane runs, its "quiet …" label is brought up to date + // (and a session silent for RUNNING_MAX_QUIET_MS stops showing + // one), with or without an update. + setInterval(function () { + if (!started || !layout || document.hidden || !window.claudeLogLiveUpdate) return; + const next = runningLanes(layout.newest, layout.activity); + if (next.size || !sameSet(next, layout.running)) relayout(); + }, RUNNING_RECHECK_MS); + + function start() { + const transcript = document.getElementById('transcript'); + if (!transcript) return; + lanes = readLanes(); + applyGlobal(globalMode); + lanes.forEach(function (lane, id) { + if (!modes.has(id)) modes.set(id, 'folded'); + }); + started = true; + pendingReveals.splice(0).forEach(function (lane) { + if (lanes.has(lane) && !isOpen(lane, new Map())) openForReveal(lane); + }); + const hash = window.location.hash; + const hashTarget = hash && hash.indexOf('#msg-') === 0 ? document.getElementById(hash.slice(1)) : null; + if (hashTarget) revealLanes(hashTarget); + observe(transcript); + relayout(); + if (hashTarget && hashTarget.scrollIntoView) hashTarget.scrollIntoView({ block: 'start' }); + } + + // For tests and the ?debug-dag timing log. + window.claudeLogDag = { + relayout: function () { + relayout(); + return lastTiming; + }, + timing: function () { + return lastTiming; + }, + // 'folded' | 'interleaved' | 'column' | 'strip', as shown (a + // lane inside a folded one reads 'folded'). + mode: function (id) { + const memo = new Map(); + const mode = modeOf(id); + if (!parentOpen(id, memo)) return 'folded'; + return mode; + }, + // True while the lane is drawn as running (P7b). + running: function (id) { + return !!(layout && layout.running.has(id)); + }, + // How long a running lane has been quiet (ms), else null (P7c). + quietMs: function (id) { + return layout && layout.running.has(id) ? layout.running.get(id) : null; + }, + runningMaxQuietMs: RUNNING_MAX_QUIET_MS + }; - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); - else start(); - })(); + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); + else start(); +})(); diff --git a/claude_code_log/html/templates/components/minimal/pages.css b/claude_code_log/html/templates/components/minimal/pages.css index 685efe23..17c8dff1 100644 --- a/claude_code_log/html/templates/components/minimal/pages.css +++ b/claude_code_log/html/templates/components/minimal/pages.css @@ -411,8 +411,8 @@ padding: 3px 0; border: 0; border-radius: 4px; - background: linear-gradient(var(--rule), var(--rule)) no-repeat - calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / 1px 100%; + background: linear-gradient(var(--rule), var(--rule)) no-repeat calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / + 1px 100%; color: var(--fg); text-decoration: none; transition: none; @@ -688,11 +688,21 @@ } /* Role colours, as on a transcript row (layout.css). */ -.mn-search .mn-hit.mn-k-user { --rc: var(--user); } -.mn-search .mn-hit.mn-k-assistant { --rc: var(--asst); } -.mn-search .mn-hit.mn-k-tool { --rc: var(--tool); } -.mn-search .mn-hit.mn-k-system { --rc: var(--sys); } -.mn-search .mn-hit.mn-k-note { --rc: var(--note); } +.mn-search .mn-hit.mn-k-user { + --rc: var(--user); +} +.mn-search .mn-hit.mn-k-assistant { + --rc: var(--asst); +} +.mn-search .mn-hit.mn-k-tool { + --rc: var(--tool); +} +.mn-search .mn-hit.mn-k-system { + --rc: var(--sys); +} +.mn-search .mn-hit.mn-k-note { + --rc: var(--note); +} .mn-search .mn-hit.mn-k-thinking > a::before { width: 5px; diff --git a/claude_code_log/html/templates/components/minimal/pages.js b/claude_code_log/html/templates/components/minimal/pages.js index 19631e7e..8c3b7a53 100644 --- a/claude_code_log/html/templates/components/minimal/pages.js +++ b/claude_code_log/html/templates/components/minimal/pages.js @@ -1,52 +1,58 @@ - // Minimal theme runtime for the project index and the archive search - // page: the Auto / Light / Dark toggle — the same code, storage key - // (`claude-code-log:theme`) and markup as the transcript pages, so a - // choice made on one page carries to every other — and local dates. - (function () { - 'use strict'; +// Minimal theme runtime for the project index and the archive search +// page: the Auto / Light / Dark toggle — the same code, storage key +// (`claude-code-log:theme`) and markup as the transcript pages, so a +// choice made on one page carries to every other — and local dates. +(function () { + 'use strict'; {% include 'components/minimal/scheme.js' %} - // ---- local dates --------------------------------------------- - // The server writes UTC dates (correct without JavaScript); this - // rewrites them in the viewer's time zone, the job - // timezone_converter.js does for the classic page, in the compact - // forms the rows use. The full local date and time, with the - // zone, becomes the tooltip. - function pad(n) { return (n < 10 ? '0' : '') + n; } - function parse(stamp) { - const date = stamp ? new Date(stamp) : null; - return date && !isNaN(date.getTime()) ? date : null; - } - function day(date) { - return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); - } - function clock(date) { - return pad(date.getHours()) + ':' + pad(date.getMinutes()); - } - const full = new Intl.DateTimeFormat(undefined, { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', - hour12: false, timeZoneName: 'short' - }); - function span(from, to) { - return to ? full.format(from) + ' – ' + full.format(to) : full.format(from); - } - // A date range: `2025-01-01 – 2025-01-15`, one date if same day. - document.querySelectorAll('[data-mn-from]').forEach(function (el) { - const from = parse(el.getAttribute('data-mn-from')); - if (!from) return; - const to = parse(el.getAttribute('data-mn-to')); - el.textContent = to && day(to) !== day(from) ? day(from) + ' – ' + day(to) : day(from); - el.title = span(from, to); - }); - // A session row's gutter: its first message's local date and time. - document.querySelectorAll('[data-mn-ts]').forEach(function (el) { - const at = parse(el.getAttribute('data-mn-ts')); - if (!at) return; - const dayEl = el.querySelector('.mn-sd'); - const timeEl = el.querySelector('.mn-st'); - if (dayEl) dayEl.textContent = day(at); - if (timeEl) timeEl.textContent = clock(at); - el.title = span(at, parse(el.getAttribute('data-mn-to'))); - }); - })(); + // ---- local dates --------------------------------------------- + // The server writes UTC dates (correct without JavaScript); this + // rewrites them in the viewer's time zone, the job + // timezone_converter.js does for the classic page, in the compact + // forms the rows use. The full local date and time, with the + // zone, becomes the tooltip. + function pad(n) { + return (n < 10 ? '0' : '') + n; + } + function parse(stamp) { + const date = stamp ? new Date(stamp) : null; + return date && !isNaN(date.getTime()) ? date : null; + } + function day(date) { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); + } + function clock(date) { + return pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + const full = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZoneName: 'short' + }); + function span(from, to) { + return to ? full.format(from) + ' – ' + full.format(to) : full.format(from); + } + // A date range: `2025-01-01 – 2025-01-15`, one date if same day. + document.querySelectorAll('[data-mn-from]').forEach(function (el) { + const from = parse(el.getAttribute('data-mn-from')); + if (!from) return; + const to = parse(el.getAttribute('data-mn-to')); + el.textContent = to && day(to) !== day(from) ? day(from) + ' – ' + day(to) : day(from); + el.title = span(from, to); + }); + // A session row's gutter: its first message's local date and time. + document.querySelectorAll('[data-mn-ts]').forEach(function (el) { + const at = parse(el.getAttribute('data-mn-ts')); + if (!at) return; + const dayEl = el.querySelector('.mn-sd'); + const timeEl = el.querySelector('.mn-st'); + if (dayEl) dayEl.textContent = day(at); + if (timeEl) timeEl.textContent = clock(at); + el.title = span(at, parse(el.getAttribute('data-mn-to'))); + }); +})(); diff --git a/claude_code_log/html/templates/components/minimal/scheme.js b/claude_code_log/html/templates/components/minimal/scheme.js index cdcf73f8..9f418f7e 100644 --- a/claude_code_log/html/templates/components/minimal/scheme.js +++ b/claude_code_log/html/templates/components/minimal/scheme.js @@ -1,40 +1,42 @@ - const THEME_KEY = 'claude-code-log:theme'; - const MODES = ['auto', 'light', 'dark']; - const root = document.documentElement; +const THEME_KEY = 'claude-code-log:theme'; +const MODES = ['auto', 'light', 'dark']; +const root = document.documentElement; - // ---- colour scheme: Auto / Light / Dark --------------------- - function readMode() { - try { - const stored = window.localStorage.getItem(THEME_KEY); - return MODES.indexOf(stored) >= 0 ? stored : 'auto'; - } catch (err) { - return 'auto'; - } - } - function writeMode(mode) { - try { - if (mode === 'auto') window.localStorage.removeItem(THEME_KEY); - else window.localStorage.setItem(THEME_KEY, mode); - } catch (err) { /* storage unavailable: choice lasts this page */ } - } - function applyMode(mode) { - if (mode === 'light' || mode === 'dark') root.setAttribute('data-theme', mode); - else root.removeAttribute('data-theme'); - document.querySelectorAll('[data-mn-theme]').forEach(function (btn) { - const on = btn.getAttribute('data-mn-theme') === mode; - btn.classList.toggle('on', on); - btn.setAttribute('aria-pressed', on ? 'true' : 'false'); - }); - } - document.addEventListener('click', function (event) { - const btn = event.target.closest('[data-mn-theme]'); - if (!btn) return; - const mode = btn.getAttribute('data-mn-theme'); - writeMode(mode); - applyMode(mode); - }); - // Another tab changed the choice: follow it. - window.addEventListener('storage', function (event) { - if (event.key === THEME_KEY || event.key === null) applyMode(readMode()); - }); - applyMode(readMode()); +// ---- colour scheme: Auto / Light / Dark --------------------- +function readMode() { + try { + const stored = window.localStorage.getItem(THEME_KEY); + return MODES.indexOf(stored) >= 0 ? stored : 'auto'; + } catch (err) { + return 'auto'; + } +} +function writeMode(mode) { + try { + if (mode === 'auto') window.localStorage.removeItem(THEME_KEY); + else window.localStorage.setItem(THEME_KEY, mode); + } catch (err) { + /* storage unavailable: choice lasts this page */ + } +} +function applyMode(mode) { + if (mode === 'light' || mode === 'dark') root.setAttribute('data-theme', mode); + else root.removeAttribute('data-theme'); + document.querySelectorAll('[data-mn-theme]').forEach(function (btn) { + const on = btn.getAttribute('data-mn-theme') === mode; + btn.classList.toggle('on', on); + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + }); +} +document.addEventListener('click', function (event) { + const btn = event.target.closest('[data-mn-theme]'); + if (!btn) return; + const mode = btn.getAttribute('data-mn-theme'); + writeMode(mode); + applyMode(mode); +}); +// Another tab changed the choice: follow it. +window.addEventListener('storage', function (event) { + if (event.key === THEME_KEY || event.key === null) applyMode(readMode()); +}); +applyMode(readMode()); diff --git a/claude_code_log/html/templates/components/minimal/scheme_init.js b/claude_code_log/html/templates/components/minimal/scheme_init.js index 49f759cf..dd910307 100644 --- a/claude_code_log/html/templates/components/minimal/scheme_init.js +++ b/claude_code_log/html/templates/components/minimal/scheme_init.js @@ -1,12 +1,14 @@ - // Minimal theme: apply the stored colour scheme before first paint. - // `claude-code-log:theme` holds 'auto' | 'light' | 'dark'; only an - // explicit choice sets , so 'auto' (and a missing, - // blocked or unreadable store) follows prefers-color-scheme. - (function () { - try { - var mode = window.localStorage.getItem('claude-code-log:theme'); - if (mode === 'light' || mode === 'dark') { - document.documentElement.setAttribute('data-theme', mode); - } - } catch (err) { /* storage unavailable: follow the system */ } - })(); +// Minimal theme: apply the stored colour scheme before first paint. +// `claude-code-log:theme` holds 'auto' | 'light' | 'dark'; only an +// explicit choice sets , so 'auto' (and a missing, +// blocked or unreadable store) follows prefers-color-scheme. +(function () { + try { + var mode = window.localStorage.getItem('claude-code-log:theme'); + if (mode === 'light' || mode === 'dark') { + document.documentElement.setAttribute('data-theme', mode); + } + } catch (err) { + /* storage unavailable: follow the system */ + } +})(); diff --git a/claude_code_log/html/templates/components/minimal/search_rows.js b/claude_code_log/html/templates/components/minimal/search_rows.js index e86034b7..fefd5591 100644 --- a/claude_code_log/html/templates/components/minimal/search_rows.js +++ b/claude_code_log/html/templates/components/minimal/search_rows.js @@ -1,75 +1,110 @@ - // ---- minimal theme: results as transcript rows --------------- - // Included inside the archive search page's script (minimal - // only), next to the classic renderGroups it replaces, so it - // shares escapeHtml / highlight / resultsEl. Each project is a - // header line; each hit is a row in the transcript's row - // language: a gutter (local date and time, the role), a - // role-coloured dot on the rail, then the session and the - // field it matched in (dim mono) above the snippet. The classic - // hooks stay (`.search-result-item a`, `.search-result-excerpt`). - function mnPad(n) { return (n < 10 ? '0' : '') + n; } - const mnFull = new Intl.DateTimeFormat(undefined, { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false, timeZoneName: 'short' - }); - function mnWhen(stamp) { - const date = stamp ? new Date(stamp) : null; - if (!date || isNaN(date.getTime())) return { day: '', time: '', full: '' }; - return { - day: date.getFullYear() + '-' + mnPad(date.getMonth() + 1) + '-' + mnPad(date.getDate()), - time: mnPad(date.getHours()) + ':' + mnPad(date.getMinutes()), - full: mnFull.format(date) - }; - } - // [role class, gutter label] from the hit's field group and - // entry type — the transcript's role colours. - function mnRole(r) { - switch (r.field) { - case 'tool_input': return ['tool', 'Tool']; - case 'tool_result': return ['tool', 'Result']; - case 'thinking': return ['thinking', 'Thinking']; - case 'attachment': return ['note', 'Attach']; - case 'meta': return ['system', 'Meta']; - } - if (r.type === 'user') return ['user', 'User']; - if (r.type === 'assistant') return ['assistant', 'Assistant']; - if (r.type === 'system') return ['system', 'System']; - return ['other', r.type || r.field || '']; - } - // Called from archive_search.html, which picks this or renderGroups per theme. - // oxlint-disable-next-line no-unused-vars - function mnRenderGroups(results, query, append) { - if (!append) resultsEl.innerHTML = ''; - const groups = new Map(); - results.forEach((r) => { - if (!groups.has(r.project)) groups.set(r.project, []); - groups.get(r.project).push(r); - }); - let html = ''; - groups.forEach((items, project) => { - html += '
' + - '
' + - '' + escapeHtml(project) + '' + - '' + items.length + - (items.length === 1 ? ' match' : ' matches') + '
'; - items.forEach((r) => { - const when = mnWhen(r.timestamp); - const role = mnRole(r); - const session = r.session_id ? String(r.session_id).slice(0, 8) : 'unknown'; - html += ''; - }); - html += '
'; - }); - resultsEl.insertAdjacentHTML('beforeend', html); - } +// ---- minimal theme: results as transcript rows --------------- +// Included inside the archive search page's script (minimal +// only), next to the classic renderGroups it replaces, so it +// shares escapeHtml / highlight / resultsEl. Each project is a +// header line; each hit is a row in the transcript's row +// language: a gutter (local date and time, the role), a +// role-coloured dot on the rail, then the session and the +// field it matched in (dim mono) above the snippet. The classic +// hooks stay (`.search-result-item a`, `.search-result-excerpt`). +function mnPad(n) { + return (n < 10 ? '0' : '') + n; +} +const mnFull = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short' +}); +function mnWhen(stamp) { + const date = stamp ? new Date(stamp) : null; + if (!date || isNaN(date.getTime())) return { day: '', time: '', full: '' }; + return { + day: date.getFullYear() + '-' + mnPad(date.getMonth() + 1) + '-' + mnPad(date.getDate()), + time: mnPad(date.getHours()) + ':' + mnPad(date.getMinutes()), + full: mnFull.format(date) + }; +} +// [role class, gutter label] from the hit's field group and +// entry type — the transcript's role colours. +function mnRole(r) { + switch (r.field) { + case 'tool_input': + return ['tool', 'Tool']; + case 'tool_result': + return ['tool', 'Result']; + case 'thinking': + return ['thinking', 'Thinking']; + case 'attachment': + return ['note', 'Attach']; + case 'meta': + return ['system', 'Meta']; + } + if (r.type === 'user') return ['user', 'User']; + if (r.type === 'assistant') return ['assistant', 'Assistant']; + if (r.type === 'system') return ['system', 'System']; + return ['other', r.type || r.field || '']; +} +// Called from archive_search.html, which picks this or renderGroups per theme. +// oxlint-disable-next-line no-unused-vars +function mnRenderGroups(results, query, append) { + if (!append) resultsEl.innerHTML = ''; + const groups = new Map(); + results.forEach(r => { + if (!groups.has(r.project)) groups.set(r.project, []); + groups.get(r.project).push(r); + }); + let html = ''; + groups.forEach((items, project) => { + html += + '
' + + '
' + + '' + + escapeHtml(project) + + '' + + '' + + items.length + + (items.length === 1 ? ' match' : ' matches') + + '
'; + items.forEach(r => { + const when = mnWhen(r.timestamp); + const role = mnRole(r); + const session = r.session_id ? String(r.session_id).slice(0, 8) : 'unknown'; + html += + ''; + }); + html += '
'; + }); + resultsEl.insertAdjacentHTML('beforeend', html); +} diff --git a/claude_code_log/html/templates/components/minimal/theme_init.js b/claude_code_log/html/templates/components/minimal/theme_init.js index 92cc16d0..360fd617 100644 --- a/claude_code_log/html/templates/components/minimal/theme_init.js +++ b/claude_code_log/html/templates/components/minimal/theme_init.js @@ -1,16 +1,16 @@ {% include 'components/minimal/scheme_init.js' %} - // Lay the transcript out once: hide the stage while the page parses - // (dag.css, `mn-parsing`), so the first layout it gets is the DAG - // engine's grid at DOMContentLoaded, not a nested page laid out - // frame by frame during the parse and then thrown away. This is the - // page's first DOMContentLoaded listener, so every other one — - // folds, filter, deep links, search, the engine — sees the stage - // rendered, as before; they run in one task, so nothing paints in - // between. - (function () { - var root = document.documentElement; - root.classList.add('mn-parsing'); - document.addEventListener('DOMContentLoaded', function () { - root.classList.remove('mn-parsing'); - }); - })(); +// Lay the transcript out once: hide the stage while the page parses +// (dag.css, `mn-parsing`), so the first layout it gets is the DAG +// engine's grid at DOMContentLoaded, not a nested page laid out +// frame by frame during the parse and then thrown away. This is the +// page's first DOMContentLoaded listener, so every other one — +// folds, filter, deep links, search, the engine — sees the stage +// rendered, as before; they run in one task, so nothing paints in +// between. +(function () { + var root = document.documentElement; + root.classList.add('mn-parsing'); + document.addEventListener('DOMContentLoaded', function () { + root.classList.remove('mn-parsing'); + }); +})(); diff --git a/claude_code_log/html/templates/components/page_nav_styles.css b/claude_code_log/html/templates/components/page_nav_styles.css index dfa81398..2d72f2d2 100644 --- a/claude_code_log/html/templates/components/page_nav_styles.css +++ b/claude_code_log/html/templates/components/page_nav_styles.css @@ -6,7 +6,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -66,11 +68,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ diff --git a/claude_code_log/html/templates/components/project_card_styles.css b/claude_code_log/html/templates/components/project_card_styles.css index 1a596718..7c1af610 100644 --- a/claude_code_log/html/templates/components/project_card_styles.css +++ b/claude_code_log/html/templates/components/project_card_styles.css @@ -40,7 +40,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 20px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -48,7 +50,9 @@ } .project-card:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -95,7 +99,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; diff --git a/claude_code_log/html/templates/components/search_styles.css b/claude_code_log/html/templates/components/search_styles.css index 41fa45a6..88ba1380 100644 --- a/claude_code_log/html/templates/components/search_styles.css +++ b/claude_code_log/html/templates/components/search_styles.css @@ -243,7 +243,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -317,7 +319,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -327,7 +331,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -390,14 +396,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -411,4 +425,4 @@ .search-options { font-size: 0.7em; } -} \ No newline at end of file +} diff --git a/claude_code_log/html/templates/components/session_nav_styles.css b/claude_code_log/html/templates/components/session_nav_styles.css index f141a44d..879a82d1 100644 --- a/claude_code_log/html/templates/components/session_nav_styles.css +++ b/claude_code_log/html/templates/components/session_nav_styles.css @@ -4,7 +4,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -216,4 +218,4 @@ a.session-backlink:hover { .branch-link:hover, .compaction-link:hover { color: var(--text-secondary); -} \ No newline at end of file +} diff --git a/claude_code_log/html/templates/components/timeline_styles.css b/claude_code_log/html/templates/components/timeline_styles.css index 408cf5a6..bd95eb8c 100644 --- a/claude_code_log/html/templates/components/timeline_styles.css +++ b/claude_code_log/html/templates/components/timeline_styles.css @@ -27,7 +27,7 @@ transition: background 0.2s ease; } -#timeline-resize-handle>div { +#timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -35,7 +35,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } -#timeline-resize-handle:hover>div { +#timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -43,7 +43,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } -#timeline-resize-handle:active>div { +#timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -242,4 +242,4 @@ .vis-tooltip div { white-space: normal; -} \ No newline at end of file +} diff --git a/claude_code_log/html/templates/components/timezone_converter.js b/claude_code_log/html/templates/components/timezone_converter.js index ed81cc99..03f17713 100644 --- a/claude_code_log/html/templates/components/timezone_converter.js +++ b/claude_code_log/html/templates/components/timezone_converter.js @@ -1,6 +1,6 @@ // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone -(function() { +(function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -57,7 +57,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -68,18 +70,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -87,7 +100,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -106,10 +118,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -130,7 +153,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } diff --git a/claude_code_log/html/templates/components/todo_styles.css b/claude_code_log/html/templates/components/todo_styles.css index 0559cc4c..4b3ab36f 100644 --- a/claude_code_log/html/templates/components/todo_styles.css +++ b/claude_code_log/html/templates/components/todo_styles.css @@ -16,7 +16,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; From 33a1bab6c2b29405ef04eeaeede3b2006516505c Mon Sep 17 00:00:00 2001 From: Christian Boos Date: Thu, 8 Oct 2026 20:34:02 +0200 Subject: [PATCH 2/2] Regenerate HTML snapshots for the oxfmt reformat `just update-snapshot` after the reformat. All 16 HTML blocks change, with the same names, and every change is inside @@ -22329,81 +22764,116 @@ }); resultsEl.insertAdjacentHTML('beforeend', html); } - // ---- minimal theme: results as transcript rows --------------- - // Included inside the archive search page's script (minimal - // only), next to the classic renderGroups it replaces, so it - // shares escapeHtml / highlight / resultsEl. Each project is a - // header line; each hit is a row in the transcript's row - // language: a gutter (local date and time, the role), a - // role-coloured dot on the rail, then the session and the - // field it matched in (dim mono) above the snippet. The classic - // hooks stay (`.search-result-item a`, `.search-result-excerpt`). - function mnPad(n) { return (n < 10 ? '0' : '') + n; } - const mnFull = new Intl.DateTimeFormat(undefined, { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false, timeZoneName: 'short' - }); - function mnWhen(stamp) { - const date = stamp ? new Date(stamp) : null; - if (!date || isNaN(date.getTime())) return { day: '', time: '', full: '' }; - return { - day: date.getFullYear() + '-' + mnPad(date.getMonth() + 1) + '-' + mnPad(date.getDate()), - time: mnPad(date.getHours()) + ':' + mnPad(date.getMinutes()), - full: mnFull.format(date) - }; - } - // [role class, gutter label] from the hit's field group and - // entry type — the transcript's role colours. - function mnRole(r) { - switch (r.field) { - case 'tool_input': return ['tool', 'Tool']; - case 'tool_result': return ['tool', 'Result']; - case 'thinking': return ['thinking', 'Thinking']; - case 'attachment': return ['note', 'Attach']; - case 'meta': return ['system', 'Meta']; - } - if (r.type === 'user') return ['user', 'User']; - if (r.type === 'assistant') return ['assistant', 'Assistant']; - if (r.type === 'system') return ['system', 'System']; - return ['other', r.type || r.field || '']; - } - // Called from archive_search.html, which picks this or renderGroups per theme. - // oxlint-disable-next-line no-unused-vars - function mnRenderGroups(results, query, append) { - if (!append) resultsEl.innerHTML = ''; - const groups = new Map(); - results.forEach((r) => { - if (!groups.has(r.project)) groups.set(r.project, []); - groups.get(r.project).push(r); - }); - let html = ''; - groups.forEach((items, project) => { - html += '
' + - '
' + - '' + escapeHtml(project) + '' + - '' + items.length + - (items.length === 1 ? ' match' : ' matches') + '
'; - items.forEach((r) => { - const when = mnWhen(r.timestamp); - const role = mnRole(r); - const session = r.session_id ? String(r.session_id).slice(0, 8) : 'unknown'; - html += ''; - }); - html += '
'; - }); - resultsEl.insertAdjacentHTML('beforeend', html); - } + // ---- minimal theme: results as transcript rows --------------- + // Included inside the archive search page's script (minimal + // only), next to the classic renderGroups it replaces, so it + // shares escapeHtml / highlight / resultsEl. Each project is a + // header line; each hit is a row in the transcript's row + // language: a gutter (local date and time, the role), a + // role-coloured dot on the rail, then the session and the + // field it matched in (dim mono) above the snippet. The classic + // hooks stay (`.search-result-item a`, `.search-result-excerpt`). + function mnPad(n) { + return (n < 10 ? '0' : '') + n; + } + const mnFull = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short' + }); + function mnWhen(stamp) { + const date = stamp ? new Date(stamp) : null; + if (!date || isNaN(date.getTime())) return { day: '', time: '', full: '' }; + return { + day: date.getFullYear() + '-' + mnPad(date.getMonth() + 1) + '-' + mnPad(date.getDate()), + time: mnPad(date.getHours()) + ':' + mnPad(date.getMinutes()), + full: mnFull.format(date) + }; + } + // [role class, gutter label] from the hit's field group and + // entry type — the transcript's role colours. + function mnRole(r) { + switch (r.field) { + case 'tool_input': + return ['tool', 'Tool']; + case 'tool_result': + return ['tool', 'Result']; + case 'thinking': + return ['thinking', 'Thinking']; + case 'attachment': + return ['note', 'Attach']; + case 'meta': + return ['system', 'Meta']; + } + if (r.type === 'user') return ['user', 'User']; + if (r.type === 'assistant') return ['assistant', 'Assistant']; + if (r.type === 'system') return ['system', 'System']; + return ['other', r.type || r.field || '']; + } + // Called from archive_search.html, which picks this or renderGroups per theme. + // oxlint-disable-next-line no-unused-vars + function mnRenderGroups(results, query, append) { + if (!append) resultsEl.innerHTML = ''; + const groups = new Map(); + results.forEach(r => { + if (!groups.has(r.project)) groups.set(r.project, []); + groups.get(r.project).push(r); + }); + let html = ''; + groups.forEach((items, project) => { + html += + '
' + + '
' + + '' + + escapeHtml(project) + + '' + + '' + + items.length + + (items.length === 1 ? ' match' : ' matches') + + '
'; + items.forEach(r => { + const when = mnWhen(r.timestamp); + const role = mnRole(r); + const session = r.session_id ? String(r.session_id).slice(0, 8) : 'unknown'; + html += + ''; + }); + html += '
'; + }); + resultsEl.insertAdjacentHTML('beforeend', html); + } // --- searching ---------------------------------------------- @@ -22658,97 +23128,105 @@ })(); @@ -22886,8 +23364,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -22896,7 +23376,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -22917,7 +23409,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -22939,7 +23430,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -22947,7 +23440,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -23003,7 +23498,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -23039,7 +23536,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -23111,7 +23608,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -23150,7 +23647,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -23180,7 +23677,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -23223,7 +23720,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -23862,7 +24361,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -23961,7 +24460,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -23998,7 +24498,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -24094,7 +24596,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -24117,12 +24619,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -24138,7 +24640,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -24338,8 +24842,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -24428,7 +24932,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -24443,7 +24949,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -24547,39 +25055,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -24591,20 +25147,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -25118,7 +25698,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -25141,7 +25721,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -25360,7 +25942,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -25424,7 +26008,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -25433,7 +26016,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -25544,7 +26126,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -25760,7 +26344,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -25768,7 +26352,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -25776,7 +26360,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -26221,7 +26805,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -26295,7 +26881,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -26305,7 +26893,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -26368,14 +26958,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -26709,7 +27307,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -26769,11 +27369,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -27932,7 +28532,10 @@ color: var(--fg); } - .theme-minimal #transcript .mn-title :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { + .theme-minimal + #transcript + .mn-title + :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { font: inherit; font-weight: 400; color: var(--muted); @@ -28099,13 +28702,29 @@ file, say — or the output) starts right under it: no top margin on it, nor on the code box at the head of a preview inside it. */ .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child, - .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child > details:first-child > summary > .preview-content > .highlight:first-child { + .theme-minimal + #transcript + .message:is(.tool_use, .pair_middle, .pair_last) + > .content + > :first-child + > details:first-child + > summary + > .preview-content + > .highlight:first-child { margin-top: 0; } /* Turns: one rule above each main-lane prompt, except a session's first. A slash command's output (the pair's last half) belongs to its turn. */ - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last) { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last) { margin-top: 4px; padding-top: 7px; border-top: 1px solid var(--rule2); @@ -28134,8 +28753,21 @@ .theme-minimal #transcript .message:is(.pair_middle, .pair_last):not(.dag-split)::after, .theme-minimal #transcript .message.dag-cfirst::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node:first-child > .message::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last)::after { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node:first-child + > .message::after, + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last)::after { content: none; } @@ -28189,7 +28821,11 @@ transition: color 0.12s; } - .theme-minimal #transcript .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) > .fold-bar > .fold-bar-section, + .theme-minimal + #transcript + .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) + > .fold-bar + > .fold-bar-section, .theme-minimal #transcript .message:hover > .fold-bar > .fold-bar-section, .theme-minimal #transcript .message > .fold-bar:focus-within > .fold-bar-section { color: var(--muted); @@ -28380,7 +29016,9 @@ 2px line; the classic per-depth ring colours still apply. Same selector shape as the classic rule, one class more specific. */ .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin: 4px 0 4px calc(var(--gut) + var(--rail)); padding-left: 8px; border-left-width: 2px; @@ -28388,7 +29026,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.agent-deep) { margin-left: 12px; } @@ -28464,7 +29104,12 @@ } /* User prompt: a warm tint (mockup .turn > .k-user .main). */ - .theme-minimal #transcript .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not(.sidechain) > .content { + .theme-minimal + #transcript + .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not( + .sidechain + ) + > .content { margin-left: 4px; padding: 2px 8px; border-radius: 4px; @@ -28681,7 +29326,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin-left: 16px; padding-left: 6px; } @@ -29148,14 +29795,23 @@ display: flex; } - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last) > .header > .header-info > :not(.mn-role) { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last) + > .header + > .header-info + > :not(.mn-role) { display: none; } /* …one gutter line down, below the call's role, which hangs into this row (layout.css; P7c — the pill used to cover it). */ @media (min-width: 641px) { - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { padding-top: 1.85em; } } @@ -29181,7 +29837,10 @@ --pv: 2.8em; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary { display: flex; flex-direction: column; align-items: stretch; @@ -29194,7 +29853,10 @@ cursor: pointer; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::-webkit-details-marker { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::-webkit-details-marker { display: none; } @@ -29215,7 +29877,10 @@ } /* The "+N lines" / "− less" line (mockup .more). */ - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after, .theme-minimal #transcript details.collapsible-code > summary > .line-count, .theme-minimal #transcript .mn-less { order: 2; @@ -29230,11 +29895,17 @@ color: var(--muted); } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after { content: "+ more"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary[data-more]::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary[data-more]::after { content: attr(data-more); } @@ -29256,11 +29927,17 @@ display: none; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] + > summary::after { content: "− less"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary:hover::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary:hover::after, .theme-minimal #transcript details.collapsible-code > summary:hover > .line-count, .theme-minimal #transcript .mn-less:hover { color: var(--fg); @@ -29365,7 +30042,7 @@ table), so the size is one length resolved on #transcript: a registered computes where it is declared and inherits as that value. */ @property --mn-ctl { - syntax: ''; + syntax: ""; inherits: true; initial-value: 10px; } @@ -29922,14 +30599,30 @@ transform: none; } - .theme-minimal .filter-toggle[data-type="user"] { --rc: var(--user); } - .theme-minimal .filter-toggle[data-type="system"] { --rc: var(--sys); } - .theme-minimal .filter-toggle[data-type="assistant"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="thinking"] { --rc: var(--muted); } - .theme-minimal .filter-toggle[data-type="tool"] { --rc: var(--tool); } - .theme-minimal .filter-toggle[data-type="sidechain"] { --rc: var(--ring); } - .theme-minimal .filter-toggle[data-type="memory"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="image"] { --rc: var(--note); } + .theme-minimal .filter-toggle[data-type="user"] { + --rc: var(--user); + } + .theme-minimal .filter-toggle[data-type="system"] { + --rc: var(--sys); + } + .theme-minimal .filter-toggle[data-type="assistant"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="thinking"] { + --rc: var(--muted); + } + .theme-minimal .filter-toggle[data-type="tool"] { + --rc: var(--tool); + } + .theme-minimal .filter-toggle[data-type="sidechain"] { + --rc: var(--ring); + } + .theme-minimal .filter-toggle[data-type="memory"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="image"] { + --rc: var(--note); + } .theme-minimal .filter-action-btn, .theme-minimal .search-nav-btn { @@ -30053,30 +30746,56 @@ font-size: 0.94em; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary { flex-direction: row; align-items: flex-end; column-gap: 10px; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .preview-content { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .preview-content { flex: 1 1 0; min-width: 0; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary::after, - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .line-count { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary::after, + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .line-count { flex: none; align-self: flex-end; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(p, ul, ol, pre, blockquote, table) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(p, ul, ol, pre, blockquote, table) { margin: 0; } /* A heading at the top of an agent's answer (`## Summary`) reads as a bold line of the preview, not a title twice the size that fills the clip. */ - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(h1, h2, h3, h4, h5, h6) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(h1, h2, h3, h4, h5, h6) { margin: 0; font-size: 1em; line-height: inherit; @@ -30086,7 +30805,12 @@ metadata (agent id, worktree, branch, usage) and an async notification's task fields (id, status, tokens, tool uses, duration) as one dim wrapped line of key value pairs instead of a card of rows. */ - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) { display: flex; flex-wrap: wrap; align-items: baseline; @@ -30098,11 +30822,24 @@ color: var(--muted); } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dt { font-size: 1em; } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dd + dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dd + + dt { margin-left: 8px; } /* GENERATED by scripts/generate_minimal_pygments_css.py - do not edit. @@ -30289,18 +31026,40 @@ carry at least two classes (layout.css "Specificity, two tiers"). */ /* Lane colours (rail slot → colour; forks are always --lF). */ - .theme-minimal .dag-lc-a { --lc: var(--lA); } - .theme-minimal .dag-lc-b { --lc: var(--lB); } - .theme-minimal .dag-lc-f { --lc: var(--lF); } - .theme-minimal .dag-lc-s { --lc: var(--sys); } - .theme-minimal .dag-lc-p { --lc: var(--asst); } - - .theme-minimal .dag-s1 { --dag-slot: 1; } - .theme-minimal .dag-s2 { --dag-slot: 2; } - .theme-minimal .dag-s3 { --dag-slot: 3; } - .theme-minimal .dag-s4 { --dag-slot: 4; } - .theme-minimal .dag-s5 { --dag-slot: 5; } - .theme-minimal .dag-s6 { --dag-slot: 6; } + .theme-minimal .dag-lc-a { + --lc: var(--lA); + } + .theme-minimal .dag-lc-b { + --lc: var(--lB); + } + .theme-minimal .dag-lc-f { + --lc: var(--lF); + } + .theme-minimal .dag-lc-s { + --lc: var(--sys); + } + .theme-minimal .dag-lc-p { + --lc: var(--asst); + } + + .theme-minimal .dag-s1 { + --dag-slot: 1; + } + .theme-minimal .dag-s2 { + --dag-slot: 2; + } + .theme-minimal .dag-s3 { + --dag-slot: 3; + } + .theme-minimal .dag-s4 { + --dag-slot: 4; + } + .theme-minimal .dag-s5 { + --dag-slot: 5; + } + .theme-minimal .dag-s6 { + --dag-slot: 6; + } /* ------------------------------------------------------------- stage */ @@ -30369,7 +31128,9 @@ } @keyframes dag-run { - to { stroke-dashoffset: -7; } + to { + stroke-dashoffset: -7; + } } /* A column lane's pointers (`col-in`, `col-out`): a short curve towards the @@ -30388,11 +31149,21 @@ stop-opacity: 0; } - .theme-minimal #dag-rail path.dag-fade.dag-lc-a { stroke: url(#dag-fade-a); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-b { stroke: url(#dag-fade-b); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-f { stroke: url(#dag-fade-f); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-s { stroke: url(#dag-fade-s); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-p { stroke: url(#dag-fade-p); } + .theme-minimal #dag-rail path.dag-fade.dag-lc-a { + stroke: url(#dag-fade-a); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-b { + stroke: url(#dag-fade-b); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-f { + stroke: url(#dag-fade-f); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-s { + stroke: url(#dag-fade-s); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-p { + stroke: url(#dag-fade-p); + } @media (prefers-reduced-motion: reduce) { .theme-minimal #dag-rail path.dag-running.dag-dash, @@ -30450,9 +31221,23 @@ /* Workflow phase and agent cards, and the agents' transcripts (lanes), are rows of the grid too: the nested workflow gutter (components.css) only applies without the engine, or inside a block. */ - .theme-minimal .mn-stage.dag-on #transcript .children:has(> .message-node > .message.workflow_phase) > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_phase) > .children > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_agent) > .children > .message-node:not(.dag-block *) { + .theme-minimal + .mn-stage.dag-on + #transcript + .children:has(> .message-node > .message.workflow_phase) + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_phase) + > .children + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_agent) + > .children + > .message-node:not(.dag-block *) { --gut: var(--dag-gut); --rail: var(--dag-rail); } @@ -30488,10 +31273,12 @@ --dag-indent: calc(var(--dag-ind0) + var(--dag-ind-step) * var(--dag-slot, 1)); --dag-c3: calc(var(--gut) + var(--rail)); grid-template-columns: var(--gut) calc(var(--rail) + var(--dag-indent)) minmax(0, 1fr); - background: linear-gradient(90deg, + background: linear-gradient( + 90deg, transparent var(--dag-c3), color-mix(in srgb, var(--lc) 9%, transparent) var(--dag-c3), - transparent 75%); + transparent 75% + ); } .theme-minimal .mn-stage.dag-on #transcript .message.dag-in > .header > .header-info::after { @@ -30516,17 +31303,39 @@ the call, then the tag (and an error pill) from the result, which grows its row if it must rather than overlap the next. Wide layout only. */ @media (min-width: 641px) { - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.pair_first:not(.dag-split) > .header > .header-info::after { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.pair_first:not(.dag-split) + > .header + > .header-info::after { content: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { display: flex; padding-top: 1.85em; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) > .header > .header-info > *, - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info > :not(.mn-role) { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) + > .header + > .header-info + > *, + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info + > :not(.mn-role) { display: none; } } @@ -30726,7 +31535,9 @@ } @keyframes dag-pulse { - 50% { opacity: 0.25; } + 50% { + opacity: 0.25; + } } .theme-minimal #transcript .mn-bmini { @@ -30796,7 +31607,7 @@ display: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded='false']) { + .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded="false"]) { padding-bottom: 3px; } @@ -30860,7 +31671,8 @@ min-width: calc(var(--dag-min, 0px) + 32px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { position: sticky; left: 16px; max-width: calc(var(--dag-view, 100vw) - 32px); @@ -30901,7 +31713,9 @@ overflow: hidden; white-space: nowrap; border-bottom: 1px solid var(--rule); - background: linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), var(--bg); + background: + linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), + var(--bg); } /* A column's head: the name and two compact actions (`⇤`, `−`; their @@ -31130,7 +31944,8 @@ min-width: calc(var(--dag-min, 0px) + 20px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { left: 10px; max-width: calc(var(--dag-view, 100vw) - 20px); } @@ -31162,33 +31977,35 @@ } @@ -32404,7 +33221,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -32461,7 +33278,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -32472,18 +33291,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -32491,7 +33321,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -32510,10 +33339,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -32534,7 +33374,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -32778,10 +33620,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -32836,8 +33688,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -32959,9 +33810,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -32992,7 +33843,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -33079,7 +33930,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -33114,8 +33965,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -33144,17 +33997,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -33170,7 +34023,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -33198,15 +34051,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -33220,8 +34071,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -33253,7 +34104,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -33275,19 +34128,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -35337,2374 +36194,2809 @@ })(); @@ -37842,8 +39134,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -37852,7 +39146,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -37873,7 +39179,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -37895,7 +39200,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -37903,7 +39210,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -37959,7 +39268,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -37995,7 +39306,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -38067,7 +39378,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -38106,7 +39417,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -38136,7 +39447,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -38179,7 +39490,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -38818,7 +40131,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -38917,7 +40230,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -38954,7 +40268,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -39050,7 +40366,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -39073,12 +40389,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -39094,7 +40410,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -39294,8 +40612,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -39384,7 +40702,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -39399,7 +40719,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -39503,39 +40825,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -39547,20 +40917,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -40074,7 +41468,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -40097,7 +41491,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -40316,7 +41712,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -40380,7 +41778,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -40389,7 +41786,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -40500,7 +41896,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -40716,7 +42114,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -40724,7 +42122,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -40732,7 +42130,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -41177,7 +42575,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -41251,7 +42651,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -41261,7 +42663,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -41324,14 +42728,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -41665,7 +43077,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -41725,11 +43139,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -42888,7 +44302,10 @@ color: var(--fg); } - .theme-minimal #transcript .mn-title :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { + .theme-minimal + #transcript + .mn-title + :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { font: inherit; font-weight: 400; color: var(--muted); @@ -43055,13 +44472,29 @@ file, say — or the output) starts right under it: no top margin on it, nor on the code box at the head of a preview inside it. */ .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child, - .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child > details:first-child > summary > .preview-content > .highlight:first-child { + .theme-minimal + #transcript + .message:is(.tool_use, .pair_middle, .pair_last) + > .content + > :first-child + > details:first-child + > summary + > .preview-content + > .highlight:first-child { margin-top: 0; } /* Turns: one rule above each main-lane prompt, except a session's first. A slash command's output (the pair's last half) belongs to its turn. */ - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last) { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last) { margin-top: 4px; padding-top: 7px; border-top: 1px solid var(--rule2); @@ -43090,8 +44523,21 @@ .theme-minimal #transcript .message:is(.pair_middle, .pair_last):not(.dag-split)::after, .theme-minimal #transcript .message.dag-cfirst::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node:first-child > .message::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last)::after { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node:first-child + > .message::after, + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last)::after { content: none; } @@ -43145,7 +44591,11 @@ transition: color 0.12s; } - .theme-minimal #transcript .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) > .fold-bar > .fold-bar-section, + .theme-minimal + #transcript + .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) + > .fold-bar + > .fold-bar-section, .theme-minimal #transcript .message:hover > .fold-bar > .fold-bar-section, .theme-minimal #transcript .message > .fold-bar:focus-within > .fold-bar-section { color: var(--muted); @@ -43336,7 +44786,9 @@ 2px line; the classic per-depth ring colours still apply. Same selector shape as the classic rule, one class more specific. */ .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin: 4px 0 4px calc(var(--gut) + var(--rail)); padding-left: 8px; border-left-width: 2px; @@ -43344,7 +44796,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.agent-deep) { margin-left: 12px; } @@ -43420,7 +44874,12 @@ } /* User prompt: a warm tint (mockup .turn > .k-user .main). */ - .theme-minimal #transcript .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not(.sidechain) > .content { + .theme-minimal + #transcript + .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not( + .sidechain + ) + > .content { margin-left: 4px; padding: 2px 8px; border-radius: 4px; @@ -43637,7 +45096,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin-left: 16px; padding-left: 6px; } @@ -44104,14 +45565,23 @@ display: flex; } - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last) > .header > .header-info > :not(.mn-role) { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last) + > .header + > .header-info + > :not(.mn-role) { display: none; } /* …one gutter line down, below the call's role, which hangs into this row (layout.css; P7c — the pill used to cover it). */ @media (min-width: 641px) { - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { padding-top: 1.85em; } } @@ -44137,7 +45607,10 @@ --pv: 2.8em; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary { display: flex; flex-direction: column; align-items: stretch; @@ -44150,7 +45623,10 @@ cursor: pointer; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::-webkit-details-marker { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::-webkit-details-marker { display: none; } @@ -44171,7 +45647,10 @@ } /* The "+N lines" / "− less" line (mockup .more). */ - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after, .theme-minimal #transcript details.collapsible-code > summary > .line-count, .theme-minimal #transcript .mn-less { order: 2; @@ -44186,11 +45665,17 @@ color: var(--muted); } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after { content: "+ more"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary[data-more]::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary[data-more]::after { content: attr(data-more); } @@ -44212,11 +45697,17 @@ display: none; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] + > summary::after { content: "− less"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary:hover::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary:hover::after, .theme-minimal #transcript details.collapsible-code > summary:hover > .line-count, .theme-minimal #transcript .mn-less:hover { color: var(--fg); @@ -44321,7 +45812,7 @@ table), so the size is one length resolved on #transcript: a registered computes where it is declared and inherits as that value. */ @property --mn-ctl { - syntax: ''; + syntax: ""; inherits: true; initial-value: 10px; } @@ -44878,14 +46369,30 @@ transform: none; } - .theme-minimal .filter-toggle[data-type="user"] { --rc: var(--user); } - .theme-minimal .filter-toggle[data-type="system"] { --rc: var(--sys); } - .theme-minimal .filter-toggle[data-type="assistant"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="thinking"] { --rc: var(--muted); } - .theme-minimal .filter-toggle[data-type="tool"] { --rc: var(--tool); } - .theme-minimal .filter-toggle[data-type="sidechain"] { --rc: var(--ring); } - .theme-minimal .filter-toggle[data-type="memory"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="image"] { --rc: var(--note); } + .theme-minimal .filter-toggle[data-type="user"] { + --rc: var(--user); + } + .theme-minimal .filter-toggle[data-type="system"] { + --rc: var(--sys); + } + .theme-minimal .filter-toggle[data-type="assistant"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="thinking"] { + --rc: var(--muted); + } + .theme-minimal .filter-toggle[data-type="tool"] { + --rc: var(--tool); + } + .theme-minimal .filter-toggle[data-type="sidechain"] { + --rc: var(--ring); + } + .theme-minimal .filter-toggle[data-type="memory"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="image"] { + --rc: var(--note); + } .theme-minimal .filter-action-btn, .theme-minimal .search-nav-btn { @@ -45009,30 +46516,56 @@ font-size: 0.94em; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary { flex-direction: row; align-items: flex-end; column-gap: 10px; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .preview-content { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .preview-content { flex: 1 1 0; min-width: 0; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary::after, - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .line-count { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary::after, + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .line-count { flex: none; align-self: flex-end; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(p, ul, ol, pre, blockquote, table) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(p, ul, ol, pre, blockquote, table) { margin: 0; } /* A heading at the top of an agent's answer (`## Summary`) reads as a bold line of the preview, not a title twice the size that fills the clip. */ - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(h1, h2, h3, h4, h5, h6) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(h1, h2, h3, h4, h5, h6) { margin: 0; font-size: 1em; line-height: inherit; @@ -45042,7 +46575,12 @@ metadata (agent id, worktree, branch, usage) and an async notification's task fields (id, status, tokens, tool uses, duration) as one dim wrapped line of key value pairs instead of a card of rows. */ - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) { display: flex; flex-wrap: wrap; align-items: baseline; @@ -45054,11 +46592,24 @@ color: var(--muted); } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dt { font-size: 1em; } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dd + dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dd + + dt { margin-left: 8px; } /* GENERATED by scripts/generate_minimal_pygments_css.py - do not edit. @@ -45245,18 +46796,40 @@ carry at least two classes (layout.css "Specificity, two tiers"). */ /* Lane colours (rail slot → colour; forks are always --lF). */ - .theme-minimal .dag-lc-a { --lc: var(--lA); } - .theme-minimal .dag-lc-b { --lc: var(--lB); } - .theme-minimal .dag-lc-f { --lc: var(--lF); } - .theme-minimal .dag-lc-s { --lc: var(--sys); } - .theme-minimal .dag-lc-p { --lc: var(--asst); } - - .theme-minimal .dag-s1 { --dag-slot: 1; } - .theme-minimal .dag-s2 { --dag-slot: 2; } - .theme-minimal .dag-s3 { --dag-slot: 3; } - .theme-minimal .dag-s4 { --dag-slot: 4; } - .theme-minimal .dag-s5 { --dag-slot: 5; } - .theme-minimal .dag-s6 { --dag-slot: 6; } + .theme-minimal .dag-lc-a { + --lc: var(--lA); + } + .theme-minimal .dag-lc-b { + --lc: var(--lB); + } + .theme-minimal .dag-lc-f { + --lc: var(--lF); + } + .theme-minimal .dag-lc-s { + --lc: var(--sys); + } + .theme-minimal .dag-lc-p { + --lc: var(--asst); + } + + .theme-minimal .dag-s1 { + --dag-slot: 1; + } + .theme-minimal .dag-s2 { + --dag-slot: 2; + } + .theme-minimal .dag-s3 { + --dag-slot: 3; + } + .theme-minimal .dag-s4 { + --dag-slot: 4; + } + .theme-minimal .dag-s5 { + --dag-slot: 5; + } + .theme-minimal .dag-s6 { + --dag-slot: 6; + } /* ------------------------------------------------------------- stage */ @@ -45325,7 +46898,9 @@ } @keyframes dag-run { - to { stroke-dashoffset: -7; } + to { + stroke-dashoffset: -7; + } } /* A column lane's pointers (`col-in`, `col-out`): a short curve towards the @@ -45344,11 +46919,21 @@ stop-opacity: 0; } - .theme-minimal #dag-rail path.dag-fade.dag-lc-a { stroke: url(#dag-fade-a); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-b { stroke: url(#dag-fade-b); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-f { stroke: url(#dag-fade-f); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-s { stroke: url(#dag-fade-s); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-p { stroke: url(#dag-fade-p); } + .theme-minimal #dag-rail path.dag-fade.dag-lc-a { + stroke: url(#dag-fade-a); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-b { + stroke: url(#dag-fade-b); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-f { + stroke: url(#dag-fade-f); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-s { + stroke: url(#dag-fade-s); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-p { + stroke: url(#dag-fade-p); + } @media (prefers-reduced-motion: reduce) { .theme-minimal #dag-rail path.dag-running.dag-dash, @@ -45406,9 +46991,23 @@ /* Workflow phase and agent cards, and the agents' transcripts (lanes), are rows of the grid too: the nested workflow gutter (components.css) only applies without the engine, or inside a block. */ - .theme-minimal .mn-stage.dag-on #transcript .children:has(> .message-node > .message.workflow_phase) > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_phase) > .children > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_agent) > .children > .message-node:not(.dag-block *) { + .theme-minimal + .mn-stage.dag-on + #transcript + .children:has(> .message-node > .message.workflow_phase) + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_phase) + > .children + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_agent) + > .children + > .message-node:not(.dag-block *) { --gut: var(--dag-gut); --rail: var(--dag-rail); } @@ -45444,10 +47043,12 @@ --dag-indent: calc(var(--dag-ind0) + var(--dag-ind-step) * var(--dag-slot, 1)); --dag-c3: calc(var(--gut) + var(--rail)); grid-template-columns: var(--gut) calc(var(--rail) + var(--dag-indent)) minmax(0, 1fr); - background: linear-gradient(90deg, + background: linear-gradient( + 90deg, transparent var(--dag-c3), color-mix(in srgb, var(--lc) 9%, transparent) var(--dag-c3), - transparent 75%); + transparent 75% + ); } .theme-minimal .mn-stage.dag-on #transcript .message.dag-in > .header > .header-info::after { @@ -45472,17 +47073,39 @@ the call, then the tag (and an error pill) from the result, which grows its row if it must rather than overlap the next. Wide layout only. */ @media (min-width: 641px) { - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.pair_first:not(.dag-split) > .header > .header-info::after { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.pair_first:not(.dag-split) + > .header + > .header-info::after { content: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { display: flex; padding-top: 1.85em; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) > .header > .header-info > *, - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info > :not(.mn-role) { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) + > .header + > .header-info + > *, + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info + > :not(.mn-role) { display: none; } } @@ -45682,7 +47305,9 @@ } @keyframes dag-pulse { - 50% { opacity: 0.25; } + 50% { + opacity: 0.25; + } } .theme-minimal #transcript .mn-bmini { @@ -45752,7 +47377,7 @@ display: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded='false']) { + .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded="false"]) { padding-bottom: 3px; } @@ -45816,7 +47441,8 @@ min-width: calc(var(--dag-min, 0px) + 32px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { position: sticky; left: 16px; max-width: calc(var(--dag-view, 100vw) - 32px); @@ -45857,7 +47483,9 @@ overflow: hidden; white-space: nowrap; border-bottom: 1px solid var(--rule); - background: linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), var(--bg); + background: + linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), + var(--bg); } /* A column's head: the name and two compact actions (`⇤`, `−`; their @@ -46086,7 +47714,8 @@ min-width: calc(var(--dag-min, 0px) + 20px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { left: 10px; max-width: calc(var(--dag-view, 100vw) - 20px); } @@ -46118,33 +47747,35 @@ } @@ -47927,7 +49558,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -47984,7 +49615,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -47995,18 +49628,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -48014,7 +49658,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -48033,10 +49676,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -48057,7 +49711,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -48301,10 +49957,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -48359,8 +50025,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -48482,9 +50147,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -48515,7 +50180,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -48602,7 +50267,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -48637,8 +50302,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -48667,17 +50334,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -48693,7 +50360,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -48721,15 +50388,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -48743,8 +50408,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -48776,7 +50441,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -48798,19 +50465,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -50860,2374 +52531,2809 @@ })(); @@ -53365,8 +55471,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -53375,7 +55483,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -53396,7 +55516,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -53418,7 +55537,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -53426,7 +55547,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -53482,7 +55605,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -53518,7 +55643,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -53590,7 +55715,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -53629,7 +55754,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -53659,7 +55784,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -53690,7 +55815,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -53945,7 +56072,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 20px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -53953,7 +56082,9 @@ } .project-card:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -54000,7 +56131,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -54342,7 +56475,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -54416,7 +56551,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -54426,7 +56563,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -54489,14 +56628,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -55332,8 +57479,8 @@ padding: 3px 0; border: 0; border-radius: 4px; - background: linear-gradient(var(--rule), var(--rule)) no-repeat - calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / 1px 100%; + background: linear-gradient(var(--rule), var(--rule)) no-repeat calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / + 1px 100%; color: var(--fg); text-decoration: none; transition: none; @@ -55609,11 +57756,21 @@ } /* Role colours, as on a transcript row (layout.css). */ - .mn-search .mn-hit.mn-k-user { --rc: var(--user); } - .mn-search .mn-hit.mn-k-assistant { --rc: var(--asst); } - .mn-search .mn-hit.mn-k-tool { --rc: var(--tool); } - .mn-search .mn-hit.mn-k-system { --rc: var(--sys); } - .mn-search .mn-hit.mn-k-note { --rc: var(--note); } + .mn-search .mn-hit.mn-k-user { + --rc: var(--user); + } + .mn-search .mn-hit.mn-k-assistant { + --rc: var(--asst); + } + .mn-search .mn-hit.mn-k-tool { + --rc: var(--tool); + } + .mn-search .mn-hit.mn-k-system { + --rc: var(--sys); + } + .mn-search .mn-hit.mn-k-note { + --rc: var(--note); + } .mn-search .mn-hit.mn-k-thinking > a::before { width: 5px; @@ -55697,18 +57854,20 @@ } @@ -56918,7 +59077,7 @@ document.addEventListener('DOMContentLoaded', function () { // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -56975,7 +59134,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -56986,18 +59147,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -57005,7 +59177,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -57024,10 +59195,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -57048,7 +59230,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -57067,97 +59251,105 @@ }); @@ -57295,8 +59487,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -57305,7 +59499,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -57326,7 +59532,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -57348,7 +59553,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -57356,7 +59563,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -57412,7 +59621,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -57448,7 +59659,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -57520,7 +59731,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -57559,7 +59770,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -57589,7 +59800,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -57620,7 +59831,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -57875,7 +60088,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 20px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -57883,7 +60098,9 @@ } .project-card:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -57930,7 +60147,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -58272,7 +60491,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -58346,7 +60567,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -58356,7 +60579,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -58419,14 +60644,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -59262,8 +61495,8 @@ padding: 3px 0; border: 0; border-radius: 4px; - background: linear-gradient(var(--rule), var(--rule)) no-repeat - calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / 1px 100%; + background: linear-gradient(var(--rule), var(--rule)) no-repeat calc(var(--mn-dgut) + var(--rail) / 2 - 0.5px) 0 / + 1px 100%; color: var(--fg); text-decoration: none; transition: none; @@ -59539,11 +61772,21 @@ } /* Role colours, as on a transcript row (layout.css). */ - .mn-search .mn-hit.mn-k-user { --rc: var(--user); } - .mn-search .mn-hit.mn-k-assistant { --rc: var(--asst); } - .mn-search .mn-hit.mn-k-tool { --rc: var(--tool); } - .mn-search .mn-hit.mn-k-system { --rc: var(--sys); } - .mn-search .mn-hit.mn-k-note { --rc: var(--note); } + .mn-search .mn-hit.mn-k-user { + --rc: var(--user); + } + .mn-search .mn-hit.mn-k-assistant { + --rc: var(--asst); + } + .mn-search .mn-hit.mn-k-tool { + --rc: var(--tool); + } + .mn-search .mn-hit.mn-k-system { + --rc: var(--sys); + } + .mn-search .mn-hit.mn-k-note { + --rc: var(--note); + } .mn-search .mn-hit.mn-k-thinking > a::before { width: 5px; @@ -59627,18 +61870,20 @@ } @@ -60880,7 +63125,7 @@ document.addEventListener('DOMContentLoaded', function () { // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -60937,7 +63182,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -60948,18 +63195,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -60967,7 +63225,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -60986,10 +63243,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -61010,7 +63278,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -61029,97 +63299,105 @@ }); @@ -61257,8 +63535,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -61267,7 +63547,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -61288,7 +63580,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -61310,7 +63601,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -61318,7 +63611,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -61374,7 +63669,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -61410,7 +63707,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -61482,7 +63779,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -61521,7 +63818,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -61551,7 +63848,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -61594,7 +63891,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -62233,7 +64532,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -62332,7 +64631,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -62369,7 +64669,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -62465,7 +64767,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -62488,12 +64790,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -62509,7 +64811,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -62709,8 +65013,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -62799,7 +65103,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -62814,7 +65120,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -62918,39 +65226,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -62962,20 +65318,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -63489,7 +65869,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -63512,7 +65892,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -63731,7 +66113,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -63795,7 +66179,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -63804,7 +66187,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -63915,7 +66297,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -64131,7 +66515,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -64139,7 +66523,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -64147,7 +66531,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -64592,7 +66976,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -64666,7 +67052,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -64676,7 +67064,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -64739,14 +67129,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -65080,7 +67478,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -65140,11 +67540,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -66303,7 +68703,10 @@ color: var(--fg); } - .theme-minimal #transcript .mn-title :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { + .theme-minimal + #transcript + .mn-title + :is(.tool-subagent, .message-model, .task-action, .task-async-hint, .pair-duration) { font: inherit; font-weight: 400; color: var(--muted); @@ -66470,13 +68873,29 @@ file, say — or the output) starts right under it: no top margin on it, nor on the code box at the head of a preview inside it. */ .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child, - .theme-minimal #transcript .message:is(.tool_use, .pair_middle, .pair_last) > .content > :first-child > details:first-child > summary > .preview-content > .highlight:first-child { + .theme-minimal + #transcript + .message:is(.tool_use, .pair_middle, .pair_last) + > .content + > :first-child + > details:first-child + > summary + > .preview-content + > .highlight:first-child { margin-top: 0; } /* Turns: one rule above each main-lane prompt, except a session's first. A slash command's output (the pair's last half) belongs to its turn. */ - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last) { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last) { margin-top: 4px; padding-top: 7px; border-top: 1px solid var(--rule2); @@ -66505,8 +68924,21 @@ .theme-minimal #transcript .message:is(.pair_middle, .pair_last):not(.dag-split)::after, .theme-minimal #transcript .message.dag-cfirst::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node:first-child > .message::after, - .theme-minimal #transcript .message-node:has(> .message.session-header) > .children > .message-node ~ .message-node > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not(.pair_middle):not(.pair_last)::after { + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node:first-child + > .message::after, + .theme-minimal + #transcript + .message-node:has(> .message.session-header) + > .children + > .message-node + ~ .message-node + > .message.user:not(.steering):not(.task-notification):not(.teammate):not(.sidechain):not(.command-output):not( + .pair_middle + ):not(.pair_last)::after { content: none; } @@ -66560,7 +68992,11 @@ transition: color 0.12s; } - .theme-minimal #transcript .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) > .fold-bar > .fold-bar-section, + .theme-minimal + #transcript + .message.user:not(.task-notification):not(.teammate):not(.sidechain):not(.steering) + > .fold-bar + > .fold-bar-section, .theme-minimal #transcript .message:hover > .fold-bar > .fold-bar-section, .theme-minimal #transcript .message > .fold-bar:focus-within > .fold-bar-section { color: var(--muted); @@ -66751,7 +69187,9 @@ 2px line; the classic per-depth ring colours still apply. Same selector shape as the classic rule, one class more specific. */ .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin: 4px 0 4px calc(var(--gut) + var(--rail)); padding-left: 8px; border-left-width: 2px; @@ -66759,7 +69197,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.agent-deep) { margin-left: 12px; } @@ -66835,7 +69275,12 @@ } /* User prompt: a warm tint (mockup .turn > .k-user .main). */ - .theme-minimal #transcript .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not(.sidechain) > .content { + .theme-minimal + #transcript + .message.user:not(.steering):not(.task-notification):not(.teammate):not(.compacted):not(.command-output):not( + .sidechain + ) + > .content { margin-left: 4px; padding: 2px 8px; border-radius: 4px; @@ -67052,7 +69497,9 @@ } .theme-minimal .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), - .theme-minimal .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { + .theme-minimal + .message-node:has(> .message.tool_use:not(.workflow_agent)) + > .children:has(> .message-node > .message.sidechain) { margin-left: 16px; padding-left: 6px; } @@ -67519,14 +69966,23 @@ display: flex; } - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last) > .header > .header-info > :not(.mn-role) { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last) + > .header + > .header-info + > :not(.mn-role) { display: none; } /* …one gutter line down, below the call's role, which hangs into this row (layout.css; P7c — the pill used to cover it). */ @media (min-width: 641px) { - .theme-minimal #transcript .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + #transcript + .message.tool_result.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { padding-top: 1.85em; } } @@ -67552,7 +70008,10 @@ --pv: 2.8em; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary { display: flex; flex-direction: column; align-items: stretch; @@ -67565,7 +70024,10 @@ cursor: pointer; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::-webkit-details-marker { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::-webkit-details-marker { display: none; } @@ -67586,7 +70048,10 @@ } /* The "+N lines" / "− less" line (mockup .more). */ - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after, .theme-minimal #transcript details.collapsible-code > summary > .line-count, .theme-minimal #transcript .mn-less { order: 2; @@ -67601,11 +70066,17 @@ color: var(--muted); } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary::after { content: "+ more"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary[data-more]::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary[data-more]::after { content: attr(data-more); } @@ -67627,11 +70098,17 @@ display: none; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] > summary::after { + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible)[open] + > summary::after { content: "− less"; } - .theme-minimal #transcript :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) > summary:hover::after, + .theme-minimal + #transcript + :is(details.collapsible-code, details.collapsible-details, details.tool-param-collapsible) + > summary:hover::after, .theme-minimal #transcript details.collapsible-code > summary:hover > .line-count, .theme-minimal #transcript .mn-less:hover { color: var(--fg); @@ -67736,7 +70213,7 @@ table), so the size is one length resolved on #transcript: a registered computes where it is declared and inherits as that value. */ @property --mn-ctl { - syntax: ''; + syntax: ""; inherits: true; initial-value: 10px; } @@ -68293,14 +70770,30 @@ transform: none; } - .theme-minimal .filter-toggle[data-type="user"] { --rc: var(--user); } - .theme-minimal .filter-toggle[data-type="system"] { --rc: var(--sys); } - .theme-minimal .filter-toggle[data-type="assistant"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="thinking"] { --rc: var(--muted); } - .theme-minimal .filter-toggle[data-type="tool"] { --rc: var(--tool); } - .theme-minimal .filter-toggle[data-type="sidechain"] { --rc: var(--ring); } - .theme-minimal .filter-toggle[data-type="memory"] { --rc: var(--asst); } - .theme-minimal .filter-toggle[data-type="image"] { --rc: var(--note); } + .theme-minimal .filter-toggle[data-type="user"] { + --rc: var(--user); + } + .theme-minimal .filter-toggle[data-type="system"] { + --rc: var(--sys); + } + .theme-minimal .filter-toggle[data-type="assistant"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="thinking"] { + --rc: var(--muted); + } + .theme-minimal .filter-toggle[data-type="tool"] { + --rc: var(--tool); + } + .theme-minimal .filter-toggle[data-type="sidechain"] { + --rc: var(--ring); + } + .theme-minimal .filter-toggle[data-type="memory"] { + --rc: var(--asst); + } + .theme-minimal .filter-toggle[data-type="image"] { + --rc: var(--note); + } .theme-minimal .filter-action-btn, .theme-minimal .search-nav-btn { @@ -68424,30 +70917,56 @@ font-size: 0.94em; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary { flex-direction: row; align-items: flex-end; column-gap: 10px; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .preview-content { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .preview-content { flex: 1 1 0; min-width: 0; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary::after, - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) > details.collapsible-code > summary > .line-count { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary::after, + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + > details.collapsible-code + > summary + > .line-count { flex: none; align-self: flex-end; } - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(p, ul, ol, pre, blockquote, table) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(p, ul, ol, pre, blockquote, table) { margin: 0; } /* A heading at the top of an agent's answer (`## Summary`) reads as a bold line of the preview, not a title twice the size that fills the clip. */ - .theme-minimal #transcript :is(.task-prompt, .task-result, .task-notification-result) .preview-content :is(h1, h2, h3, h4, h5, h6) { + .theme-minimal + #transcript + :is(.task-prompt, .task-result, .task-notification-result) + .preview-content + :is(h1, h2, h3, h4, h5, h6) { margin: 0; font-size: 1em; line-height: inherit; @@ -68457,7 +70976,12 @@ metadata (agent id, worktree, branch, usage) and an async notification's task fields (id, status, tokens, tool uses, duration) as one dim wrapped line of key value pairs instead of a card of rows. */ - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) { display: flex; flex-wrap: wrap; align-items: baseline; @@ -68469,11 +70993,24 @@ color: var(--muted); } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dt { font-size: 1em; } - .theme-minimal #transcript :is(.message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, .message.task-notification > .content > .task-notification-card) dd + dt { + .theme-minimal + #transcript + :is( + .message:is(.tool_use, .tool_result) > .content > .teammate-spawn-card, + .message.task-notification > .content > .task-notification-card + ) + dd + + dt { margin-left: 8px; } /* GENERATED by scripts/generate_minimal_pygments_css.py - do not edit. @@ -68660,18 +71197,40 @@ carry at least two classes (layout.css "Specificity, two tiers"). */ /* Lane colours (rail slot → colour; forks are always --lF). */ - .theme-minimal .dag-lc-a { --lc: var(--lA); } - .theme-minimal .dag-lc-b { --lc: var(--lB); } - .theme-minimal .dag-lc-f { --lc: var(--lF); } - .theme-minimal .dag-lc-s { --lc: var(--sys); } - .theme-minimal .dag-lc-p { --lc: var(--asst); } - - .theme-minimal .dag-s1 { --dag-slot: 1; } - .theme-minimal .dag-s2 { --dag-slot: 2; } - .theme-minimal .dag-s3 { --dag-slot: 3; } - .theme-minimal .dag-s4 { --dag-slot: 4; } - .theme-minimal .dag-s5 { --dag-slot: 5; } - .theme-minimal .dag-s6 { --dag-slot: 6; } + .theme-minimal .dag-lc-a { + --lc: var(--lA); + } + .theme-minimal .dag-lc-b { + --lc: var(--lB); + } + .theme-minimal .dag-lc-f { + --lc: var(--lF); + } + .theme-minimal .dag-lc-s { + --lc: var(--sys); + } + .theme-minimal .dag-lc-p { + --lc: var(--asst); + } + + .theme-minimal .dag-s1 { + --dag-slot: 1; + } + .theme-minimal .dag-s2 { + --dag-slot: 2; + } + .theme-minimal .dag-s3 { + --dag-slot: 3; + } + .theme-minimal .dag-s4 { + --dag-slot: 4; + } + .theme-minimal .dag-s5 { + --dag-slot: 5; + } + .theme-minimal .dag-s6 { + --dag-slot: 6; + } /* ------------------------------------------------------------- stage */ @@ -68740,7 +71299,9 @@ } @keyframes dag-run { - to { stroke-dashoffset: -7; } + to { + stroke-dashoffset: -7; + } } /* A column lane's pointers (`col-in`, `col-out`): a short curve towards the @@ -68759,11 +71320,21 @@ stop-opacity: 0; } - .theme-minimal #dag-rail path.dag-fade.dag-lc-a { stroke: url(#dag-fade-a); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-b { stroke: url(#dag-fade-b); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-f { stroke: url(#dag-fade-f); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-s { stroke: url(#dag-fade-s); } - .theme-minimal #dag-rail path.dag-fade.dag-lc-p { stroke: url(#dag-fade-p); } + .theme-minimal #dag-rail path.dag-fade.dag-lc-a { + stroke: url(#dag-fade-a); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-b { + stroke: url(#dag-fade-b); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-f { + stroke: url(#dag-fade-f); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-s { + stroke: url(#dag-fade-s); + } + .theme-minimal #dag-rail path.dag-fade.dag-lc-p { + stroke: url(#dag-fade-p); + } @media (prefers-reduced-motion: reduce) { .theme-minimal #dag-rail path.dag-running.dag-dash, @@ -68821,9 +71392,23 @@ /* Workflow phase and agent cards, and the agents' transcripts (lanes), are rows of the grid too: the nested workflow gutter (components.css) only applies without the engine, or inside a block. */ - .theme-minimal .mn-stage.dag-on #transcript .children:has(> .message-node > .message.workflow_phase) > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_phase) > .children > .message-node:not(.dag-block *), - .theme-minimal .mn-stage.dag-on #transcript .message-node:has(> .message.workflow_agent) > .children > .message-node:not(.dag-block *) { + .theme-minimal + .mn-stage.dag-on + #transcript + .children:has(> .message-node > .message.workflow_phase) + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_phase) + > .children + > .message-node:not(.dag-block *), + .theme-minimal + .mn-stage.dag-on + #transcript + .message-node:has(> .message.workflow_agent) + > .children + > .message-node:not(.dag-block *) { --gut: var(--dag-gut); --rail: var(--dag-rail); } @@ -68859,10 +71444,12 @@ --dag-indent: calc(var(--dag-ind0) + var(--dag-ind-step) * var(--dag-slot, 1)); --dag-c3: calc(var(--gut) + var(--rail)); grid-template-columns: var(--gut) calc(var(--rail) + var(--dag-indent)) minmax(0, 1fr); - background: linear-gradient(90deg, + background: linear-gradient( + 90deg, transparent var(--dag-c3), color-mix(in srgb, var(--lc) 9%, transparent) var(--dag-c3), - transparent 75%); + transparent 75% + ); } .theme-minimal .mn-stage.dag-on #transcript .message.dag-in > .header > .header-info::after { @@ -68887,17 +71474,39 @@ the call, then the tag (and an error pill) from the result, which grows its row if it must rather than overlap the next. Wide layout only. */ @media (min-width: 641px) { - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.pair_first:not(.dag-split) > .header > .header-info::after { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.pair_first:not(.dag-split) + > .header + > .header-info::after { content: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info { display: flex; padding-top: 1.85em; } - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) > .header > .header-info > *, - .theme-minimal .mn-stage.dag-on #transcript .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) > .header > .header-info > :not(.mn-role) { + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in:is(.pair_middle, .pair_last):not(.dag-split):not(.error) + > .header + > .header-info + > *, + .theme-minimal + .mn-stage.dag-on + #transcript + .message.dag-in.error:is(.pair_middle, .pair_last):not(.dag-split) + > .header + > .header-info + > :not(.mn-role) { display: none; } } @@ -69097,7 +71706,9 @@ } @keyframes dag-pulse { - 50% { opacity: 0.25; } + 50% { + opacity: 0.25; + } } .theme-minimal #transcript .mn-bmini { @@ -69167,7 +71778,7 @@ display: none; } - .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded='false']) { + .theme-minimal .mn-stage.dag-on #transcript .message.pair_first:has(> .mn-bctls .mn-back[aria-expanded="false"]) { padding-bottom: 3px; } @@ -69231,7 +71842,8 @@ min-width: calc(var(--dag-min, 0px) + 32px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { position: sticky; left: 16px; max-width: calc(var(--dag-view, 100vw) - 32px); @@ -69272,7 +71884,9 @@ overflow: hidden; white-space: nowrap; border-bottom: 1px solid var(--rule); - background: linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), var(--bg); + background: + linear-gradient(color-mix(in srgb, var(--lc) 5%, transparent), color-mix(in srgb, var(--lc) 5%, transparent)), + var(--bg); } /* A column's head: the name and two compact actions (`⇤`, `−`; their @@ -69501,7 +72115,8 @@ min-width: calc(var(--dag-min, 0px) + 20px); } - body.theme-minimal.dag-wide :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { + body.theme-minimal.dag-wide + :is(.mn-top, .mn-toolbar, .filter-toolbar, #timeline-container, .navigation, .page-navigation) { left: 10px; max-width: calc(var(--dag-view, 100vw) - 20px); } @@ -69533,33 +72148,35 @@ } @@ -70891,7 +73508,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -70948,7 +73565,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -70959,18 +73578,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -70978,7 +73608,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -70997,10 +73626,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -71021,7 +73661,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -71265,10 +73907,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -71323,8 +73975,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -71446,9 +74097,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -71479,7 +74130,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -71566,7 +74217,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -71601,8 +74252,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -71631,17 +74284,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -71657,7 +74310,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -71685,15 +74338,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -71707,8 +74358,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -71740,7 +74391,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -71762,19 +74415,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -73824,2374 +76481,2809 @@ })(); @@ -76329,8 +79421,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -76339,7 +79433,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -76360,7 +79466,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -76382,7 +79487,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -76390,7 +79497,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -76446,7 +79555,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -76482,7 +79593,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -76554,7 +79665,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -76593,7 +79704,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -76623,7 +79734,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -76666,7 +79777,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -77305,7 +80418,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -77404,7 +80517,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -77441,7 +80555,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -77537,7 +80653,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -77560,12 +80676,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -77581,7 +80697,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -77781,8 +80899,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -77871,7 +80989,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -77886,7 +81006,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -77990,39 +81112,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -78034,20 +81204,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -78561,7 +81755,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -78584,7 +81778,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -78803,7 +81999,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -78867,7 +82065,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -78876,7 +82073,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -78987,7 +82183,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -79203,7 +82401,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -79211,7 +82409,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -79219,7 +82417,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -79664,7 +82862,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -79738,7 +82938,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -79748,7 +82950,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -79811,14 +83015,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -80152,7 +83364,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -80212,11 +83426,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -81971,7 +85185,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -82028,7 +85242,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -82039,18 +85255,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -82058,7 +85285,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -82077,10 +85303,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -82101,7 +85338,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -82345,10 +85584,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -82403,8 +85652,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -82526,9 +85774,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -82559,7 +85807,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -82646,7 +85894,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -82681,8 +85929,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -82711,17 +85961,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -82737,7 +85987,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -82765,15 +86015,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -82787,8 +86035,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -82820,7 +86068,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -82842,19 +86092,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -85020,8 +88274,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -85030,7 +88286,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -85051,7 +88319,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -85073,7 +88340,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -85081,7 +88350,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -85137,7 +88408,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -85173,7 +88446,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -85245,7 +88518,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -85284,7 +88557,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -85314,7 +88587,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -85357,7 +88630,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -85996,7 +89271,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -86095,7 +89370,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -86132,7 +89408,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -86228,7 +89506,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -86251,12 +89529,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -86272,7 +89550,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -86472,8 +89752,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -86562,7 +89842,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -86577,7 +89859,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -86681,39 +89965,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -86725,20 +90057,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -87252,7 +90608,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -87275,7 +90631,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -87494,7 +90852,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -87558,7 +90918,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -87567,7 +90926,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -87678,7 +91036,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -87894,7 +91254,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -87902,7 +91262,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -87910,7 +91270,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -88355,7 +91715,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -88429,7 +91791,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -88439,7 +91803,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -88502,14 +91868,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -88843,7 +92217,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -88903,11 +92279,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -90942,7 +94318,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -90999,7 +94375,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -91010,18 +94388,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -91029,7 +94418,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -91048,10 +94436,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -91072,7 +94471,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -91316,10 +94717,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -91374,8 +94785,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -91497,9 +94907,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -91530,7 +94940,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -91617,7 +95027,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -91652,8 +95062,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -91682,17 +95094,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -91708,7 +95120,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -91736,15 +95148,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -91758,8 +95168,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -91791,7 +95201,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -91813,19 +95225,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -93991,8 +97407,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -94001,7 +97419,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -94022,7 +97452,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -94044,7 +97473,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -94052,7 +97483,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -94108,7 +97541,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -94144,7 +97579,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -94216,7 +97651,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -94255,7 +97690,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -94285,7 +97720,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -94328,7 +97763,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -94967,7 +98404,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -95066,7 +98503,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -95103,7 +98541,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -95199,7 +98639,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -95222,12 +98662,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -95243,7 +98683,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -95443,8 +98885,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -95533,7 +98975,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -95548,7 +98992,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -95652,39 +99098,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -95696,20 +99190,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -96223,7 +99741,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -96246,7 +99764,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -96465,7 +99985,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -96529,7 +100051,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -96538,7 +100059,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -96649,7 +100169,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -96865,7 +100387,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -96873,7 +100395,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -96881,7 +100403,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -97326,7 +100848,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -97400,7 +100924,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -97410,7 +100936,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -97473,14 +101001,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -97814,7 +101350,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -97874,11 +101412,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -99860,7 +103398,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -99917,7 +103455,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -99928,18 +103468,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -99947,7 +103498,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -99966,10 +103516,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -99990,7 +103551,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -100234,10 +103797,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -100292,8 +103865,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -100415,9 +103987,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -100448,7 +104020,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -100535,7 +104107,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -100570,8 +104142,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -100600,17 +104174,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -100626,7 +104200,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -100654,15 +104228,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -100676,8 +104248,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -100709,7 +104281,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -100731,19 +104305,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -102909,8 +106487,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -102919,7 +106499,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -102940,7 +106532,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -102962,7 +106553,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -102970,7 +106563,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -103026,7 +106621,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -103062,7 +106659,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -103134,7 +106731,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -103173,7 +106770,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -103203,7 +106800,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -103246,7 +106843,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -103885,7 +107484,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -103984,7 +107583,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -104021,7 +107621,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -104117,7 +107719,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -104140,12 +107742,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -104161,7 +107763,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -104361,8 +107965,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -104451,7 +108055,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -104466,7 +108072,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -104570,39 +108178,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -104614,20 +108270,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -105141,7 +108821,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -105164,7 +108844,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -105383,7 +109065,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -105447,7 +109131,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -105456,7 +109139,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -105567,7 +109249,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -105783,7 +109467,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -105791,7 +109475,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -105799,7 +109483,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -106244,7 +109928,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -106318,7 +110004,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -106328,7 +110016,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -106391,14 +110081,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -106732,7 +110430,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -106792,11 +110492,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -108723,7 +112423,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -108780,7 +112480,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -108791,18 +112493,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -108810,7 +112523,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -108829,10 +112541,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -108853,7 +112576,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -109097,10 +112822,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -109155,8 +112890,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -109278,9 +113012,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -109311,7 +113045,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -109398,7 +113132,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -109433,8 +113167,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -109463,17 +113199,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -109489,7 +113225,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -109517,15 +113253,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -109539,8 +113273,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -109572,7 +113306,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -109594,19 +113330,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -111772,8 +115512,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -111782,7 +115524,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -111803,7 +115557,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -111825,7 +115578,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -111833,7 +115588,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -111889,7 +115646,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -111925,7 +115684,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -111997,7 +115756,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -112036,7 +115795,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -112066,7 +115825,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -112109,7 +115868,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -112748,7 +116509,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -112847,7 +116608,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -112884,7 +116646,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -112980,7 +116744,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -113003,12 +116767,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -113024,7 +116788,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -113224,8 +116990,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -113314,7 +117080,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -113329,7 +117097,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -113433,39 +117203,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -113477,20 +117295,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -114004,7 +117846,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -114027,7 +117869,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -114246,7 +118090,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -114310,7 +118156,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -114319,7 +118164,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -114430,7 +118274,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -114646,7 +118492,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -114654,7 +118500,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -114662,7 +118508,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -115107,7 +118953,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -115181,7 +119029,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -115191,7 +119041,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -115254,14 +119106,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -115595,7 +119455,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -115655,11 +119517,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -117414,7 +121276,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -117471,7 +121333,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -117482,18 +121346,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -117501,7 +121376,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -117520,10 +121394,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -117544,7 +121429,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -117788,10 +121675,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -117846,8 +121743,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -117969,9 +121865,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -118002,7 +121898,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -118089,7 +121985,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -118124,8 +122020,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -118154,17 +122052,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -118180,7 +122078,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -118208,15 +122106,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -118230,8 +122126,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -118263,7 +122159,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -118285,19 +122183,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -120463,8 +124365,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -120473,7 +124377,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -120494,7 +124410,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -120516,7 +124431,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -120524,7 +124441,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -120580,7 +124499,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -120616,7 +124537,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -120688,7 +124609,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -120727,7 +124648,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -120757,7 +124678,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -120800,7 +124721,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -121439,7 +125362,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -121538,7 +125461,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -121575,7 +125499,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -121671,7 +125597,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -121694,12 +125620,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -121715,7 +125641,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -121915,8 +125843,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -122005,7 +125933,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -122020,7 +125950,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -122124,39 +126056,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -122168,20 +126148,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -122695,7 +126699,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -122718,7 +126722,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -122937,7 +126943,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -123001,7 +127009,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -123010,7 +127017,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -123121,7 +127127,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -123337,7 +127345,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -123345,7 +127353,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -123353,7 +127361,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -123798,7 +127806,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -123872,7 +127882,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -123882,7 +127894,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -123945,14 +127959,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -124286,7 +128308,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -124346,11 +128370,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -125910,7 +129934,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -125967,7 +129991,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -125978,18 +130004,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -125997,7 +130034,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -126016,10 +130052,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -126040,7 +130087,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -126284,10 +130333,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -126342,8 +130401,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -126465,9 +130523,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -126498,7 +130556,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -126585,7 +130643,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -126620,8 +130678,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -126650,17 +130710,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -126676,7 +130736,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -126704,15 +130764,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -126726,8 +130784,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -126759,7 +130817,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -126781,19 +130841,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) { @@ -128959,8 +133023,10 @@ --message-padding: 1em; /* Font families */ - --font-monospace: 'Fira Code', 'Monaco', 'Consolas', 'SF Mono', 'Inconsolata', 'Droid Sans Mono', 'Source Code Pro', 'Ubuntu Mono', 'Cascadia Code', 'Menlo', monospace; - --font-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-monospace: + "Fira Code", "Monaco", "Consolas", "SF Mono", "Inconsolata", "Droid Sans Mono", "Source Code Pro", + "Ubuntu Mono", "Cascadia Code", "Menlo", monospace; + --font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } body { @@ -128969,7 +133035,19 @@ max-width: 1200px; margin: 0 auto; padding: 10px; - background: linear-gradient(90deg, #f3d6d2, #f1dcce, #f0e4ca, #eeecc7, #e3ecc3, #d5eac0, #c6e8bd, #b9e6bc, #b6e3c5, #b3e1cf); + background: linear-gradient( + 90deg, + #f3d6d2, + #f1dcce, + #f0e4ca, + #eeecc7, + #e3ecc3, + #d5eac0, + #c6e8bd, + #b9e6bc, + #b6e3c5, + #b3e1cf + ); color: var(--text-primary); } @@ -128990,7 +133068,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -129012,7 +133089,9 @@ background-color: #ffffff66; border-radius: 8px; padding: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -129020,7 +133099,9 @@ } .card-base:hover { - box-shadow: -10px -10px 15px #eeeeee66, 10px 10px 15px #00000022; + box-shadow: + -10px -10px 15px #eeeeee66, + 10px 10px 15px #00000022; transform: translateY(-1px); transition: all 0.2s ease; } @@ -129076,7 +133157,9 @@ align-items: center; justify-content: center; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: background-color 0.3s, transform 0.2s; + transition: + background-color 0.3s, + transform 0.2s; z-index: 1000; text-decoration: none; } @@ -129112,7 +133195,7 @@ .debug-toggle.floating-btn { bottom: 260px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -129184,7 +133267,7 @@ right: 2px; padding: 1px 6px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; background: transparent; border: 1px solid #ccc; border-radius: 3px; @@ -129223,7 +133306,7 @@ .user-view-global.floating-btn { bottom: 320px; font-size: 0.7em; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-weight: 600; } @@ -129253,7 +133336,7 @@ * wraps to one or two lines depending on the viewport. */ .resume-toast { position: fixed; - right: calc(20px + 50px + 12px); /* button right + width + gap */ + right: calc(20px + 50px + 12px); /* button right + width + gap */ bottom: calc(var(--resume-btn-bottom) + 25px); transform: translateY(50%); max-width: min(320px, calc(100vw - 120px)); @@ -129296,7 +133379,9 @@ border-radius: 8px; border-left: var(--white-dimmed) 2px solid; background-color: var(--highlight-light); - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -129935,7 +134020,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -130034,7 +134119,8 @@ background-color: var(--error-semi); } - .message.tool_use pre, .message.tool_result pre { + .message.tool_use pre, + .message.tool_result pre { font-size: 80%; } @@ -130071,7 +134157,9 @@ border-radius: 8px; padding: 16px; margin: 30px 0 20px 0; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -130167,7 +134255,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -130190,12 +134278,12 @@ } .markdown td { - padding: .5em; + padding: 0.5em; border-top: 1px solid #bbb; } .markdown th { - padding: .5em; + padding: 0.5em; border-bottom: 1px solid #888; } @@ -130211,7 +134299,9 @@ padding: 8px; margin: 8px 0; overflow-x: auto; - box-shadow: -4px -4px 10px #eeeeee33, 4px 4px 10px #00000007; + box-shadow: + -4px -4px 10px #eeeeee33, + 4px 4px 10px #00000007; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -130411,8 +134501,8 @@ list-style-type: disclosure-closed; } - .tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph, - [data-state='expanded'] > .tool-param-fold-glyph { + .tool-param-key-toggle[aria-expanded="true"] > .tool-param-fold-glyph, + [data-state="expanded"] > .tool-param-fold-glyph { list-style-type: "\23F7"; list-style-type: disclosure-open; } @@ -130501,7 +134591,9 @@ padding: 6px; margin: 4px 0; font-size: 0.9em; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -130516,7 +134608,9 @@ margin: 8px 0; border-radius: 0 4px 4px 0; font-style: italic; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; border-right: #00000017 1px solid; @@ -130620,39 +134714,87 @@ /* ANSI color classes */ /* Standard colors */ - .ansi-black { color: #000000; } - .ansi-red { color: #cd3131; } - .ansi-green { color: #0dbc79; } - .ansi-yellow { color: #e5e510; } - .ansi-blue { color: #2472c8; } - .ansi-magenta { color: #bc3fbc; } - .ansi-cyan { color: #11a8cd; } - .ansi-white { color: #e5e5e5; } + .ansi-black { + color: #000000; + } + .ansi-red { + color: #cd3131; + } + .ansi-green { + color: #0dbc79; + } + .ansi-yellow { + color: #e5e510; + } + .ansi-blue { + color: #2472c8; + } + .ansi-magenta { + color: #bc3fbc; + } + .ansi-cyan { + color: #11a8cd; + } + .ansi-white { + color: #e5e5e5; + } /* Bright colors */ - .ansi-bright-black { color: #666666; } - .ansi-bright-red { color: #f14c4c; } - .ansi-bright-green { color: #23d18b; } - .ansi-bright-yellow { color: #f5f543; } - .ansi-bright-blue { color: #3b8eea; } - .ansi-bright-magenta { color: #d670d6; } - .ansi-bright-cyan { color: #29b8db; } - .ansi-bright-white { color: #ffffff; } + .ansi-bright-black { + color: #666666; + } + .ansi-bright-red { + color: #f14c4c; + } + .ansi-bright-green { + color: #23d18b; + } + .ansi-bright-yellow { + color: #f5f543; + } + .ansi-bright-blue { + color: #3b8eea; + } + .ansi-bright-magenta { + color: #d670d6; + } + .ansi-bright-cyan { + color: #29b8db; + } + .ansi-bright-white { + color: #ffffff; + } /* Background colors */ - .ansi-bg-black { background-color: #000000; } - .ansi-bg-red { background-color: #cd3131; } - .ansi-bg-green { background-color: #0dbc79; } - .ansi-bg-yellow { background-color: #e5e510; } - .ansi-bg-blue { background-color: #2472c8; } - .ansi-bg-magenta { background-color: #bc3fbc; } - .ansi-bg-cyan { background-color: #11a8cd; } - .ansi-bg-white { background-color: #e5e5e5; } + .ansi-bg-black { + background-color: #000000; + } + .ansi-bg-red { + background-color: #cd3131; + } + .ansi-bg-green { + background-color: #0dbc79; + } + .ansi-bg-yellow { + background-color: #e5e510; + } + .ansi-bg-blue { + background-color: #2472c8; + } + .ansi-bg-magenta { + background-color: #bc3fbc; + } + .ansi-bg-cyan { + background-color: #11a8cd; + } + .ansi-bg-white { + background-color: #e5e5e5; + } /* Debug UUID info */ .debug-info { display: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.7em; color: #999; padding: 2px 0; @@ -130664,20 +134806,44 @@ } /* Bright background colors */ - .ansi-bg-bright-black { background-color: #666666; } - .ansi-bg-bright-red { background-color: #f14c4c; } - .ansi-bg-bright-green { background-color: #23d18b; } - .ansi-bg-bright-yellow { background-color: #f5f543; } - .ansi-bg-bright-blue { background-color: #3b8eea; } - .ansi-bg-bright-magenta { background-color: #d670d6; } - .ansi-bg-bright-cyan { background-color: #29b8db; } - .ansi-bg-bright-white { background-color: #ffffff; } + .ansi-bg-bright-black { + background-color: #666666; + } + .ansi-bg-bright-red { + background-color: #f14c4c; + } + .ansi-bg-bright-green { + background-color: #23d18b; + } + .ansi-bg-bright-yellow { + background-color: #f5f543; + } + .ansi-bg-bright-blue { + background-color: #3b8eea; + } + .ansi-bg-bright-magenta { + background-color: #d670d6; + } + .ansi-bg-bright-cyan { + background-color: #29b8db; + } + .ansi-bg-bright-white { + background-color: #ffffff; + } /* Text styles */ - .ansi-bold { font-weight: bold; } - .ansi-dim { opacity: 0.7; } - .ansi-italic { font-style: italic; } - .ansi-underline { text-decoration: underline; } + .ansi-bold { + font-weight: bold; + } + .ansi-dim { + opacity: 0.7; + } + .ansi-italic { + font-style: italic; + } + .ansi-underline { + text-decoration: underline; + } /* Image styling */ /* Any image a message carries (one from rendered Markdown too) stays in the @@ -131191,7 +135357,7 @@ border-radius: 9px; background-color: #d64545; color: #fff; - font-family: 'SFMono-Regular', Consolas, monospace; + font-family: "SFMono-Regular", Consolas, monospace; font-size: 10px; font-weight: 600; line-height: 17px; @@ -131214,7 +135380,9 @@ border-radius: 8px; padding: 16px; margin-bottom: 24px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -131433,7 +135601,9 @@ border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -131497,7 +135667,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -131506,7 +135675,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -131617,7 +135785,9 @@ background-color: var(--bg-card); border-radius: 6px; padding: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--bg-card) 1px solid; border-top: var(--bg-card) 1px solid; border-bottom: #00000017 1px solid; @@ -131833,7 +136003,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -131841,7 +136011,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-hover-bg, #bbb)); } - #timeline-resize-handle:hover>div { + #timeline-resize-handle:hover > div { background: var(--timeline-grip-hover, #777); } @@ -131849,7 +136019,7 @@ background: linear-gradient(to bottom, transparent, var(--timeline-handle-active-bg, #999)); } - #timeline-resize-handle:active>div { + #timeline-resize-handle:active > div { background: var(--timeline-grip-active, #555); } @@ -132294,7 +136464,9 @@ padding: 20px; background-color: var(--white-dimmed); border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: var(--white-dimmed) 1px solid; border-top: var(--white-dimmed) 1px solid; border-bottom: #00000017 1px solid; @@ -132368,7 +136540,9 @@ border-radius: 6px; cursor: pointer; transition: all 0.2s; - box-shadow: -3px -3px 5px #eeeeee22, 3px 3px 5px #00000008; + box-shadow: + -3px -3px 5px #eeeeee22, + 3px 3px 5px #00000008; border-left: #ffffff44 1px solid; border-top: #ffffff44 1px solid; border-bottom: #00000012 1px solid; @@ -132378,7 +136552,9 @@ .search-result-item:hover { background-color: #ffffff99; transform: translateY(-1px); - box-shadow: -5px -5px 8px #eeeeee33, 5px 5px 8px #00000012; + box-shadow: + -5px -5px 8px #eeeeee33, + 5px 5px 8px #00000012; } .search-result-item a { @@ -132441,14 +136617,22 @@ } .search-loading::after { - content: '...'; + content: "..."; animation: dots 1.5s infinite; } @keyframes dots { - 0%, 20% { content: '.'; } - 40% { content: '..'; } - 60%, 100% { content: '...'; } + 0%, + 20% { + content: "."; + } + 40% { + content: ".."; + } + 60%, + 100% { + content: "..."; + } } /* Keyboard shortcut hint */ @@ -132782,7 +136966,9 @@ padding: 15px; background-color: #ffffff66; border-radius: 8px; - box-shadow: -7px -7px 10px #eeeeee44, 7px 7px 10px #00000011; + box-shadow: + -7px -7px 10px #eeeeee44, + 7px 7px 10px #00000011; border-left: #ffffff66 1px solid; border-top: #ffffff66 1px solid; border-bottom: #00000017 1px solid; @@ -132842,11 +137028,11 @@ } .page-nav-link.prev::before { - content: ''; + content: ""; } .page-nav-link.next::after { - content: ''; + content: ""; } /* Hide next link on last page (will be revealed via in-place editing when new page is created) */ @@ -134355,7 +138541,7 @@ // Timezone conversion (included as component) // Convert timestamps to user's timezone // This function can be called directly or will auto-run on DOMContentLoaded if included standalone - (function() { + (function () { // `root` scopes the work to a subtree. A live update replaces only the // transcript container, and its new cards carry raw ISO timestamps; // re-converting the whole document would redo every card that is @@ -134412,7 +138598,9 @@ const utcTime = utcFormatter.format(date).replace(/, /g, ' '); // Get timezone abbreviation (reuse formatter) - const timezoneName = tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || userTimezone; + const timezoneName = + tzNameFormatter.formatToParts(date).find(part => part.type === 'timeZoneName')?.value || + userTimezone; // Handle time ranges (earliest to latest) if (rawTimestampEnd) { @@ -134423,18 +138611,29 @@ // Update the element with range if (localTime !== utcTime || localTimeEnd !== utcTimeEnd) { - element.innerHTML = localTime + ' to ' + localTimeEnd + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + + ' to ' + + localTimeEnd + + ' (' + + timezoneName + + ')'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } else { // If they're the same (user is in UTC), just show UTC - element.innerHTML = utcTime + ' to ' + utcTimeEnd + ' (UTC)'; + element.innerHTML = + utcTime + + ' to ' + + utcTimeEnd + + ' (UTC)'; element.title = 'UTC: ' + utcTime + ' to ' + utcTimeEnd; } } } else { // Single timestamp if (localTime !== utcTime) { - element.innerHTML = localTime + ' (' + timezoneName + ')'; + element.innerHTML = + localTime + ' (' + timezoneName + ')'; element.title = duration ? duration : 'UTC: ' + utcTime; } else { // If they're the same (user is in UTC), just show UTC @@ -134442,7 +138641,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -134461,10 +138659,21 @@ // within a few ms of a straight synchronous pass, while still handing // the main thread back whenever the browser wants it. const scheduleWork = window.requestIdleCallback - ? function(cb) { window.requestIdleCallback(cb, { timeout: 200 }); } - // No requestIdleCallback (Safari < 16): a macrotask still yields - // between slices, and the synthetic deadline keeps them bounded. - : function(cb) { setTimeout(function() { cb({ timeRemaining: function() { return 8; }, didTimeout: false }); }, 0); }; + ? function (cb) { + window.requestIdleCallback(cb, { timeout: 200 }); + } + : // No requestIdleCallback (Safari < 16): a macrotask still yields + // between slices, and the synthetic deadline keeps them bounded. + function (cb) { + setTimeout(function () { + cb({ + timeRemaining: function () { + return 8; + }, + didTimeout: false + }); + }, 0); + }; let cursor = 0; function drain(deadline) { @@ -134485,7 +138694,9 @@ // one, so the largest pages yield instead of blocking on load. const firstSliceEnds = performance.now() + 24; drain({ - timeRemaining: function() { return Math.max(0, firstSliceEnds - performance.now()); }, + timeRemaining: function () { + return Math.max(0, firstSliceEnds - performance.now()); + }, didTimeout: false }); } @@ -134729,10 +138940,20 @@ let ok = true; root.querySelectorAll('.message-node').forEach(node => { const key = nodeKey(node); - if (!key) { ok = false; return; } + if (!key) { + ok = false; + return; + } ids.push(key); if (withHashes) { - hashes.set(key, hashOf(ownParts(node).map(el => el.outerHTML).join(''))); + hashes.set( + key, + hashOf( + ownParts(node) + .map(el => el.outerHTML) + .join('') + ) + ); } }); return { ids, hashes, ok }; @@ -134787,8 +139008,7 @@ } function insertNode(newNode, imported) { - const parentNode = newNode.parentElement - && newNode.parentElement.closest('.message-node'); + const parentNode = newNode.parentElement && newNode.parentElement.closest('.message-node'); let liveKids; if (!parentNode) { liveKids = container(); @@ -134910,9 +139130,9 @@ followBtn.dataset.unseen = String(unseen); followBtn.title = following ? 'Following new messages — click to stop' - : (unseen - ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` - : 'Follow new messages as they arrive'); + : unseen + ? `${unseen} new message${unseen === 1 ? '' : 's'} — click to follow` + : 'Follow new messages as they arrive'; document.body.classList.toggle('live-following', following); } @@ -134943,7 +139163,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -135030,7 +139250,7 @@ head.headers.get('Last-Modified') || '', length, head.headers.get('ETag') || '', - head.headers.get('X-Content-Revision') || '', + head.headers.get('X-Content-Revision') || '' ].join('|'); const served = lastStamp === null ? loadedLength() : null; const missedOnLoad = !!served && !!length && Number(length) !== served; @@ -135065,8 +139285,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -135095,17 +139317,17 @@ (function () { 'use strict'; - const MAX_FRACTION = 0.9; // of the viewport, each way - const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) - const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) + const MAX_FRACTION = 0.9; // of the viewport, each way + const WHEEL_RATE = 0.0015; // zoom factor per wheel pixel (exp) + const LINE_PX = 16; // pixels per wheel "line" (deltaMode 1) let dialog = null; let frame = null; let zoomImg = null; - let source = null; // the page image the dialog was opened from - let nat = { w: 0, h: 0 }; // natural size - let box = { w: 0, h: 0 }; // frame size - let fit = 1; // scale at which the image fills the frame + let source = null; // the page image the dialog was opened from + let nat = { w: 0, h: 0 }; // natural size + let box = { w: 0, h: 0 }; // frame size + let fit = 1; // scale at which the image fills the frame let scale = 1; let x = 0; let y = 0; @@ -135121,7 +139343,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -135149,15 +139371,13 @@ const maxW = Math.floor(vp.w * MAX_FRACTION); const maxH = Math.floor(vp.h * MAX_FRACTION); const oldFit = fit; - const centre = keepView - ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } - : null; + const centre = keepView ? { u: (box.w / 2 - x) / scale, v: (box.h / 2 - y) / scale } : null; fit = Math.min(maxW / nat.w, maxH / nat.h, 1); box = { w: Math.round(nat.w * fit), h: Math.round(nat.h * fit) }; frame.style.width = box.w + 'px'; frame.style.height = box.h + 'px'; if (centre) { - scale = clamp(scale * fit / oldFit, fit, 1); + scale = clamp((scale * fit) / oldFit, fit, 1); x = box.w / 2 - centre.u * scale; y = box.h / 2 - centre.v * scale; } else { @@ -135171,8 +139391,8 @@ function zoomAt(px, py, next) { next = clamp(next, fit, 1); - x = px - (px - x) * next / scale; - y = py - (py - y) * next / scale; + x = px - ((px - x) * next) / scale; + y = py - ((py - y) * next) / scale; scale = next; clampPan(); apply(); @@ -135204,7 +139424,9 @@ dialog.appendChild(close); document.body.appendChild(dialog); - close.addEventListener('click', function () { dialog.close(); }); + close.addEventListener('click', function () { + dialog.close(); + }); // The layer around the frame is the dialog element itself, so a // click whose target is the dialog was outside the frame. A drag @@ -135226,19 +139448,23 @@ // Over the frame the wheel zooms about the cursor; anywhere in the // dialog (the backdrop included) it never scrolls the page behind. - dialog.addEventListener('wheel', function (event) { - event.preventDefault(); - const r = frame.getBoundingClientRect(); - const px = event.clientX - r.left; - const py = event.clientY - r.top; - if (px < 0 || py < 0 || px > r.width || py > r.height) return; - // WHEEL_RATE is per pixel. Firefox reports a notch in lines - // (deltaMode 1), and a wheel can report pages (2). - let deltaY = event.deltaY; - if (event.deltaMode === 1) deltaY *= LINE_PX; - else if (event.deltaMode === 2) deltaY *= box.h; - zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); - }, { passive: false }); + dialog.addEventListener( + 'wheel', + function (event) { + event.preventDefault(); + const r = frame.getBoundingClientRect(); + const px = event.clientX - r.left; + const py = event.clientY - r.top; + if (px < 0 || py < 0 || px > r.width || py > r.height) return; + // WHEEL_RATE is per pixel. Firefox reports a notch in lines + // (deltaMode 1), and a wheel can report pages (2). + let deltaY = event.deltaY; + if (event.deltaMode === 1) deltaY *= LINE_PX; + else if (event.deltaMode === 2) deltaY *= box.h; + zoomAt(px, py, scale * Math.exp(-deltaY * WHEEL_RATE)); + }, + { passive: false } + ); let drag = null; frame.addEventListener('pointerdown', function (event) {