diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 9320100fa..f7e4983b0 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -35,6 +35,7 @@ Historical groups are indexed in the linked archive. This file migration changes | Group / bounded head | Sources | Outcome and remaining scope | Pylon PR / verification | | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Composer banner density and scroll-pill stability / `b44c1ce5d25ee0d5a5be82e380618a886c19ea96` | `4749035bda13b4b6260499caedbc0d69a2f60e6f` (#12166), `fcfd9f911a27ee3b813d64582c53d82dfa2e892b` (#12317) | Adopted both. Banner stack items gain an optional `compact` flag: compact rows keep the narrow layout and the description that collapses into an info popover below 400px, while other banners get room for the description beside the title, with a labelled, bounded, scrollable popover. The scroll-to-end clearance is split out of the overlay-height publisher and recomputed from a stored height when the pill mounts, so a fling during the composer's resting tween no longer publishes a stale height and yanks the scroll position. **Excluded:** upstream's `approval` banner layout classes — Pylon's `ComposerBanner.Row` union is `inline | wrap-actions | wrap-actions-narrow`and the approval layout arrives in an unadopted commit; only this source's`justify-end`change was applied. Pylon adaptations:`ComposerTasksBadge`keeps its delegates label, expanded guard and`TaskProgressSegments fit`, with the delegates label moved to the same container query as the segment bar so the pair no longer flips at different widths; `ChatView`'s pull-request resolution block is preserved; and five Pylon banners with short static descriptions (environment update, thread woke, snoozed/settled, resume-compaction, auto-balance) are marked `compact` so they do not grow a permanently redundant details popover. Cursor unchanged. | [Chat rendering #668](https://github.com/pylon-code/pylon/pull/668); independent adversarial review, 184 focused tests including new `compact` coverage, `@t3tools/web` typecheck, scoped lint. Two upstream-inherited defects recorded in the PR rather than fixed here. | | Checkpoint capture reliability and shared-workspace rewind / `93e04160a0c0dece8a258384d2118a660415a53d` | `869347bc26051bba7c3c0274a7dfcae7770d3e3a` (#11665), `1455cb5c35e50211145073be6ca155d56684f0f4` (#12181), `67993623afb14c017bb0b959478dc892f9c427da` (#12307), `b4620d595554654c259ec7c0afb3d3d5532959a8` (#12306), `d17f46d7631b178b6834596264f0f44f210d584b` (#12308) | Adopted all five. Transient git exits retry during capture behind a new optional `retryable` hint on `VcsProcessExitError`; capture recovers nested repositories without commits under index-lock recovery and bounded probe timeouts; a failed baseline lookup logs and treats the ref as absent instead of losing the checkpoint; the `@`-mention entry refresh moves to a coalescing drainable worker chained into `CheckpointReactor.drain`. File rewind is refused unless the thread's cwd is its own worktree with no other thread or live provider session nested inside or above it, and web hides the destructive control for shared directories. **Capability removal, maintainer-approved 2026-09-18**: threads running in the project directory rewind the conversation only, with no override path. Pylon adaptation: the isolation check is scoped with `restoreFiles !== false` so conversation-only rewind still reaches Pylon's existing provider-capability explanation rather than the isolation message or silence, since Pylon's rollback saga services are optional. Pylon git identity, rollback revert action and wire compatibility preserved. No excluded source behavior; cursor unchanged. | [Checkpoint reliability #665](https://github.com/pylon-code/pylon/pull/665); 122 focused tests including three new `restoreFiles` regressions where none existed, `t3`/`contracts`/`web` typechecks, scoped lint (0 errors), rebased onto `origin/pylon` after #653. | | Provider event log bounds and Codex image attachments / `93e04160a0c0dece8a258384d2118a660415a53d` | `b17cc2ab5d5029f898121798ca19db3288cba616` (#12305, partial), `3fd21df62da3191f1abf12a0cd74df6de9ed30a2` (#11050) | `3fd21df62d` adopted: Codex image attachments pass as `{ type: "localImage", path }` instead of base64 data URLs, so `turn/start` no longer scales with file size. `localImage` is a real content type in the generated Codex app-server schema; Pylon's attachment-id validation against `attachmentsDir` and its `FileSystem` wiring are retained. `b17cc2ab5d` **partially adopted**: record traversal is bounded before serialization with character, field and depth budgets, cycle detection, a `summarizeProviderEvent` fallback that keeps routing and failure fields, and a post-serialization byte-length fallback; new transient suppressions for `turn/diff/updated` and `stream_event`/`content_block_delta`. Pylon's `commitGuard` and existing transient filters preserved. **Excluded: the `stage === "raw"` suppression and `stage === "decoded"` unwrap** (see EXC-1). Cursor unchanged. | [Provider payload bounds #653](https://github.com/pylon-code/pylon/pull/653), merged `4be539f4d48af6ffbe545e015ec246fbb271c92e`; 133 focused tests, `t3` typecheck, scoped format/lint, all final-head CI jobs green, no bot findings. | | PR comments, controls and avatars / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `f0a0ead946ca3f3f310460a91b7dabb0e3fe4834` (#11962), `f45c6b4a755905485431872f4a4188de3ed05974` (#11994), `c12ba7581ac00ba2a623ad23283da5d03d34c2d8` (#12150), `962bf62921c0ecbc9320f9b071e16b1d41484aad` (#12125), `df466c79810046d20c0f3ccc4e51709c5703814e` (#11728) | Adopted all five. Narrow controls, keyboard submission, paged bot/finished groups, long-comment previews, detail avatars and image-failure fallback. Pylon corrections align bot app links, lazily page finished groups, and scroll after collapsing. Optional actor metadata preserves wire compatibility; existing bounded GitHub enrichment/fallback limits remain. No excluded source behavior; cursor unchanged. | [PR review #621](https://github.com/pylon-code/pylon/pull/621); focused regressions, scoped types/lint, independent Opus adversarial reviews, browser before/after evidence and comment interaction recording. | @@ -156,7 +157,11 @@ All three sources in the range `d1d15c67f4a5fb82fd8d5e01e5e3b288296789c3..a43f9b ## Deferred register -No open functional deferrals remain in this register; one open exclusion, EXC-1, is recorded below with its revisit trigger. DEF-7 and DEF-16 are completed above; remaining exact native rollback compatibility limits are documented above. Revisit a limit when the native provider exposes the missing proof or fork behavior; do not substitute relative counts. Historical reasons and revisit conditions remain in the archive and earlier cycle records. +One open functional deferral, DEF-18, and one open exclusion, EXC-1, are recorded below with their revisit triggers. DEF-7 and DEF-16 are completed above; remaining exact native rollback compatibility limits are documented above. Revisit a limit when the native provider exposes the missing proof or fork behavior; do not substitute relative counts. Historical reasons and revisit conditions remain in the archive and earlier cycle records. + +| Deferral | Sources | Remaining behavior and reason | Opened | Revisit trigger | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DEF-18: activity-group timeline refinements | `592021f008010ee947c326df0452d093ead8aeb1` (#12302), `fbc8b960026e408c163ea55db8075bbf1a42d628` (#12270) | Both refine upstream's activity-group timeline: thoughts collapse inside a tool group behind their own disclosure, and a failed tool call stays in the same activity row instead of ending the live run. Neither is portable — Pylon has no `ActivityGroupTimelineRow`, `ReasoningTraceBlock`, `expandedReasoningMessageIds`, `onToggleReasoning`, `isActivityEntry` or `reasoningMessages`, and `packages/contracts/src/orchestration.ts` has no reasoning fields at all. Pylon's `ThinkingTimelineRow` is a transient indicator, not persisted provider reasoning. Adopting them first requires `052c7ae53e46c705d5fd5dfa995275bc3100744e` (#11784, 32 files / 1,923 insertions, including contracts, client-runtime and a settings toggle) and `22539f2a428947ab218589c984b2af44e434012e` (#12147, web + mobile) — a new full-stack capability, not a catch-up adaptation. **Parked behind the orchestration upgrade at the maintainer's decision on 2026-09-19**: orchestration v2 is unmerged on `t3code-upstream/t3code/codex-turn-mapping` (442 commits off `main`, last touched 2026-09-12) and will rewrite orchestration contracts, so main-line reasoning fields adopted now risk rework. There is no orchestration v3 upstream. | 2026-09-19 | Pylon resolves the orchestration v2 adoption question, or that line merges upstream: `git merge-base --is-ancestor t3code-upstream/t3code/codex-turn-mapping t3code-upstream/main`. Also triggered if Pylon adopts the activity-group timeline independently — check for `ActivityGroupTimelineRow` in `apps/web/src/components/chat/MessagesTimeline.tsx`. Dependency-driven, not time-driven; no earliest date applies. | | Exclusion | Source | Excluded behavior and reason | Opened | Revisit trigger | | ------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7a701d7d0..fdd3b3ec5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1879,6 +1879,8 @@ export default function ChatView(props: ChatViewProps) { const [composerTimelineInset, setComposerTimelineInset] = useState(0); const composerTimelineInsetRef = useRef(0); const composerRestingRef = useRef(false); + // The last overlay height the composer published for its settled layout. + const composerOverlayHeightRef = useRef(0); const [scrollToEndClearance, setScrollToEndClearance] = useState(0); const isAtEndRef = useRef(true); const isTimelineAtLogicalEnd = useCallback( @@ -2295,6 +2297,7 @@ export default function ChatView(props: ChatViewProps) { return { id: `project-clone:${projectId}`, variant: "info", + compact: true, priority: "activity", icon: , title: `Cloning ${name}`, @@ -2318,6 +2321,7 @@ export default function ChatView(props: ChatViewProps) { return { id: `project-clone:${projectId}`, variant: cancelled ? "warning" : "error", + compact: true, icon: , title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`, description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error, @@ -2781,6 +2785,7 @@ export default function ChatView(props: ChatViewProps) { ), title: `${unavailableConnection.phase === "connecting" ? "Connecting" : "Reconnecting"} to ${activeEnvironmentUnavailableState.label}`, description: "Finishing an update", + compact: true, }); } else { items.push({ @@ -6070,19 +6075,8 @@ export default function ChatView(props: ChatViewProps) { [activeComposerPlan?.turnId, activeComposerTaskSteps, agentSessionLive, threadActivities], ); - const publishComposerOverlayHeight = useCallback( - (height: number) => { - const nextHeight = Math.ceil(height); - if (nextHeight <= 0) return; - const nextInset = resolveComposerTimelineInset({ - currentInset: composerTimelineInsetRef.current, - overlayHeight: nextHeight, - isResting: composerRestingRef.current, - }); - if (composerTimelineInsetRef.current !== nextInset) { - composerTimelineInsetRef.current = nextInset; - setComposerTimelineInset(nextInset); - } + const publishScrollToEndClearance = useCallback( + (overlayHeight: number) => { const mainSurface = composerOverlayElement?.querySelector( '[data-chat-composer-main-surface="true"]', ); @@ -6092,7 +6086,7 @@ export default function ChatView(props: ChatViewProps) { const clearance = composerOverlayElement && mainSurface && button ? resolveScrollToEndClearance({ - overlayHeight: nextHeight, + overlayHeight, mainSurfaceTop: mainSurface.getBoundingClientRect().top, button: button.getBoundingClientRect(), attachments: Array.from( @@ -6102,11 +6096,29 @@ export default function ChatView(props: ChatViewProps) { (element) => element.getBoundingClientRect(), ), }) - : nextHeight; + : overlayHeight; setScrollToEndClearance(clearance); }, [composerOverlayElement], ); + const publishComposerOverlayHeight = useCallback( + (height: number) => { + const nextHeight = Math.ceil(height); + if (nextHeight <= 0) return; + composerOverlayHeightRef.current = nextHeight; + const nextInset = resolveComposerTimelineInset({ + currentInset: composerTimelineInsetRef.current, + overlayHeight: nextHeight, + isResting: composerRestingRef.current, + }); + if (composerTimelineInsetRef.current !== nextInset) { + composerTimelineInsetRef.current = nextInset; + setComposerTimelineInset(nextInset); + } + publishScrollToEndClearance(nextHeight); + }, + [publishScrollToEndClearance], + ); // The composer reports its resting flag from a layout effect, which runs // before this component's own layout effects and before any resize // observation, so every measurement below sees the flag for its layout. @@ -6139,7 +6151,17 @@ export default function ChatView(props: ChatViewProps) { return () => { resizeObserver.disconnect(); }; - }, [composerOverlayElement, publishComposerOverlayHeight, showScrollToBottom]); + }, [composerOverlayElement, publishComposerOverlayHeight]); + // The pill mounts and unmounts in the same commits that expand or rest the + // composer, and a fast fling lands there while the previous resting tween + // still pins the overlay at its old height. Measuring the overlay here would + // publish that stale height against the new resting flag, drop the timeline + // reservation, and yank the scroll position. The pill only needs its + // clearance, so it reuses the height the composer last published. + useLayoutEffect(() => { + if (!composerOverlayElement) return; + publishScrollToEndClearance(composerOverlayHeightRef.current); + }, [composerOverlayElement, publishScrollToEndClearance, showScrollToBottom]); const linkedPullRequestStatus = useLinkedThreadPullRequest( activeThreadRef?.environmentId ?? null, activeThreadMetadata?.linkedPullRequest, @@ -6655,6 +6677,7 @@ export default function ChatView(props: ChatViewProps) { icon: , title: "Thread woke from snooze", description: "Send a message to continue", + compact: true, dismissLabel: "Dismiss Woke notification", onDismiss: acknowledgeActiveThreadWoke, }; @@ -6670,6 +6693,7 @@ export default function ChatView(props: ChatViewProps) { icon: isSnoozed ? : , title: `This thread is ${isSnoozed ? "snoozed" : "settled"}`, description: `Send a message to ${isSnoozed ? "wake" : "unsettle"}`, + compact: true, actions: ( - ); - })} - - + + ); + return ( + + ); + })} + + + ); diff --git a/apps/web/src/components/chat/ComposerTasksBadge.tsx b/apps/web/src/components/chat/ComposerTasksBadge.tsx index 494415c2b..e0014f363 100644 --- a/apps/web/src/components/chat/ComposerTasksBadge.tsx +++ b/apps/web/src/components/chat/ComposerTasksBadge.tsx @@ -159,7 +159,7 @@ function TaskSummary({ {delegatesLabel ? ( @@ -168,7 +168,7 @@ function TaskSummary({ ) : null} {/* The expanded list already shows every step; the bar would repeat it. */} {expanded ? null : ( - + )} diff --git a/apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx b/apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx index 0d4584533..b91a0eebb 100644 --- a/apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx +++ b/apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx @@ -127,6 +127,8 @@ export function useAutoBalanceUpdateBanner( ), description: manual > 0 ? `${manual} ${manual === 1 ? "needs" : "need"} a manual update` : undefined, + // Short and static: it never truncates, so it needs no details popover. + compact: true, actions: running === 0 && targets.length > 0 ? (