Skip to content
Merged
15 changes: 13 additions & 2 deletions apps/web/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
useQuickNoteDraft,
} from "@/stores/quick-note-draft"
import { analytics } from "@/lib/analytics"
import type { ModelId } from "@/lib/models"
import type { ModelId, ReasoningEffort } from "@/lib/models"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
Expand Down Expand Up @@ -161,6 +161,8 @@ export default function NewPage() {
const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
const [queuedChatSeed, setQueuedChatSeed] = useState<string | null>(null)
const [queuedChatModel, setQueuedChatModel] = useState<ModelId | null>(null)
const [queuedChatReasoningEffort, setQueuedChatReasoningEffort] =
useState<ReasoningEffort | null>(null)
const [queuedChatProject, setQueuedChatProject] = useState<string | null>(
null,
)
Expand Down Expand Up @@ -490,6 +492,7 @@ export default function NewPage() {
setQueuedHighlightContent(highlightContent)
setQueuedChatSeed(userReply)
setQueuedChatModel(null)
setQueuedChatReasoningEffort(null)
setQueuedChatProject(null)
setQueuedMessageSource("highlight")
void setViewMode("chat")
Expand All @@ -498,10 +501,16 @@ export default function NewPage() {
)

const handleHomeChatStart = useCallback(
(message: string, model: ModelId, projectId: string) => {
(
message: string,
model: ModelId,
projectId: string,
reasoningEffort: ReasoningEffort,
) => {
setQueuedHighlightContent(null)
setQueuedChatSeed(message)
setQueuedChatModel(model)
setQueuedChatReasoningEffort(reasoningEffort)
setQueuedChatProject(projectId)
setQueuedMessageSource("home")
void setViewMode("chat")
Expand All @@ -512,6 +521,7 @@ export default function NewPage() {
const consumeQueuedChat = useCallback(() => {
setQueuedChatSeed(null)
setQueuedChatModel(null)
setQueuedChatReasoningEffort(null)
setQueuedChatProject(null)
setQueuedHighlightContent(null)
setQueuedMessageSource("highlight")
Expand Down Expand Up @@ -633,6 +643,7 @@ export default function NewPage() {
onConsumeQueuedMessage={consumeQueuedChat}
queuedMessageSource={queuedMessageSource}
initialSelectedModel={queuedChatModel}
initialReasoningEffort={queuedChatReasoningEffort}
initialChatProject={queuedChatProject}
/>
</div>
Expand Down
39 changes: 35 additions & 4 deletions apps/web/components/chat/home-chat-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,54 @@ import { cn } from "@lib/utils"
import type { ModelId } from "@/lib/models"
import { SpaceSelector } from "@/components/space-selector"
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
import { ReasoningSelector } from "./reasoning-selector"
import { getDefaultReasoningEffort, type ReasoningEffort } from "@/lib/models"

export function HomeChatComposer({
onStartChat,
className,
}: {
onStartChat: (message: string, model: ModelId, projectId: string) => void
onStartChat: (
message: string,
model: ModelId,
projectId: string,
reasoningEffort: ReasoningEffort,
) => void
className?: string
}) {
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(
getDefaultReasoningEffort("gemini-2.5-pro"),
)
const { selectedProject } = useProject()
const [chatSpaceProjects, setChatSpaceProjects] = useState<string[]>([
AUTO_CHAT_SPACE_ID,
])

const handleModelChange = useCallback((model: ModelId) => {
setSelectedModel(model)
setReasoningEffort(getDefaultReasoningEffort(model))
}, [])

const send = useCallback(() => {
const t = input.trim()
if (!t) return
onStartChat(t, selectedModel, chatSpaceProjects[0] ?? selectedProject)
onStartChat(
t,
selectedModel,
chatSpaceProjects[0] ?? selectedProject,
reasoningEffort,
)
setInput("")
}, [chatSpaceProjects, input, onStartChat, selectedModel, selectedProject])
}, [
chatSpaceProjects,
input,
onStartChat,
reasoningEffort,
selectedModel,
selectedProject,
])

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
Expand All @@ -52,9 +79,13 @@ export function HomeChatComposer({
<>
<ChatModelSelector
selectedModel={selectedModel}
onModelChange={setSelectedModel}
onModelChange={handleModelChange}
minimal
/>
<ReasoningSelector
value={reasoningEffort}
onChange={setReasoningEffort}
/>
<SpaceSelector
selectedProjects={chatSpaceProjects}
onValueChange={setChatSpaceProjects}
Expand Down
102 changes: 101 additions & 1 deletion apps/web/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ import { getNovaChatErrorCopy } from "@/lib/chat-stream-error"
import { useProject } from "@/stores"
import { useContainerTags } from "@/hooks/use-container-tags"
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
import { modelNames, type ModelId } from "@/lib/models"
import {
getDefaultReasoningEffort,
modelNames,
type ModelId,
type ReasoningEffort,
} from "@/lib/models"
import { SpaceSelector } from "@/components/space-selector"
import { SuperLoader } from "../superloader"
import { UserMessage } from "./message/user-message"
Expand All @@ -52,6 +57,7 @@ import { useViewMode } from "@/lib/view-mode-context"
import { threadParam } from "@/lib/search-params"
import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space"
import { ChatEmptyStatePlaceholder } from "./chat-empty-state"
import { ReasoningSelector } from "./reasoning-selector"

export function ChatLaunchFab({
onOpen,
Expand Down Expand Up @@ -103,6 +109,7 @@ export function ChatSidebar({
onConsumeQueuedMessage,
queuedMessageSource = "highlight",
initialSelectedModel = null,
initialReasoningEffort = null,
initialChatProject = null,
emptyStateSuggestions,
layout = "sidebar",
Expand All @@ -114,6 +121,7 @@ export function ChatSidebar({
onConsumeQueuedMessage?: () => void
queuedMessageSource?: "highlight" | "home"
initialSelectedModel?: ModelId | null
initialReasoningEffort?: ReasoningEffort | null
initialChatProject?: string | null
emptyStateSuggestions?: string[]
layout?: "sidebar" | "page"
Expand All @@ -124,8 +132,14 @@ export function ChatSidebar({
const [selectedModel, setSelectedModel] = useState<ModelId>(
initialSelectedModel ?? "claude-sonnet-4.6",
)
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(
initialReasoningEffort ??
getDefaultReasoningEffort(initialSelectedModel ?? "claude-sonnet-4.6"),
)
const selectedModelRef = useRef(selectedModel)
selectedModelRef.current = selectedModel
const reasoningEffortRef = useRef(reasoningEffort)
reasoningEffortRef.current = reasoningEffort
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
const [hoveredMessageId, setHoveredMessageId] = useState<string | null>(null)
const [messageFeedback, setMessageFeedback] = useState<
Expand All @@ -147,6 +161,13 @@ export function ChatSidebar({
const isScrolledToBottomRef = useRef(true)
const userJustSentRef = useRef(false)
const sentQueuedMessageRef = useRef<string | null>(null)
const truncateFromMessageIdRef = useRef<string | null>(null)
const pendingRegenerationRef = useRef<{
text: string
} | null>(null)
const [regenerationBaseLength, setRegenerationBaseLength] = useState<
number | null
>(null)
const pendingHighlightReplyRef = useRef<string | null>(null)
const awaitingHighlightInjectionRef = useRef(false)
const pendingHighlightMessageRef = useRef<UIMessage[] | null>(null)
Expand Down Expand Up @@ -238,6 +259,8 @@ export function ChatSidebar({
enableSpaceDiscovery:
selectedProjectRef.current === AUTO_CHAT_SPACE_ID,
model: selectedModelRef.current,
reasoningEffort: reasoningEffortRef.current,
truncateFromMessageId: truncateFromMessageIdRef.current,
},
},
}),
Expand Down Expand Up @@ -295,6 +318,7 @@ export function ChatSidebar({
const handleModelChange = useCallback(
(modelId: ModelId) => {
setSelectedModel(modelId)
setReasoningEffort(getDefaultReasoningEffort(modelId))
clearError()
},
[clearError],
Expand Down Expand Up @@ -336,6 +360,7 @@ export function ChatSidebar({
const handleSend = () => {
if (!input.trim() || status === "submitted" || status === "streaming")
return
truncateFromMessageIdRef.current = null
if (!threadId) setThreadId(fallbackChatId)
analytics.chatMessageSent({ source: "typed" })
sendMessage({ text: input })
Expand All @@ -347,6 +372,7 @@ export function ChatSidebar({
const handleSuggestedQuestion = useCallback(
(suggestion: string) => {
if (status === "submitted" || status === "streaming") return
truncateFromMessageIdRef.current = null
if (!threadId) setThreadId(fallbackChatId)
analytics.chatSuggestedQuestionClicked()
analytics.chatMessageSent({ source: "suggested" })
Expand All @@ -364,6 +390,57 @@ export function ChatSidebar({
],
)

const handleRegenerateFromUserMessage = useCallback(
(
messageId: string,
text: string,
model: ModelId,
nextReasoningEffort: ReasoningEffort,
) => {
const trimmed = text.trim()
if (!trimmed || status === "submitted" || status === "streaming") return
const messageIndex = messages.findIndex(
(message) => message.id === messageId,
)
if (messageIndex === -1) return

truncateFromMessageIdRef.current = messageId
selectedModelRef.current = model
reasoningEffortRef.current = nextReasoningEffort
setSelectedModel(model)
setReasoningEffort(nextReasoningEffort)
clearError()
pendingRegenerationRef.current = {
text: trimmed,
}
setRegenerationBaseLength(messageIndex)
setMessages(messages.slice(0, messageIndex))
userJustSentRef.current = true
scrollToBottom()
},
[clearError, messages, scrollToBottom, setMessages, status],
)

useEffect(() => {
const pending = pendingRegenerationRef.current
if (
!pending ||
regenerationBaseLength === null ||
messages.length !== regenerationBaseLength ||
status !== "ready"
) {
return
}

pendingRegenerationRef.current = null
setRegenerationBaseLength(null)
analytics.chatMessageSent({ source: "typed" })
sendMessage({ text: pending.text })
window.setTimeout(() => {
truncateFromMessageIdRef.current = null
}, 0)
}, [messages.length, regenerationBaseLength, sendMessage, status])

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
Expand Down Expand Up @@ -569,10 +646,18 @@ export function ChatSidebar({
setSelectedModel(initialSelectedModel)
return
}
if (
initialReasoningEffort &&
reasoningEffort !== initialReasoningEffort
) {
setReasoningEffort(initialReasoningEffort)
return
}
sentQueuedMessageRef.current = queuedMessage
analytics.chatMessageSent({ source: queuedMessageSource })

if (queuedHighlightContent) {
truncateFromMessageIdRef.current = null
// Start a fresh thread for highlight-based chats to avoid overwriting existing conversations
const newChatId = generateId()
chatIdRef.current = newChatId
Expand Down Expand Up @@ -603,6 +688,7 @@ export function ChatSidebar({
},
]
} else {
truncateFromMessageIdRef.current = null
if (!threadId) setThreadId(fallbackChatId)
sendMessage({ text: queuedMessage })
}
Expand All @@ -614,7 +700,9 @@ export function ChatSidebar({
queuedHighlightContent,
queuedMessageSource,
initialSelectedModel,
initialReasoningEffort,
selectedModel,
reasoningEffort,
status,
sendMessage,
onConsumeQueuedMessage,
Expand Down Expand Up @@ -654,6 +742,7 @@ export function ChatSidebar({
awaitingHighlightInjectionRef.current = false
const reply = pendingHighlightReplyRef.current
pendingHighlightReplyRef.current = null
truncateFromMessageIdRef.current = null
sendMessage({ text: reply })
}
}, [messages, sendMessage, status])
Expand Down Expand Up @@ -975,6 +1064,10 @@ export function ChatSidebar({
selectedModel={selectedModel}
onModelChange={handleModelChange}
/>
<ReasoningSelector
value={reasoningEffort}
onChange={setReasoningEffort}
/>
<SpaceSelector
selectedProjects={chatSpaceProjects}
onValueChange={setChatSpaceProjects}
Expand Down Expand Up @@ -1044,7 +1137,10 @@ export function ChatSidebar({
<UserMessage
message={message}
copiedMessageId={copiedMessageId}
selectedModel={selectedModel}
reasoningEffort={reasoningEffort}
onCopy={handleCopyMessage}
onRegenerate={handleRegenerateFromUserMessage}
/>
) : (
<AgentMessage
Expand Down Expand Up @@ -1172,6 +1268,10 @@ export function ChatSidebar({
onModelChange={handleModelChange}
minimal
/>
<ReasoningSelector
value={reasoningEffort}
onChange={setReasoningEffort}
/>
<SpaceSelector
selectedProjects={chatSpaceProjects}
onValueChange={setChatSpaceProjects}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/chat/message/message-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Copy, Check, ThumbsUp, ThumbsDown } from "lucide-react"
import { Check, Copy, ThumbsDown, ThumbsUp } from "lucide-react"
import { cn } from "@lib/utils"

interface MessageActionsProps {
Expand Down
Loading
Loading