Custom Hooks
Reusable hooks for chat, sandbox, attachments, and model selection
Custom Hooks
Custom hooks encapsulate data fetching, mutations, and complex UI logic. The app provides hooks for chat interactions, sandbox management, file uploads, and AI model selection.
Chat Hooks
useChat (Agent Actions)
Wraps an AgentChatInstance from @cloudflare/ai-chat/react. Provides submit, edit, regenerate, tool approval, and message extraction utilities.
// hooks/use-chat.ts
import type { useAgentChat } from "@cloudflare/ai-chat/react";
type AgentChatInstance = ReturnType<typeof useAgentChat>;
export function useChat(chat: AgentChatInstance | null, agent: AgentConnection) {
// Returns:
return {
status, // "ready" | "streaming" | ...
error, // Error | undefined
stop, // () => void — stop streaming
messages, // UIMessage[]
isStreaming, // boolean
handleSubmit, // ({ text, files }) => Promise<void>
handleToolApprovalResponse, // approve/reject tool calls
handleRegenerate, // (messageId) => regenerate from that point
handleEditMessage, // (messageId, newText, files) => edit + re-send
};
}Helper exports for extracting structured data from messages:
// Extract text content from a UIMessage
extractMessageText(message: UIMessage): string
// Extract file parts from a UIMessage
extractMessageFiles(message: UIMessage): FileUIPart[]
// Extract write_todos tool state from the last assistant message
extractTodosFromMessage(message, isLastAssistantMessage): TodosState | null
// Extract ask_user tool state from the last assistant message
extractAskUserFromMessage(message, isLastAssistantMessage): AskUserState | nulluseMessageEditing
Full editing flow for user messages, including file add/remove and keyboard shortcuts.
export function useMessageEditing(message, onEditMessage?) {
return {
isEditing,
editedText, setEditedText,
editedFiles,
textareaRef, fileInputRef,
hasChanges,
handleStartEdit,
handleCancelEdit,
handleRemoveFile,
handleAddFiles,
handleConfirmEdit, // Ctrl+Enter to confirm
handleKeyDown, // Escape to cancel
};
}useCopyToClipboard
export function useCopyToClipboard(text: string) {
return { copied, handleCopy }; // copied resets after 2s
}useChats / useChatsList
Chat CRUD mutations and paginated listing via useConvexMutation and useConvexPaginatedQuery.
// hooks/use-chats.ts
export function useChats() {
return {
createChat, // mutateAsync — create a new chat
updateChat, // mutateAsync — update chat fields
deleteChat, // mutateAsync — delete a chat
updateTitle, // (id, title) => update just the title
togglePin, // (id, isPinned) => pin/unpin
isCreating, isUpdating, isDeleting,
};
}
export function useChatsList(filters?: ChatFilters) {
// Returns useConvexPaginatedQuery result with cursor-based pagination
// initialNumItems: 20
}
// Single chat query
export function useChat(chatId: Id<"chats"> | undefined) {
// Conditional convexQuery, skips when chatId is undefined
}
// Chat stats for the current member
export function useChatStats() {
// Returns getMemberStats query
}Exported types derived from the API:
export type Chat = FunctionReturnType<typeof api.chats.index.list>["page"][number];
export type ChatWithDetails = FunctionReturnType<typeof api.chats.index.get>;
export type ChatStats = FunctionReturnType<typeof api.chats.index.getMemberStats>;
export type ChatFilters = FunctionArgs<typeof api.chats.index.list>["filters"];Sandbox Hooks
useChatSandbox
Full sandbox UI state for a chat: SSH sessions, PTY terminal (xterm.js + FitAddon), file explorer, preview URLs. All terminal I/O streams through the agent's callable methods.
// hooks/use-sandbox.ts
export function useChatSandbox(chatId: Id<"chats">, agent: AgentConnection) {
return {
// Panel state
isOpen, open, close, toggle,
// SSH
sshSession, // { ssh: { token, host, expiresAt } } | null
reconnectSsh,
openInEditor, // (editor: "vscode" | "cursor") => open remote SSH
// Terminal (PTY via agent proxy)
terminalContainerRef, // ref for xterm.js mount
terminalBackground, // CSS color
terminalSessions, // TerminalSession[] — active sessions
activeTerminalId,
switchTerminalSession,
createTerminalSession,
closeTerminalSession,
reconnectTerminal,
refreshTerminalSessions,
focusTerminal,
// File explorer
explorer, // { path, entries: ExplorerEntry[] } | null
refreshExplorer,
navigateExplorer,
downloadFile,
downloadFolder,
deleteEntry,
// Preview
previewPort, previewUrl,
setPreviewPort,
createPreviewAccess,
// Loading states
isConnectingSsh,
isConnectingPreview,
};
}The terminal setup dynamically imports xterm.js and @xterm/addon-fit, opens a PTY session via agent.call("openPtyTerminal"), then streams output via agent.call("streamPtyTerminal") with onChunk. Input is batched at 25ms intervals and written via agent.call("writePtyTerminal"). A ResizeObserver keeps the terminal fitted and syncs dimensions to the remote PTY.
Exported types:
export type ExplorerEntry = { name, path, isDir, size, modifiedAt };
export type ExplorerState = { path, entries: ExplorerEntry[] };
export type TerminalSession = { id, active?, processId?, cwd?, cols?, rows?, createdAt? };useSandboxes / useSandboxesList
Sandbox CRUD mutations and paginated listing, same pattern as useChats.
// hooks/use-sandboxes.ts
export function useSandboxes() {
return {
createSandbox, // mutateAsync
updateSandbox, // mutateAsync
deleteSandbox, // mutateAsync
updateName, // (id, name) shortcut
isCreating, isUpdating, isDeleting,
};
}
export function useSandboxesList(filters?: SandboxFilters) {
// Paginated query, initialNumItems: 20
}
export function useSandbox(sandboxId: Id<"sandboxes"> | undefined) {
// Conditional single-sandbox query
}
export function useSandboxStats() {
// Returns getUserStats query
}Attachment Hooks
useAttachments
Upload flow with XHR progress tracking, SHA-256 hashing for dedup, and Convex storage integration.
// hooks/use-attachments.ts
export function useAttachments() {
return {
uploadAttachment, // (args: { fileBytes, fileName, contentType, onProgress?, signal? }) => Promise
deleteAttachment, // mutateAsync
isUploading, // true during any phase (URL gen, upload, hash creation)
isDeleting,
};
}
export function useAttachmentsList(filters?: AttachmentFilters) {
// Paginated query
}The upload pipeline:
generateUploadUrl-- get a signed Convex upload URLuploadBlobWithProgress-- XHR upload withonProgresscallback (0-100%)toHexHash-- SHA-256 hash viacrypto.subtle.digestcreateFromHash-- dedup check + create attachment record
Supports AbortSignal for cancellation.
Model Selection Hooks
useOpenRouterModels
Fetches models from /api/models, caches in localStorage for 1 hour, groups by author. Filters out free-tier and models older than 1 year.
// hooks/use-openrouter-models.ts
export function useOpenRouterModels() {
return {
models, // OpenRouterModel[] — all models
groupedModels, // [author, OpenRouterModel[]][] — grouped by author
reasoningModels, // OpenRouterModel[] — models with reasoning support
isLoading,
error,
};
}
export type OpenRouterModel = {
slug: string; // e.g. "openai/gpt-5.2-chat"
name: string;
short_name: string;
author: string;
description: string;
context_length: number;
supports_reasoning: boolean;
created_at: string;
input_modalities?: string[];
output_modalities?: string[];
reasoning_config?: { ... };
};useModelFiltering
Fuzzy search across models, provider grouping with logo mapping, favorites via Jotai favoriteModelsAtom.
// hooks/use-model-filtering.ts
export function useModelFiltering({ groupedModels, models }) {
return {
selectedAuthor, // "all" | "favorites" | provider display name
setSelectedAuthor,
authors, // Available filter options
filteredModels, // [author, OpenRouterModel[]][] — filtered result
toggleFavorite, // (slug) => add/remove from favorites
isFavorite, // (slug) => boolean
favorites, // string[] — favorite model slugs
};
}
// Utility exports
getProviderLabel(author: string): string | null // Match author to known provider
getProviderDisplayName(label: string): string // Display name for filter tab
getProviderLogoSlug(displayLabel: string): string // Logo slug for provider iconSupported providers: Anthropic, OpenAI, Google, xAI, MiniMax, DeepSeek, Moonshot AI, Z-AI, Black Forest Labs.
Auth Hooks
Current User
export function useCurrentUser() {
return {
user, // User object or null
isLoading,
isAuthenticated,
organizationId, // Active org from session
teamId,
role, // "owner" | "admin" | "member"
};
}Permissions
export function usePermissions() {
return {
isOwner, isAdmin, isMember,
canManageMembers,
canDeleteOrg,
canEditSettings,
canEditResource, // (resourceUserId) => boolean
};
}Hook Patterns
Mutation Pattern
All mutation hooks follow the same structure using useConvexMutation + useMutation:
const mutation = useMutation({
mutationFn: useConvexMutation(api.resource.index.action),
onSuccess: () => toast.success("Done"),
onError: (error: Error) => toast.error(error.message || "Failed"),
});Paginated Query Pattern
All list hooks use useConvexPaginatedQuery with cursor-based pagination:
export function useResourceList(filters = {}) {
return useConvexPaginatedQuery(
api.resource.index.list,
{ filters },
{ initialNumItems: 20 }
);
}Conditional Query Pattern
Single-resource queries skip when the ID is undefined:
export function useResource(id: Id<"table"> | undefined) {
return useQuery({
...convexQuery(api.resource.index.get, id ? { _id: id } : "skip"),
enabled: !!id,
});
}Next, learn about Optimistic Updates.