diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.baseline.json b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.baseline.json index 56c940f158..3e392a44e7 100644 --- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.baseline.json +++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.baseline.json @@ -1,4 +1,4 @@ { - "default": "
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

please summarize the plan

Open in chat
12:00 PM
Thinking
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

weighing the options

Updatedplan
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">
    \n
  • read the transcript
  • \n
  • write the summary (in progress)
  • \n
  • ship it
  • \n
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

Here is the summary with code.

", - "compactPreview": "
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

please summarize the plan

12:00 PM
Thinking
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

weighing the options

Updatedplan
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">
    \n
  • read the transcript
  • \n
  • write the summary (in progress)
  • \n
  • ship it
  • \n
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

Here is the summary with code.

" + "default": "
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

please summarize the plan

Open in chat
Thinking
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

weighing the options

Updatedplan
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">
    \n
  • read the transcript
  • \n
  • write the summary (in progress)
  • \n
  • ship it
  • \n
Rancargo test1.0s

cargo test

Permission requested · write src/main.rs
Options: Allow, Deny
Approved (once)
Context compacted
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

Here is the summary with code.

Turn failed · the harness exited
Most recent observed session · 6/14/2026, 12:05:00 PM
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

next task

Open in chat
12:05 PM
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

on it

", + "compactPreview": "
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

please summarize the plan

Thinking
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">

weighing the options

Updatedplan
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 leading-5\">
    \n
  • read the transcript
  • \n
  • write the summary (in progress)
  • \n
  • ship it
  • \n
Rancargo test1.0s

cargo test

Permission requested · write src/main.rs
Options: Allow, Deny
Approved (once)
Context compacted
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

Here is the summary with code.

Turn failed · the harness exited
Most recent observed session · 6/14/2026, 12:05:00 PM
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

next task

12:05 PM
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-3 [&>p+p]:mt-conversation-paragraph [&>ol]:space-y-conversation-list [&>ul]:space-y-conversation-list [&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5 [&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5 [&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5! [&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5 [&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5 [&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5 [&>*+hr]:mt-4 [&>hr+*]:mt-4 [&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5 text-xs leading-4\">

on it

" } diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs index f9c2507606..0dc89acffc 100644 --- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs +++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs @@ -5,12 +5,14 @@ * (variant context + derived turn meta) is exercised end to end rather than * asserting against re-implemented render classes. * - * The last test is the important one: `conversation` is purely additive, so the - * `default` and `compactPreview` markup for the same transcript must be - * byte-identical to the markup captured before the variant existed. That - * snapshot lives in AgentSessionTranscriptList.conversation.baseline.json and - * was produced by mounting this same transcript on the pre-change commit - * (f79421673) — regenerate it only when a deliberate change to the other + * The byte-for-byte tests at the bottom are the important ones: `conversation` + * is purely additive, so the `default` and `compactPreview` markup for the same + * transcript must be byte-identical to the markup captured before the variant + * existed. That snapshot lives in + * AgentSessionTranscriptList.conversation.baseline.json and was produced by + * mounting `baselineItems()` — a transcript containing every renderable item + * kind across two sessions — on pre-change main (074561233) in a clean + * throwaway worktree. Regenerate it only when a deliberate change to the other * variants is being made. */ @@ -41,10 +43,13 @@ class NoopObserver { } Object.assign(globalThis, { + Element: dom.window.Element, + Event: dom.window.Event, HTMLElement: dom.window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true, IntersectionObserver: NoopObserver, MutationObserver: dom.window.MutationObserver, + Node: dom.window.Node, ResizeObserver: NoopObserver, document: dom.window.document, getComputedStyle: (...args) => dom.window.getComputedStyle(...args), @@ -67,9 +72,11 @@ dom.window.cancelAnimationFrame = (id) => clearTimeout(id); globalThis.requestAnimationFrame = dom.window.requestAnimationFrame; globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame; +let act; let cleanup; let render; let createElement; +let useState; let createMemoryHistory; let createRootRoute; let createRouter; @@ -131,6 +138,163 @@ function items() { ]; } +/** + * Everything the legacy variants can render, in one transcript. + * + * The byte-for-byte contract covers `default`/`compactPreview` for EVERY item + * kind, so the baseline input has to contain every kind rather than the happy + * path: prompt (with prompt context and setup lifecycle so the ingress chrome + * renders), assistant message, thought, plan, a tool item, ordinary lifecycle + * status, error, permission — across two sessions so a session-boundary divider + * is forced too. Where `compactPreview` deliberately suppresses a kind, that + * absence is captured in the fixture and is therefore also protected. + * + * Single tool item on purpose: a run of three would collapse into a grouped + * summary and the leaf tool row would never be captured. + */ +function baselineItems() { + const first = { channelId: "chan-1", sessionId: "sess-1", turnId: "turn-1" }; + const second = { channelId: "chan-1", sessionId: "sess-2", turnId: "turn-2" }; + return [ + { + ...first, + id: "life:setup", + type: "lifecycle", + renderClass: "status", + title: "Turn started", + text: "1 trigger", + timestamp: "2026-06-14T19:00:00.000Z", + acpSource: "turn_started", + }, + { + ...first, + id: "meta:context", + type: "metadata", + renderClass: "raw-rail", + title: "Prompt context", + sections: [{ title: "Channel", body: "engineering" }], + timestamp: "2026-06-14T19:00:00.500Z", + acpSource: "session/prompt:context", + }, + { + ...first, + id: "msg:user", + type: "message", + renderClass: "message", + role: "user", + title: "Ada", + text: "please summarize the plan", + timestamp: "2026-06-14T19:00:01.000Z", + messageId: "event-1", + authorPubkey: AUTHOR, + acpSource: "session/prompt:user", + }, + { + ...first, + id: "thought:1", + type: "thought", + renderClass: "thought", + title: "Thinking", + text: "weighing the options", + timestamp: "2026-06-14T19:00:02.000Z", + }, + { + ...first, + id: "plan:1", + type: "plan", + renderClass: "plan", + title: "Plan", + text: "- [x] read the transcript\n- [ ] write the summary (in progress)\n- [ ] ship it", + timestamp: "2026-06-14T19:00:03.000Z", + }, + { + ...first, + id: "tool:1", + type: "tool", + renderClass: "shell", + descriptor: { + renderClass: "shell", + label: "Ran a command", + preview: "cargo test", + tone: "neutral", + source: "shell", + }, + title: "Ran a command", + toolName: "shell", + buzzToolName: null, + status: "completed", + args: { command: "cargo test" }, + result: "ok", + isError: false, + timestamp: "2026-06-14T19:00:04.000Z", + startedAt: "2026-06-14T19:00:04.000Z", + completedAt: "2026-06-14T19:00:05.000Z", + }, + { + ...first, + id: "life:permission", + type: "lifecycle", + renderClass: "permission", + title: "Permission requested", + text: "write src/main.rs\nOptions: Allow, Deny", + outcome: "Approved (once)", + timestamp: "2026-06-14T19:00:06.000Z", + }, + { + ...first, + id: "life:status", + type: "lifecycle", + renderClass: "status", + title: "Context compacted", + text: "", + timestamp: "2026-06-14T19:00:07.000Z", + }, + { + ...first, + id: "msg:assistant", + type: "message", + renderClass: "message", + role: "assistant", + title: "Test Agent", + text: "Here is the summary with `code`.", + timestamp: "2026-06-14T19:00:08.000Z", + }, + { + ...first, + id: "life:error", + type: "lifecycle", + renderClass: "error", + title: "Turn failed", + text: "the harness exited", + timestamp: "2026-06-14T19:00:09.000Z", + }, + // Second session run: forces a session-boundary divider between the runs. + { + ...second, + id: "msg:user2", + type: "message", + renderClass: "message", + role: "user", + title: "Ada", + text: "next task", + timestamp: "2026-06-14T19:05:00.000Z", + messageId: "event-2", + authorPubkey: AUTHOR, + acpSource: "session/prompt:user", + }, + { + ...second, + id: "msg:assistant2", + type: "message", + renderClass: "message", + role: "assistant", + title: "Test Agent", + text: "on it", + timestamp: "2026-06-14T19:05:01.000Z", + }, + ]; +} + async function renderTranscript(variant, overrides = {}) { const rootRoute = createRootRoute({ component: () => @@ -150,9 +314,44 @@ async function renderTranscript(variant, overrides = {}) { return render(createElement(RouterProvider, { router })); } +/** + * Same mount, but the caller can swap the list props afterwards. Needed for the + * contracts that are only visible across a rerender: a streaming thought + * folding once the turn moves on, and a plan mutating in place. + */ +async function renderRerenderableTranscript(variant, initialOverrides = {}) { + let applyProps; + const Harness = () => { + const [overrides, setOverrides] = useState(initialOverrides); + applyProps = setOverrides; + return createElement(AgentSessionTranscriptList, { + ...AGENT, + emptyDescription: "nothing yet", + items: items(), + variant, + ...overrides, + }); + }; + const rootRoute = createRootRoute({ component: Harness }); + const router = createRouter({ + history: createMemoryHistory({ initialEntries: ["/"] }), + routeTree: rootRoute, + }); + await router.load(); + const utils = render(createElement(RouterProvider, { router })); + return { + ...utils, + async setOverrides(next) { + await act(async () => { + applyProps(next); + }); + }, + }; +} + before(async () => { - ({ cleanup, render } = await import("@testing-library/react")); - ({ createElement } = await import("react")); + ({ act, cleanup, render } = await import("@testing-library/react")); + ({ createElement, useState } = await import("react")); ({ createMemoryHistory, createRootRoute, createRouter, RouterProvider } = await import("@tanstack/react-router")); ({ AgentSessionTranscriptList } = await import( @@ -258,6 +457,109 @@ test("conversation auto-opens the thought disclosure while it is streaming", asy assert.doesNotMatch(summary.textContent, /Thought for/); }); +test("conversation folds the thought when the turn moves on, even after the browser echoes the auto-open toggle", async () => { + // Regression guard for the programmatic-toggle echo trap: a real browser fires + // `toggle` when React flips `open`, so the auto-open for a streaming thought + // arrives back at the component as if the reader had clicked. JSDOM does not + // fire that event itself, so the test injects it. If the handler records it as + // a reader choice, the disclosure stays pinned open forever and reasoning + // never recedes once the agent acts. + syncAgentTurnsFromEvents(AGENT.agentPubkey, [ + { + seq: 1, + timestamp: "2026-06-14T19:00:02.000Z", + kind: "turn_started", + agentIndex: 0, + channelId: "chan-1", + sessionId: "sess-1", + turnId: "turn-1", + payload: null, + }, + ]); + const streaming = { channelId: "chan-1", items: items().slice(0, 2) }; + const { container, setOverrides } = await renderRerenderableTranscript( + "conversation", + streaming, + ); + + const disclosure = container.querySelector( + '[data-testid="transcript-thought-item"]', + ); + assert.equal(disclosure.open, true, "streaming thought should auto-open"); + + // The browser echo: `open` already agrees with what React rendered, and the + // event follows rather than causes that state. + await act(async () => { + disclosure.open = true; + disclosure.dispatchEvent(new dom.window.Event("toggle")); + }); + assert.equal( + disclosure.open, + true, + "the echo must not disturb the open tail", + ); + + // The turn produces its next item, so the thought is no longer the streaming + // tail and should recede. + await setOverrides({ ...streaming, items: items().slice(0, 3) }); + + const settled = container.querySelector( + '[data-testid="transcript-thought-item"]', + ); + assert.equal( + settled.open, + false, + "a thought the agent has acted on should fold again", + ); + assert.match( + settled + .querySelector('[data-testid="transcript-thought-disclosure"]') + .textContent.trim(), + /^Thought for 5s/, + ); +}); + +test("conversation keeps a reader-opened thought open after the turn moves on", async () => { + // The other half of the guard: a toggle that DISAGREES with the rendered state + // is a genuine reader choice and must win over the stream transition. + syncAgentTurnsFromEvents(AGENT.agentPubkey, [ + { + seq: 1, + timestamp: "2026-06-14T19:00:02.000Z", + kind: "turn_started", + agentIndex: 0, + channelId: "chan-1", + sessionId: "sess-1", + turnId: "turn-1", + payload: null, + }, + ]); + // Three items so the thought is not the streaming tail: it renders collapsed. + const settledItems = { channelId: "chan-1", items: items().slice(0, 3) }; + const { container, setOverrides } = await renderRerenderableTranscript( + "conversation", + settledItems, + ); + + const disclosure = container.querySelector( + '[data-testid="transcript-thought-item"]', + ); + assert.equal(disclosure.open, false, "a settled thought starts collapsed"); + + await act(async () => { + disclosure.open = true; + disclosure.dispatchEvent(new dom.window.Event("toggle")); + }); + + await setOverrides({ ...settledItems, items: items() }); + + assert.equal( + container.querySelector('[data-testid="transcript-thought-item"]').open, + true, + "the reader's choice should survive later transcript items", + ); +}); + test("conversation renders the plan as a checklist card with in-place progress", async () => { const { container } = await renderTranscript("conversation"); const card = container.querySelector('[data-testid="transcript-plan-item"]'); @@ -273,6 +575,46 @@ test("conversation renders the plan as a checklist card with in-place progress", assert.deepEqual(statuses, ["completed", "in_progress", "pending"]); }); +test("conversation updates the same plan card in place as entries advance", async () => { + // The transcript reducer mutates the plan item in place (same item id), so the + // card must be re-rendered rather than replaced: progress copy changes while + // the same DOM node is retained. A new node here would mean the reader's + // scroll position and any card-level state get thrown away on every update. + const { container, setOverrides } = + await renderRerenderableTranscript("conversation"); + const before = container.querySelector( + '[data-testid="transcript-plan-item"]', + ); + assert.equal( + before.querySelector('[data-testid="transcript-plan-progress"]') + .textContent, + "1/3 complete", + ); + + const advanced = items().map((item) => + item.id === "plan:1" + ? { + ...item, + text: "- [x] read the transcript\n- [x] write the summary\n- [ ] ship it (in progress)", + } + : item, + ); + await setOverrides({ items: advanced }); + + const after = container.querySelector('[data-testid="transcript-plan-item"]'); + assert.equal(after, before, "the same plan card node should be reused"); + assert.equal( + after.querySelector('[data-testid="transcript-plan-progress"]').textContent, + "2/3 complete", + ); + assert.deepEqual( + [...after.querySelectorAll("[data-plan-entry-status]")].map((entry) => + entry.getAttribute("data-plan-entry-status"), + ), + ["completed", "completed", "in_progress"], + ); +}); + test("conversation quiets session boundaries and status rows into centered dividers", async () => { const shared = { channelId: "chan-1", sessionId: "sess-2", turnId: "turn-2" }; const { container } = await renderTranscript("conversation", { @@ -376,7 +718,9 @@ test("conversation keeps errors and permission gates loud", async () => { test("default and compactPreview markup is unchanged by the conversation variant", async () => { for (const variant of ["default", "compactPreview"]) { - const { container } = await renderTranscript(variant); + const { container } = await renderTranscript(variant, { + items: baselineItems(), + }); assert.equal( container.innerHTML, BASELINE_MARKUP[variant], @@ -385,3 +729,34 @@ test("default and compactPreview markup is unchanged by the conversation variant cleanup(); } }); + +test("the byte-for-byte baseline actually exercises every renderable item kind", async () => { + // Guards the fixture itself: a baseline that silently stopped covering a kind + // would keep passing the comparison above while protecting nothing. Asserted + // against `default`, which renders every kind. + const { container } = await renderTranscript("default", { + items: baselineItems(), + }); + const present = (selector) => container.querySelector(selector) !== null; + for (const [kind, selector] of [ + ["prompt", '[data-testid="transcript-user-message"]'], + ["prompt setup", '[data-testid="transcript-turn-setup"]'], + ["assistant message", '[data-testid="transcript-assistant-message"]'], + ["thought", '[data-testid="transcript-thought-item"]'], + ["plan", '[data-testid="transcript-plan-item"]'], + ["tool", '[data-testid="transcript-tool-item"]'], + ["permission", '[data-testid="transcript-permission-item"]'], + ["lifecycle status/error", '[data-testid="transcript-lifecycle-item"]'], + ["session boundary", '[data-testid="session-boundary-divider"]'], + ]) { + assert.ok(present(selector), `baseline must cover ${kind}`); + } + // Status and error share the lifecycle testid, so check both are really there. + const lifecycleText = [ + ...container.querySelectorAll('[data-testid="transcript-lifecycle-item"]'), + ] + .map((node) => node.textContent) + .join("\n"); + assert.match(lifecycleText, /Context compacted/); + assert.match(lifecycleText, /Turn failed/); +}); diff --git a/desktop/src/features/agents/ui/activityRenderClasses/ThoughtActivity.tsx b/desktop/src/features/agents/ui/activityRenderClasses/ThoughtActivity.tsx index 260ab6b915..b5701b5fe3 100644 --- a/desktop/src/features/agents/ui/activityRenderClasses/ThoughtActivity.tsx +++ b/desktop/src/features/agents/ui/activityRenderClasses/ThoughtActivity.tsx @@ -77,13 +77,34 @@ function ConversationThought({ isStreaming, }); + // `
` fires `toggle` for programmatic `open` changes as well as + // clicks. Without this guard the auto-open for a streaming thought would be + // recorded as a reader choice and would then pin the disclosure open forever, + // so reasoning would never fold once the agent acted on it. Only a toggle + // that DISAGREES with the state we last rendered can have come from the + // reader. + // + // Same trap, same guard as the tool-run card's disclosure in the tool-chain + // slice; once that lands and extracts a shared controlled-details hook this + // should consume it rather than keeping a second copy. + const renderedOpenRef = React.useRef(isOpen); + React.useLayoutEffect(() => { + renderedOpenRef.current = isOpen; + }, [isOpen]); + const handleToggle = React.useCallback( + (event: React.SyntheticEvent) => { + const open = event.currentTarget.open; + if (open === renderedOpenRef.current) return; + setUserOpen(open); + }, + [], + ); + return (
{ - setUserOpen(event.currentTarget.open); - }} + onToggle={handleToggle} open={isOpen} title={formatTranscriptTimestampTitle(item.timestamp)} > diff --git a/desktop/src/features/agents/ui/agentSessionConversationMeta.test.mjs b/desktop/src/features/agents/ui/agentSessionConversationMeta.test.mjs index c8506c4266..9eb09c8558 100644 --- a/desktop/src/features/agents/ui/agentSessionConversationMeta.test.mjs +++ b/desktop/src/features/agents/ui/agentSessionConversationMeta.test.mjs @@ -10,7 +10,6 @@ import { formatPlanChecklistProgress, parsePlanChecklist, } from "./agentSessionPlanChecklist.ts"; -import { resolveAgentSessionTranscriptVariant } from "./agentSessionTranscriptVariantChoice.ts"; function item(overrides) { return { @@ -161,6 +160,64 @@ test("buildConversationTurnMeta counts a prompt as an item and skips setup segme // ---- formatThoughtDisclosureLabel ---- +/** + * Product ruling (ss-core-02, Slice B review): reasoning folds as soon as the + * agent ACTS on it — the next leaf item of any kind — not only when the next + * assistant message arrives. A thought pinned open across a long tool run is + * exactly the noise the focus view removes. This test exists so the rule is + * explicit rather than an accident of `items[index + 1]`. + */ +test("buildConversationTurnMeta ends a thought at the next tool call, not the next message", () => { + const meta = buildConversationTurnMeta( + [ + turnBlock([ + { + kind: "item", + item: item({ + id: "thought:1", + type: "thought", + renderClass: "thought", + title: "Thinking", + text: "…", + timestamp: "2026-06-14T19:00:02.000Z", + }), + }, + { + kind: "item", + item: item({ + id: "tool:1", + type: "tool", + renderClass: "shell", + title: "Ran a command", + text: "", + descriptor: { label: "Ran a command", preview: "cargo test" }, + timestamp: "2026-06-14T19:00:06.000Z", + }), + }, + { + kind: "item", + item: item({ + id: "msg:1", + type: "message", + renderClass: "message", + role: "assistant", + title: "Agent", + text: "tests pass", + timestamp: "2026-06-14T19:01:30.000Z", + }), + }, + ]), + ], + { isTurnLive: true, variant: "conversation" }, + ); + + // 4s to the tool call, NOT 88s to the assistant message: the thought is done + // the moment the agent acts, so it folds instead of staying open for the run. + assert.deepEqual(meta.thoughtDurationSecondsById, { "thought:1": 4 }); + // The trailing message is the streaming tail, so the thought is not streaming. + assert.equal(meta.streamingItemId, "msg:1"); +}); + test("formatThoughtDisclosureLabel says Thinking while streaming", () => { assert.equal( formatThoughtDisclosureLabel({ durationSeconds: 4, isStreaming: true }), @@ -208,32 +265,3 @@ test("parsePlanChecklist yields nothing for free-form plan text", () => { assert.deepEqual(checklist.entries, []); assert.equal(formatPlanChecklistProgress(checklist), null); }); - -// ---- resolveAgentSessionTranscriptVariant ---- - -test("resolveAgentSessionTranscriptVariant opts a wide panel into conversation", () => { - assert.equal( - resolveAgentSessionTranscriptVariant({ - isSinglePanelView: false, - widthPx: 720, - }), - "conversation", - ); -}); - -test("resolveAgentSessionTranscriptVariant keeps narrow and single-panel layouts on default", () => { - assert.equal( - resolveAgentSessionTranscriptVariant({ - isSinglePanelView: false, - widthPx: 380, - }), - "default", - ); - assert.equal( - resolveAgentSessionTranscriptVariant({ - isSinglePanelView: true, - widthPx: 1200, - }), - "default", - ); -}); diff --git a/desktop/src/features/agents/ui/agentSessionConversationMeta.ts b/desktop/src/features/agents/ui/agentSessionConversationMeta.ts index b5aa0962da..d1e33b5825 100644 --- a/desktop/src/features/agents/ui/agentSessionConversationMeta.ts +++ b/desktop/src/features/agents/ui/agentSessionConversationMeta.ts @@ -40,8 +40,13 @@ function elapsedSeconds(from: string, to: string): number | null { * can be mid-stream, and only when the agent's turn is actually live. The * thought disclosure auto-opens for exactly that item, so the reader watches * reasoning arrive rather than clicking to find it. - * - **How long a thought ran.** A thought is finished once the turn produces a - * later item; the gap between the thought's timestamp and that next item's + * - **How long a thought ran.** A thought is finished once the turn produces + * ANY later leaf item — a tool call or plan update, not just the next + * assistant message. This is deliberate: reasoning recedes as soon as the + * agent *acts* on it, which is the moment the reader's attention should move + * to the action. Folding only on the next message would pin reasoning open + * across a long tool run, which is exactly the noise the focus view exists + * to remove. The gap between the thought's timestamp and that next item's * timestamp is the "Thought for Ns" duration. Thoughts with no following * item are still open, so they get no duration. * diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptVariantChoice.ts b/desktop/src/features/agents/ui/agentSessionTranscriptVariantChoice.ts deleted file mode 100644 index 55fd4621b9..0000000000 --- a/desktop/src/features/agents/ui/agentSessionTranscriptVariantChoice.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { AgentSessionTranscriptVariant } from "./agentSessionTranscriptContext"; - -/** - * Narrowest panel width that still reads as a conversation. - * - * The `conversation` variant lays the transcript out as a document: a centered - * reading column with right-aligned prompt bubbles capped well inside it. Below - * roughly this width the bubble/prose asymmetry collapses and the dense - * `default` activity feed is the more legible presentation, so the wide layout - * is the only one that opts in. - * - * Calibrated against `shared/layout/auxiliaryPanelLayout.ts`: the panel opens at - * `AUXILIARY_PANEL_DEFAULT_WIDTH_PX` (380) and clamps to at least - * `AUXILIARY_PANEL_MAX_WIDTH_PX` (720), so 640 sits deliberately in the top of - * that range — a panel at its default or merely-widened size keeps the feed, and - * only a reader who has pulled it near full width (or a full-cover host that - * pins the variant explicitly) gets the reading layout. - */ -export const CONVERSATION_TRANSCRIPT_MIN_WIDTH_PX = 640; - -/** - * Pick the transcript variant for an agent session panel from its layout. - * - * Callers may always override by passing an explicit `transcriptVariant`; this - * is only the default when they don't. Kept as a pure function so the threshold - * is testable without mounting the panel. - */ -export function resolveAgentSessionTranscriptVariant({ - isSinglePanelView, - widthPx, -}: { - isSinglePanelView: boolean; - widthPx: number; -}): AgentSessionTranscriptVariant { - // Single-panel view stacks the panel over the channel on a narrow viewport - // and renders it at 100% width, so `widthPx` (the stored/resizable width) - // does not describe what the reader sees. Keep the default feed there. - if (isSinglePanelView) { - return "default"; - } - - return widthPx >= CONVERSATION_TRANSCRIPT_MIN_WIDTH_PX - ? "conversation" - : "default"; -} diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx index 6779517638..6065cce15e 100644 --- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx +++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx @@ -17,7 +17,6 @@ import { } from "@/features/agents/ui/agentSessionPanelLayout"; import { deriveTranscriptBlockIds } from "@/features/agents/ui/agentSessionTranscriptGrouping"; import type { AgentSessionTranscriptVariant } from "@/features/agents/ui/agentSessionTranscriptContext"; -import { resolveAgentSessionTranscriptVariant } from "@/features/agents/ui/agentSessionTranscriptVariantChoice"; import type { ObserverEvent } from "@/features/agents/ui/agentSessionTypes"; import { ManagedAgentSessionPanel } from "@/features/agents/ui/ManagedAgentSessionPanel"; import { @@ -84,8 +83,12 @@ type AgentSessionThreadPanelProps = { widthPx: number; transparentChrome?: boolean; /** - * Overrides the transcript presentation. Omit to let the panel pick from its - * own layout — see `resolveAgentSessionTranscriptVariant`. + * Transcript presentation. Caller-pinned only: the panel keeps the dense + * activity feed unless a host explicitly asks for `conversation` (the + * full-cover focus view does). There is deliberately no width or layout + * heuristic here — an automatic wide-pane mode would be a separate product + * decision, and swapping the whole presentation as a reader drags a resize + * handle across a threshold is not one we want to make implicitly. */ transcriptVariant?: AgentSessionTranscriptVariant; }; @@ -102,16 +105,10 @@ export function AgentSessionThreadPanel({ onClose, widthPx, transparentChrome = false, - transcriptVariant, + transcriptVariant = "default", }: AgentSessionThreadPanelProps) { const isLive = isManagedAgentActive(agent); const isOverlay = useIsThreadPanelOverlay(); - // Wide/standalone reading layouts get the conversation transcript; narrow and - // single-panel layouts keep the dense activity feed. An explicit prop always - // wins so callers can pin a presentation. - const resolvedTranscriptVariant = - transcriptVariant ?? - resolveAgentSessionTranscriptVariant({ isSinglePanelView, widthPx }); const sessionChannelId = channelId ?? channel?.id ?? null; // Unified working signal, scoped to this panel's channel (or all channels // when the panel is unscoped) — observer turns primary, typing fallback. @@ -509,7 +506,7 @@ export function AgentSessionThreadPanel({ rawLayout="exclusive" showHeader={false} showRaw={showRawFeed} - transcriptVariant={resolvedTranscriptVariant} + transcriptVariant={transcriptVariant} />