Skip to content

Commit a7f706f

Browse files
authored
feat: add tanstack/react-query, refactor/remove useEffects (#195)
* feat: add tanstack-query * feat: update byok-section with tanstack/react-query * refactor: move tools-section, layout-client to react-query * refactor: remove useEffect in developer-tools, model-preferences, system-prompt * refactor: add use-key-shortcut, remove useEffect in utton-new-chat, model-selector/base * refactor remove useEffect pro-dialog, user-preference-store/provider * refactor: create-agent-form, dialog-trigger-create-agent, agent-store/provider * refactor: improve use-agent-command * refactor: remove useEffect agent-detail, use-search-agent, reasoning * feat: increase MESSAGE_MAX_LENGTH * refactor: remove useEffect chat-preview-panel * refactor: remove reasoning, response-stream * refactor: remove useEffect suggestions, command-history, history-trigger * refactor, remove use-effect agent-command, agents.tsx, tool-invocation * refactor: remove useEffect chat.tsx * refactor: remove useEffect agent-command, sidebar-item, use-chat-draft, feedback-form
1 parent f014bcb commit a7f706f

37 files changed

+968
-1596
lines changed

app/components/agents/agent-detail.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
X,
4242
} from "@phosphor-icons/react"
4343
import { useRouter } from "next/navigation"
44-
import { useEffect, useState } from "react"
44+
import { useRef, useState } from "react"
4545

