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.