Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 2 additions & 6 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ export const AI = createAI<AIState, UIState>({
userId: actualUserId,
path,
title,
messages: updatedMessages.filter(msg => !(msg.content === 'end' && msg.type === 'end'))
messages: updatedMessages

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

end messages are now persisted to the DB and accumulate across saves.

updatedMessages includes the appended { content: 'end', type: 'end' } message (lines 641–649). Since the mapping in saveChat (lib/actions/chat.ts, lines 213–219) drops type, this message is stored as role: 'assistant', content: 'end'. Each subsequent save appends a new end message with a fresh nanoid(), so multiple end rows accumulate per chat over time. While these are filtered from the UI on rehydration (no typenull), they waste storage and could confuse message-count or ordering logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/actions.tsx` at line 664, Prevent the synthetic `{ content: 'end', type:
'end' }` message from being included in the `updatedMessages` payload passed to
`saveChat` in the save flow around `app/actions.tsx`. Filter or remove this
terminal marker before persistence while preserving all genuine chat messages
and the existing UI completion behavior.

}
await saveChat(chat, actualUserId)
}
Expand Down Expand Up @@ -710,8 +710,6 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
id,
component: <CopilotDisplay content={content as string} />
}
default:
return null
}
break
case 'assistant':
Expand Down Expand Up @@ -772,8 +770,6 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
)
}
}
default:
return null
}
break
case 'tool':
Expand Down Expand Up @@ -872,5 +868,5 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}
})
.filter(message => message !== null && message !== undefined) as UIState
.filter(message => message !== null) as UIState
}
11 changes: 7 additions & 4 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function generateMetadata({ params }: SearchPageProps) {
// TODO: Metadata generation might need authenticated user if chats are private
// For now, assuming getChat can be called or it handles anon access for metadata appropriately
const userId = await getCurrentUserIdOnServer(); // Attempt to get user for metadata
const chat = await getChat(id, userId || undefined); // Pass userId or undefined if none (allows public fallback)
const chat = await getChat(id, userId || 'anonymous'); // Pass userId or 'anonymous' if none
return {
title: chat?.title?.toString().slice(0, 50) || 'Search',
};
Expand Down Expand Up @@ -48,11 +48,14 @@ export default async function SearchPage({ params }: SearchPageProps) {
const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => {
return {
id: dbMsg.id,
role: dbMsg.role as AIMessage['role'],
role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
type: dbMsg.type ? (dbMsg.type as AIMessage['type']) : undefined,
name: dbMsg.name || undefined,
// 'type' and 'name' are not in the basic Drizzle 'messages' schema.
// These would be undefined unless specific logic is added to derive them.
// For instance, if a message with role 'tool' should have a 'name',
// or if some messages have a specific 'type' based on content or other flags.
// This mapping assumes standard user/assistant messages primarily.
Comment on lines +51 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the Chat component or any other mechanism reconstructs UI state
# from AI state on page load, which could bypass getUIStateFromAIState.

# Check how the Chat component consumes AI/UI state
ast-grep outline components/chat.tsx --items all --type function,component

# Search for getUIState or onGetUIState usage to understand when it's called
rg -nC3 'getUIState|onGetUIState' --type=ts --glob '!**/node_modules/**'

# Check if initialUIState is set to anything other than empty array
rg -nC3 'initialUIState' app/actions.tsx

Repository: QueueLab/QCX

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files 'app/search/[id]/page.tsx' 'app/actions.tsx' 'components/chat.tsx'

printf '\n== Outline: app/search/[id]/page.tsx ==\n'
ast-grep outline 'app/search/[id]/page.tsx' || true

printf '\n== Outline: app/actions.tsx ==\n'
ast-grep outline 'app/actions.tsx' || true

printf '\n== Outline: components/chat.tsx ==\n'
ast-grep outline 'components/chat.tsx' || true

printf '\n== Search for getUIState / onGetUIState / initialUIState ==\n'
rg -nC3 'getUIState|onGetUIState|initialUIState' --glob '!**/node_modules/**' app components || true

printf '\n== Relevant slices: app/actions.tsx ==\n'
sed -n '640,720p' app/actions.tsx || true

printf '\n== Relevant slices: components/chat.tsx ==\n'
sed -n '1,260p' components/chat.tsx || true

printf '\n== Relevant slice: app/search/[id]/page.tsx ==\n'
sed -n '1,180p' 'app/search/[id]/page.tsx' || true

Repository: QueueLab/QCX

Length of output: 17459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the specific message/UI types to understand whether `type` is required
# and how the page reconstructs state.
rg -nC3 'type:|UIMessage|AIMessage|Message' app components --glob '!**/node_modules/**' || true

# Look for any conversion from DB messages to UI messages besides app/search/[id]/page.tsx
rg -nC3 'messages.map|createdAt:|role: dbMsg.role|content: dbMsg.content' app components --glob '!**/node_modules/**' || true

Repository: QueueLab/QCX

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Match files ==\n'
git ls-files | rg '^(app/actions\.tsx|app/search/\[id\]/page\.tsx|components/chat\.tsx)$'

printf '\n== app/actions.tsx around getUIStateFromAIState ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('app/actions.tsx')
text = p.read_text()
for needle in ['getUIStateFromAIState', 'initialUIState', 'onGetUIState', 'type ===', '!type']:
    idx = text.find(needle)
    print(f'needle={needle!r} idx={idx}')
    if idx != -1:
        start = max(0, text.rfind('\n', 0, idx-200))
        end = text.find('\n', idx+400)
        print(text[start:end])
        print('---')
PY

printf '\n== components/chat.tsx around state wiring ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('components/chat.tsx')
text = p.read_text()
for needle in ['initialUIState', 'onGetUIState', 'getUIState', 'useUIState', 'useChat']:
    idx = text.find(needle)
    print(f'needle={needle!r} idx={idx}')
    if idx != -1:
        start = max(0, text.rfind('\n', 0, idx-250))
        end = text.find('\n', idx+500)
        print(text[start:end])
        print('---')
PY

printf '\n== app/search/[id]/page.tsx around DB message mapping ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('app/search/[id]/page.tsx')
text = p.read_text()
for needle in ['createdAt: dbMsg.createdAt', 'role: dbMsg.role', 'type:', 'messages.map', 'getUIStateFromAIState']:
    idx = text.find(needle)
    print(f'needle={needle!r} idx={idx}')
    if idx != -1:
        start = max(0, text.rfind('\n', 0, idx-250))
        end = text.find('\n', idx+500)
        print(text[start:end])
        print('---')
PY

Repository: QueueLab/QCX

Length of output: 6472


Restore type when hydrating messages (app/search/[id]/page.tsx:51-58) getUIStateFromAIState drops messages with no type, so reopening an existing chat renders an empty transcript unless the message type is reconstructed or persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/search/`[id]/page.tsx around lines 51 - 58, Update
getUIStateFromAIState’s message hydration mapping to restore a valid type for
every persisted message instead of leaving type undefined. Reconstruct the type
from each dbMsg’s available role/content or persisted metadata, preserving the
existing role, content, and createdAt mappings so reopening a chat renders its
transcript.

};
});

Expand Down
3 changes: 0 additions & 3 deletions drizzle/migrations/0009_add_message_type_name.sql

This file was deleted.

Loading