feat(mobile): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar
, '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): pick, share, and receive files in threads - #231

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files
Sep 1, 2026
Merged

feat(mobile): pick, share, and receive files in threads#231
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-31-mobile-files

Conversation

@rynfar

@rynfarrynfar commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Third of the mobile batch. Adopted from upstream pingdotgg/t3code#8237 (86c9a9288). Follows #223 and #224.

Mobile gains file attachments: the composer's + offers Files when the server supports uploads, the system share sheet can send a file into Pylon, and received files can be saved or opened elsewhere. Brings a new features/sharing surface, upload/download helpers, and outbox support.

Port fidelity

41 of the 43 changed source files apply upstream's diff line-for-line. The two that differ are the two resolved by hand, and I diffed the diffs to confirm the deltas are only what was intended.

7 files conflicted. The shape is not obvious, so it's worth recording:

Upstream landed this at 13:35 the same day it landed #8793 at 12:22, so #8237's tree already contains #8793's composer restructure. Every ThreadComposer conflict here was that restructure re-arriving as context, not the attachment feature. Pylon declined it in #223 to keep ComposerToolbarScroller and the thirteen toolbar controls that need it, so it is declined again — useLayoutEffect (used only by the ComposerSurface radius animation) and the collapsed-row wrapper are not taken.

Upstream's conflict side also carried copies of Pylon's attachment strip and stop/send blocks. Pylon's own copies sit below the conflict as context and include ContextWindowIndicator, which upstream has no equivalent of — taking that side would have duplicated all three. The file-glyph rendering is ported into Pylon's block instead.

Review confirmed no Pylon features were lost: occurrence counts match origin/pylon exactly for ComposerToolbarScroller (10), ContextWindowIndicator (3), QuickQuestionTrigger (6), sessionGoal (14), renderCodexDirectivesForCopy (2), and all 12 ComposerToolbarButton instances. No duplication. Pylon identity intact across all three app variants.

A real bug this PR introduced and then fixed

Worth calling out because the first commit got it wrong.

onQueueFollowUp originally filtered files out, described as a queue limitation. It isn't one: Pylon's follow-up command already takes the same attachment union as thread.turn.start, its own contract comment says the server normalizer branches on "dataUrl" in attachment for both, and web passes identical attachments to followUpInputQueue and startTurn. The filter was a fix for a type error dressed as a design decision.

The consequence was silent data loss on an ordinary path — the composer's send button routes to handleQueueFollowUp whenever a session is running. Attaching a file mid-turn and pressing send filtered it from the payload, and because clearComposerDraftContent ran without deferAttachmentCleanup, the sweep then deleted the local copy. Composer cleared, message queued without the file, no warning, bytes gone.

Now mirrors onSendMessage: enforce the cap, validate against the server's advertised limits, upload via prepareTurnAttachments, defer cleanup until the queue call succeeds, release minted uploads and restore with allowOverflow on failure. This also makes the docs sentence "queued messages keep their files until they deliver" true, which it wasn't.

Pylon-first details

The four file glyphs use tintColorClassName="accent-icon-subtle" rather than upstream's hardcoded #a3a3a3. A first attempt subscribed to the theme directly and tripped no-mobile-uniwind-theme-escape-hatchesAppSymbol exists precisely so callers don't do that.

docs/user/composer.md was hand-merged to keep Pylon's voice and branding; upstream's text said "T3 Code".

Verification

@t3tools/mobile typecheck clean, lint clean, 1021 tests passing across 141 files (including 15 new test files this brings).

Not yet run on a simulator. The share-sheet path in particular wants a device pass.

Filed, not fixed

Three upstream design issues found in review, filed rather than folded in to keep this to one concern: #228 (share inbox has no cap, expiry, or way out), #229 (mixed image+file share skips the image size gate), #230 (one corrupt share file permanently disables attachment cleanup).

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.

