feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@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

feat(mobile): update tool summaries and chat transitions - #223

Merged
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log
Sep 1, 2026
Merged

feat(mobile): update tool summaries and chat transitions#223
rynfar merged 4 commits into
pylonfrom
upstream/2026-08-31-mobile-work-log

Conversation

@rynfar

@rynfarrynfar commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

First of the mobile batch. Adopted from upstream pingdotgg/t3code#8793 (f15680bd3).

What this buys

Partial convergence. Two modules move into
packages/client-runtime/src/work-log/, but only one of them actually
converges anything, and an earlier draft of this description overstated it.

command-label genuinely converges: web's MessagesTimeline deletes its own
commandProgramName and the shell tokenizer behind it (tokenizeShellCommand,
COMMAND_WRAPPER_FLAGS, isWindowsDrivePath, and friends) and imports the
shared one. Two copies become one, verified byte-identical to what was deleted.

presentation does not. Mobile imports it; web still owns verbatim copies of
summarizeToolGroup, toolGroupAction, normalizeCompactToolLabel,
omitSupersededLifecycleMarkers and the rest in MessagesTimeline.logic.ts.
Nothing was deleted on the web side, so that module is currently a third copy.
Upstream has the same gap. Collapsing it is follow-up work, not this PR.

It also brings the thread feed's disclosure enter/exit transitions and extracts
FloatingWorkingControl, which C-21 (#8862, Expo glass) needs — that PR
modifies the file this one creates.

What Pylon does not take

Upstream's composer toolbar restructure. Pylon's mobile composer is a
deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller; upstream's new View nesting
cannot hold them without losing behaviour. Pylon's ThreadComposer is kept whole
and now exports its layout-transition constants for the detail screen. The two
upstream-only couplings that arrived with it — a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop — go with it,
since both exist only to drive upstream's composer animation.

Its failure glyph.thread-work-log keeps Pylon's amber-warning / rose-error
split rather than upstream's collapse of both into destructive, and declines the
xmark swap — taking it would move mobile in the opposite direction from the
circle-alert decision web just made. Upstream's live shimmer and grouped-detail
icon gating are taken.

Restored after the cherry-pick dropped them silently

Taking upstream's ThreadDetailScreen removed a set of Pylon surfaces through
non-conflicting hunks, so no conflict marker and no type error flagged them.
Found by diffing symbol counts against origin/pylon:

  • both SessionPresentationSurface blocks (above- and below-editor widgets)
  • PendingSessionInteractionCard and the third activePendingInteraction branch
  • sessionInputBlocked on the composer
  • the hasBelowEditorWidgets ? 0 : composerBottomInset gate, without which the
    composer double-pads when below-editor widgets are present
  • 23 composer props — the whole ThreadComposer call site had been replaced
    by upstream's much shorter one

This is the same failure mode that bit #221, so I audited for it deliberately
rather than trusting the conflict list.

Verified: @t3tools/mobile, @t3tools/web and @t3tools/client-runtime
typecheck clean; vp lint clean; 390 tests passing across
apps/mobile/src/lib and apps/mobile/src/features/threads, plus web's
MessagesTimeline (43) unaffected.

Review fixes (second commit)

Adversarial review found two Pylon features the merge dropped and four defects
inherited from upstream. All are fixed in c4e286be0; see that commit message
for the full accounting. The short version:

  • the floating pill kept counting "Working for Xs" during a Pylon session
    interaction, because the new gate only knew upstream's two pending kinds;
  • failed tool calls lost all visual failure signal — the port took upstream's
    removal of Pylon's status column but declined its replacement glyph;
  • the terminal-response notice got regrouped into a "show 1 tool call" toggle;
  • at the active tail a finished activity outranked the still-running one, so the
    row named a command that had already ended and shimmer stayed off;
  • all-non-tool groups of 2+ collapsed to "Received N updates", discarding the
    only text a runtime warning carries (web documents the opposite rule);
  • the shared workLogEntryIsToolLike dropped web's missing-response guard.

The two upstream behaviour fixes ship with regression tests that were confirmed
to fail without them. One pre-existing test assertion here was tautological —
JSON.stringify(entry) contains the message via workEntry.label no matter
what the row displays — and is replaced with the precise check it had before.

It also completes a half-taken coupling: COMPOSER_TRANSITION_DURATION_MS was
exported with no consumer while the adopted patch gated its animation on
animationDuration > 0, so both the export and the patch parameter were dead.
The constant now drives the composer's own transition and is passed through to
useKeyboardChatComposerInset.

Vendored patch fix

doMaintainScrollAtEnd in the adopted @legendapp/list patch hard-gates on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on the
way out, dropping a queued end-follow instead of deferring it — where the
pendingNativeMVCPAdjust branch two lines below carries the request forward.
Nothing re-triggers afterwards, and eligibility re-arms on every content-size
bump while an animation runs, so scrolling up mid-stream and then tapping
scroll-to-end could stop the feed following the agent for the rest of the burst.

Both the epoch machinery and the itemLayoutAnimation prop that arms it are new
here, so the exposure is new. Changed to defer; the consumers at
react-native.js:1840 and :2141 re-check isWithinMaintainScrollAtEndThreshold,
so a deferred follow cannot fire once the user has scrolled away. Applied to both
CJS and ESM builds with lockfile hashes recomputed.

Mechanism confirmed by reading the applied bundle; not reproduced, since that
needs a live streaming turn.

Simulator pass

Run on an iPhone 17 Pro against an isolated backend seeded read-only from
~/.pylon-code via VACUUM INTO. Verified: the work-log disclosure expands
with correct Ran N command(s) grouping and expanded state; Pylon's composer
toolbar is intact and still horizontally scrollable; the feed's bottom inset
clears the composer with no clipping and no phantom gap in both collapsed and
expanded states; and the working-timer branch renders with content correctly
cleared, including the timer/scroll-to-end separation animation. The working
branch needs a live turn, so it was exercised by temporarily forcing
showWorkingControl on and then reverting — no such change is in the diff.

Not covered: Android, and a real provider turn.

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.

Adapted for Pylon.
Adopted: the work-log presentation and command-label logic move into
client-runtime, which is genuine convergence - web's MessagesTimeline drops its
local commandProgramName and the shell tokenizer behind it in favour of the
shared module. Also adopted are the thread feed's disclosure enter/exit
transitions and the FloatingWorkingControl extraction, which C-21 builds on.
Not adopted: upstream's composer toolbar restructure. Pylon's mobile composer is
a deliberate fork carrying QuickQuestionTrigger, ContextWindowIndicator, the
follow-up queue pill and ComposerToolbarScroller, and upstream's new View
nesting cannot hold them without losing behaviour. Pylon's ThreadComposer is
kept whole and now exports its layout transition constants for the detail
screen. The two upstream-only couplings that came with it - a fifth argument to
useKeyboardChatComposerInset and a queueCount composer prop - are dropped with
it, since both exist only to drive upstream's composer animation.
Restored after the cherry-pick dropped them through non-conflicting hunks: the
session-interaction surfaces on ThreadDetailScreen. Taking upstream's version
silently removed the above- and below-editor SessionPresentationSurface blocks,
PendingSessionInteractionCard, the third pending-interaction condition,
sessionInputBlocked, the below-widget bottom-inset gate, and 23 composer props.
thread-work-log keeps Pylon's amber-warning / rose-error split rather than
upstream's collapse of both into destructive, and does not adopt its xmark
failure glyph - that would move mobile opposite to the circle-alert decision
web already made. Upstream's live shimmer and grouped-detail icon gating are
taken.
@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.6 KiB13.6 KiB−1 B (−0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB0 B (0.0%)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.6 KiB−13 B (−0.1%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB0 B (0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB−13 B (−0.2%)7.8 KiB
ClaudeLive turn WebSocket decoded58.1 KiB58.1 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: a8e59e4 · PR result: 9257858 · 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 of the port turned up two Pylon features the merge
dropped and four defects inherited from upstream's own implementation.
Pylon regressions:
- `showWorkingControl` gated on upstream's two pending kinds only, so the
floating pill kept counting "Working for Xs" while a Pylon session
interaction waited on the user, and kept 52px of coverage in the feed
inset. The third condition was restored in the JSX but not in the gate.
- Upstream removed the trailing status column and moved failure onto an
xmark glyph; the port took the removal and declined the replacement, so
failed tool calls rendered identically to successful ones for sighted
users. Failed rows now paint destructive, matching web's reserve of red
for failures and amber for warnings.
- The terminal-response notice was regrouped into a work toggle that
announces "show 1 tool call". Only the info-toned outcome was affected,
so one feature rendered as two different row species.
Upstream defects, fixed with regression tests that fail without them:
- At the active tail the finished activity won over the still-running one,
naming a command that had already ended and leaving shimmer off.
- All-non-tool groups of two or more collapsed to "Received N updates",
discarding the only text a runtime warning carries. Web documents the
opposite rule and follows it.
- The shared `workLogEntryIsToolLike` dropped web's missing-response guard,
so an error-toned notice summarised as "Used 1 tool".
Also completes the composer coupling: `COMPOSER_TRANSITION_DURATION_MS` was
exported with no consumer while the adopted patch gated its animation on
`animationDuration > 0`, leaving both dead. The constant now drives the
composer's own transition and is passed to `useKeyboardChatComposerInset`
so the feed inset glides with the card on iOS.
Upstream grew all three work-log elements together; the port took its
metrics for the live row and the group toggle but kept Pylon's denser
settled row. A tool call therefore shrank from text-sm/24px to
text-xs/18px the instant it completed, and the row wrapper carries a
layout transition, so the shrink animated. The live row now tracks the
settled row exactly.
Also corrects the pre-measurement comment, which still described the rows
as text-sm, and drops a leftover alias left behind by the deleted
visibleWorkLogActivities call.
The vendored @legendapp/list patch hard-gates doMaintainScrollAtEnd on
contentSizeAnimationActiveEpoch and clears pendingMaintainScrollAtEnd on
the way out, so a queued end-follow is dropped rather than deferred. The
branch two lines below, for pendingNativeMVCPAdjust, does the opposite and
carries the request forward.
Nothing re-triggers after the animation completes, and eligibility re-arms
on every content-size bump while an animation is running, so a user who
scrolls up mid-stream and then taps scroll-to-end can have the feed stop
following the agent for the rest of the burst. Both the epoch machinery
and the itemLayoutAnimation prop that arms it are new here, so the
exposure is new too.
Carry the request forward instead. The consumers at react-native.js:1840
and :2141 re-check isWithinMaintainScrollAtEndThreshold, so a deferred
follow cannot fire once the user has scrolled away. Applied to both the
CJS and ESM builds; lockfile hashes recomputed.
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.

2 participants

@rynfar@juliusmarminge