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; diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index de7bea92..390a2ea6 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -131,8 +131,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 { @@ -141,7 +143,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); } @@ -162,7 +176,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -184,7 +197,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; @@ -192,7 +207,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; } @@ -248,7 +265,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; } @@ -284,7 +303,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; } @@ -356,7 +375,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; @@ -395,7 +414,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; } @@ -425,7 +444,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)); @@ -468,7 +487,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; @@ -1107,7 +1128,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -1206,7 +1227,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%; } @@ -1243,7 +1265,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; @@ -1339,7 +1363,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -1362,12 +1386,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; } @@ -1383,7 +1407,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; @@ -1583,8 +1609,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; } @@ -1673,7 +1699,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; @@ -1688,7 +1716,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; @@ -1792,39 +1822,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; @@ -1836,20 +1914,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 @@ -2363,7 +2465,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; @@ -2386,7 +2488,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; @@ -2605,7 +2709,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; @@ -2669,7 +2775,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -2678,7 +2783,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -2789,7 +2893,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; @@ -3005,7 +3111,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -3013,7 +3119,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); } @@ -3021,7 +3127,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); } @@ -3466,7 +3572,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; @@ -3540,7 +3648,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; @@ -3550,7 +3660,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 { @@ -3613,14 +3725,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 */ @@ -3954,7 +4074,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; @@ -4014,11 +4136,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) */ @@ -5655,7 +5777,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 @@ -5712,7 +5834,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) { @@ -5723,18 +5847,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 @@ -5742,7 +5877,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -5761,10 +5895,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) { @@ -5785,7 +5930,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 }); } @@ -6029,10 +6176,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 }; @@ -6087,8 +6244,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(); @@ -6210,9 +6366,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); } @@ -6243,7 +6399,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -6330,7 +6486,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; @@ -6365,8 +6521,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -6395,17 +6553,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; @@ -6421,7 +6579,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -6449,15 +6607,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 { @@ -6471,8 +6627,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(); @@ -6504,7 +6660,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 @@ -6526,19 +6684,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) { @@ -8704,8 +8866,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 { @@ -8714,7 +8878,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); } @@ -8735,7 +8911,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -8757,7 +8932,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; @@ -8765,7 +8942,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; } @@ -8821,7 +9000,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; } @@ -8857,7 +9038,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; } @@ -8929,7 +9110,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; @@ -8968,7 +9149,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; } @@ -8998,7 +9179,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)); @@ -9041,7 +9222,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; @@ -9680,7 +9863,7 @@ } .content pre.command-output-content { - padding: .5em; + padding: 0.5em; } /* Bash command styling (user-initiated, right-aligned) */ @@ -9779,7 +9962,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%; } @@ -9816,7 +10000,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; @@ -9912,7 +10098,7 @@ overflow-wrap: break-word; } - .content>pre { + .content > pre { background-color: transparent; padding: 0; border-radius: 0; @@ -9935,12 +10121,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; } @@ -9956,7 +10142,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; @@ -10156,8 +10344,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; } @@ -10246,7 +10434,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; @@ -10261,7 +10451,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; @@ -10365,39 +10557,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; @@ -10409,20 +10649,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 @@ -10936,7 +11200,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; @@ -10959,7 +11223,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; @@ -11178,7 +11444,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; @@ -11242,7 +11510,6 @@ border-top: 1px solid #dee2e633; } - .filter-toggles { display: flex; flex-wrap: wrap; @@ -11251,7 +11518,6 @@ margin-bottom: 12px; } - .filter-toggle { padding: 6px 12px; border: 1px solid #dee2e6; @@ -11362,7 +11628,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; @@ -11578,7 +11846,7 @@ transition: background 0.2s ease; } - #timeline-resize-handle>div { + #timeline-resize-handle > div { background: var(--timeline-grip, #999); } @@ -11586,7 +11854,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); } @@ -11594,7 +11862,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); } @@ -12039,7 +12307,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; @@ -12113,7 +12383,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; @@ -12123,7 +12395,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 { @@ -12186,14 +12460,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 */ @@ -12527,7 +12809,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; @@ -12587,11 +12871,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) */ @@ -14123,7 +14407,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 @@ -14180,7 +14464,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) { @@ -14191,18 +14477,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 @@ -14210,7 +14507,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -14229,10 +14525,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) { @@ -14253,7 +14560,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 }); } @@ -14497,10 +14806,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 }; @@ -14555,8 +14874,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(); @@ -14678,9 +14996,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); } @@ -14711,7 +15029,7 @@ function scrollToEnd() { window.scrollTo({ top: document.documentElement.scrollHeight, - behavior: 'smooth', + behavior: 'smooth' }); } @@ -14798,7 +15116,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; @@ -14833,8 +15151,10 @@ window.claudeLogLiveUpdate = { poll, - stop() { stopped = true; }, - setFollowing, + stop() { + stopped = true; + }, + setFollowing }; })(); @@ -14863,17 +15183,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; @@ -14889,7 +15209,7 @@ function viewport() { return { w: document.documentElement.clientWidth || window.innerWidth, - h: window.innerHeight, + h: window.innerHeight }; } @@ -14917,15 +15237,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 { @@ -14939,8 +15257,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(); @@ -14972,7 +15290,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 @@ -14994,19 +15314,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) { @@ -17172,8 +17496,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 { @@ -17182,7 +17508,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); } @@ -17203,7 +17541,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -17225,7 +17562,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; @@ -17233,7 +17572,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; } @@ -17289,7 +17630,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; } @@ -17325,7 +17668,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; } @@ -17397,7 +17740,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; @@ -17436,7 +17779,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; } @@ -17466,7 +17809,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)); @@ -17497,7 +17840,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; @@ -17752,7 +18097,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; @@ -17760,7 +18107,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; } @@ -17807,7 +18156,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; @@ -18149,7 +18500,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; @@ -18223,7 +18576,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; @@ -18233,7 +18588,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 { @@ -18296,14 +18653,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 */ @@ -19637,7 +20002,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 @@ -19694,7 +20059,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) { @@ -19705,18 +20072,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 @@ -19724,7 +20102,6 @@ element.title = duration ? duration : 'UTC: ' + utcTime; } } - } catch (error) { // If conversion fails, leave the original timestamp console.warn('Failed to convert timestamp:', rawTimestamp, error); @@ -19743,10 +20120,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) { @@ -19767,7 +20155,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 }); } @@ -19920,8 +20310,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 { @@ -19930,7 +20322,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); } @@ -19951,7 +20355,6 @@ line-height: 1.5; } - pre { background-color: #12121212; padding: 10px; @@ -19973,7 +20376,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; @@ -19981,7 +20386,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; } @@ -20037,7 +20444,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; } @@ -20073,7 +20482,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; } @@ -20145,7 +20554,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; @@ -20184,7 +20593,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; } @@ -20214,7 +20623,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)); @@ -20484,7 +20893,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; @@ -20558,7 +20969,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; @@ -20568,7 +20981,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 { @@ -20631,14 +21046,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 */ @@ -21640,8 +22063,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; @@ -21917,11 +22340,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; @@ -22005,18 +22438,20 @@ } @@ -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) {