From de6c5c1828ad3fab5b0e8536ed1dd1f9b8b4ff8f Mon Sep 17 00:00:00 2001 From: Bilal Bakr <62337003+Bil0000@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:42:22 +0300 Subject: [PATCH 1/4] fix(web): keep composer banners compact and readable (#12166) (cherry picked from commit 4749035bda13b4b6260499caedbc0d69a2f60e6f) --- apps/web/src/components/ChatView.tsx | 2 + .../src/components/chat/ComposerBanner.tsx | 2 +- .../components/chat/ComposerBannerStack.tsx | 35 ++--- .../chat/ComposerPendingUserInputPanel.tsx | 122 +++++++++--------- .../components/chat/ComposerTasksBadge.tsx | 2 +- 5 files changed, 87 insertions(+), 76 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7a701d7d01..dea624fb6a 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2295,6 +2295,7 @@ export default function ChatView(props: ChatViewProps) { return { id: `project-clone:${projectId}`, variant: "info", + compact: true, priority: "activity", icon: , title: `Cloning ${name}`, @@ -2318,6 +2319,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, diff --git a/apps/web/src/components/chat/ComposerBanner.tsx b/apps/web/src/components/chat/ComposerBanner.tsx index fee3ff75a6..6aeff5e0b3 100644 --- a/apps/web/src/components/chat/ComposerBanner.tsx +++ b/apps/web/src/components/chat/ComposerBanner.tsx @@ -252,7 +252,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-actions" className={cn( "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", - "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start", + "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-end", "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start", className, )} diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index c540613b00..b47790b960 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -13,6 +13,7 @@ export interface ComposerBannerStackItem { readonly id: string; readonly variant: ComposerBannerVariant; readonly priority?: "urgent" | "activity" | "notice"; + readonly compact?: boolean; readonly icon: ReactNode; readonly title: ReactNode; readonly description?: ReactNode; @@ -270,22 +271,20 @@ function ComposerBannerStackAlert({ variant={item.variant} density="comfortable" > - + {item.icon} - - {item.title} - + {item.title} {item.description ? ( - <> - + + {item.description} @@ -296,21 +295,27 @@ function ComposerBannerStackAlert({ size="icon-xs" variant="ghost" aria-label="Show notice details" - className="hidden flex-none text-muted-foreground hover:text-foreground @max-[400px]:inline-flex" + className={cn( + "flex-none text-muted-foreground hover:text-foreground", + item.compact && "hidden @max-[400px]:inline-flex", + )} /> } > - {item.description} + + {item.description} + - + ) : null} {item.actions || item.onDismiss ? ( diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index e93877db77..5d1adea44e 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -222,66 +222,70 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( ) : null} - -

{activeQuestion.question}

- {activeQuestion.multiSelect ? ( -

Select one or more options.

- ) : null} -
- {activeQuestion.options.map((option, index) => { - const optionValue = option.value ?? option.label; - const isOptimisticallySelected = - optimisticSingleSelect?.questionId === activeQuestion.id && - optimisticSingleSelect.optionValue === optionValue; - const isSelected = - isOptimisticallySelected || - (!customAnswerActive && progress.selectedOptionValues.includes(optionValue)); - const shortcutKey = index < 9 ? index + 1 : null; - const className = cn( - "group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left outline-none transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-primary/25", - isSelected - ? "bg-muted/55 text-foreground" - : "bg-transparent text-foreground/85 hover:bg-muted/30", - isResponding && "opacity-50 cursor-not-allowed", - !isResponding && "cursor-pointer", - ); - const content = ( - <> -
- {option.label} - {option.description && option.description !== option.label ? ( - {option.description} + + +

{activeQuestion.question}

+ {activeQuestion.multiSelect ? ( +

Select one or more options.

+ ) : null} +
+ {activeQuestion.options.map((option, index) => { + const optionValue = option.value ?? option.label; + const isOptimisticallySelected = + optimisticSingleSelect?.questionId === activeQuestion.id && + optimisticSingleSelect.optionValue === optionValue; + const isSelected = + isOptimisticallySelected || + (!customAnswerActive && progress.selectedOptionValues.includes(optionValue)); + const shortcutKey = index < 9 ? index + 1 : null; + const className = cn( + "group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left outline-none transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-primary/25", + isSelected + ? "bg-muted/55 text-foreground" + : "bg-transparent text-foreground/85 hover:bg-muted/30", + isResponding && "opacity-50 cursor-not-allowed", + !isResponding && "cursor-pointer", + ); + const content = ( + <> +
+ {option.label} + {option.description && option.description !== option.label ? ( + + {option.description} + + ) : null} +
+ {isSelected ? ( + + ) : shortcutKey !== null ? ( + + {shortcutKey} + ) : null} -
- {isSelected ? ( - - ) : shortcutKey !== null ? ( - - {shortcutKey} - - ) : null} - - ); - return ( - - ); - })} -
- + + ); + return ( + + ); + })} +
+
+
); diff --git a/apps/web/src/components/chat/ComposerTasksBadge.tsx b/apps/web/src/components/chat/ComposerTasksBadge.tsx index 494415c2bf..9baba84969 100644 --- a/apps/web/src/components/chat/ComposerTasksBadge.tsx +++ b/apps/web/src/components/chat/ComposerTasksBadge.tsx @@ -168,7 +168,7 @@ function TaskSummary({ ) : null} {/* The expanded list already shows every step; the bar would repeat it. */} {expanded ? null : ( - + )} From 5a7bfdb0a806ff68c080ad0f97344ad2cf04ada2 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Thu, 17 Sep 2026 18:47:13 -0500 Subject: [PATCH 2/4] fix(web): keep chat from jumping when the scroll-to-end pill mounts (#12317) (cherry picked from commit fcfd9f911a27ee3b813d64582c53d82dfa2e892b) --- apps/web/src/components/ChatView.tsx | 51 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index dea624fb6a..caf2798793 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( @@ -6072,19 +6074,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"]', ); @@ -6094,7 +6085,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( @@ -6104,11 +6095,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. @@ -6141,7 +6150,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, From 6c1a4b25b72b9867064550fad5a74ffcec959eb7 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Sat, 19 Sep 2026 11:29:27 -0600 Subject: [PATCH 3/4] docs: park the activity-group timeline refinements behind orchestration v2 Records DEF-18 for upstream #12302 and #12270. Neither refines anything Pylon has: there is no ActivityGroupTimelineRow, ReasoningTraceBlock or reasoning field in contracts, so adopting them first means adopting #11784 and #12147, a new full-stack capability. Orchestration v2 is unmerged on t3code/codex-turn-mapping and will rewrite orchestration contracts, so the maintainer parked this behind that decision. --- .agents/upstream-review.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 9320100fa0..10eeb8e894 100644 --- a/.agents/upstream-review.md +++ b/.agents/upstream-review.md @@ -156,7 +156,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 | | ------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | From 979cd5dd1f5ec316ca130bc9880f035e568c9bd3 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Sat, 19 Sep 2026 12:03:37 -0600 Subject: [PATCH 4/4] fix(web): keep short composer banners compact and cover the flag Adversarial review found that upstream's non-compact layout renders a permanently visible details popover next to descriptions that can never truncate. Five Pylon banners qualify: environment update, thread woke, snoozed/settled, resume-compaction and auto-balance. Marks them compact, aligns the delegates label with the segment bar's container query, and adds the ComposerBannerStack coverage the flag shipped without. --- .agents/upstream-review.md | 1 + apps/web/src/components/ChatView.tsx | 4 +++ .../chat/ComposerBannerStack.test.tsx | 34 +++++++++++++++++++ .../components/chat/ComposerTasksBadge.tsx | 2 +- .../chat/useAutoBalanceUpdateBanner.tsx | 2 ++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.agents/upstream-review.md b/.agents/upstream-review.md index 10eeb8e894..f7e4983b0a 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. | diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index caf2798793..fdd3b3ec5a 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2785,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({ @@ -6676,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, }; @@ -6691,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: (