4646
function SystemPromptDisplay({ prompt }: { prompt: string }) {
4747
const [expanded, setExpanded] = useState(false)
@@ -106,21 +106,21 @@ export function AgentDetail({
106106
const [isDeleting, setIsDeleting] = useState(false)
107107
const router = useRouter()
108108
const { user } = useUser()
109+
const didPrefetch = useRef(false)
110+
111+
if (!didPrefetch.current && isFullPage && randomAgents.length > 0) {
112+
randomAgents.forEach((agent) => {
113+
router.prefetch(`/agents/${agent.slug}`)
114+
})
115+
didPrefetch.current = true
116+
}
109117

110118
const copyToClipboard = () => {
111119
navigator.clipboard.writeText(`${window.location.origin}/agents/${slug}`)
112120
setCopied(true)
113121
setTimeout(() => setCopied(false), 1000)
114122
}
115123

116-
useEffect(() => {
117-
if (randomAgents.length > 0 && isFullPage) {
118-
randomAgents.forEach((agent) => {
119-
router.prefetch(`/agents/${agent.slug}`)
120-
})
121-
}
122-
}, [randomAgents, router, isFullPage])
123-
124124
const handleAgentClick = (agent: AgentSummary) => {
125125
if (onAgentClick) {
126126
onAgentClick(agent.id)
@@ -424,8 +424,8 @@ export function AgentDetail({
424424
<AlertDialogHeader>
425425
<AlertDialogTitle>Delete Agent</AlertDialogTitle>
426426
<AlertDialogDescription>
427-
Are you sure you want to delete &quot;{name}&quot;? This action cannot be
428-
undone.
427+
Are you sure you want to delete &quot;{name}&quot;? This action
428+
cannot be undone.
429429
</AlertDialogDescription>
430430
</AlertDialogHeader>
431431
<AlertDialogFooter>

app/components/agents/dialog-create-agent/create-agent-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { AlertCircle, Check, Github, X } from "lucide-react"
1616
import type React from "react"
1717
import { ToolsSection } from "./tools-section"
1818

19-
type AgentFormData = {
19+
export type AgentFormData = {
2020
name: string
2121
description: string
2222
systemPrompt: string

app/components/agents/dialog-create-agent/dialog-trigger-create-agent.tsx

Lines changed: 52 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,25 @@ import {
99
DialogTrigger,
1010
} from "@/components/ui/dialog"
1111
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
12-
import {
13-
Popover,
14-
PopoverTrigger,
15-
} from "@/components/ui/popover"
12+
import { Popover, PopoverTrigger } from "@/components/ui/popover"
1613
import { toast } from "@/components/ui/toast"
14+
import { useAgent } from "@/lib/agent-store/provider"
1715
import { fetchClient } from "@/lib/fetch"
1816
import { API_ROUTE_CREATE_AGENT } from "@/lib/routes"
1917
import { useUser } from "@/lib/user-store/provider"
2018
import { useRouter } from "next/navigation"
2119
import type React from "react"
2220
import { useState } from "react"
2321
import { useBreakpoint } from "../../../hooks/use-breakpoint"
24-
import { CreateAgentForm } from "./create-agent-form"
25-
26-
type AgentFormData = {
27-
name: string
28-
description: string
29-
systemPrompt: string
30-
mcp: "none" | "git-mcp"
31-
repository?: string
32-
tools: string[]
33-
}
22+
import { AgentFormData, CreateAgentForm } from "./create-agent-form"
3423

35-
type DialogCreateAgentTrigger = {
36-
trigger: React.ReactNode
37-
}
38-
39-
// @todo: add drawer
4024
export function DialogCreateAgentTrigger({
4125
trigger,
42-
}: DialogCreateAgentTrigger) {
26+
}: {
27+
trigger: React.ReactNode
28+
}) {
4329
const { user } = useUser()
30+
const { refetchUserAgents } = useAgent()
4431
const isAuthenticated = !!user?.id
4532
const [open, setOpen] = useState(false)
4633
const [formData, setFormData] = useState<AgentFormData>({
@@ -58,142 +45,52 @@ export function DialogCreateAgentTrigger({
5845

5946
const generateSystemPrompt = (owner: string, repo: string) => {
6047
return `You are a helpful GitHub assistant focused on the repository: ${owner}/${repo}.
61-
48+
6249
Use the available tools below to answer any questions. Always prefer using tools over guessing.
63-
50+
6451
Tools available for this repository:
65-
- \`fetch_${repo}_documentation\`: Fetch the entire documentation file. Use this first when asked about general concepts in ${owner}/${repo}.
66-
- \`search_${repo}_documentation\`: Semantically search the documentation. Use this for specific questions.
67-
- \`search_${repo}_code\`: Search code with exact matches using the GitHub API. Use when asked about file contents or code examples.
52+
- \`fetch_${repo}_documentation\`: Fetch the entire documentation file.
53+
- \`search_${repo}_documentation\`: Semantically search the documentation.
54+
- \`search_${repo}_code\`: Search code with exact matches using the GitHub API.
6855
- \`fetch_generic_url_content\`: Fetch absolute URLs when referenced in the docs or needed for context.
69-
70-
Never invent answers. Use tools and return what you find.`
71-
}
72-
73-
const handleInputChange = (
74-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
75-
) => {
76-
const { name, value } = e.target
77-
setFormData({ ...formData, [name]: value })
78-
79-
// Clear error for this field if it exists
80-
if (error[name]) {
81-
setError({ ...error, [name]: "" })
82-
}
83-
}
84-
85-
const handleRepositoryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
86-
const repoValue = e.target.value
87-
setRepository(repoValue)
88-
89-
// Clear repository error if it exists
90-
if (error.repository) {
91-
setError({ ...error, repository: "" })
92-
}
93-
94-
// Update system prompt if git-mcp is selected and repository format is valid
95-
if (formData.mcp === "git-mcp" && validateRepository(repoValue)) {
96-
const [owner, repo] = repoValue.split("/")
97-
setFormData((prev) => ({
98-
...prev,
99-
systemPrompt: generateSystemPrompt(owner, repo),
100-
}))
101-
}
102-
}
103-
104-
const handleSelectChange = (value: string) => {
105-
setFormData({ ...formData, mcp: value as "none" | "git-mcp" })
106-
107-
// Clear repository error if switching away from git-mcp
108-
if (value !== "git-mcp" && error.repository) {
109-
setError({ ...error, repository: "" })
110-
}
111-
112-
// If switching to git-mcp and repository is already valid, update system prompt
113-
if (value === "git-mcp" && validateRepository(repository)) {
114-
const [owner, repo] = repository.split("/")
115-
setFormData((prev) => ({
116-
...prev,
117-
systemPrompt: generateSystemPrompt(owner, repo),
118-
}))
119-
}
120-
}
12156
122-
const handleToolsChange = (selectedTools: string[]) => {
123-
setFormData({ ...formData, tools: selectedTools })
124-
}
125-
126-
const validateRepository = (repo: string) => {
127-
// Simple validation for owner/repo format
128-
const regex = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/
129-
return regex.test(repo)
57+
Never invent answers. Use tools and return what you find.`
13058
}
13159

132-
const validateForm = () => {
60+
const handleSubmit = async (e: React.FormEvent) => {
61+
e.preventDefault()
13362
const newErrors: { [key: string]: string } = {}
134-
135-
if (!formData.name.trim()) {
136-
newErrors.name = "Agent name is required"
137-
}
138-
139-
if (!formData.description.trim()) {
63+
if (!formData.name.trim()) newErrors.name = "Agent name is required"
64+
if (!formData.description.trim())
14065
newErrors.description = "Description is required"
141-
}
142-
143-
if (!formData.systemPrompt.trim()) {
66+
if (!formData.systemPrompt.trim())
14467
newErrors.systemPrompt = "System prompt is required"
145-
}
146-
147-
if (formData.mcp === "git-mcp" && !validateRepository(repository)) {
68+
if (
69+
formData.mcp === "git-mcp" &&
70+
!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repository)
71+
) {
14872
newErrors.repository =
14973
'Please enter a valid repository in the format "owner/repo"'
15074
}
151-
152-
setError(newErrors)
153-
return Object.keys(newErrors).length === 0
154-
}
155-
156-
const handleSubmit = async (e: React.FormEvent) => {
157-
e.preventDefault()
158-
159-
if (!validateForm()) {
160-
return
161-
}
75+
if (Object.keys(newErrors).length) return setError(newErrors)
16276

16377
setIsLoading(true)
164-
16578
try {
166-
// If git-mcp is selected, validate the repository
16779
if (formData.mcp === "git-mcp") {
16880
const response = await fetch(
16981
`https://api.github.com/repos/${repository}`
17082
)
171-
17283
if (!response.ok) {
173-
if (response.status === 404) {
174-
setError({
175-
...error,
176-
repository:
177-
"Repository not found. Please check the repository name and try again.",
178-
})
179-
} else {
180-
setError({
181-
...error,
182-
repository: `GitHub API error: ${response.statusText}`,
183-
})
184-
}
84+
setError({ repository: "Repository not found." })
18585
setIsLoading(false)
18686
return
18787
}
188-
189-
// Add repository to form data
19088
formData.repository = repository
19189
}
19290

193-
const owner = repository ? repository.split("/")[0] : null
194-
const repo = repository ? repository.split("/")[1] : null
91+
const [owner, repo] = repository.split("/")
19592

196-
const apiResponse = await fetchClient(API_ROUTE_CREATE_AGENT, {
93+
const res = await fetchClient(API_ROUTE_CREATE_AGENT, {
19794
method: "POST",
19895
headers: { "Content-Type": "application/json" },
19996
body: JSON.stringify({
@@ -202,10 +99,7 @@ Never invent answers. Use tools and return what you find.`
20299
systemPrompt: formData.systemPrompt,
203100
avatar_url: repository ? `https://github.com/${owner}.png` : null,
204101
mcp_config: repository
205-
? {
206-
server: `https://gitmcp.io/${owner}/${repo}`,
207-
variables: [],
208-
}
102+
? { server: `https://gitmcp.io/${owner}/${repo}`, variables: [] }
209103
: null,
210104
example_inputs: repository
211105
? [
@@ -222,24 +116,17 @@ Never invent answers. Use tools and return what you find.`
222116
}),
223117
})
224118

225-
if (!apiResponse.ok) {
226-
const errorData = await apiResponse.json()
227-
throw new Error(errorData.error || "Failed to create agent")
228-
}
229-
230-
const { agent } = await apiResponse.json()
231-
232-
// Close the dialog and redirect
119+
if (!res.ok) throw new Error("Failed to create agent")
120+
const { agent } = await res.json()
121+
refetchUserAgents()
233122
setOpen(false)
234123
router.push(`/?agent=${agent.slug}`)
235-
} catch (error: unknown) {
236-
console.error("Agent creation error:", error)
124+
} catch (err) {
125+
console.error(err)
237126
toast({
238-
title: "Error creating agent",
239-
description:
240-
(error as Error).message || "Failed to create agent. Please try again.",
127+
title: "Error",
128+
description: "Could not create agent. Try again.",
241129
})
242-
setError({ form: "Failed to create agent. Please try again." })
243130
} finally {
244131
setIsLoading(false)
245132
}
@@ -249,12 +136,26 @@ Never invent answers. Use tools and return what you find.`
249136
<CreateAgentForm
250137
formData={formData}
251138
repository={repository}
252-
setRepository={handleRepositoryChange}
139+
setRepository={(e) => setRepository(e.target.value)}
253140
error={error}
254141
isLoading={isLoading}
255-
handleInputChange={handleInputChange}
256-
handleSelectChange={handleSelectChange}
257-
handleToolsChange={handleToolsChange}
142+
handleInputChange={(e) => {
143+
const { name, value } = e.target
144+
setFormData((f) => ({ ...f, [name]: value }))
145+
if (error[name]) setError((err) => ({ ...err, [name]: "" }))
146+
}}
147+
handleSelectChange={(value) => {
148+
setFormData((f) => ({ ...f, mcp: value as "none" | "git-mcp" }))
149+
if (value !== "git-mcp") setError((e) => ({ ...e, repository: "" }))
150+
if (value === "git-mcp" && /^[^/]+\/[^/]+$/.test(repository)) {
151+
const [owner, repo] = repository.split("/")
152+
setFormData((f) => ({
153+
...f,
154+
systemPrompt: generateSystemPrompt(owner, repo),
155+
}))
156+
}
157+
}}
158+
handleToolsChange={(tools) => setFormData((f) => ({ ...f, tools }))}
258159
handleSubmit={handleSubmit}
259160
onClose={() => setOpen(false)}
260161
isDrawer={isMobile}
@@ -285,7 +186,6 @@ Never invent answers. Use tools and return what you find.`
285186
<DialogContent className="max-h-[90vh] gap-0 overflow-y-auto p-0 sm:max-w-xl">
286187
<div
287188
className="h-full w-full"
288-
// Prevent the dialog from closing when clicking on the content, needed because of the agent-command component
289189
onClick={(e) => e.stopPropagation()}
290190
onMouseDown={(e) => e.stopPropagation()}
291191
>

app/components/agents/dialog-create-agent/tools-section.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"use client"
22

33
import { Label } from "@/components/ui/label"
4+
import { fetchClient } from "@/lib/fetch"
45
import { getAllTools } from "@/lib/tools"
56
import { cn } from "@/lib/utils"
6-
import React, { useEffect, useState } from "react"
7+
import { useQuery } from "@tanstack/react-query"
8+
import React, { useState } from "react"
79

810
const isDev = process.env.NODE_ENV === "development"
911

@@ -12,24 +14,19 @@ type ToolsSectionProps = {
1214
}
1315

1416
export function ToolsSection({ onSelectTools }: ToolsSectionProps) {
15-
const [availableTools, setAvailableTools] = useState<string[] | null>(null)
1617
const [selectedTools, setSelectedTools] = useState<string[]>([])
1718
const tools = getAllTools()
1819

19-
useEffect(() => {
20-
if (!isDev) return
21-
22-
const fetchTools = async () => {
23-
try {
24-
const response = await fetch("/api/tools-available")
25-
const data = await response.json()
26-
setAvailableTools(data.available || [])
27-
} catch (error) {
28-
console.error("Failed to fetch available tools:", error)
29-
}
30-
}
31-
fetchTools()
32-
}, [])
20+
const { data: availableTools } = useQuery<string[]>({
21+
queryKey: ["available-tools"],
22+
queryFn: async () => {
23+
if (!isDev) return []
24+
const res = await fetchClient("/api/tools-available")
25+
const json = await res.json()
26+
return json.available || []
27+
},
28+
enabled: isDev,
29+
})
3330

3431
const handleToolToggle = (toolId: string) => {
3532
const newSelection = selectedTools.includes(toolId)

0 commit comments

Comments
 (0)