fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(web): unify activity logs and composer banners - #221

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification
Aug 31, 2026
Merged

fix(web): unify activity logs and composer banners#221
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-composer-unification

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes DEF-9 from the upstream review ledger.

Pylon's composer grew a set of bespoke .chat-composer-* CSS rules that every
notice, badge and drawer positioned itself against by hand. Upstream replaced
them with ComposerSurface/ComposerBanner primitives and deleted 524 lines of
that CSS. This adopts the primitives and moves Pylon's fork-only surfaces onto
them rather than dropping them.

Adopted from upstream pingdotgg/t3code: #8734 (30175a8af), #8850
(9842518c9), #8855 (3f62e6fa6).

Why three commits and not four

The ledger recorded this as #8693. Upstream reverted #8693 twelve hours after
merging it (#8733), then re-landed a substantially different version as
#8734 — 979 insertions of difference across 18 files, including keeping the
timeline working row that #8693 had deleted. #8733 restores the pre-#8693
tree exactly, so the revert/re-land pair cancels and #8734 is a clean re-land
onto a tree shaped like Pylon's. Porting #8693 would have adopted a version
upstream itself abandoned.

What Pylon keeps

  • ComposerTasksBadge takes upstream's banner shape but keeps waitingOn
    ("Needs your input" / "Waiting on agents" / "Waiting on external system"),
    per-turn dismiss with its turnId + step:waitingOn snapshot, delegated-agent
    counts, and Pylon's TaskProgressSegments/TaskStatusIndicator. Upstream's
    badge has none of these. Segments hide while the list is expanded, since the
    list already shows every step.
  • The timeline working row and workingStepLabel.#8734 keeps the row;
    #8855 strips the step label so the timer shows only elapsed time. That one
    hunk is declined — the row still says what the agent is doing, not merely how
    long.
  • BranchToolbar adopts ComposerSurface.ContextStrip while keeping the
    branch selector in the left run with Usage opposite, per the comment already
    there. Upstream's right-hand copy would have rendered it twice.
  • ChatComposer keeps ThreadHandoffTab, the quick-question and
    session-resource dialogs, the attachment control, externalDrawerAttached and
    the collapsed-mobile stash toggle. The last two were dropped silently by the
    cherry-pick in non-conflicting hunks; typecheck caught them.
  • session-logic keeps Pylon's activity skip list and adds
    turn.plan.updated.

What Pylon gives up

  • The work-group overflow toggle.MAX_VISIBLE_WORK_LOG_ENTRIES and
    onlyToolEntries are gone in favour of upstream's simpler grouping, which
    excludes agent-spawn and error rows from grouping outright. That preserves the
    #7893 and #7999 findings by construction — a spawn row can no longer be
    inside a group, so it can never hide behind a "+N tool calls" toggle — and
    hasFailure keys off latestToolEntry, which is exactly the "#7893 retried
    successfully is not a failure" rule.
  • The inline per-turn plan chip.deriveTurnPlans, TurnPlanTimelineRow and
    the turn-plan row kind are removed. Upstream folds plans into the composer
    tasks badge, which Pylon keeps and extends with waitingOn, so the
    transcript chip would show the same plan twice. Reversible if you disagree —
    it needs the row kind restored into upstream's rewritten grouping.

One behaviour note worth a look: a missing-response notice now renders as a
work-toggle rather than a plain work row. It stays outside the fold and a
lone non-tool entry surfaces its own label as the summary, so the message text is
still what the user reads.

Verified: vp run -F @t3tools/web typecheck (exit 0, no errors);
vp test run --dir apps/web apps/web/src/components/chat apps/web/src/session-logic.test.ts apps/web/src/components/ChatView.logic.test.ts apps/web/src/versionSkew.test.ts
(637 passed); vp fmt --check apps/web/src clean. Also checked that no JSX
still references a removed .chat-composer-* class.

Not exercised in a browser. This restructures the composer's banner, drawer
and shoulder-tab layout, which unit tests cannot judge — it wants a real-client
pass before merge.

Reviewed and integrated with Claude Opus 5 in Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

t3dotggand others added 3 commits August 31, 2026 15:37
Adapted for Pylon. Upstream replaces the bespoke .chat-composer-* CSS with
ComposerSurface/ComposerBanner primitives; Pylon's fork-only surfaces move onto
the same primitives rather than being dropped:
- ComposerTasksBadge keeps waitingOn, per-turn dismiss and delegated-agent
counts on top of upstream's banner shape, and keeps Pylon's task segments and
status indicator so a waiting step still names who it waits on;
- the timeline working row and its workingStepLabel stay, which #8734 also
keeps;
- BranchToolbar keeps the branch selector in the left run with Usage opposite,
per the comment already there, and adopts ComposerSurface.ContextStrip;
- ChatComposer keeps ThreadHandoffTab, the quick-question and session-resource
dialogs, the attachment control, externalDrawerAttached and the collapsed
mobile stash toggle;
- session-logic keeps Pylon's activity skip list and adds turn.plan.updated.
Upstream's simpler work grouping is adopted: it excludes agent-spawn and error
rows from grouping outright, which preserves the #7893 and #7999 findings by
construction, so MAX_VISIBLE_WORK_LOG_ENTRIES and onlyToolEntries go.
The inline per-turn plan chip goes with it. Upstream folds plans into the
composer tasks badge, which Pylon keeps and extends with waitingOn, so the
transcript chip would duplicate it; deriveTurnPlans, TurnPlanTimelineRow and
the turn-plan row kind are removed.
(cherry picked from commit 30175a8afc42d2e8cb4e0b3a0e2b1c0aa96a0e2e)
(cherry picked from commit 9842518c9a0af14fb3a3e90993692a6e34032682)
Adapted for Pylon: upstream also strips workingStepLabel so the timeline timer
shows only elapsed time. Pylon keeps it, so the row still says what the agent is
doing and not merely how long it has been doing it. Everything else in the
commit is taken as-is.
(cherry picked from commit 3f62e6fa65c2a2a91a367be4ee95da1b50007bde)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 31, 2026
@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+38 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+11 B (+0.2%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+27 B (+0.4%)7.8 KiB
CodexLive turn WebSocket decoded57.2 KiB57.2 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.6 KiB13.8 KiB+198 B (+1.4%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+1 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.8 KiB+197 B (+2.9%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB59.5 KiB+1.5 KiB (+2.5%)66.4 KiB
ClaudeLive turn messages1011+1 (+10.0%)21

Baseline: 35b8380 · PR result: 6279bf4 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.5 KiB
  • Claude decoded thread snapshot: 110.2 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Adversarial review found the cherry-pick had left features wired to nothing and
two blocks rendered twice. Typecheck stayed green throughout because none of it
is a type error.
Rendered twice: the collapsed-mobile prompt row, and a second
data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main
repeating composerFrameClassName - which in ultrathink mode ran the infinite
rainbow animation on two nested elements. ComposerSurface.Main already carries
that attribute, so the wrapper was pure duplication.
Wired to nothing: per-turn tasks dismiss reached no surface, so dismissTasks and
the dismissal snapshot were dead; delegated-agent counts reached only the inline
badge; and #8855's removal of workingStepLabel applied through a non-conflicting
hunk despite being declined, leaving the prop and memo dep behind while the row
stopped rendering the step.
Also: the dock and inline stash badges were no longer mutually exclusive; the
inline stash badge wrapped itself in a glass banner inside a compact toolbar;
the drawer-inset fallback was dropped, and since the custom property went with
the deleted CSS, parseFloat("") gave NaN and a NaN never settles; the composer's
activity item was prepended, tying with background liveness and tucking its Stop
button - the only stop affordance for a settled turn - behind the peek;
dismissing a failed update also wrote the permanent version-mismatch dismissal;
the tasks aria-label hardcoded "Current task" for waiting, next and completed
plans, and the per-step status moved after the step text; and groups of two or
more non-tool rows collapsed to "Received N updates", hiding text that is the
whole point of a runtime warning.
Restores tests for ComposerBannerStack and ComposerStashBadge, deleted by the
same change that rewrote them, including one that pins the ordering above.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review found thirteen defects, and it was right to be blunt: several features this PR claimed to keep were wired to nothing. Typecheck stayed green throughout, because none of it is a type error. All fixed on the branch.

Claims in the original description that were false

  • Per-turn tasks dismiss was dead. No onDismiss reached the badge, the banner row or the drawer, so dismissTasks, visibleTasksProgress, visibleTaskSteps and dismissedTasksSnapshot were all unused. The feature you chose to keep did nothing.
  • Delegated-agent counts reached only the inline badge, not the tab, banner row or drawer.
  • workingStepLabel was gone from the working row.#8855's removal applied through a non-conflicting hunk, so declining the conflict wasn't enough — the prop and memo dep were left behind while the row stopped rendering the step. Exactly the same silent-loss mode as externalDrawerAttached.

All three are now wired through every surface, and the task surfaces gate on the dismissal-aware values rather than the raw ones.

Rendered twice

  • The collapsed-mobile prompt row.
  • A second data-chat-composer-main-surface wrapper nested inside ComposerSurface.Main, repeating composerFrameClassName — in ultrathink mode that ran the infinite .ultrathink-frame::before rainbow animation on two nested elements. ComposerSurface.Main already carries the attribute, so the wrapper was pure duplication.

Other defects fixed

  • The dock and inline stash badges were no longer mutually exclusive; both rendered when a banner was attached or the drawer was open.
  • The inline stash badge wrapped itself in a full glass ComposerBanner.Root inside a compact toolbar. placement="inline" restored.
  • The || 1.375 drawer-inset fallback was dropped. The custom property went with the deleted CSS, so parseFloat("") gives NaN — and a NaN never equals itself, so the settle loop would never converge.
  • The composer's activity item was prepended, tying with background liveness at priority: "activity" and tucking its Stop button — the only stop affordance for a settled turn — behind the peek. Now appended; a test pins the ordering.
  • Dismissing a failed update also wrote the permanent version-mismatch dismissal, burying the "update available" offer for that version pair forever.
  • tasksAriaLabel hardcoded "Current task" for waiting, next and completed plans, and the per-step sr-only status moved after the step text.
  • Groups of 2+ non-tool rows collapsed entirely to "Received N updates", hiding text that is the whole point of a runtime warning or missing-response notice. Any all-non-tool group now shows its newest label.
  • ComposerBannerStack.test.tsx and ComposerStashBadge.test.tsx had been deleted by the same change that rewrote both. Replacements written.

Also removed dead imports and a dead waitingOwnerLabel left by the plan-chip removal — caught by vp lint, not typecheck.

Re-verified: @t3tools/web typecheck clean, vp lint clean on the touched files, vp fmt --check clean, 628 tests passing across apps/web/src/components/chat, session-logic and ChatView.logic.

The review's closing point stands and I'm acting on it: findings 1, 2, 4 and 5 are exactly what a single real-client pass catches. Doing that next before this merges.

@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Real-client pass done. Ran the branch against an isolated dev environment (--home-dir on a temp dir seeded via VACUUM INTO from a read-only snapshot — the live ~/.pylon-code was never opened for writing), paired the browser, and asserted directly against the DOM.

The structural defects the review found are gone:

CheckBeforeNow
[data-chat-composer-main-surface]2 (nested)1
[data-chat-composer-footer]21
[data-chat-composer-actions="right"]21
Orphaned .chat-composer-* classes in the DOM0
Console errors / warnings / unhandled rejections0

Also verified live:

  • The composer renders and is interactive: clicking focuses the editor, typing lands text, and the Send button transitions to enabled. That exercises the primary-actions region where ComposerFooterPrimaryActions' props, canQueueFollowUp/onQueueFollowUp and supportedRuntimeModes were repaired — all five footer controls render ("Claude Fable 5", "High · 1M", "Full access").
  • The empty ComposerBanner.Attachment collapses to 0×0 via empty:hidden, so no phantom gap above the composer when there is nothing to show.

Two honest gaps in this pass:

  1. The preview tool's snapshot/screenshot kept failing, so there are no before/after images — the table above is DOM assertions instead. For these particular findings (duplicate nodes, orphaned classes) that is stronger evidence than a picture, but AGENTS.md asks for images on UI changes and I could not produce them.
  2. The tasks badge, stash badge and drawer states were not exercised. They need a thread with an active plan or stashed prompts, and the seeded threads' projects do not resolve in an isolated environment. So the onDismiss wiring, delegated-agent counts and inline-vs-tab stash placement are verified by unit tests and code reading, not by clicking them. Worth a look on a live thread before merge.

@rynfar
rynfar merged commit a8e59e4 into pylonAug 31, 2026
19 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-composer-unification branch August 31, 2026 22:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXLvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@rynfar@t3dotgg@juliusmarminge