Adopted from upstream `pingdotgg/t3code#8237` (`86c9a9288`).
Mobile gains file attachments: the composer's + control adds Files when the
server supports uploads, the system share sheet can send a file into Pylon,
and received files can be saved or opened elsewhere. Brings a new
`features/sharing` surface, upload/download helpers, and outbox support.
Conflict resolution notes, because the shape is not obvious:
Upstream landed this at 13:35 on the same day it landed #8793 at 12:22, so
#8237's tree already contains #8793's composer restructure. Every
ThreadComposer conflict here was that restructure re-arriving as context,
not the attachment feature. Pylon declined it in #223 to keep
ComposerToolbarScroller and the thirteen toolbar controls that need it, so
it is declined again: `useLayoutEffect` (only used by the ComposerSurface
radius animation) and the collapsed-row wrapper are not taken.
Upstream's conflict side also carried Pylon's attachment and send blocks
wholesale. Pylon's copies of those sit below the conflict as context and
include `ContextWindowIndicator`, which upstream has no equivalent of, so
taking that side would have duplicated all three. The file-glyph rendering
is ported into Pylon's own block instead, reading `--color-icon-subtle`
rather than upstream's hardcoded `#a3a3a3`.
`onQueueFollowUp` has no upload step, so it now filters to images, matching
what `projectThreadStartTurn` does when no upload has run. Queuing a
follow-up that carries a file is a known gap, recorded rather than papered
over with a cast.
41 of the 43 changed source files apply upstream's diff line-for-line; the
two that differ are the two resolved by hand above.
The previous commit filtered files out of onQueueFollowUp and called it a
queue limitation. That was wrong, and worse than it looked.
Pylon's follow-up command already takes the same attachment union as
thread.turn.start (orchestration.ts), its own comment says the server
normalizer branches on `"dataUrl" in attachment` for both, and web passes
identical attachments to followUpInputQueue and startTurn. Files were
always supported; the filter was a fix for a type error dressed up as a
design decision.
The consequence was silent data loss on an ordinary path, not an edge case:
the composer's send button routes to handleQueueFollowUp whenever a session
is running, so attaching a file mid-turn and pressing send filtered it out
of the payload, and because clearComposerDraftContent ran without
deferAttachmentCleanup the sweep then deleted the local copy. Composer
cleared, message queued without the file, no warning, bytes gone.
Now mirrors onSendMessage and use-project-actions: enforce the attachment
cap, validate against the server's advertised limits, upload through
prepareTurnAttachments, defer cleanup until the queue call succeeds, release
minted uploads and restore with allowOverflow on failure.
Also switches the four file glyphs to `tintColorClassName` rather than
upstream's hardcoded #a3a3a3. An earlier attempt subscribed to the theme
directly and tripped no-mobile-uniwind-theme-escape-hatches; AppSymbol
exists precisely so callers do not do that.
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.6 KiB+45 B (+0.3%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+4 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+41 B (+0.6%)7.8 KiB
CodexLive turn WebSocket decoded57.1 KiB57.2 KiB+44 B (+0.1%)66.4 KiB
CodexLive turn messages910+1 (+11.1%)21
ClaudeTotal thread wire13.5 KiB13.6 KiB+36 B (+0.3%)15.1 KiB
ClaudeThread snapshot wire7.0 KiB6.9 KiB−10 B (−0.1%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.6 KiB+46 B (+0.7%)7.8 KiB
ClaudeLive turn WebSocket decoded58.0 KiB58.1 KiB+44 B (+0.1%)66.4 KiB
ClaudeLive turn messages910+1 (+11.1%)21

Baseline: 9d82e68 · PR result: 8221250 · 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.

@rynfar
rynfar merged commit c908bb2 into pylonSep 1, 2026
18 checks passed
@rynfar
rynfar deleted the upstream/2026-08-31-mobile-files branch September 1, 2026 06:55
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.

1 participant

@rynfar