Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ aef_index.csv
# Environment variables with GCP credentials
.env.local
.env.production.local
dev_server.log
41 changes: 32 additions & 9 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { saveChat, getSystemPrompt } from '@/lib/actions/chat' // Added getSyste
import { Chat, AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
import { BotMessage } from '@/components/message'
import { ReasoningDisplay } from '@/components/reasoning-display'
import { SearchSection } from '@/components/search-section'
import SearchRelated from '@/components/search-related'
import { GeoJsonLayer } from '@/components/map/geojson-layer'
Expand Down Expand Up @@ -320,24 +321,37 @@ async function submit(formData?: FormData, skip?: boolean) {
let toolOutputs: ToolResultPart[] = []
let errorOccurred = false
const streamText = createStreamableValue<string>()
uiStream.update(<Spinner />)
const reasoningStream = createStreamableValue<string>()
uiStream.update(
<>
<Section title="Thinking">
<ReasoningDisplay content={reasoningStream.value} />
</Section>
<Spinner />
</>
)

while (
useSpecificAPI
? answer.length === 0
: answer.length === 0 && !errorOccurred
) {
const { fullResponse, hasError, toolResponses } = await researcher(
currentSystemPrompt,
uiStream,
streamText,
messages,
mapProvider,
useSpecificAPI
)
const { fullResponse, hasError, toolResponses, reasoningResponse } =
await researcher(
currentSystemPrompt,
uiStream,
streamText,
reasoningStream,
messages,
mapProvider,
useSpecificAPI
)
answer = fullResponse
toolOutputs = toolResponses
errorOccurred = hasError
if (reasoningResponse) {
reasoningStream.done(reasoningResponse)
}

if (toolOutputs.length > 0) {
toolOutputs.map(output => {
Expand Down Expand Up @@ -595,6 +609,15 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
const answer = createStreamableValue()
answer.done(content)
switch (type) {
case 'reasoning':
return {
id,
component: (
<Section title="Thinking">
<ReasoningDisplay content={answer.value} />
</Section>
)
}
case 'response':
return {
id,
Expand Down
10 changes: 6 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions components/reasoning-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import { StreamableValue, useStreamableValue } from 'ai/rsc'
import { MemoizedReactMarkdown } from './ui/markdown'

export function ReasoningDisplay({
content
}: {
content: StreamableValue<string>
}) {
const [data, error, pending] = useStreamableValue(content)

if (error) {
return <div>Error</div>
}

if (pending) {
return null
}

return (
<div className="overflow-x-auto">
<MemoizedReactMarkdown className="prose-sm prose-neutral prose-a:text-accent-foreground/50">
{data || ''}
</MemoizedReactMarkdown>
</div>
)
}
4 changes: 4 additions & 0 deletions components/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { cn } from '@/lib/utils'
import {
BookCheck,
Bot,
Film,
Image,
MessageCircleMore,
Expand Down Expand Up @@ -49,6 +50,9 @@ export const Section: React.FC<SectionProps> = ({
case 'Follow-up':
icon = <MessageCircleMore size={18} className="mr-2" />
break
case 'Thinking':
icon = <Bot size={18} className="mr-2" />
break
default:
icon = <Search size={18} className="mr-2" />
}
Expand Down
11 changes: 0 additions & 11 deletions dev_server.log

This file was deleted.

30 changes: 27 additions & 3 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// lib/agents/researcher.tsx
import { createStreamableUI, createStreamableValue } from 'ai/rsc'
import { createStreamableUI, createStreamableValue, getMutableAIState } from 'ai/rsc'
import {
CoreMessage,
LanguageModel,
Expand All @@ -12,6 +12,7 @@ import { BotMessage } from '@/components/message'
import { getTools } from './tools'
import { getModel } from '../utils'
import { MapProvider } from '@/lib/store/settings'
import { nanoid } from 'nanoid'

// This magic tag lets us write raw multi-line strings with backticks, arrows, etc.
const raw = String.raw
Expand Down Expand Up @@ -78,11 +79,13 @@ export async function researcher(
dynamicSystemPrompt: string,
uiStream: ReturnType<typeof createStreamableUI>,
streamText: ReturnType<typeof createStreamableValue<string>>,
reasoningStream: ReturnType<typeof createStreamableValue<string>>,
messages: CoreMessage[],
mapProvider: MapProvider,
useSpecificModel?: boolean
) {
let fullResponse = ''
let reasoningResponse = ''
let hasError = false

const answerSection = (
Expand Down Expand Up @@ -128,7 +131,12 @@ export async function researcher(
streamText.update(fullResponse)
}
break

case 'reasoning':
if (delta.textDelta) {
reasoningResponse += delta.textDelta
reasoningStream.update(reasoningResponse)
}
break
case 'tool-call':
toolCalls.push(delta)
break
Expand Down Expand Up @@ -157,5 +165,21 @@ export async function researcher(
messages.push({ role: 'tool', content: toolResponses })
}

return { result, fullResponse, hasError, toolResponses }
if (reasoningResponse) {
const aiState = getMutableAIState()
aiState.update({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: nanoid(),
role: 'assistant',
content: reasoningResponse,
type: 'reasoning'
}
]
})
}
Comment on lines +172 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change writes reasoningResponse to persisted aiState and renders it with MemoizedReactMarkdown. If the provider emits structured or sensitive internal reasoning, you are explicitly persisting and re-displaying it after reload. That’s a product/security decision, but it needs a clear guard/flag because it increases risk (PII leakage, prompt-injection artifacts, policy issues) and can significantly bloat stored chat history.

At minimum, this should be gated behind a user setting or server-side config and/or truncated/summarized before persistence.

Suggestion

Gate persistence behind an explicit flag (e.g., persistReasoning), and consider truncation to a safe max length.

const MAX_REASONING_CHARS = 20_000

if (persistReasoning && reasoningResponse) {
  const persisted = reasoningResponse.slice(0, MAX_REASONING_CHARS)
  const aiState = getMutableAIState()
  aiState.update({
    ...aiState.get(),
    messages: [
      ...aiState.get().messages,
      { id: nanoid(), role: 'assistant', content: persisted, type: 'reasoning' }
    ]
  })
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.


return { result, fullResponse, hasError, toolResponses, reasoningResponse }
}
1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export type AIMessage = {
| 'end'
| 'drawing_context' // Added custom type for drawing context messages
| 'resolution_search_result'
| 'reasoning'
}

export type CalendarNote = {
Expand Down
16 changes: 13 additions & 3 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ export function getModel(requireVision: boolean = false) {
// Gemini 3 Pro
if (gemini3ProApiKey) {
const google = createGoogleGenerativeAI({
apiKey: gemini3ProApiKey,
apiKey: gemini3ProApiKey
})
try {
return google('gemini-3-pro-preview')
// Enable Gemini's "thinking mode" to stream reasoning steps.
// See: https://ai-sdk.dev/cookbook/guides/gemini#enhanced-reasoning-with-thinking-mode
const google = createGoogleGenerativeAI({
apiKey: gemini3ProApiKey
})
return google('gemini-3-pro-preview', {
thinkingLevel: 'low'
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getModel() now creates the Gemini client twice (createGoogleGenerativeAI(...) before the try, and again inside the try). This is redundant and makes it harder to reason about provider configuration and future options (e.g., shared headers, retries).

Suggestion

Create the client once and reuse it. Also avoid shadowing the google identifier.

// Gemini 3 Pro
if (gemini3ProApiKey) {
  const gemini = createGoogleGenerativeAI({ apiKey: gemini3ProApiKey })
  try {
    return gemini('gemini-3-pro-preview', {
      // Enable Gemini's "thinking mode"
      thinkingLevel: 'low'
    })
  } catch (error) {
    console.warn('Gemini 3 Pro API unavailable, falling back to next provider:', error)
  }
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

} catch (error) {
console.warn('Gemini 3 Pro API unavailable, falling back to next provider:', error)
console.warn(
'Gemini 3 Pro API unavailable, falling back to next provider:',
error
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dependencies": {
"@ai-sdk/amazon-bedrock": "^1.1.6",
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/google": "^1.2.22",
"@ai-sdk/google": "^3.0.6",
"@ai-sdk/openai": "^1.3.24",
"@ai-sdk/xai": "^1.2.18",
"@composio/core": "^0.3.3",
Expand Down