Skip to content

Commit 3093b87

Browse files
committed
feat(web): attach composer state drawers
1 parent 229c6ee commit 3093b87

21 files changed

Lines changed: 1115 additions & 716 deletions

apps/web/src/components/ChatView.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4463,19 +4463,21 @@ function ChatViewContent(props: ChatViewProps) {
44634463
),
44644464
title: working
44654465
? liveCount > 0
4466-
? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working in the background`
4467-
: "Background work running"
4468-
: "Monitoring in the background",
4466+
? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working`
4467+
: "Background work"
4468+
: "Monitoring",
44694469
actions: (
44704470
<Button
4471-
size="xs"
4472-
variant="outline"
4471+
size="micro"
4472+
variant="ghost-muted"
4473+
className="text-[11px] sm:text-[11px]"
44734474
disabled={isStoppingBackgroundWork}
44744475
onClick={() => void handleStopBackgroundWork()}
44754476
>
44764477
{isStoppingBackgroundWork ? "Stopping..." : "Stop"}
44774478
</Button>
44784479
),
4480+
className: "px-3 pt-1.5 text-xs",
44794481
};
44804482
}, [
44814483
activeBackgroundLiveness,
@@ -4631,7 +4633,6 @@ function ChatViewContent(props: ChatViewProps) {
46314633
systemComposerBannerItems,
46324634
wokeThreadBannerItem,
46334635
]);
4634-
46354636
useEffect(() => {
46364637
setPendingServerThreadEnvMode(null);
46374638
setPendingServerThreadBranch(undefined);

apps/web/src/components/ComposerPromptEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ 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";
@@ -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

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 479 additions & 461 deletions
Large diffs are not rendered by default.

apps/web/src/components/chat/ComposerBannerStack.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ describe("ComposerBannerStack", () => {
4949
const markup = renderToStaticMarkup(<ComposerBannerStack items={[banner("front")]} />);
5050

5151
expect(markup).not.toContain("data-composer-banner-stack-expanded-items");
52-
expect(markup).toContain("alert-glass");
52+
expect(markup).toContain("chat-composer-drawer-surface");
53+
expect(markup).toContain("chat-composer-drawer-attached");
54+
expect(markup).toContain('data-composer-banner-drawer="true"');
5355
expect(markup).toContain('data-variant="warning"');
5456
expect(markup).toContain("transform:none");
5557
expect(markup).not.toContain("will-change:transform");
5658
});
57-
5859
it("applies item-specific surface and action layout classes", () => {
5960
const markup = renderToStaticMarkup(
6061
<ComposerBannerStack

apps/web/src/components/chat/ComposerBannerStack.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
9898
};
9999

100100
return (
101-
<div className={cn("group/banner-stack mx-auto mb-2 max-w-3xl", className)}>
101+
<div
102+
className={cn(
103+
"group/banner-stack chat-composer-drawer-slot max-w-[calc(48rem-2.75rem)]",
104+
className,
105+
)}
106+
data-composer-banner-drawer="true"
107+
>
102108
<div
103109
className={cn(
104110
"relative flex flex-col-reverse",
@@ -108,7 +114,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
108114
{showCollapsedStackCap && firstStackedItem ? (
109115
<div
110116
className={cn(
111-
"pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-[22px]",
117+
"pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-2xl",
112118
"border border-b-0 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
113119
stackCapBorderClass[firstStackedItem.variant],
114120
"transition-opacity duration-150 ease-out",
@@ -130,6 +136,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
130136
>
131137
<ComposerBannerStackAlert
132138
item={frontItem}
139+
attached
133140
exiting={exitingItemId === frontItem.id}
134141
onDismissRequest={() => requestDismiss(frontItem)}
135142
/>
@@ -162,6 +169,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
162169
>
163170
<ComposerBannerStackAlert
164171
item={item}
172+
attached={false}
165173
exiting={exitingItemId === item.id}
166174
onDismissRequest={() => requestDismiss(item)}
167175
/>
@@ -178,10 +186,12 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
178186

179187
function ComposerBannerStackAlert({
180188
item,
189+
attached,
181190
exiting,
182191
onDismissRequest,
183192
}: {
184193
readonly item: ComposerBannerStackItem;
194+
readonly attached: boolean;
185195
readonly exiting: boolean;
186196
readonly onDismissRequest: () => void;
187197
}) {
@@ -190,7 +200,12 @@ function ComposerBannerStackAlert({
190200
return (
191201
<Alert
192202
variant={item.variant}
193-
className={cn("alert-glass rounded-[22px]", item.className)}
203+
className={cn(
204+
attached
205+
? "chat-composer-drawer-surface chat-composer-drawer-attached px-3 pt-2 pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)]"
206+
: "alert-glass rounded-[22px]",
207+
item.className,
208+
)}
194209
data-variant={item.variant}
195210
>
196211
{item.icon}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { renderToStaticMarkup } from "react-dom/server";
2+
import { describe, expect, it } from "vite-plus/test";
3+
4+
import { ComposerCommandMenu } from "./ComposerCommandMenu";
5+
6+
describe("ComposerCommandMenu", () => {
7+
it("renders slash-command results as an attached composer drawer", () => {
8+
const markup = renderToStaticMarkup(
9+
<ComposerCommandMenu
10+
items={[]}
11+
resolvedTheme="dark"
12+
isLoading={false}
13+
triggerKind="slash-command"
14+
activeItemId={null}
15+
onHighlightedItemChange={() => {}}
16+
onSelect={() => {}}
17+
/>,
18+
);
19+
20+
expect(markup).toContain('data-composer-command-drawer="true"');
21+
expect(markup).toContain("chat-composer-drawer-surface");
22+
expect(markup).toContain("chat-composer-drawer-attached");
23+
expect(markup).not.toContain("dropdown-glass");
24+
});
25+
26+
it("renders commands without a category heading or invented icons", () => {
27+
const markup = renderToStaticMarkup(
28+
<ComposerCommandMenu
29+
items={[
30+
{
31+
id: "slash:model",
32+
type: "slash-command",
33+
command: "model",
34+
label: "/model",
35+
description: "Switch response model for this thread",
36+
},
37+
]}
38+
resolvedTheme="dark"
39+
isLoading={false}
40+
triggerKind="slash-command"
41+
activeItemId="slash:model"
42+
onHighlightedItemChange={() => {}}
43+
onSelect={() => {}}
44+
/>,
45+
);
46+
47+
expect(markup).toContain("/model");
48+
expect(markup).toContain("Switch response model for this thread");
49+
expect(markup).not.toContain("Built-in");
50+
expect(markup).not.toContain("<svg");
51+
expect(markup).toContain("font-sans text-xs font-medium");
52+
expect(markup).not.toContain("font-mono");
53+
expect(markup).toContain("text-right");
54+
});
55+
});

0 commit comments

Comments
 (0)