Skip to content

Commit ca7ee11

Browse files
authored
Suggestion pattern experiments (commontoolsinc#2146)
* Create `todo-with-suggestion.tsx` * Add `write-and-run.tsx` * Working write-and-run! * Fix lint error * Use `haiku` for suggestions
1 parent f9b5870 commit ca7ee11

File tree

5 files changed

+330
-1
lines changed

5 files changed

+330
-1
lines changed

.beads/issues.jsonl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{"id":"ct-labs-f8g","content_hash":"1b69b91461a3f1d847e8faea516eecc43f978445dd2146c1e0498cffc2466b91","title":"Integrate Suggestion pattern","description":"Add AI suggestion powered by Suggestion pattern","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-21T11:25:33.60042-08:00","updated_at":"2025-11-21T11:25:33.60042-08:00","source_repo":".","dependencies":[{"issue_id":"ct-labs-f8g","depends_on_id":"ct-labs-lk6","type":"blocks","created_at":"2025-11-21T11:25:33.600965-08:00","created_by":"daemon"}]}
2+
{"id":"ct-labs-i1z","content_hash":"e6a28322e1a86178e563e140ec0ffb70e178ce7ec7be55e94ff471cae06646cd","title":"Implement todo list base functionality","description":"Add/remove/toggle todo items with bidirectional binding","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-21T11:25:33.517179-08:00","updated_at":"2025-11-21T11:25:33.517179-08:00","source_repo":".","dependencies":[{"issue_id":"ct-labs-i1z","depends_on_id":"ct-labs-lk6","type":"blocks","created_at":"2025-11-21T11:25:33.517738-08:00","created_by":"daemon"}]}
3+
{"id":"ct-labs-l3a","content_hash":"3c78bd4e9fbc0f4a6211a62eee827e01b9b12d406c5a804aa5ec8d880cb8119e","title":"Test pattern with ct dev","description":"Verify pattern works with deno task ct dev","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-21T11:25:33.682807-08:00","updated_at":"2025-11-21T11:25:33.682807-08:00","source_repo":".","dependencies":[{"issue_id":"ct-labs-l3a","depends_on_id":"ct-labs-lk6","type":"blocks","created_at":"2025-11-21T11:25:33.68338-08:00","created_by":"daemon"}]}
4+
{"id":"ct-labs-lk6","content_hash":"99a834f63aa5afe4143a8539ad34234e981eb88826bbe12f016faa8dcd44cce4","title":"Create todo-with-suggestion pattern","description":"Create packages/patterns/todo-with-suggestion.tsx - a todo list pattern that uses the Suggestion pattern for AI-powered suggestions","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-21T11:25:24.899876-08:00","updated_at":"2025-11-21T11:25:24.899876-08:00","source_repo":"."}

packages/patterns/suggestion.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const Suggestion = pattern(
2929
fetchAndRunPattern: patternTool(fetchAndRunPattern),
3030
listPatternIndex: patternTool(listPatternIndex),
3131
},
32-
model: "anthropic:claude-sonnet-4-5",
32+
model: "anthropic:claude-haiku-4-5",
3333
schema: toSchema<{ cell: Cell<any> }>(),
3434
});
3535

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/// <cts-enable />
2+
import { Cell, Default, derive, NAME, pattern, UI } from "commontools";
3+
import { Suggestion } from "./suggestion.tsx";
4+
5+
interface TodoItem {
6+
title: string;
7+
done: Default<boolean, false>;
8+
}
9+
10+
interface Input {
11+
items: Cell<TodoItem[]>;
12+
}
13+
14+
interface Output {
15+
items: Cell<TodoItem[]>;
16+
}
17+
18+
export default pattern<Input, Output>(({ items }) => {
19+
// AI suggestion based on current todos
20+
const suggestion = Suggestion({
21+
situation: "Based on my todo list, use a pattern to help me.",
22+
context: { items },
23+
});
24+
25+
return {
26+
[NAME]: "Todo with Suggestions",
27+
[UI]: (
28+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
29+
<h2>Todo List</h2>
30+
31+
{/* Add new item */}
32+
<ct-message-input
33+
placeholder="Add a todo item..."
34+
onct-send={(e: { detail?: { message?: string } }) => {
35+
const title = e.detail?.message?.trim();
36+
if (title) {
37+
items.push({ title, done: false });
38+
}
39+
}}
40+
/>
41+
42+
{/* Todo items with per-item suggestions */}
43+
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
44+
{items.map((item) => {
45+
return (
46+
<div
47+
style={{
48+
display: "flex",
49+
flexDirection: "column",
50+
gap: "4px",
51+
padding: "8px",
52+
border: "1px solid #e0e0e0",
53+
borderRadius: "8px",
54+
}}
55+
>
56+
<div
57+
style={{ display: "flex", alignItems: "center", gap: "8px" }}
58+
>
59+
<ct-checkbox $checked={item.done}>
60+
<span
61+
style={item.done
62+
? { textDecoration: "line-through", opacity: 0.6 }
63+
: {}}
64+
>
65+
{item.title}
66+
</span>
67+
</ct-checkbox>
68+
<ct-button
69+
onClick={() => {
70+
const current = items.get();
71+
const index = current.findIndex((el) =>
72+
Cell.equals(item, el)
73+
);
74+
if (index >= 0) {
75+
items.set(current.toSpliced(index, 1));
76+
}
77+
}}
78+
>
79+
×
80+
</ct-button>
81+
</div>
82+
</div>
83+
);
84+
})}
85+
</div>
86+
87+
{/* AI Suggestion */}
88+
<div
89+
style={{
90+
marginTop: "16px",
91+
padding: "12px",
92+
backgroundColor: "#f5f5f5",
93+
borderRadius: "8px",
94+
}}
95+
>
96+
<h3>AI Suggestion</h3>
97+
{derive(suggestion, (s) =>
98+
s?.cell ?? (
99+
<span style={{ opacity: 0.6 }}>Getting suggestion...</span>
100+
))}
101+
</div>
102+
</div>
103+
),
104+
items,
105+
suggestion,
106+
};
107+
});
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/// <cts-enable />
2+
import {
3+
Cell,
4+
compileAndRun,
5+
computed,
6+
Default,
7+
derive,
8+
generateText,
9+
handler,
10+
ifElse,
11+
NAME,
12+
navigateTo,
13+
pattern,
14+
UI,
15+
} from "commontools";
16+
17+
// Template for the AI to reference
18+
const TEMPLATE = `/// <cts-enable />
19+
import { Cell, derive, handler, Default, NAME, pattern, UI } from "commontools";
20+
21+
interface Input {
22+
value: Default<number, 0>;
23+
}
24+
25+
const increment = handler<unknown, { value: Cell<number> }>(
26+
(_, { value }) => value.set(value.get() + 1)
27+
);
28+
29+
const decrement = handler<unknown, { value: Cell<number> }>(
30+
(_, { value }) => value.set(value.get() - 1)
31+
);
32+
33+
export default pattern<Input>(({ value }) => {
34+
return {
35+
[NAME]: derive(value, (v) => \`Counter: \${v}\`),
36+
[UI]: (
37+
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
38+
<ct-button onClick={decrement({ value })}>-</ct-button>
39+
<span>{value}</span>
40+
<ct-button onClick={increment({ value })}>+</ct-button>
41+
</div>
42+
),
43+
value,
44+
};
45+
});`;
46+
47+
const SYSTEM_PROMPT =
48+
`You are a CommonTools pattern generator. Given a user request, generate a complete TypeScript pattern file.
49+
50+
IMPORTANT RULES:
51+
1. Start with: /// <cts-enable />
52+
2. Import from "commontools": Cell, Default, derive, handler, NAME, pattern, UI, computed, ifElse
53+
3. Use the pattern<Input>() or pattern<Input, Output>() API
54+
4. For arrays that need mutation, use Cell<T[]> in the interface
55+
5. Use $checked, $value for bidirectional binding on ct-checkbox, ct-input
56+
6. Use inline handlers for simple operations, handler() for complex ones
57+
7. Always return [NAME] and [UI] from the pattern
58+
59+
TEMPLATE FOR REFERENCE:
60+
${TEMPLATE}
61+
62+
Generate ONLY the TypeScript code, no explanations or markdown.`;
63+
64+
interface Input {
65+
prompt: Default<string, "Create a simple counter">;
66+
}
67+
68+
interface Output {
69+
prompt: Cell<string>;
70+
}
71+
72+
const updatePrompt = handler<
73+
{ detail: { message: string } },
74+
{ prompt: Cell<string> }
75+
>((event, { prompt }) => {
76+
const newPrompt = event.detail?.message?.trim();
77+
if (newPrompt) {
78+
prompt.set(newPrompt);
79+
}
80+
});
81+
82+
const visit = handler<unknown, { result: Cell<any> }>((_, { result }) => {
83+
return navigateTo(result);
84+
});
85+
86+
export default pattern<Input, Output>(({ prompt }) => {
87+
// Step 1: Generate pattern source code from prompt
88+
const generated = generateText({
89+
system: SYSTEM_PROMPT,
90+
prompt,
91+
model: "anthropic:claude-sonnet-4-5",
92+
});
93+
94+
const processedResult = computed(() => {
95+
const result = generated?.result ?? "";
96+
// Remove wrapping ```typescript``` if it exists
97+
return result.replace(/^```typescript\n?/, "").replace(/\n?```$/, "");
98+
});
99+
100+
// Step 2: Compile the generated code when ready
101+
const compileParams = derive(processedResult, (p) => ({
102+
files: p ? [{ name: "/main.tsx", contents: p }] : [],
103+
main: p ? "/main.tsx" : "",
104+
}));
105+
106+
const compiled = compileAndRun(compileParams);
107+
108+
// Compute states
109+
const isGenerating = generated.pending;
110+
const hasCode = computed(() => !!generated.result);
111+
const hasError = computed(() => !!compiled.error);
112+
const isReady = computed(() =>
113+
!compiled.pending && !!compiled.result && !compiled.error
114+
);
115+
116+
return {
117+
[NAME]: "Write and Run",
118+
[UI]: (
119+
<div
120+
style={{
121+
display: "flex",
122+
flexDirection: "column",
123+
gap: "16px",
124+
padding: "16px",
125+
}}
126+
>
127+
<h2>Write and Run</h2>
128+
<p style={{ color: "#666" }}>
129+
Describe a pattern and I'll generate, compile, and run it.
130+
</p>
131+
132+
<ct-message-input
133+
placeholder="Describe the pattern you want..."
134+
onct-send={updatePrompt({ prompt })}
135+
/>
136+
137+
<div
138+
style={{
139+
padding: "12px",
140+
backgroundColor: "#f5f5f5",
141+
borderRadius: "8px",
142+
}}
143+
>
144+
{ifElse(
145+
isGenerating,
146+
<span>Generating code...</span>,
147+
ifElse(
148+
hasError,
149+
<div style={{ color: "red" }}>
150+
<b>Compile error:</b> {compiled.error}
151+
</div>,
152+
ifElse(
153+
isReady,
154+
<ct-button onClick={visit({ result: compiled.result })}>
155+
Open Generated Pattern
156+
</ct-button>,
157+
<span style={{ opacity: 0.6 }}>
158+
Enter a prompt to generate a pattern
159+
</span>,
160+
),
161+
),
162+
)}
163+
</div>
164+
165+
{ifElse(
166+
isReady,
167+
<div>
168+
<h3>Generated Pattern</h3>
169+
<div
170+
style={{
171+
border: "1px solid #ccc",
172+
borderRadius: "8px",
173+
padding: "16px",
174+
backgroundColor: "#fff",
175+
}}
176+
>
177+
{compiled.result}
178+
</div>
179+
</div>,
180+
<span />,
181+
)}
182+
183+
{ifElse(
184+
hasCode,
185+
<div>
186+
<h3>Generated Code</h3>
187+
<ct-code-editor
188+
value={generated.result}
189+
language="text/x.typescript"
190+
readonly
191+
/>
192+
</div>,
193+
<span />,
194+
)}
195+
</div>
196+
),
197+
prompt,
198+
generatedCode: generated.result,
199+
compiledCharm: compiled.result,
200+
error: compiled.error,
201+
};
202+
});

packages/toolshed/routes/ai/llm/models.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,22 @@ if (env.CTTS_AI_LLM_ANTHROPIC_API_KEY) {
210210
},
211211
},
212212
});
213+
214+
addModel({
215+
provider: anthropicProvider,
216+
name: "anthropic:claude-haiku-4-5",
217+
aliases: ["haiku-4-5", "haiku-4.5"],
218+
capabilities: {
219+
contextWindow: 200_000,
220+
maxOutputTokens: 8192,
221+
images: true,
222+
prefill: true,
223+
systemPrompt: true,
224+
stopSequences: true,
225+
streaming: true,
226+
reasoning: false,
227+
},
228+
});
213229
}
214230

215231
if (env.CTTS_AI_LLM_GROQ_API_KEY) {

0 commit comments

Comments
 (0)