diff --git a/claude_code_log/html/renderer.py b/claude_code_log/html/renderer.py index 313d167c..0727c1fa 100644 --- a/claude_code_log/html/renderer.py +++ b/claude_code_log/html/renderer.py @@ -941,6 +941,25 @@ def title_ToolUseMessage( return "" return super().title_ToolUseMessage(content, message) + def title_ToolResultMessage( + self, content: ToolResultMessage, message: TemplateMessage + ) -> str: + """Tool-result title, plus the fully-collapsed-transcript marker + (#213 visual layer): when a nested Task/Agent spawn's entire + transcript deduped away (the sub-agent answered directly, no tool + calls), tag the result so it reads as 'this IS the whole transcript' + rather than a spawn that produced nothing.""" + base = super().title_ToolResultMessage(content, message) + if message.spawns_collapsed_transcript: + marker = ( + "" + "≑ full transcript" + ) + return f"{base} {marker}" if base else marker + return base + def title_TaskInput(self, input: TaskInput, message: TemplateMessage) -> str: """Title β†’ 'πŸ”§ Task (subagent_type) [async #]'. @@ -968,21 +987,42 @@ def title_TaskInput(self, input: TaskInput, message: TemplateMessage) -> str: if input.run_in_background else "" ) + suffix = async_hint + self._agent_depth_badge(message) if input.description and input.subagent_type: escaped_desc = escape_html(input.description) return ( f"πŸ”§ {escaped_name} {escaped_desc}" f" ({escaped_subagent})" - f"{async_hint}" + f"{suffix}" ) elif input.description: - return self._tool_title(message, "πŸ”§", input.description) + async_hint + return self._tool_title(message, "πŸ”§", input.description) + suffix elif input.subagent_type: return ( f"πŸ”§ {escaped_name} ({escaped_subagent})" - f"{async_hint}" + f"{suffix}" ) - return f"πŸ”§ {escaped_name}{async_hint}" + return f"πŸ”§ {escaped_name}{suffix}" + + def _agent_depth_badge(self, message: TemplateMessage) -> str: + """Depth badge for a nested spawn card (#213 visual layer). + + Shows the depth of the sub-agent this Task/Agent call opens β€” the + card's own ``agent_depth`` + 1, since the spawned agent sits exactly + one level deeper. Empty for top-level spawns (which open a depth-1 + transcript) so the common shallow case stays uncluttered. The badge's + ring class colour-matches the group line that frames the transcript + directly below it. + """ + spawned_depth = message.agent_depth + 1 + if spawned_depth < 2: + return "" + ring = ((spawned_depth - 1) % 5) + 1 + return ( + f" " + f"Depth {spawned_depth}" + ) def _async_id_suffix( self, diff --git a/claude_code_log/html/templates/components/global_styles.css b/claude_code_log/html/templates/components/global_styles.css index 3e3d8616..ded12c90 100644 --- a/claude_code_log/html/templates/components/global_styles.css +++ b/claude_code_log/html/templates/components/global_styles.css @@ -43,6 +43,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; diff --git a/claude_code_log/html/templates/components/message_styles.css b/claude_code_log/html/templates/components/message_styles.css index c73c4d7f..cefdd61b 100644 --- a/claude_code_log/html/templates/components/message_styles.css +++ b/claude_code_log/html/templates/components/message_styles.css @@ -1544,14 +1544,97 @@ details summary { to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } +/* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ +.message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), +.message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); +} + +.message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), +.message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); +} + +.message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), +.message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); +} + +.message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), +.message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); +} + +/* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ +.message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), +.message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; +} + +/* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ +.agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); +} + +.agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); +} + +.agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); +} + +.agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); +} + +.agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); +} + +.agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); +} + +/* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ +.spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; +} + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -1580,6 +1663,12 @@ details summary { .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice diff --git a/claude_code_log/html/utils.py b/claude_code_log/html/utils.py index 52dd2349..fc252f2e 100644 --- a/claude_code_log/html/utils.py +++ b/claude_code_log/html/utils.py @@ -265,6 +265,20 @@ def css_class_from_message(msg: "TemplateMessage") -> str: if msg.is_sidechain: parts.append("sidechain") + # Agent-nesting depth (#213 visual layer). A message at depth d (d >= 1) + # carries: + # - ``agent-depth-{d}`` exact depth (data / tests / timeline) + # - ``agent-ring-{1..5}`` 5-colour cycle bucket for the group line + # - ``agent-deep`` d >= 6 β†’ compress the indent step + # All three are derivable from d, but CSS can't compute them, so we emit + # the classes. Depth 0 (trunk / non-agent) adds nothing. + if msg.agent_depth >= 1: + d = msg.agent_depth + parts.append(f"agent-depth-{d}") + parts.append(f"agent-ring-{((d - 1) % 5) + 1}") + if d >= 6: + parts.append("agent-deep") + return " ".join(parts) diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index dee2d1fa..d02673f0 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -268,6 +268,22 @@ def __init__( # JSON blocks extracted into params tables. self.in_workflow_sidechannel: bool = False + # Agent-nesting depth of this message's session line (#213 visual + # layer): 0 for the trunk / non-agent messages, 1 for a directly + # spawned sub-agent, 2 for a sub-agent of a sub-agent, … Set by + # _build_message_hierarchy (chasing spawned_agent_id links); drives + # the per-depth group-line colour ramp, the spawn-card depth badge, + # and the deep-chain indent compression. + self.agent_depth: int = 0 + + # Set by _cleanup_sidechain_duplicates on a Task/Agent spawn + # tool_result whose sub-agent transcript collapsed ENTIRELY into the + # prompt + result already shown (the agent answered directly, with no + # surviving tool calls or thinking). Lets the renderer mark it so a + # fully-elided transcript reads as "nothing hidden" rather than as a + # spawn that produced no transcript at all (#213 visual layer). + self.spawns_collapsed_transcript: bool = False + # Per-render annotations populated by the HTML renderer's tree walk # (HtmlRenderer._annotate_tree_for_render). The recursive template # macro reads these instead of receiving a flat (msg, title, html, @@ -2422,9 +2438,9 @@ def _agent_depth(sid: str) -> int: # Determine level from message type and modifiers, shifted by # the agent-nesting depth of the message's session line. current_level = _get_message_hierarchy_level(message) - agent_depth = _agent_depth(message.meta.session_id or "") - if agent_depth > 1: - current_level += 2 * (agent_depth - 1) + message.agent_depth = _agent_depth(message.meta.session_id or "") + if message.agent_depth > 1: + current_level += 2 * (message.agent_depth - 1) # Pop stack until we find the appropriate parent level while hierarchy_stack and hierarchy_stack[-1][0] >= current_level: @@ -3581,6 +3597,16 @@ def process_message(message: TemplateMessage) -> None: del children[i] break + # Fully-collapsed nested spawn (#213 visual layer): the sub-agent's + # whole transcript was just prompt β†’ answer (both already shown), so + # dedup emptied it. Mark it β€” but only for a NESTED spawn (the result + # card itself sits inside an agent transcript, ``agent_depth >= 1``); + # trunk-level direct sub-agents keep their pre-#213 rendering. The + # marker distinguishes "transcript identical to result" from "spawn + # with no transcript at all", which otherwise look the same. + if not children and message.agent_depth >= 1: + message.spawns_collapsed_transcript = True + for root in root_messages: process_message(root) diff --git a/dev-docs/agents.md b/dev-docs/agents.md index a2ba8e0b..4b56046b 100644 --- a/dev-docs/agents.md +++ b/dev-docs/agents.md @@ -300,9 +300,47 @@ round-trip verbatim through its spawn pair collapses entirely β€” a trivial leaf shows nothing beyond its parent's tool_use/tool_result pair, exactly like depth-1 sync agents. -### 5.4 Fixture +### 5.4 Visual layer + +`_build_message_hierarchy` stores the chased depth on every message as +`TemplateMessage.agent_depth` (0 = trunk). `css_class_from_message` +turns `agent_depth >= 1` into three classes on the card: +`agent-depth-{d}` (exact, for the badge/tests), `agent-ring-{1..5}` +(the `((d-1) mod 5)+1` colour-cycle bucket), and `agent-deep` for +`d >= 6`. The CSS (`message_styles.css`) then keys off the inner +sidechain card's class so the group line framing a depth-`d` +transcript: + +- takes the **ring colour** for that depth (depth 1 = tool-green, the + pre-existing look; 2 blue, 3 purple, 4 orange, 5 teal β€” see the + `--agent-ring-*` vars), and +- **compresses its indent step** at `d >= 6` (2em for depths 1-5, then + 0.5em) so very deep chains (79 levels seen in the wild) stay + on-screen β€” depth is carried by the badge + colour, not the indent. + +Two card-level annotations complete the layer: + +- **Depth badge** (`_agent_depth_badge`, `html/renderer.py`): a "Depth + N" pill on a spawn card showing the depth of the sub-agent it *opens* + (`agent_depth + 1`), suppressed for top-level spawns (depth 1) to + keep shallow transcripts clean; its ring colour matches the group + line below. +- **Collapsed marker** (`spawns_collapsed_transcript`, set in + `_cleanup_sidechain_duplicates` when a NESTED spawn's transcript + dedups to nothing): `title_ToolResultMessage` tags the result + "≑ full transcript" β€” the sub-agent answered directly, so what's + shown is its whole transcript, distinct from a spawn that produced + none. Nested-only (`agent_depth >= 1`); trunk-level direct + sub-agents keep their pre-#213 rendering. + +### 5.5 Fixture `test/test_data/nested_agents/` (generated by `scripts/gen_nested_agents_fixture.py`): a 2Γ—2 fan-out, a 3-deep chain, and an interrupted spawn linkable only via its sidecar. Pinned by -`test/test_nested_agents.py`. +`test/test_nested_agents.py` (structure + visual-layer logic) and +`test/test_nested_agents_browser.py` (runtime CSS: ramp colours, +indent, badge, marker). Note the fixture's agents answer directly with +no thinking blocks; real sub-agents usually *think* before spawning, +so an agent's own thinkingβ†’spawn nesting renders as an invisible +0-width passthrough group β€” only true agent boundaries draw a line. diff --git a/test/__snapshots__/test_snapshot_html.ambr b/test/__snapshots__/test_snapshot_html.ambr index 38680b1d..e4181c72 100644 --- a/test/__snapshots__/test_snapshot_html.ambr +++ b/test/__snapshots__/test_snapshot_html.ambr @@ -55,6 +55,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -1903,14 +1913,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -1939,6 +2032,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -6373,6 +6472,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -8221,14 +8330,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -8257,6 +8449,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -12590,6 +12788,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -14778,6 +14986,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -16626,14 +16844,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -16662,6 +16963,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -21211,6 +21518,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -23059,14 +23376,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -23095,6 +23495,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -27911,6 +28317,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -29759,14 +30175,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -29795,6 +30294,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -34574,6 +35079,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -36422,14 +36937,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -36458,6 +37056,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice @@ -41180,6 +41784,16 @@ --workflow-phase-color: #3a7d3c; --workflow-agent-color: #9e9e9e; + /* Nested-agent depth ramp (#213 visual layer): the group line framing a + depth-d sub-agent transcript cycles through these 5 colors via + ((d-1) mod 5). Ring 1 is tool-green so a plain depth-1 sub-agent keeps + its existing look; deeper rings stay distinct against the cream bg. */ + --agent-ring-1: #4caf50; + --agent-ring-2: #1e88e5; + --agent-ring-3: #8e44ad; + --agent-ring-4: #e67e22; + --agent-ring-5: #00897b; + /* Fork/branch structural colors */ --fork-point-color: #adb5bd; --branch-point-color: #adb5bd; @@ -43028,14 +43642,97 @@ to the tool_use when the spawn has no result yet (running / interrupted). The :not(.workflow_agent) keeps this higher-specificity rule off workflow agents' side-channel groups (their card is ALSO - tool_use-classed; their group line is grey, see below). Per-depth - line colors + depth badges: PR2 of #213. */ + tool_use-classed; their group line is grey, see below). This is the + BASE: indent 2em + tool-green; the per-depth ramp below recolours + rings 2-5 and the deep-indent rule below compresses the step. */ .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.sidechain), .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2em; border-left: 2px solid var(--tool-use-color); } + /* Per-depth colour ramp (#213 visual layer): the group line takes the colour + of the depth it frames, cycling every 5 levels. Ring 1 = tool-green is the + base above (no override needed). Rings 2-5 override border-left-color only; + same selector shape as the base β†’ equal specificity, so source order (these + come after) wins the tie while margin-left is inherited from the base. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-2), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-2) { + border-left-color: var(--agent-ring-2); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-3), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-3) { + border-left-color: var(--agent-ring-3); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-4), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-4) { + border-left-color: var(--agent-ring-4); + } + + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-ring-5), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-ring-5) { + border-left-color: var(--agent-ring-5); + } + + /* Deep-chain indent compression (#213 visual layer): the cumulative 2em step + marches very deep chains off-screen (observed 79 levels in the wild). Once + the depth badge carries the absolute depth, levels 6+ (cards tagged + .agent-deep) need only a token step β€” depth stays legible via the badge and + the cycling line colour, not the indent. Steps 1-5 keep the comfortable 2em. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.5em; + } + + /* Depth badge on a nested spawn card's title (#213 visual layer): a small + pill reading e.g. "d3" for "opens a depth-3 sub-agent". Its ring class + colour-matches the group line framing the transcript directly below. */ + .agent-depth-badge { + display: inline-block; + font-size: 0.7em; + font-weight: 700; + line-height: 1.4; + padding: 0 0.45em; + border-radius: 0.8em; + vertical-align: middle; + color: #fff; + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-1 { + background-color: var(--agent-ring-1); + } + + .agent-depth-badge.agent-ring-2 { + background-color: var(--agent-ring-2); + } + + .agent-depth-badge.agent-ring-3 { + background-color: var(--agent-ring-3); + } + + .agent-depth-badge.agent-ring-4 { + background-color: var(--agent-ring-4); + } + + .agent-depth-badge.agent-ring-5 { + background-color: var(--agent-ring-5); + } + + /* "≑ full transcript" marker on a fully-collapsed nested spawn's result + (#213 visual layer): the sub-agent answered directly, so its whole + transcript was just the prompt + this result β€” nothing was hidden. Muted + so it reads as reassurance, not a warning. */ + .spawn-collapsed-marker { + font-size: 0.8em; + font-weight: 500; + color: var(--text-muted, #888); + font-style: italic; + white-space: nowrap; + } + /* A phase's agents group β€” continues the phase card's dark green. */ .message-node:has(> .message.workflow_phase) > .children { margin-left: 2em; @@ -43064,6 +43761,12 @@ .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.sidechain) { margin-left: 2%; } + + /* Deep-chain compression mirrored for the percentage scale. */ + .message-node:has(> .message.tool_result) > .children:has(> .message-node > .message.agent-deep), + .message-node:has(> .message.tool_use:not(.workflow_agent)) > .children:has(> .message-node > .message.agent-deep) { + margin-left: 0.6%; + } } /* Phase pills double as anchor links to their phase card when the splice diff --git a/test/test_nested_agents.py b/test/test_nested_agents.py index 509a9173..84c825a0 100644 --- a/test/test_nested_agents.py +++ b/test/test_nested_agents.py @@ -208,7 +208,60 @@ def test_interrupted_transcript_nests_under_error_result(self) -> None: assert lines_above.get(INTR) == [] -class TestNestedCacheInvalidation: +class TestNestedVisualLayer: + """The #213 visual layer: per-message agent_depth, the fully-collapsed + marker, and the spawn-card depth badge.""" + + def _ctx_messages(self) -> list[TemplateMessage]: + _roots, _nav, ctx = generate_template_messages(_load_integrated()) + return [m for m in ctx.messages if m is not None] + + def test_agent_depth_set_per_session_line(self) -> None: + msgs = self._ctx_messages() + # Highest depth among messages that survived rendering. chain3 (d3) + # and the leaves (d2) mostly collapse; mid/chain1 content is d1, and + # chain2's surviving spawn pair is d2. + by_line_depth = { + _line_of(m.meta.session_id or ""): m.agent_depth + for m in msgs + if _line_of(m.meta.session_id or "") + } + assert by_line_depth.get(MID1) == 1 + assert by_line_depth.get(MID2) == 1 + assert by_line_depth.get(CHAIN1) == 1 + assert by_line_depth.get(CHAIN2) == 2 + assert by_line_depth.get("nsleaf22") == 2 + # Trunk messages stay at depth 0. + assert all( + m.agent_depth == 0 for m in msgs if not _line_of(m.meta.session_id or "") + ) + + def test_collapsed_flag_marks_verbatim_nested_spawns_only(self) -> None: + msgs = self._ctx_messages() + collapsed = { + m.meta.spawned_agent_id for m in msgs if m.spawns_collapsed_transcript + } + # Three verbatim leaves + the chain bottom collapse; the divergent + # leaf22 and the interrupted spawn do not. + assert collapsed == {"nsleaf11", "nsleaf12", "nsleaf21", CHAIN3} + + def test_collapsed_flag_never_on_trunk_level_spawns(self) -> None: + # Trunk-level (depth-1-spawning) Task/Agent results keep their + # pre-#213 rendering β€” the marker is nested-only. + msgs = self._ctx_messages() + assert all(m.agent_depth >= 1 for m in msgs if m.spawns_collapsed_transcript) + + def test_depth_badge_html_uses_spawned_depth(self) -> None: + from claude_code_log.html.renderer import generate_html + + html = generate_html(_load_integrated(), "badge") + # A leaf-spawn card (inside a depth-1 agent) opens depth 2. + assert "Depth 2" in html + # chain2's spawn of chain3 opens depth 3. + assert "Depth 3" in html + # The collapsed marker renders. + assert "≑ full transcript" in html + def test_new_sidecar_invalidates_cached_trunk(self, tmp_path: Path) -> None: """Sidecar inputs are part of the cache key (PR #218 review). diff --git a/test/test_nested_agents_browser.py b/test/test_nested_agents_browser.py index 726e05e1..29d96e06 100644 --- a/test/test_nested_agents_browser.py +++ b/test/test_nested_agents_browser.py @@ -1,9 +1,11 @@ """Runtime CSS contract for nested sub-agent groups (#213). -Every spawn boundary β€” at ANY depth β€” indents its transcript group by -2em and frames it with the tool-green line; depth accumulates through -DOM nesting (a depth-2 group lives inside a depth-1 group). Computed -styles are read directly so default fold state doesn't matter. +Every spawn boundary indents its transcript group and frames it with a +line whose colour cycles by agent-nesting depth (the #213 visual layer: +depth 1 = tool-green, depth 2 = blue, … via a 5-colour ramp). Depth +accumulates through DOM nesting (a depth-2 group lives inside a depth-1 +group). Computed styles are read directly so default fold state doesn't +matter. """ from __future__ import annotations @@ -19,22 +21,35 @@ TRUNK_SID = "33330000-0000-4000-8000-000000000001" TRUNK = Path(__file__).parent / "test_data" / "nested_agents" / f"{TRUNK_SID}.jsonl" +# Ring colours (global_styles.css --agent-ring-N) as computed rgb(). +RING_RGB = { + 1: "rgb(76, 175, 80)", # #4caf50 tool-green + 2: "rgb(30, 136, 229)", # #1e88e5 blue + 3: "rgb(142, 68, 173)", # #8e44ad purple + 4: "rgb(230, 126, 34)", # #e67e22 orange + 5: "rgb(0, 137, 123)", # #00897b teal +} + GROUPS_JS = """() => { const isGroup = el => el.classList && el.classList.contains('children') && el.querySelector(':scope > .message-node > .message.sidechain'); const groups = Array.from(document.querySelectorAll('.children')).filter(isGroup); return groups.map(g => { const cs = getComputedStyle(g); - let depth = 0, el = g.parentElement; + // Agent depth of the cards this group frames (from agent-depth-N). + const inner = g.querySelector(':scope > .message-node > .message.sidechain'); + const m = (inner.className.match(/agent-depth-(\\d+)/) || [])[1]; + let nesting = 0, el = g.parentElement; while (el) { - if (isGroup(el)) depth += 1; + if (isGroup(el)) nesting += 1; el = el.parentElement; } return { marginLeft: cs.marginLeft, borderWidth: cs.borderLeftWidth, borderColor: cs.borderLeftColor, - enclosingGroups: depth, + innerDepth: m ? parseInt(m, 10) : null, + enclosingGroups: nesting, }; }); }""" @@ -42,7 +57,7 @@ class TestNestedAgentGroupCss: @pytest.mark.browser - def test_every_spawn_boundary_indents_and_draws_the_line( + def test_group_line_colour_cycles_by_depth( self, page: Page, tmp_path: Path ) -> None: entries = load_transcript(TRUNK, silent=True) @@ -56,12 +71,61 @@ def test_every_spawn_boundary_indents_and_draws_the_line( # 2Γ—2: mid1 + mid2 (d1) with leaf22's surviving group inside mid2; # chain: chain1 (d1) with chain2's group inside; interrupted (d1). assert len(groups) == 6 + by_depth: dict[int, int] = {} for g in groups: - assert g["marginLeft"] == "32px", g # 2em at 16px root assert g["borderWidth"] == "2px", g - assert g["borderColor"] == "rgb(76, 175, 80)", g # tool-green + # Shallow levels keep the comfortable 2em step. + assert g["marginLeft"] == "32px", g + assert g["innerDepth"] in (1, 2), g + ring = ((g["innerDepth"] - 1) % 5) + 1 + assert g["borderColor"] == RING_RGB[ring], g + by_depth[g["innerDepth"]] = by_depth.get(g["innerDepth"], 0) + 1 + + # Four depth-1 groups (green), two depth-2 groups (blue). + assert by_depth == {1: 4, 2: 2}, by_depth - # Depth accumulates structurally: exactly two groups are nested - # inside another group's subtree (depth-2 boundaries). + # Depth accumulates structurally: the two depth-2 groups are nested + # inside a depth-1 group's subtree. nested = [g for g in groups if g["enclosingGroups"] > 0] assert len(nested) == 2, groups + + @pytest.mark.browser + def test_depth_badge_and_collapsed_marker_present( + self, page: Page, tmp_path: Path + ) -> None: + entries = load_transcript(TRUNK, silent=True) + _integrate_agent_entries(entries) + html_path = tmp_path / "nested.html" + html_path.write_text(generate_html(entries, "Nested CSS"), encoding="utf-8") + page.set_viewport_size({"width": 1600, "height": 1200}) + page.goto(f"file://{html_path}") + + # Depth badges appear only on spawns that open depth >= 2; top-level + # spawns (β†’ depth 1) carry none. + badges = page.evaluate( + "() => Array.from(document.querySelectorAll('.agent-depth-badge'))" + ".map(b => b.textContent)" + ) + assert sorted(badges) == [ + "Depth 2", + "Depth 2", + "Depth 2", + "Depth 2", + "Depth 2", + "Depth 3", + ], badges + + # A badge's pill colour matches the ring of the depth it opens. + d3_colour = page.evaluate( + "() => { const b = Array.from(document.querySelectorAll(" + "'.agent-depth-badge')).find(x => x.textContent === 'Depth 3');" + " return getComputedStyle(b).backgroundColor; }" + ) + assert d3_colour == RING_RGB[3], d3_colour # depth 3 β†’ ring 3 purple + + # Fully-collapsed nested spawns (leaf11/12/21 + chain3) carry the + # "≑ full transcript" marker; leaf22 (divergent result) does not. + markers = page.evaluate( + "() => document.querySelectorAll('.spawn-collapsed-marker').length" + ) + assert markers == 4, markers diff --git a/work/agent-hierarchies-design.md b/work/agent-hierarchies-design.md index 182f988d..25a06a88 100644 --- a/work/agent-hierarchies-design.md +++ b/work/agent-hierarchies-design.md @@ -203,3 +203,27 @@ fixtures must come from real spawns, not from trusting the narrative. is treated as unbounded throughout. 4. **PR slicing per Β§5 approved**: PR1 structural (Phases A+B+D), PR2 visual (Phase C). + +## 8. PR2 scope (visual layer) + +Branch `dev/agent-hierarchies-visuals` (PR #219, vs main). As-built +reference: [agents.md](../dev-docs/agents.md) Β§5.4. + +- [x] Depth badge ("Depth N") on nested spawn cards β€” shows the depth + the spawn opens; nothing at depth 1. +- [x] Per-depth group-line colour ramp (5-cycle, depth 1 = tool-green; + 2 blue, 3 purple, 4 orange, 5 teal). +- [x] "≑ full transcript" marker for fully-collapsed nested spawns + (the sub-agent answered directly; what's shown is its whole + transcript) β€” distinguishes it from a spawn with no transcript. +- [x] Deep-chain indent ergonomics: 2em step for depths 1-5, compressed + to 0.5em for depths 6+ (cards tagged `.agent-deep`), so an + 80-level chain fits (~1118px vs ~2560px); depth read from the + badge + colour. +- [x] Interactive polish round on cboos's real nested session + (2026-06-23): badge wording "Depth N", palette + marker + indent + confirmed. + +Deferred follow-up (monk review of #219, optional): a fixture variant +with a thinkingβ†’spawn block to unit-pin the 0-width passthrough that +Β§5.4/Β§5.5 currently only verify on real data.