Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion claude_code_log/html/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
format_user_memory_content,
format_user_slash_command_content,
format_user_text_model_content,
format_workflow_sidechannel_user_content,
)
from .assistant_formatters import (
format_assistant_text_content,
Expand Down Expand Up @@ -498,8 +499,15 @@ def format_SessionHeaderMessage(
# -------------------------------------------------------------------------

def format_UserTextMessage(
self, content: UserTextMessage, _: TemplateMessage
self, content: UserTextMessage, message: TemplateMessage
) -> str:
# User prompts grafted from a workflow agent's side-channel are large
# prose+JSON hybrids: render them as collapsible Markdown with the
# embedded JSON blocks extracted into params tables (#174 follow-up).
if message.in_workflow_sidechannel:
return format_workflow_sidechannel_user_content(
content, image_formatter=self._format_image
)
return format_user_text_model_content(
content, image_formatter=self._format_image
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

/* Dynamic-workflow tree colors (#174): each level's card border and its
group border (the line framing the card's subtree) share a color. */
--workflow-phase-color: #1b5e20;
--workflow-phase-color: #3a7d3c;
--workflow-agent-color: #9e9e9e;

/* Fork/branch structural colors */
Expand Down
128 changes: 105 additions & 23 deletions claude_code_log/html/templates/components/message_styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1014,40 +1014,108 @@ pre > code {
word-break: break-all;
}

/* Structured-table folds carry an explicit collapse hint plus a
rows-toggle button in the summary, so suppress the generic ::after
"collapse" hint for them and show the real elements only when open. */
.tool-param-collapsible-rows[open] > summary::after {
content: none;
/* The rows-toggle lives in a controls strip AFTER the summary — never
inside it (an interactive element within <summary> is an accessibility
violation: not consistently reachable by keyboard/AT). The strip is
details content, so it's natively hidden while the fold is closed. */
.tool-param-fold-controls {
margin: 2px 0;
}

.tool-param-collapse-hint,
.tool-param-rows-toggle {
display: none;
}

.tool-param-collapsible-rows[open] > summary > .tool-param-collapse-hint {
display: inline;
padding: 0;
background: none;
border: none;
font: inherit;
color: var(--text-muted);
font-style: italic;
cursor: pointer;
}

.tool-param-collapsible-rows[open] > summary > .tool-param-rows-toggle {
display: inline;
margin-left: 1.5em;
.tool-param-rows-toggle:hover {
color: var(--text-primary);
}

/* Fold-valued rows hoist their toggle into the KEY column: the key cell's
▶/▼ button (wired in transcript.html, state derived from toggle events)
replaces the per-fold in-summary collapse chrome, which only remained
useful en masse — every row showing "▼ collapse" drowned the content.
Scoped to keyed rows: the top-level root fold has no key column and
keeps its summary affordances (the pure-CSS ::after "collapse" hint —
no interactive child). */
.tool-param-key-toggle {
/* The whole key (glyph + text) is the click target — inherit the key
cell's look so fold keys read exactly like scalar keys. */
padding: 0;
background: none;
border: none;
font: inherit;
color: var(--text-muted);
font-style: italic;
color: inherit;
cursor: pointer;
text-align: left;
white-space: nowrap;
}

.tool-param-rows-toggle:hover {
.tool-param-key-toggle:hover {
color: var(--text-primary);
}

/* The shared fold glyph: the BROWSER'S native disclosure marker (the same
UA-drawn icon a <summary> shows) via display:list-item — geometrically
symmetric open/closed, and immune to the italic style of the button
labels (font glyphs like ▶/▼ skew in italics; the marker icon doesn't).
The empty span doubles as the key-column alignment slot: every key cell
renders one (markerless on scalar rows), so all key texts start at the
same x. */
.tool-param-fold-glyph {
display: inline-block;
width: 1.3em;
color: var(--text-muted);
font-style: normal;
}

/* Marker only inside the toggle buttons — the scalar-row spacer stays
empty. list-style-type string values are the fallback where the
disclosure-* keywords are unsupported. */
button > .tool-param-fold-glyph {
display: list-item;
list-style-position: inside;
list-style-type: "\25B8";
list-style-type: disclosure-closed;
}

.tool-param-key-toggle[aria-expanded='true'] > .tool-param-fold-glyph,
[data-state='expanded'] > .tool-param-fold-glyph {
list-style-type: "\25BE";
list-style-type: disclosure-open;
}

/* display:list-item is block-level — lay the buttons out as inline-flex so
the marker slot and the label sit on one line. */
.tool-param-key-toggle,
.tool-params-expand-all,
.tool-param-rows-toggle {
display: inline-flex;
align-items: baseline;
}

/* The key glyph is THE marker for these rows — drop the duplicate native
one from the value summary (preview text stays clickable). */
.tool-param-row-fold > .tool-param-value > details > summary {
list-style: none;
}

.tool-param-row-fold > .tool-param-value > details > summary::-webkit-details-marker {
display: none;
}

/* A keyed fold's open summary carries nothing (the key glyph shows ▼ and
the rows-toggle sits in the controls strip below) — drop the line it
would occupy. This also suppresses the generic ::after "collapse". */
.tool-param-row-fold > .tool-param-value > details[open] > summary {
display: none;
}

/* Top-level expand/collapse-all control above a params/result table —
same quiet look as the in-summary rows-toggle. */
.tool-params-controls {
Expand Down Expand Up @@ -1464,14 +1532,16 @@ details summary {

/* A standard sub-agent's sidechain group (Task-spawned agents — sync,
async, teammates): the agent's transcript hangs under the spawning
tool_result, framed by ONE grey line (the same color/concept as a
workflow agent's side-channel — an agent transcript is an agent
transcript). The :not(.sidechain) parent filter keeps the line at the
block's top level only (containers deeper inside the sidechain hang
under .sidechain-flagged cards). */
tool_result, framed by ONE line in tool-green — per the color-pairing
principle, the group line CONTINUES its parent card's border, and the
spawning tool_result card's border is tool-green. (Workflow agents use
the same principle with their own grey: grey workflow_agent card →
grey side-channel line.) The :not(.sidechain) parent filter keeps the
line at the block's top level only (containers deeper inside the
sidechain hang under .sidechain-flagged cards). */
.message-node:has(> .message.tool_result:not(.sidechain)) > .children:has(> .message-node > .message.sidechain) {
margin-left: 2em;
border-left: 2px solid var(--workflow-agent-color);
border-left: 2px solid var(--tool-use-color);
}

/* A phase's agents group — continues the phase card's dark green. */
Expand Down Expand Up @@ -1554,3 +1624,15 @@ a.workflow-phase-pill:hover {
color: var(--text-muted);
font-size: 0.9em;
}

/* Embedded JSON blocks extracted from workflow side-channel user prompts
(#174 follow-up): the block renders as a params table in place of the raw
JSON text; the hint marks a block whose table lives below the fold. */
.embedded-json {
margin: 0.5em 0;
}

.embedded-json-hint {
color: var(--text-muted);
font-family: var(--font-monospace);
}
49 changes: 40 additions & 9 deletions claude_code_log/html/templates/transcript.html
Original file line number Diff line number Diff line change
Expand Up @@ -418,16 +418,22 @@ <h3>🔍 Search & Filter</h3>
// it stays in sync whatever opened the rows (button click,
// toggle-all, manual clicks).
function syncRowsToggle(details) {
const button = details.querySelector(':scope > summary .tool-param-rows-toggle');
// The button lives in a controls strip AFTER the summary —
// interactive elements inside <summary> are an accessibility
// violation (unreachable by keyboard/AT in some browsers).
const button = details.querySelector(':scope > .tool-param-fold-controls > .tool-param-rows-toggle');
if (!button) return;
const rows = details.querySelectorAll(rowDetailsSelector);
const allOpen = rows.length > 0 &&
Array.from(rows).every(row => row.open);
const kind = button.dataset.kind || 'rows';
// data-state drives the CSS rotation of the constant ▸ glyph;
// only the label text changes.
button.dataset.state = allOpen ? 'expanded' : 'collapsed';
button.textContent = allOpen
? '▼ collapse all ' + kind
: '▶ expand all ' + kind;
const label = button.querySelector('.tool-param-fold-label');
if (label) {
label.textContent = (allOpen ? 'collapse all ' : 'expand all ') + kind;
}
}

// Top-level expand-all for a whole params/result renderer:
Expand All @@ -440,7 +446,10 @@ <h3>🔍 Search & Filter</h3>
const allOpen = folds.length > 0 &&
Array.from(folds).every(fold => fold.open);
button.dataset.state = allOpen ? 'expanded' : 'collapsed';
button.textContent = allOpen ? '▼ collapse all' : '▶ expand all';
const label = button.querySelector('.tool-param-fold-label');
if (label) {
label.textContent = allOpen ? 'collapse all' : 'expand all';
}
}

document.addEventListener('click', function (event) {
Expand All @@ -454,12 +463,20 @@ <h3>🔍 Search & Filter</h3>
syncExpandAll(root);
return;
}
// Key-column fold toggle (▶/▼): drives the value cell's
// details from the key cell, keeping the summary free of
// per-fold collapse chrome. Glyph state is derived in the
// toggle listener below, so any other opener stays in sync.
const keyToggle = event.target.closest('.tool-param-key-toggle');
if (keyToggle) {
const row = keyToggle.closest('tr');
const details = row &&
row.querySelector(':scope > .tool-param-value > details');
if (details) details.open = !details.open;
return;
}
const button = event.target.closest('.tool-param-rows-toggle');
if (!button) return;
// The button sits inside a <summary>: keep the click from
// toggling the enclosing details.
event.preventDefault();
event.stopPropagation();
const details = button.closest('details');
const expand = button.dataset.state !== 'expanded';
details.querySelectorAll(rowDetailsSelector).forEach(row => {
Expand All @@ -486,6 +503,20 @@ <h3>🔍 Search & Filter</h3>
const cell = details.parentElement;
const parent = cell ? cell.closest('details.tool-param-collapsible-rows') : null;
if (parent && parent !== details) syncRowsToggle(parent);
// Sync the key-column glyph of a fold-valued row — derived
// from the actual open state, so summary clicks, rows-toggle
// bulk opens, and expand-all all keep it truthful.
if (cell && cell.classList && cell.classList.contains('tool-param-value')) {
const row = cell.closest('tr');
const keyBtn = row && row.querySelector(
':scope > .tool-param-key > .tool-param-key-toggle');
if (keyBtn) {
// CSS rotates the single ▸ glyph on aria-expanded —
// symmetric open/closed states by construction.
keyBtn.setAttribute('aria-expanded',
details.open ? 'true' : 'false');
}
}
// Keep the renderer's top-level expand-all button in step
// with whatever opened/closed this fold (rows-toggle,
// manual click, global toggle-all).
Expand Down
54 changes: 43 additions & 11 deletions claude_code_log/html/tool_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,20 +1116,24 @@ def _structured_value_html(value: "dict[Any, Any] | list[Any]", depth: int) -> s
def _table_fold_html(formatted_value: str, table_html: str, kind: str) -> str:
"""Wrap a rendered params table in a collapsed fold.

When the table has row-level folds, the summary carries an explicit
collapse hint (instead of the generic ::after one) followed by a
rows-toggle button that expands/collapses them all at once (wired
up in transcript.html). The "<details" probe is exact: structures
always fold, so a deeper fold can only exist inside a direct-row
fold. All-scalar containers get a plain fold — no dead button.
The summary holds ONLY the preview text — never interactive elements
(a button inside ``<summary>`` is an accessibility violation Chrome
flags: not consistently reachable by keyboard/AT). When the table has
row-level folds, the rows-toggle button lives in a controls strip
AFTER the summary, inside the details — natively hidden while closed,
visible when open (wired up in transcript.html). The "<details" probe
is exact: structures always fold, so a deeper fold can only exist
inside a direct-row fold. All-scalar containers get a plain fold —
no dead button.
"""
preview = escape_html(formatted_value[:100])
if len(formatted_value) > 100:
preview += "..."
if "<details" in table_html:
return f"""
<details class='tool-param-collapsible tool-param-collapsible-rows'>
<summary><span class='tool-param-preview'>{preview}</span><span class='tool-param-collapse-hint'>collapse</span><button type='button' class='tool-param-rows-toggle' data-state='collapsed' data-kind='{kind}'>&#9654; expand all {kind}</button></summary>
<summary><span class='tool-param-preview'>{preview}</span></summary>
<div class='tool-param-fold-controls'><button type='button' class='tool-param-rows-toggle' data-state='collapsed' data-kind='{kind}'><span class='tool-param-fold-glyph'></span><span class='tool-param-fold-label'>expand all {kind}</span></button></div>
{table_html}
</details>
"""
Expand All @@ -1155,7 +1159,9 @@ def _params_root_html(table_html: str) -> str:
"<div class='tool-params-root'>"
"<div class='tool-params-controls'>"
"<button type='button' class='tool-params-expand-all'"
" data-state='collapsed'>&#9654; expand all</button>"
" data-state='collapsed'>"
"<span class='tool-param-fold-glyph'></span>"
"<span class='tool-param-fold-label'>expand all</span></button>"
"</div>"
f"{table_html}"
"</div>"
Expand All @@ -1173,15 +1179,41 @@ def _param_value_html(value: Any, depth: int) -> str:


def _params_table_html(items: "Iterable[tuple[Any, Any]]", depth: int) -> str:
"""Build one key/value table; nested levels get a marker class."""
"""Build one key/value table; nested levels get a marker class.

A fold-valued row hoists its ▶/▼ toggle into the KEY column (wired in
transcript.html), so the value summary stays free of per-fold collapse
chrome — only the previews (closed) and the rows-toggle buttons (open)
remain there. The ``<details`` probe is exact: every fold helper emits
a ``<details`` as the value's first element, and non-fold values never
start with one (their text is escaped).
"""
css = "tool-params-table" if depth == 0 else "tool-params-table tool-params-nested"
html_parts = [f"<table class='{css}'>"]
for key, value in items:
escaped_key = escape_html(str(key))
value_html = _param_value_html(value, depth)
if value_html.lstrip().startswith("<details"):
# The button wraps glyph AND key text — the whole key is the
# click target (and keyboard-reachable, unlike a td handler).
# The empty glyph span carries the browser's native disclosure
# marker (CSS display:list-item) — the same symmetric icon a
# <summary> shows, immune to italic contexts.
key_cell = (
"<button type='button' class='tool-param-key-toggle'"
" aria-expanded='false'>"
"<span class='tool-param-fold-glyph'></span>"
f"{escaped_key}</button>"
)
row_attr = " class='tool-param-row-fold'"
else:
# Scalar rows reserve the same glyph slot (empty) so every key
# text starts at the same x.
key_cell = f"<span class='tool-param-fold-glyph'></span>{escaped_key}"
row_attr = ""
html_parts.append(f"""
<tr>
<td class='tool-param-key'>{escaped_key}</td>
<tr{row_attr}>
<td class='tool-param-key'>{key_cell}</td>
<td class='tool-param-value'>{value_html}</td>
</tr>
""")
Expand Down
Loading
Loading