Skip to content

Commit 69430aa

Browse files
rynfarmaria-rcks
andauthored
feat(web): adopt the composer state drawers (DEF-5) (#63)
* feat(web): attach composer state drawers (#7150) (cherry picked from commit 792a1404f6d25838597f8f31b0af144dc1beee12) * feat(web): collapse tool activity into one line (#7152) (cherry picked from commit 4a9edff4c1093c1d31b7c97d6d1f32489ea035bd) Adopted with MessagesTimeline reconciled Pylon-first rather than taken wholesale: its divergence is only ~93 lines, so upstream's new `isExpandedToolGroupEntry` plumbing and `GroupContainer` are unioned with Pylon's response-status group label, session-notification row, and the failure/success/neutral indicators. The three-way merge also dropped `activeTurnInProgress` as collateral — upstream never had it, so its diff context silently removed Pylon's prop from ChatView, the props interface, the row activity context, and the shared test fixture. All four are restored; the success and neutral indicators depend on it. * feat(web): rebuild the composer on upstream's structure (DEF-5) Closes DEF-5. `#7150` rewrites 1084 lines of a file where Pylon diverged by 1460, and the previous attempt — merging upstream's diff into Pylon's tree block by block — produced 8 JSX errors because upstream flattens the fragment and provider-frame divs into one `<form>` while Pylon's layers cannot host its drawer tree. The two are ends of one restructure. This takes upstream's composer as the base and replays Pylon's divergence onto it with `git apply --3way`, which is the same reconciliation in the direction that actually converges: 8 conflicts instead of a hand-rebuild, and every feature that was not structural applied clean. Resolved Pylon-first where the two disagree: - The command-menu position keeps Pylon's settle loop from `ce5371d41` and takes upstream's drawer-aware measurement. Upstream schedules from a plain ResizeObserver on the ancestors, which that commit established does not fire while the composer glides into place — the bug it fixed and browser-verified at 2200px. Its measurement is kept; the loop still owns *when*. - `resolvedRuntimeMode` / `supportedRuntimeModes` over upstream's plain `runtimeMode`, at both the footer and the compact controls menu. - Quick question and session-resource controls, and Pylon's superset of props on ComposerPrimaryActions. - `ThreadHandoffTab` was lost with upstream's replaced form root and is restored; a symbol sweep against origin/pylon caught it. Upstream's structure wins where it is merely different: the flattened `<form>`, the drawer tree, the inline task and stash badges. `formatProviderSkillDisplayName` moves to the client-runtime path `#7150` introduced. Verified: typecheck clean across 7 packages, 370 tests over 26 composer and timeline suites, and a symbol-by-symbol sweep confirming all 20 Pylon composer features survive. * docs(upstream): retire DEF-5, composer drawers adopted * style(web): collapse the timeline activity dependency array * docs(upstream): format the DEF-5 ledger entry --------- Co-authored-by: maria <maria@kuuro.net>
1 parent cd2bf98 commit 69430aa

39 files changed

Lines changed: 4071 additions & 998 deletions

.agents/upstream-review.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,51 @@ Open questions raised by this batch:
19461946
- **`#7595` made the Past-24h empty state unreachable**: `hours` is always 24 entries, so
19471947
an idle day renders 24 `$0.00` rows instead of "No activity in this window."
19481948

1949+
## 2026-08-20 (DEF-5) — composer state drawers
1950+
1951+
**DEF-5 adopted and retired.** `#7150` and `#7152` are in, on branch
1952+
`upstream/def-5-composer`. The register is empty again.
1953+
1954+
The first attempt merged upstream's diff into Pylon's composer block by block and
1955+
compiled to 8 JSX errors: upstream flattens the fragment and provider-frame divs into
1956+
one `<form>`, and Pylon's layers cannot host its drawer tree. The two are ends of one
1957+
restructure, so the merge could not converge in that direction.
1958+
1959+
**What worked was reversing it.** Take upstream's composer as the base, then replay
1960+
Pylon's divergence onto it with `git apply --3way`. That is the same reconciliation
1961+
pointed the way that converges: 8 conflicts instead of a hand-rebuild of 1460 lines, and
1962+
every non-structural Pylon feature applied clean. The developer chose this direction
1963+
explicitly — maximum fidelity to upstream over minimum divergence.
1964+
1965+
Resolved Pylon-first where the two genuinely disagree:
1966+
1967+
| Conflict | Resolution |
1968+
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1969+
| Command-menu position | Pylon's settle loop from `ce5371d41` with upstream's drawer-aware measurement inside it. Upstream schedules from a plain ResizeObserver on the ancestors, which that commit established does not fire while the composer glides into place — the bug it fixed and browser-verified at 2200px. Measurement kept, scheduling not. |
1970+
| Runtime mode | `resolvedRuntimeMode` / `supportedRuntimeModes` over upstream's plain `runtimeMode`, at the footer and the compact controls menu. |
1971+
| Quick question, session resources, ComposerPrimaryActions props | Pylon's, which are supersets. |
1972+
| Form root, drawer tree, inline task/stash badges | Upstream's. |
1973+
1974+
**`ThreadHandoffTab` was lost and restored.** It lived in the form root upstream replaced,
1975+
so it survived as an unused import — invisible to typecheck. A symbol-by-symbol sweep of
1976+
20 Pylon composer features against `origin/pylon` caught it. **`#7152` also deleted
1977+
`activeTurnInProgress` as collateral** across ChatView, the props interface, the row
1978+
activity context, and the shared test fixture — upstream never had it, so its diff context
1979+
swept it up. Both are worth remembering: a green typecheck does not prove a three-way
1980+
merge preserved fork-only code, and the sweep is what does.
1981+
1982+
MessagesTimeline was reconciled rather than replaced — its divergence is only ~93 lines —
1983+
unioning upstream's `isExpandedToolGroupEntry` plumbing with Pylon's response-status label,
1984+
session-notification row, failure/success/neutral indicators, and its `DotMatrix` marker
1985+
(upstream's replacement is three staggered pulsing dots that repaint every vsync).
1986+
1987+
Validation: typecheck clean across 7 packages, 370 tests over 26 composer and timeline
1988+
suites, and a live pass confirming the composer renders with both upstream drawer hooks and
1989+
the command menu positions to upstream's exact drawer math (offset 22px = the inset, width
1990+
= form width less twice it). **Not verified in a browser:** the settle loop's dynamic
1991+
re-anchoring, because the composer could not be made to move in the test environment, and a
1992+
thread exercising `#7152`'s collapsed tool rows.
1993+
19491994
## Deferred register
19501995

19511996
_The register is currently empty. DEF-1 and DEF-2 were adopted on 2026-08-11
@@ -1963,6 +2008,5 @@ Every review must read this register before reporting new candidates,
19632008
re-evaluate each `Revisit when` against the current upstream head, and report
19642009
the outcome. See Phase 2.5 of the `review-t3-upstream` skill.
19652010

1966-
| ID | Upstream | Deferred on | Revisit when | Why deferred |
1967-
| ----- | ----------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1968-
| DEF-5 | `#7150` `792a1404f` + `#7152` `4a9edff4c` | 2026-08-20 | Whenever the composer is next worked on deliberately. This is scheduled work, not a bet on upstream changing, so there is no waiting condition to poll. Before starting, check `git log --oneline 792a1404f..t3code-upstream/main -- apps/web/src/components/chat/ChatComposer.tsx` for follow-ups that should land with it. | `#7150` rewrites 1084 lines of `ChatComposer.tsx` against 50 Pylon commits and a 4837-vs-2824 line divergence. A full block-by-block resolution compiled to 8 JSX errors: upstream flattens the fragment and provider-frame divs into one `<form>`, so its 335-line drawer tree cannot be hosted by Pylon's layers. Needs the composer rebuilt on upstream's structure with Pylon's ThreadHandoffTab, provider frame classes, settle loop, `resolvedRuntimeMode`, and Quick question / session-resource controls grafted back. `#7152` depends on it. |
2011+
| ID | Upstream | Deferred on | Revisit when | Why deferred |
2012+
| --- | -------- | ----------- | ------------ | ------------ |

apps/mobile/src/components/AppSymbol.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
IconAdjustmentsHorizontal,
33
IconAlertCircle,
44
IconAlertTriangle,
5+
IconApps,
56
IconArchive,
67
IconArrowBackUp,
78
IconArrowDownCircle,
@@ -13,6 +14,7 @@ import {
1314
IconArrowsMaximize,
1415
IconBellRinging,
1516
IconBolt,
17+
IconBox,
1618
IconCamera,
1719
IconChartBar,
1820
IconCheck,
@@ -104,6 +106,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
104106
checkmark: IconCheck,
105107
"checkmark.circle": IconCircleCheck,
106108
clock: IconClock,
109+
cube: IconBox,
107110
"chevron.down": IconChevronDown,
108111
"chevron.left": IconChevronLeft,
109112
"chevron.left.forwardslash.chevron.right": IconCode,
@@ -141,6 +144,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
141144
"sidebar.right": IconLayoutSidebarRight,
142145
"slider.horizontal.3": IconAdjustmentsHorizontal,
143146
"square.and.pencil": IconEdit,
147+
"square.grid.2x2": IconApps,
144148
"square.split.2x1": IconLayoutColumns,
145149
"sun.max": IconSun,
146150
"stop.fill": IconPlayerStopFilled,

apps/mobile/src/features/threads/ComposerCommandPopover.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
1+
import {
2+
resolveProviderSkillSourceKind,
3+
type ProviderSkillSourceKind,
4+
} from "@t3tools/client-runtime/providerSkills";
25
import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";
3-
import { SymbolView } from "../../components/AppSymbol";
6+
import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
47
import { memo } from "react";
58
import { Pressable, ScrollView, View, type ViewStyle } from "react-native";
69

10+
import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";
711
import { AppText as Text } from "../../components/AppText";
812
import { GlassSurface } from "../../components/GlassSurface";
913
import { PierreEntryIcon } from "../../components/PierreEntryIcon";
@@ -61,13 +65,22 @@ function PopoverSurface(props: { readonly children: React.ReactNode; readonly st
6165
);
6266
}
6367

64-
function itemIcon(item: ComposerCommandItem) {
68+
const SKILL_SOURCE_SYMBOL_BY_KIND: Record<ProviderSkillSourceKind, AppSymbolName> = {
69+
app: "square.grid.2x2",
70+
repo: "folder",
71+
project: "folder",
72+
personal: "person.crop.circle",
73+
system: "gearshape",
74+
other: "cube",
75+
};
76+
77+
function itemIcon(item: ComposerCommandItem): AppSymbolName | null {
6578
switch (item.type) {
6679
case "slash-command":
6780
case "provider-slash-command":
68-
return "terminal" as const;
81+
return "terminal";
6982
case "skill":
70-
return "cube" as const;
83+
return SKILL_SOURCE_SYMBOL_BY_KIND[resolveProviderSkillSourceKind(item.skill)];
7184
case "path":
7285
return null;
7386
}

apps/web/src/components/ChatView.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,10 @@ function useLocalDispatchState(input: {
603603
threadError: string | null | undefined;
604604
}) {
605605
const [localDispatch, setLocalDispatch] = useState<LocalDispatchSnapshot | null>(null);
606-
const latestUserMessageId =
607-
input.activeThread?.messages.findLast((message) => message.role === "user")?.id ?? null;
606+
const latestUserMessage = input.activeThread?.messages.findLast(
607+
(message) => message.role === "user",
608+
);
609+
const latestUserMessageId = latestUserMessage?.id ?? null;
608610

609611
const resetLocalDispatch = useCallback(() => {
610612
setLocalDispatch(null);
@@ -654,6 +656,7 @@ function useLocalDispatchState(input: {
654656
beginLocalDispatch,
655657
resetLocalDispatch,
656658
localDispatchStartedAt: activeLocalDispatch?.startedAt ?? null,
659+
latestUserMessageAt: latestUserMessage?.createdAt ?? null,
657660
isPreparingWorktree: activeLocalDispatch?.preparingWorktree ?? false,
658661
isSendBusy: activeLocalDispatch !== null,
659662
};
@@ -2549,6 +2552,7 @@ function ChatViewContent(props: ChatViewProps) {
25492552
beginLocalDispatch,
25502553
resetLocalDispatch,
25512554
localDispatchStartedAt,
2555+
latestUserMessageAt,
25522556
isPreparingWorktree,
25532557
isSendBusy,
25542558
} = useLocalDispatchState({
@@ -2564,6 +2568,7 @@ function ChatViewContent(props: ChatViewProps) {
25642568
activeLatestTurn,
25652569
activeThread?.session ?? null,
25662570
localDispatchStartedAt,
2571+
latestUserMessageAt,
25672572
);
25682573
useEffect(() => {
25692574
attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;
@@ -4411,6 +4416,14 @@ function ChatViewContent(props: ChatViewProps) {
44114416
// partition (same shell, same capability gate, same PR auto-settle input)
44124417
// so the banner and the sidebar row never disagree.
44134418
const activeThreadShell = useThreadShell(isServerThread ? activeThreadRef : null);
4419+
const activeComposerTasksProgress =
4420+
activeLatestTurn !== null && !latestTurnSettled
4421+
? (activeThreadShell?.planProgress ?? null)
4422+
: null;
4423+
const activeComposerTaskSteps =
4424+
activeComposerTasksProgress && activePlan && activePlan.turnId === activeLatestTurn?.turnId
4425+
? activePlan.steps
4426+
: null;
44144427
const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays);
44154428
const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge);
44164429
const activeThreadPr = resolveDisplayedThreadPr({
@@ -4843,13 +4856,13 @@ function ChatViewContent(props: ChatViewProps) {
48434856
),
48444857
title: working
48454858
? liveCount > 0
4846-
? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working in the background`
4847-
: "Background work running"
4848-
: "Monitoring in the background",
4859+
? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working`
4860+
: "Background work"
4861+
: "Monitoring",
48494862
actions: (
48504863
<Button
48514864
size="xs"
4852-
variant="outline"
4865+
variant="ghost"
48534866
disabled={isStoppingBackgroundWork}
48544867
onClick={() => void handleStopBackgroundWork()}
48554868
>
@@ -5011,7 +5024,6 @@ function ChatViewContent(props: ChatViewProps) {
50115024
systemComposerBannerItems,
50125025
wokeThreadBannerItem,
50135026
]);
5014-
50155027
useEffect(() => {
50165028
setPendingServerThreadEnvMode(null);
50175029
setPendingServerThreadBranch(undefined);
@@ -7253,6 +7265,8 @@ function ChatViewContent(props: ChatViewProps) {
72537265
setDragActive: setIsWorkspaceFileDragActive,
72547266
addFiles: (files) => composerRef.current?.addDroppedFiles(files),
72557267
});
7268+
const externalComposerDrawerAttached =
7269+
composerBannerItems.length > 0 || Boolean(threadSyncPhase && !activeEnvironmentUnavailable);
72567270

72577271
return (
72587272
<div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden bg-background">
@@ -7464,6 +7478,7 @@ function ChatViewContent(props: ChatViewProps) {
74647478
<div
74657479
className={cn(
74667480
"chat-composer-glass-shell relative mx-auto w-full max-w-3xl",
7481+
externalComposerDrawerAttached && "chat-composer-glass-shell-attached",
74677482
showComposerContextStrip && "chat-composer-glass-shell-with-context",
74687483
)}
74697484
>
@@ -7516,6 +7531,7 @@ function ChatViewContent(props: ChatViewProps) {
75167531
: null
75177532
}
75187533
isPreparingWorktree={isPreparingWorktree}
7534+
externalDrawerAttached={externalComposerDrawerAttached}
75197535
environmentUnavailable={activeEnvironmentUnavailableState}
75207536
activePendingApproval={activePendingApproval}
75217537
pendingApprovals={pendingApprovals}
@@ -7528,6 +7544,8 @@ function ChatViewContent(props: ChatViewProps) {
75287544
respondingRequestIds={respondingRequestIds}
75297545
showPlanFollowUpPrompt={showPlanFollowUpPrompt}
75307546
activeProposedPlan={activeProposedPlan}
7547+
activeTasksProgress={activeComposerTasksProgress}
7548+
activeTaskSteps={activeComposerTaskSteps}
75317549
runtimeMode={runtimeMode}
75327550
interactionMode={interactionMode}
75337551
lockedProvider={lockedProvider}

apps/web/src/components/ComposerPromptEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ import { basenameOfPath } from "~/pierre-icons";
7373
import {
7474
COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME,
7575
COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
76+
COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
7677
COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
77-
COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME,
7878
SKILL_CHIP_ICON_SVG,
7979
} from "./composerInlineChip";
8080
import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
8181
import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts";
82-
import { formatProviderSkillDisplayName } from "~/providerSkillPresentation";
82+
import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
8383
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
8484
import { registerComposerInlineTokenPaste } from "./composerInlineTokenPaste";
8585

@@ -257,7 +257,7 @@ function ComposerSkillDecorator(props: { skillLabel: string; skillDescription: s
257257
className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
258258
dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
259259
/>
260-
<span className={COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME}>{props.skillLabel}</span>
260+
<span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{props.skillLabel}</span>
261261
</span>
262262
);
263263

0 commit comments

Comments
 (0)