Skip to content

Commit 9ab98ae

Browse files
adamleithpclaude
andauthored
feat(code): use Quill Chip for mention chips + PromptInput story (#1756)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 97b84ef commit 9ab98ae

2 files changed

Lines changed: 389 additions & 33 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
2+
import { Providers } from "@components/Providers";
3+
import { ReasoningLevelSelector } from "@features/sessions/components/ReasoningLevelSelector";
4+
import { UnifiedModelSelector } from "@features/sessions/components/UnifiedModelSelector";
5+
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
6+
import type { Meta, StoryObj } from "@storybook/react-vite";
7+
import { useEffect, useRef, useState } from "react";
8+
import type { EditorHandle } from "../types";
9+
import type { MentionChip } from "../utils/content";
10+
import { PromptInput } from "./PromptInput";
11+
12+
// --- Mock data matching SessionConfigOption shape ---
13+
14+
const mockModelOption = {
15+
id: "model",
16+
name: "Model",
17+
type: "select" as const,
18+
currentValue: "gpt-5.4",
19+
options: [
20+
{
21+
group: "recommended",
22+
name: "Recommended",
23+
options: [
24+
{ value: "gpt-5.4", name: "GPT 5.4" },
25+
{ value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
26+
],
27+
},
28+
{
29+
group: "other",
30+
name: "Other",
31+
options: [
32+
{ value: "claude-opus-4-6", name: "Claude Opus 4.6" },
33+
{ value: "o3-pro", name: "o3-pro" },
34+
{ value: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
35+
],
36+
},
37+
],
38+
} satisfies SessionConfigOption;
39+
40+
const mockReasoningOption = {
41+
id: "thought",
42+
name: "Reasoning",
43+
type: "select" as const,
44+
currentValue: "high",
45+
options: [
46+
{ value: "off", name: "Off" },
47+
{ value: "low", name: "Low" },
48+
{ value: "medium", name: "Medium" },
49+
{ value: "high", name: "High" },
50+
],
51+
} satisfies SessionConfigOption;
52+
53+
// --- Wrapper to inject chips after mount ---
54+
55+
function PromptInputWithChips({
56+
chips,
57+
...props
58+
}: React.ComponentProps<typeof PromptInput> & { chips?: MentionChip[] }) {
59+
const ref = useRef<EditorHandle>(null);
60+
const insertedRef = useRef(false);
61+
62+
useEffect(() => {
63+
if (!chips?.length || insertedRef.current) return;
64+
insertedRef.current = true;
65+
const timer = setTimeout(() => {
66+
for (const chip of chips) {
67+
ref.current?.insertChip(chip);
68+
}
69+
}, 200);
70+
return () => clearTimeout(timer);
71+
}, [chips]);
72+
73+
return <PromptInput ref={ref} {...props} />;
74+
}
75+
76+
// --- Wrapper with stateful selectors ---
77+
78+
function PromptInputWithSelectors({
79+
chips,
80+
showSelectors = true,
81+
...props
82+
}: React.ComponentProps<typeof PromptInput> & {
83+
chips?: MentionChip[];
84+
showSelectors?: boolean;
85+
}) {
86+
const [adapter, setAdapter] = useState<AgentAdapter>("claude");
87+
const [modelOption, setModelOption] =
88+
useState<SessionConfigOption>(mockModelOption);
89+
const [reasoningOption, setReasoningOption] =
90+
useState<SessionConfigOption>(mockReasoningOption);
91+
92+
const handleModelChange = (value: string) => {
93+
setModelOption({ ...mockModelOption, currentValue: value });
94+
};
95+
96+
const handleReasoningChange = (value: string) => {
97+
setReasoningOption({ ...mockReasoningOption, currentValue: value });
98+
};
99+
100+
return (
101+
<PromptInputWithChips
102+
chips={chips}
103+
modelSelector={
104+
showSelectors ? (
105+
<UnifiedModelSelector
106+
modelOption={modelOption}
107+
adapter={adapter}
108+
onAdapterChange={setAdapter}
109+
onModelChange={handleModelChange}
110+
/>
111+
) : (
112+
false
113+
)
114+
}
115+
reasoningSelector={
116+
showSelectors ? (
117+
<ReasoningLevelSelector
118+
thoughtOption={reasoningOption}
119+
adapter={adapter}
120+
onChange={handleReasoningChange}
121+
/>
122+
) : (
123+
false
124+
)
125+
}
126+
{...props}
127+
/>
128+
);
129+
}
130+
131+
const meta: Meta<typeof PromptInputWithSelectors> = {
132+
title: "Features/MessageEditor/PromptInput",
133+
component: PromptInputWithSelectors,
134+
parameters: {
135+
layout: "padded",
136+
},
137+
decorators: [
138+
(Story) => (
139+
<Providers>
140+
<div className="max-w-[800px]">
141+
<Story />
142+
</div>
143+
</Providers>
144+
),
145+
],
146+
args: {
147+
sessionId: "storybook-session",
148+
placeholder: "Type a message...",
149+
disabled: false,
150+
isLoading: false,
151+
autoFocus: true,
152+
isActiveSession: true,
153+
enableBashMode: true,
154+
enableCommands: true,
155+
showSelectors: true,
156+
onSubmit: () => {},
157+
onCancel: () => {},
158+
},
159+
argTypes: {
160+
disabled: { control: "boolean" },
161+
isLoading: { control: "boolean" },
162+
enableBashMode: { control: "boolean" },
163+
enableCommands: { control: "boolean" },
164+
showSelectors: { control: "boolean" },
165+
placeholder: { control: "text" },
166+
},
167+
};
168+
169+
export default meta;
170+
type Story = StoryObj<typeof PromptInputWithSelectors>;
171+
172+
export const Default: Story = {};
173+
174+
export const WithFileChip: Story = {
175+
name: "With File Chip",
176+
args: {
177+
chips: [
178+
{
179+
type: "file",
180+
id: "/src/settings.json",
181+
label: ".claude/settings.json",
182+
},
183+
],
184+
},
185+
};
186+
187+
export const WithCommandChip: Story = {
188+
name: "With Command Chip",
189+
args: {
190+
chips: [{ type: "command", id: "good", label: "good" }],
191+
},
192+
};
193+
194+
export const WithMultipleChips: Story = {
195+
name: "With Multiple Chips",
196+
args: {
197+
chips: [
198+
{
199+
type: "file",
200+
id: "/src/settings.json",
201+
label: ".claude/settings.json",
202+
},
203+
{ type: "command", id: "good", label: "good" },
204+
{
205+
type: "file",
206+
id: "/workflows/release.yml",
207+
label: "workflows/agent-release.yml",
208+
},
209+
],
210+
},
211+
};
212+
213+
export const AllChipTypes: Story = {
214+
name: "All Chip Types",
215+
args: {
216+
chips: [
217+
{ type: "file", id: "/src/index.ts", label: "src/index.ts" },
218+
{ type: "command", id: "review", label: "review" },
219+
{
220+
type: "github_issue",
221+
id: "https://github.com/org/repo/issues/123",
222+
label: "#123 Fix the bug",
223+
},
224+
{ type: "error", id: "error-1", label: "TypeError: undefined" },
225+
{ type: "experiment", id: "exp-1", label: "new-checkout-flow" },
226+
{ type: "insight", id: "insight-1", label: "Weekly active users" },
227+
{ type: "feature_flag", id: "flag-1", label: "enable-dark-mode" },
228+
{
229+
type: "file",
230+
id: "/tmp/pasted-content.txt",
231+
label: "pasted-content.txt",
232+
},
233+
],
234+
},
235+
};
236+
237+
export const BashMode: Story = {
238+
name: "Bash Mode (type ! to activate)",
239+
args: {
240+
enableBashMode: true,
241+
placeholder: "Type ! to enter bash mode...",
242+
},
243+
};
244+
245+
export const Loading: Story = {
246+
name: "Loading (With Cancel)",
247+
args: {
248+
isLoading: true,
249+
},
250+
};
251+
252+
export const Disabled: Story = {
253+
args: {
254+
disabled: true,
255+
},
256+
};
257+
258+
export const LongChipLabels: Story = {
259+
name: "Long Chip Labels",
260+
args: {
261+
chips: [
262+
{
263+
type: "file",
264+
id: "/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx",
265+
label:
266+
"apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx",
267+
},
268+
{
269+
type: "file",
270+
id: "/packages/agent/src/adapters/claude/permissions/permission-options.ts",
271+
label:
272+
"packages/agent/src/adapters/claude/permissions/permission-options.ts",
273+
},
274+
],
275+
},
276+
};
277+
278+
export const NoToolbar: Story = {
279+
name: "No Toolbar (Minimal)",
280+
args: {
281+
showSelectors: false,
282+
},
283+
};

0 commit comments

Comments
 (0)