Skip to content

Conversation

@AndlerRL
Copy link
Member

@AndlerRL AndlerRL commented Jan 13, 2026

Summary by CodeRabbit

  • New Features

    • Embedding-backed memory ingestion, similarity search, scoped RAG retrieval, and document‑operation logging.
    • Document‑outline view for document‑anchored conversation browsing.
    • Prompt Enhancer: model‑aware structured prompt builder and quick modifiers.
  • Improvements

    • Richer message/document metadata and scope‑aware thread behavior.
    • Safer, more robust client/server integration and UI stability/hydration fixes.
  • Documentation

    • Expanded architecture and onboarding guides.
  • Tests

    • New unit and integration suites for outline, markdown, RAG, and workspace flows.

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/pro-web/components/routes/thread/thread-popup.tsx (1)

178-186: Unsafe property access on untyped metadata.

Line 181 accesses currentThread?.metadata?.documents?.length but metadata is typed as unknown. This works at runtime due to optional chaining but bypasses TypeScript's type checking.

Consider using the getThreadMarkdownFromMetadata utility's pattern or the typed interface suggested earlier:

- !currentThread?.metadata?.documents?.length || !messageTitle
+ !(threadMeta as ThreadMetadata | null)?.documents?.length || !messageTitle
apps/pro-web/components/routes/chat/chat-options.tsx (1)

61-76: Fix memo deps so share URLs update with subthread changes.

useMemo now depends on isSubThread/pair, but the dependency array is still [thread]. If pair changes while thread stays stable, the URL can be stale and share the wrong subthread. Consider including the new inputs (or dropping the memo).

✅ Suggested fix
- // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
- const url = useMemo(
+ const url = useMemo(
   () =>
     !thread.slug
       ? '/'
       : urlBuilders.profilesThreadUrl({
           type: 'user',
           threadSlug: thread.slug,
           threadQuestionSlug: isSubThread
             ? (pair as any).userMessage.slug
             : undefined,
           category: thread.chatbot.department?.name || 'general',
           chatbot: toSlug(thread.chatbot.name),
           usernameSlug: thread?.user?.slug,
           domain: canonicalDomain,
         }),
-  [thread],
+  [thread, canonicalDomain, isSubThread, pair],
 )
apps/pro-web/lib/metadata.ts (1)

90-102: Verify role casing convention to prevent metadata defaults.

The role comparisons at lines 94 and 101 use uppercase literals ('USER', 'ASSISTANT') without case normalization. If roles are stored in lowercase or mixed case in the database (similar to how the codebase normalizes other data), these checks will fail silently and metadata will default to "not found."

Confirm the actual casing convention for the role field in the message schema. If roles are lowercase, either normalize the comparisons using .toUpperCase() or ensure consistent casing at the storage layer.

apps/pro-web/lib/utils.ts (1)

88-97: Guard role before lowercasing.

message.role.toLowerCase() will throw if a role is ever missing/undefined. Consider optional chaining to keep this resilient.

🛠️ Suggested fix
-		if (message.role.toLowerCase() === 'user') {
+		if (message.role?.toLowerCase() === 'user') {
🤖 Fix all issues with AI agents
In `@apps/pro-web/components/routes/chat/chat-list/message-pair-accordion.tsx`:
- Around line 276-355: The URL slug logic in the accordion open/close block
(using isOpen, activeThreadQuestionSlug, userQuestionSlug and navigateTo in
message-pair-accordion.tsx) should guard on slug equality: when opening (isOpen
&& !activeThreadQuestionSlug) only call navigateTo to set the question slug if
activeThreadQuestionSlug !== userQuestionSlug (i.e., the slug actually changed),
and when closing (!isOpen && activeThreadQuestionSlug) only navigate to remove
the slug if activeThreadQuestionSlug === userQuestionSlug (so closing a
non-active accordion doesn't clear another message's slug); update both the
'profiles' and 'org'/'pro' branches to apply these conditions before calling
navigateTo.

In `@apps/pro-web/components/routes/thread/thread-component.tsx`:
- Around line 58-61: The code uses m.role.toLowerCase() which will throw if
m.role is null/undefined; update the logic that computes threadTitle
(referencing threadTitle and the array filter callback using m.role) to safely
normalize role by using optional chaining or a fallback string (e.g., treat
missing role as empty string) before calling toLowerCase(), and keep the rest of
the expression unchanged so you still filter for 'user' roles and access message
content.

In `@apps/pro-web/components/routes/thread/user-thread-panel.tsx`:
- Around line 563-566: The code building the optimistic thread's messages uses
thread.messages.filter((msg) => msg.role.toLowerCase() === 'user') which will
throw if msg.role is null/undefined; update the predicate to guard/normalize
role before calling toLowerCase (e.g., check typeof msg.role === 'string' or use
a safe normalization like (msg.role || '').toLowerCase()) so the filter for user
messages in the optimistic thread creation does not crash.

In `@apps/pro-web/lib/hooks/use-mb-chat.tsx`:
- Around line 1119-1169: The derived scopeTitleSnapshot is computed but never
used—replace uses of scopeTitleSnapshotForMeta with the computed
scopeTitleSnapshot when building mb_op_meta and when setting opMetaRef so the
fallback title is propagated; update the enrichedChatOptions construction (and
the parallel body in the else branch) and any opMetaRef assignment in
use-mb-chat.tsx to reference scopeTitleSnapshot (or a single unified variable
like scopeTitleSnapshotFinal) instead of scopeTitleSnapshotForMeta so both
caller-provided and derived values are consistently included in mb_op_meta and
opMetaRef.

In `@apps/pro-web/lib/hooks/use-workspace-chat.tsx`:
- Around line 946-953: The scoped history fetch call to
selectScopeConversationWindow is missing docNodeId so it won't match the new
storage shape; update the call site where you pass { threadId, scopeType,
scopeId, jwt } to include docNodeId (use the local docNodeId variable) so the
function can match using docNodeId + bare scopeId, and apply the same change to
the other identical call site in this file where selectScopeConversationWindow
is invoked (the one that mirrors the heading-thread branch).
♻️ Duplicate comments (4)
apps/pro-web/components/routes/browse/browse-list-item.tsx (2)

19-76: Duplicate initialUrl effects still present.

This matches the previously reported duplicate-effect issue (two effects both writing initialUrl). Please consolidate to a single effect.


146-148: Visibility comment still conflicts with behavior.

Returning null unmounts the component; the comment says it stays mounted. This matches the previously flagged mismatch.

apps/pro-web/services/hasura/hasura.service.ts (1)

792-798: Retry failure is swallowed and falls through to a generic error.

If the retry fails, the code logs retryErr and then throws the generic error for the original attempt, which obscures the real failure cause.

apps/pro-web/lib/hooks/use-workspace-chat.tsx (1)

974-981: Avoid the scopedHistory as any cast.

This bypasses the expected MessageDB shape and hides real mismatches.

🧹 Nitpick comments (11)
apps/pro-web/components/routes/thread/thread-popup.tsx (2)

124-130: Consider creating a typed interface for thread metadata.

The biome-ignore comments suppress linting for any casts. While the runtime checks in pickString provide safety, defining a partial interface for expected metadata fields would improve type safety and IDE support.

♻️ Suggested type definition
interface ThreadMetadata {
  docNodeId?: string
  doc_node_id?: string
  documentMarkdown?: string
  markdown?: string
  docMarkdown?: string
  content?: string
  documents?: Array<{
    content?: string
    markdown?: string
    docMarkdown?: string
  }>
}

Then use const threadMeta = (currentThread?.metadata as ThreadMetadata | null) ?? null


151-163: Type safety concerns with role property access.

Multiple any casts are used to access .role on messages. If Message from mb-genql has a role property, prefer a type guard. If not, consider a union type for the messages array.

♻️ Suggested type guard approach
function hasRole(m: AiMessage | Message): m is AiMessage | (Message & { role: string }) {
  return 'role' in m && typeof (m as any).role === 'string'
}

// Usage:
if (!message || !hasRole(message) || message.role.toLowerCase() !== 'assistant') {
  // ...
}
#!/bin/bash
# Check if Message type from mb-genql includes a role property
ast-grep --pattern 'export type Message = {
  $$$
}'

# Also check the schema for message table structure
rg -n "role" packages/mb-genql/src/ --type ts -A2 -B2 | head -50
apps/pro-web/components/routes/chat/chat-message.tsx (1)

176-180: Avoid any cast for message to preserve type safety.

Passing message as any hides mismatches; better to align ChatMessageActions prop types with the actual message union and pass message directly. As per coding guidelines, avoid any.

♻️ Proposed change (plus align ChatMessageActions prop types)
-					message={message as any}
+					message={message}
apps/pro-web/components/routes/chat/chat-message-renderer.tsx (1)

50-67: Avoid any casts in render branches.

The explicit message as any masks contract drift; prefer updating the target component prop types (or a shared message union) and pass message directly. As per coding guidelines, avoid any.

♻️ Proposed change (plus align prop types)
-				message={message as any}
+				message={message}
...
-			message={message as any}
+			message={message}
apps/pro-web/lib/threads.ts (1)

11-14: Replace any in MessagePair with a minimal message shape.

Using any here makes downstream consumers brittle and hides mistakes. Consider a minimal shared interface for the fields you actually use.

♻️ Suggested refactor
+interface MessageLike {
+	role: string
+	content: string
+	id?: string
+	messageId?: string
+}
+
 export interface MessagePair {
-	userMessage: any
-	chatGptMessage: any[]
+	userMessage: MessageLike
+	chatGptMessage: MessageLike[]
 }
apps/pro-web/components/routes/chat/chat-message-actions.tsx (2)

36-38: Define a typed message shape and normalize access once.

The repeated (message as any) casts and unguarded content usage risk propagating undefined into clipboard/documents. A small normalization helper reduces casts and lets you add a single content guard. It also makes it easy to normalize role checks when locating the preceding user message.

♻️ Suggested refactor
+interface MessageLike {
+	id?: string
+	messageId?: string
+	content?: string
+	role?: string
+}
+
 export function ChatMessageActions({
 	message,
@@
 }: ChatMessageActionsProps) {
+	const normalized = message as MessageLike
+	const normalizedMessageId = normalized.messageId ?? normalized.id ?? ''
+	const normalizedContent = normalized.content ?? ''
+	const normalizedRole = normalized.role?.toLowerCase()
+
-	const messageId = (message as any)?.messageId || (message as any).id
+	const messageId = normalizedMessageId
@@
-		copyToClipboard((message as any).content)
+		if (!normalizedContent) return
+		copyToClipboard(normalizedContent)
@@
-				content: (message as any).content,
+				content: normalizedContent,
@@
-				content: (message as any).content,
+				content: normalizedContent,
@@
-	const normalizedMessageContent = ((message as any).content || '').trim()
+	const normalizedMessageContent = normalizedContent.trim()
@@
-		if (currentContent.includes((message as any).content)) {
+		if (normalizedContent && currentContent.includes(normalizedContent)) {
@@
-${(message as any).content}`
-			: (message as any).content
+${normalizedContent}`
+			: normalizedContent
@@
-	const messageTitle = toSlug((message as any).content)
+	const messageTitle = toSlug(normalizedContent)

Also applies to: 78-113, 212-219, 253-295, 322-323


341-345: Avoid inline arrow handlers in JSX.

Extract a stable handler (e.g., useCallback) so the prop reference doesn’t churn on every render.

♻️ Suggested refactor
+	const handleAddToActiveDocument = useCallback(() => {
+		;(onAddToActiveDocument ?? onAddToActiveDocumentHandler)(messageId)
+	}, [onAddToActiveDocument, onAddToActiveDocumentHandler, messageId])
@@
-							onClick={() =>
-								(onAddToActiveDocument ?? onAddToActiveDocumentHandler)(
-									(message as any)?.messageId || (message as any).id,
-								)
-							}
+							onClick={handleAddToActiveDocument}
apps/pro-web/lib/helpers/ai-helpers.ts (1)

320-334: Tighten any in reasoning helpers.

These helpers only need a small subset of fields; any removes safety and IntelliSense. Consider a narrow interface for parts/thinking.

♻️ Suggested refactor
+interface ReasoningPart {
+	type?: string
+	reasoning?: string
+}
+interface ReasoningMessageLike {
+	parts?: ReasoningPart[]
+	thinking?: string | null
+}
-
-export function hasReasoning(message: any): boolean {
+export function hasReasoning(message: ReasoningMessageLike): boolean {
 	return Boolean(
-		message.parts?.some((part: any) => part.type === 'reasoning') ||
+		message.parts?.some((part) => part.type === 'reasoning') ||
 			message?.thinking,
 	)
 }
@@
-export function extractReasoningContent(
-	message: any,
-): string | null | undefined {
+export function extractReasoningContent(
+	message: ReasoningMessageLike,
+): string | null | undefined {
 	if (message.parts?.length) {
 		const reasoningPart = message.parts.find(
-			(part: any) => part.type === 'reasoning',
+			(part) => part.type === 'reasoning',
 		)
apps/pro-web/services/hasura/hasura.service.ts (1)

726-740: Avoid any in saveNewMessage param handling.

The types already allow docNodeId / doc_node_id, so the any casts are avoidable and reduce safety.
As per coding guidelines, avoid any.

♻️ Suggested refactor
 export async function saveNewMessage(
 	params: Partial<SaveNewMessageParams> & { doc_node_id?: string | null },
 ) {
-	const { jwt, docNodeId, doc_node_id, ...object } = params as any
+	const { jwt, docNodeId, doc_node_id, ...object } = params
 	const client = getHasuraClient({ jwt })

 	// During migration, callers may pass either camelCase or snake_case.
 	// Only send the camelCase field to Hasura (GraphQL naming is camelCase here).
-	const canonicalDocNodeId: string | null = (docNodeId ??
-		doc_node_id ??
-		null) as any
+	const canonicalDocNodeId: string | null = docNodeId ?? doc_node_id ?? null
apps/pro-web/components/routes/chat/chat-list/message-pair-accordion.tsx (2)

415-415: Avoid inline onToggle handler for a stable reference.

This keeps handler identity stable and aligns with the no‑inline‑handlers guideline.

♻️ Suggested refactor
+	const isFirstQuestion = index === 0
+	const handleToggle = useCallback(
+		(open: boolean) => toggleThreadQuestionUrl(open, isFirstQuestion),
+		[toggleThreadQuestionUrl, isFirstQuestion],
+	)
@@
-			onToggle={(open) => toggleThreadQuestionUrl(open, index === 0)}
+			onToggle={handleToggle}

As per coding guidelines, prefer stable function references for JSX handlers.


456-516: Replace any casts and stabilize message keys/ids.

The any accesses bypass type safety and Fragment keys can end up undefined. Consider a lightweight message‑like type plus getMessageStableId for keys/ids. If MessageRenderer types are too narrow, widen them to the actual message shape instead of casting to any.

♻️ Suggested refactor
 type ThreadLike = {
 	slug?: string | null
 	chatbot?: { name?: string | null } | null
 	thread?: { user?: { username?: string | null } | null } | null
 }
 
+type ChatMessageLike = {
+	id?: string | null
+	slug?: string | null
+	threadId?: string | null
+	content?: string | null
+}
+
+const asChatMessageLike = (v: unknown): ChatMessageLike | undefined =>
+	isObject(v) ? (v as ChatMessageLike) : undefined
@@
-					{currentThread && shouldShowUserMessage && (
+					{currentThread && shouldShowUserMessage && (
 						<ChatOptions
-							threadId={(pair.userMessage as any).threadId}
+							threadId={asChatMessageLike(pair.userMessage)?.threadId}
 							thread={currentThread}
 							isBrowse={false}
 							pair={pair}
 						/>
 					)}
@@
-					{(pair.chatGptMessage[0] as any)?.content ? (
+					{asChatMessageLike(pair.chatGptMessage[0])?.content ? (
 						<div className="flex-1 px-1 space-y-2 overflow-hidden text-left">
 							<ShortMessage
-								content={(pair.chatGptMessage[0] as any)?.content}
+								content={asChatMessageLike(pair.chatGptMessage[0])?.content}
 							/>
 						</div>
 					) : (
 						''
 					)}
@@
-					? pair.chatGptMessage.map((message) => (
-							<Fragment key={(message as any).id}>
+					? pair.chatGptMessage.map((message, messageIndex) => {
+							const messageKey = getMessageStableId(
+								asChatMessageLike(message),
+								`${accordionId}-response-${messageIndex}`,
+							)
+							return (
+								<Fragment key={messageKey}>
 								{!isThread && (
 									<AttachmentCards
 										userAttachments={userAttachments}
 										isAccordionFocused={isAccordionFocused}
 									/>
 								)}
 								<MessageRenderer
-									id={(message as any).slug}
-									message={message as any}
+									id={asChatMessageLike(message)?.slug ?? messageKey}
+									message={message}
 									sendMessageFromResponse={props.sendMessageFn}
 									onConvertToWorkspaceDocument={onConvertToWorkspaceDocument}
 								/>
 							</Fragment>
-						))
+							)
+						})
 					: ''}

As per coding guidelines, avoid any and prefer explicit message shapes.

Comment on lines +276 to +355
// OPEN: go to question URL (if not already)
if (isOpen && !activeThreadQuestionSlug) {
if (route.kind === 'profiles') {
const usernameSlug =
route.usernameSlug ||
firstParam((params as Record<string, unknown>)?.userSlug) ||
''
if (!usernameSlug) return

navigateTo({
urlType: 'profilesThreadQuestionUrl',
shallow: true,
navigationParams: {
type: route.type,
usernameSlug,
category,
domain,
chatbot,
threadSlug,
threadQuestionSlug: userQuestionSlug,
},
})
return
}

if (!threadQuestionSlug && isOpen) {
navigateTo({
urlType: isProfile
? 'profilesThreadQuestionUrl'
: 'threadQuestionUrl',
shallow: true,
navigationParams: isProfile
? {
type: 'user',
...navigationParts,
}
: {
type: isOrg ? 'org' : isBot ? 'bot' : 'pro',
...navigationParts,
},
})
if (route.kind === 'org' || route.kind === 'pro') {
navigateTo({
urlType: 'threadQuestionUrl',
shallow: true,
navigationParams: {
type: route.type,
category,
domain,
chatbot,
threadSlug,
threadQuestionSlug: userQuestionSlug,
},
})
}

return
}
if (threadQuestionSlug && !isOpen) {
navigateTo({
urlType: isProfile ? 'profilesThreadUrl' : 'threadUrl',
shallow: true,
navigationParams: isProfile
? {
type: 'user',
...navigationParts,
}
: {
type: isOrg ? 'org' : isBot ? 'bot' : 'pro',
...navigationParts,
},
})

// CLOSE: go back to thread URL (remove question slug)
if (!isOpen && activeThreadQuestionSlug) {
if (route.kind === 'profiles') {
const usernameSlug =
route.usernameSlug ||
firstParam((params as Record<string, unknown>)?.userSlug) ||
''
if (!usernameSlug) return

navigateTo({
urlType: 'profilesThreadUrl',
shallow: true,
navigationParams: {
type: route.type,
usernameSlug,
category,
domain,
chatbot,
threadSlug,
},
})
return
}

if (route.kind === 'org' || route.kind === 'pro') {
navigateTo({
urlType: 'threadUrl',
shallow: true,
navigationParams: {
type: route.type,
category,
domain,
chatbot,
threadSlug,
},
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

URL slug can get out of sync when switching accordions.

If a slug is already present, opening a different accordion won’t update the URL, and closing a non‑active accordion can clear the slug for another message. Guard on slug equality and update when it changes.

🐛 Proposed fix
-			// OPEN: go to question URL (if not already)
-			if (isOpen && !activeThreadQuestionSlug) {
+			const isActiveSlug = activeThreadQuestionSlug === userQuestionSlug
+
+			// OPEN: go to question URL (if not already)
+			if (isOpen && (!activeThreadQuestionSlug || !isActiveSlug)) {
 				if (route.kind === 'profiles') {
 					const usernameSlug =
 						route.usernameSlug ||
 						firstParam((params as Record<string, unknown>)?.userSlug) ||
 						''
 					if (!usernameSlug) return
@@
-			// CLOSE: go back to thread URL (remove question slug)
-			if (!isOpen && activeThreadQuestionSlug) {
+			// CLOSE: go back to thread URL (remove question slug)
+			if (!isOpen && isActiveSlug) {
 				if (route.kind === 'profiles') {
 					const usernameSlug =
 						route.usernameSlug ||
 						firstParam((params as Record<string, unknown>)?.userSlug) ||
 						''
 					if (!usernameSlug) return
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/chat/chat-list/message-pair-accordion.tsx`
around lines 276 - 355, The URL slug logic in the accordion open/close block
(using isOpen, activeThreadQuestionSlug, userQuestionSlug and navigateTo in
message-pair-accordion.tsx) should guard on slug equality: when opening (isOpen
&& !activeThreadQuestionSlug) only call navigateTo to set the question slug if
activeThreadQuestionSlug !== userQuestionSlug (i.e., the slug actually changed),
and when closing (!isOpen && activeThreadQuestionSlug) only navigate to remove
the slug if activeThreadQuestionSlug === userQuestionSlug (so closing a
non-active accordion doesn't clear another message's slug); update both the
'profiles' and 'org'/'pro' branches to apply these conditions before calling
navigateTo.

Comment on lines 58 to +61
const threadId = thread.threadId
const threadTitle = (
thread.thread ? thread.thread.messages : thread.messages
).filter((m) => m.role === 'user')[0]?.content
).filter((m) => m.role.toLowerCase() === 'user')[0]?.content
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard role normalization to avoid runtime errors.

If m.role can be null/undefined, toLowerCase() will throw. Consider optional chaining or a fallback.

🔧 Safer normalization
-	).filter((m) => m.role.toLowerCase() === 'user')[0]?.content
+	).filter((m) => m.role?.toLowerCase() === 'user')[0]?.content
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const threadId = thread.threadId
const threadTitle = (
thread.thread ? thread.thread.messages : thread.messages
).filter((m) => m.role === 'user')[0]?.content
).filter((m) => m.role.toLowerCase() === 'user')[0]?.content
const threadId = thread.threadId
const threadTitle = (
thread.thread ? thread.thread.messages : thread.messages
).filter((m) => m.role?.toLowerCase() === 'user')[0]?.content
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/thread/thread-component.tsx` around lines 58 -
61, The code uses m.role.toLowerCase() which will throw if m.role is
null/undefined; update the logic that computes threadTitle (referencing
threadTitle and the array filter callback using m.role) to safely normalize role
by using optional chaining or a fallback string (e.g., treat missing role as
empty string) before calling toLowerCase(), and keep the rest of the expression
unchanged so you still filter for 'user' roles and access message content.

Comment on lines 563 to +566
// ? These messages doesn't go to the allMessages directly, allMessages filters the true content.
messages: [thread.messages.filter((msg) => msg.role === 'user')[0]],
messages: [
thread.messages.filter((msg) => msg.role.toLowerCase() === 'user')[0],
],
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard msg.role before toLowerCase() to avoid crashes.

If role is nullable, this can throw while building the optimistic thread.

🔧 Safer normalization
-			thread.messages.filter((msg) => msg.role.toLowerCase() === 'user')[0],
+			thread.messages.filter((msg) => msg.role?.toLowerCase() === 'user')[0],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ? These messages doesn't go to the allMessages directly, allMessages filters the true content.
messages: [thread.messages.filter((msg) => msg.role === 'user')[0]],
messages: [
thread.messages.filter((msg) => msg.role.toLowerCase() === 'user')[0],
],
// ? These messages doesn't go to the allMessages directly, allMessages filters the true content.
messages: [
thread.messages.filter((msg) => msg.role?.toLowerCase() === 'user')[0],
],
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/thread/user-thread-panel.tsx` around lines 563
- 566, The code building the optimistic thread's messages uses
thread.messages.filter((msg) => msg.role.toLowerCase() === 'user') which will
throw if msg.role is null/undefined; update the predicate to guard/normalize
role before calling toLowerCase (e.g., check typeof msg.role === 'string' or use
a safe normalization like (msg.role || '').toLowerCase()) so the filter for user
messages in the optimistic thread creation does not crash.

Comment on lines +1119 to +1169
let scopeTitleSnapshot: string | null = scope?.scopeTitleSnapshot ?? null

// If caller didn't provide it, derive it safely (only for heading threads)
if (
!scopeTitleSnapshot &&
scopeTypeForMeta === 'heading_thread' &&
scopeIdForMeta
) {
const effectiveSections = scope?.sections?.length
? scope.sections
: scope?.fullMarkdown
? parseMarkdownSections(scope.fullMarkdown)
: []

const match = effectiveSections.find((s) => s.id === scopeIdForMeta)
scopeTitleSnapshot = match?.title?.trim() || null
}

const enrichedChatOptions: ChatRequestOptions | undefined =
chatRequestOptions
? ({
...chatRequestOptions,
body: {
// biome-ignore lint/suspicious/noExplicitAny: passthrough metadata
...((chatRequestOptions as any).body || {}),
mb_op_meta: {
scope_type: scopeTypeForMeta,
scope_id: scopeIdForMeta,
doc_node_id: docNodeIdForMeta,
scope_title_snapshot: scopeTitleSnapshotForMeta,
doc_context_included: docContextIncluded,
doc_context_policy: docContextIncluded
? DOC_CONTEXT_POLICY
: null,
heading_content_version: headingContentVersion ?? null,
},
},
} as ChatRequestOptions)
: ({
body: {
mb_op_meta: {
scope_type: scopeTypeForMeta,
scope_id: scopeIdForMeta,
doc_node_id: docNodeIdForMeta,
scope_title_snapshot: scopeTitleSnapshotForMeta,
doc_context_included: docContextIncluded,
doc_context_policy: docContextIncluded
? DOC_CONTEXT_POLICY
: null,
heading_content_version: headingContentVersion ?? null,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Derived scopeTitleSnapshot is never propagated.

You compute scopeTitleSnapshot when the caller doesn’t supply one, but mb_op_meta and opMetaRef still use scopeTitleSnapshotForMeta, so the derived value is discarded.

🐛 Suggested fix
-		const scopeTitleSnapshotForMeta: string | null =
-			scope?.scopeTitleSnapshot ?? null
+		const scopeTitleSnapshotForMeta: string | null =
+			scope?.scopeTitleSnapshot ?? null
@@
-		let scopeTitleSnapshot: string | null = scope?.scopeTitleSnapshot ?? null
+		let scopeTitleSnapshot: string | null = scope?.scopeTitleSnapshot ?? null
@@
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
@@
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
@@
-			scopeTitleSnapshot: scopeTitleSnapshotForMeta,
+			scopeTitleSnapshot: scopeTitleSnapshot,

Also applies to: 1236-1244

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-mb-chat.tsx` around lines 1119 - 1169, The derived
scopeTitleSnapshot is computed but never used—replace uses of
scopeTitleSnapshotForMeta with the computed scopeTitleSnapshot when building
mb_op_meta and when setting opMetaRef so the fallback title is propagated;
update the enrichedChatOptions construction (and the parallel body in the else
branch) and any opMetaRef assignment in use-mb-chat.tsx to reference
scopeTitleSnapshot (or a single unified variable like scopeTitleSnapshotFinal)
instead of scopeTitleSnapshotForMeta so both caller-provided and derived values
are consistently included in mb_op_meta and opMetaRef.

Comment on lines +946 to +953
try {
if (threadId && jwt && scopeType === 'heading_thread' && scopeId) {
const window = await selectScopeConversationWindow({
threadId,
scopeType,
scopeId,
jwt,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scoped history fetch omits docNodeId, so new-scheme rows won’t match.

selectScopeConversationWindow relies on docNodeId to match the new storage shape (docNodeId + bare scopeId). Without it, heading-thread history will only match legacy namespaced rows.

🐛 Suggested fix
 				if (threadId && jwt && scopeType === 'heading_thread' && scopeId) {
 					const window = await selectScopeConversationWindow({
 						threadId,
 						scopeType,
 						scopeId,
+						docNodeId,
 						jwt,
 					})
@@
 				{
 					scopeType,
 					scopeId: scopeId ?? null,
-					docNodeId: docRootId ?? null,
+					docNodeId,
 					fullMarkdown: currentContent,
 					sections: operationOriginalSectionsRef.current,
 				},

Also applies to: 1011-1018

🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-chat.tsx` around lines 946 - 953, The
scoped history fetch call to selectScopeConversationWindow is missing docNodeId
so it won't match the new storage shape; update the call site where you pass {
threadId, scopeType, scopeId, jwt } to include docNodeId (use the local
docNodeId variable) so the function can match using docNodeId + bare scopeId,
and apply the same change to the other identical call site in this file where
selectScopeConversationWindow is invoked (the one that mirrors the
heading-thread branch).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/pro-web/lib/hooks/use-mb-chat.tsx`:
- Around line 432-446: The debug console.log inside normalizeToolCall is logging
every tool call; remove the console.log('normalizeToolCall object —> ', {
toolCall }) or wrap it so it only runs when dev mode is enabled (e.g., check
appConfig.features.devMode or similar config flag before logging). Update the
normalizeToolCall function to either delete the log line or gate it behind the
dev-mode condition so production traffic doesn't emit this debug output.
♻️ Duplicate comments (3)
apps/pro-web/lib/hooks/use-mb-chat.tsx (3)

658-673: scopeTitleSnapshot is commented out and not persisted to DB.

The scopeTitleSnapshot field is commented out at line 672, meaning it won't be persisted with the message metadata. If this is intentional, consider removing the commented line. If it should be included, uncomment it.

This relates to the existing review comment about scopeTitleSnapshot not being propagated correctly from the computed value.


1118-1168: Derived scopeTitleSnapshot is never used in mb_op_meta.

The code computes a fallback scopeTitleSnapshot at lines 1118-1134 when the caller doesn't provide one, but mb_op_meta at lines 1143-1153 and 1158-1168 still uses scopeTitleSnapshotForMeta (the original input value). The derived value is discarded.

This was flagged in a previous review. The fix should replace scopeTitleSnapshotForMeta with scopeTitleSnapshot in the mb_op_meta objects.


1235-1251: nextOpMeta also uses scopeTitleSnapshotForMeta instead of computed value.

At line 1241, nextOpMeta assigns scopeTitleSnapshot: scopeTitleSnapshotForMeta instead of the computed scopeTitleSnapshot, making the derivation logic at lines 1118-1134 ineffective for the operation metadata ref.

Part of the same issue flagged in the previous review about scopeTitleSnapshot propagation.

🧹 Nitpick comments (2)
apps/pro-web/lib/hooks/use-mb-chat.tsx (2)

116-153: Consider extracting inline types to a shared module.

The UiMode, ActionType, IntentSource, DocContextPolicy, and OperationMeta types are defined inline within the component. Per the AI summary, similar types exist in apps/pro-web/types/operation-meta.ts. Consider importing from the shared module to ensure consistency and avoid drift.


856-867: Consider gating the debug console.log.

Similar to normalizeToolCall, the console.log('Tool call:', toolCall) at line 857 will output in production. Consider gating behind appConfig.features.devMode for consistency with other debug logs in this file.

♻️ Proposed fix
 onToolCall({ toolCall }: { toolCall: ToolCall<string, unknown> }) {
-	console.log('Tool call:', toolCall)
+	if (appConfig.features.devMode) {
+		console.log('Tool call:', toolCall)
+	}
 
 	if (appConfig.features.devMode) {

Comment on lines +432 to +446
function normalizeToolCall(toolCall: ToolCall<string, unknown>): AiToolCall {
console.log('normalizeToolCall object —> ', { toolCall })
const id = toolCall.toolCallId ?? ''
const name = toolCall.toolName ?? 'unknown_tool'
const args = toolCall.args ?? {}

return {
// Keep original fields too (helps debugging; harmless)
...toolCall,
// Your internal shape
toolCallId: String(id),
toolName: name,
args: safeParseArgs(args),
} as AiToolCall
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove or gate the debug console.log.

Line 433 contains a console.log('normalizeToolCall object —> ', { toolCall }) that will log on every tool call in production. Consider removing it or gating behind appConfig.features.devMode.

🐛 Proposed fix
 function normalizeToolCall(toolCall: ToolCall<string, unknown>): AiToolCall {
-	console.log('normalizeToolCall object —> ', { toolCall })
+	if (appConfig.features.devMode) {
+		console.log('normalizeToolCall object —> ', { toolCall })
+	}
 	const id = toolCall.toolCallId ?? ''
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-mb-chat.tsx` around lines 432 - 446, The debug
console.log inside normalizeToolCall is logging every tool call; remove the
console.log('normalizeToolCall object —> ', { toolCall }) or wrap it so it only
runs when dev mode is enabled (e.g., check appConfig.features.devMode or similar
config flag before logging). Update the normalizeToolCall function to either
delete the log line or gate it behind the dev-mode condition so production
traffic doesn't emit this debug output.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/pro-web/lib/hooks/use-mb-chat.tsx (1)

627-706: Persist null scope fields instead of empty strings.
normalizeScopeId intentionally returns null for doc_root, but opMetaForDb converts nulls to '', which can break scoped queries/history. Prefer nulls (and allow null in the record type) so doc_root/heading_thread semantics remain intact.

🐛 Proposed fix
-					const opMetaForDb: Record<
-						string,
-						string | boolean | number | undefined
-					> = {
+					const opMetaForDb: Record<
+						string,
+						string | boolean | number | null | undefined
+					> = {
 						uiMode: opMetaNow.uiMode,
 						scopeType: opMetaNow.scopeType,
-						scopeId: opMetaNow.scopeId || '',
-						docNodeId: opMetaNow.docNodeId || '',
+						scopeId: opMetaNow.scopeId ?? null,
+						docNodeId: opMetaNow.docNodeId ?? null,
 						actionType: opMetaNow.actionType,
 						excludeFromQChain: opMetaNow.excludeFromQChain,
 						ragEligible: opMetaNow.ragEligible,
 						mutationAllowed: opMetaNow.mutationAllowed,
 						mutationApplied: opMetaNow.mutationApplied,
 						intentSource: opMetaNow.intentSource,
 						docContextIncluded: opMetaNow.docContextIncluded,
-						headingContentVersion: opMetaNow.headingContentVersion || '',
-						docContextPolicy: opMetaNow.docContextPolicy || undefined,
-						// scopeTitleSnapshot: opMetaNow.scopeTitleSnapshot || '',
+						headingContentVersion: opMetaNow.headingContentVersion ?? null,
+						docContextPolicy: opMetaNow.docContextPolicy ?? null,
+						// scopeTitleSnapshot: opMetaNow.scopeTitleSnapshot ?? null,
 					}

To confirm schema/type expectations for nullability, you can scan the repo for scope-related column definitions and types:

#!/bin/bash
rg -n "scope_id|doc_node_id|heading_content_version|scope_title_snapshot" -g'*.{sql,ts,tsx,yaml}'
rg -n "SaveNewMessageParams" -g'*.ts'
♻️ Duplicate comments (2)
apps/pro-web/lib/hooks/use-mb-chat.tsx (2)

416-446: Gate the normalizeToolCall debug log.
The console.log will fire on every tool call in production; please remove it or guard behind dev mode.

🐛 Proposed fix
-	console.log('normalizeToolCall object —> ', { toolCall })
+	if (appConfig.features.devMode) {
+		console.log('normalizeToolCall object —> ', { toolCall })
+	}

1119-1166: Use the derived scopeTitleSnapshot in mb_op_meta and opMetaRef.
A fallback snapshot is computed but the original value is still used, so the derived title is dropped. Propagate the computed snapshot consistently.

🐛 Proposed fix
-		const scopeTitleSnapshotForMeta: string | null =
-			scope?.scopeTitleSnapshot ?? null
...
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
...
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
...
-			scopeTitleSnapshot: scopeTitleSnapshotForMeta,
+			scopeTitleSnapshot: scopeTitleSnapshot,

Also applies to: 1238-1262

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/pro-web/lib/hooks/use-mb-chat.tsx`:
- Around line 668-686: opMetaForDb is storing empty strings for scopeId,
docNodeId, and headingContentVersion which breaks database _isNull queries;
update the assignment in use-mb-chat where opMetaForDb is built (references:
opMetaForDb and opMetaNow) to set those fields to null instead of '' (or omit
the keys when missing), and adjust the value type to allow null so the stored
rows preserve proper null semantics for _isNull filters.

In `@apps/pro-web/lib/hooks/use-workspace-chat.rag.test.tsx`:
- Around line 248-254: The test is passing a string ('section-1') as the third
argument to latestCtx.handleWorkspaceEdit where the function expects a
selectionRange of shape { start, end } | null; change the call in the test to
pass a valid selectionRange object (or null) instead of the string—e.g., provide
{ start: 0, end: 0 } or a range that corresponds to 'section-1'—so
handleWorkspaceEdit receives the correct type.

In `@CHAT_REFACTOR.md`:
- Around line 287-334: Duplicate and out-of-order Markdown headings in
CHAT_REFACTOR.md (e.g., "Suggested implementation order", "User Request",
"Handoff summary") are triggering markdownlint MD024/MD001; locate each repeated
heading block (noted around lines 287, 684-688, 848-855, 938-944, 1104-1123) and
either rename or demote duplicates and adjust heading levels so they increment
by only one step (e.g., H2 -> H3, H3 -> H4) to restore a single unique heading
instance and correct hierarchical order across the document; ensure any
references or TOC entries match the new names/levels.
- Around line 21-25: Fix the minor typos and hyphenation in the requirements
text around the heading "### 5) In our current chat threads the first question
and answer is not collapsible form the very top" and the surrounding paragraphs
that mention "third level subsection" and "level two subsections": correct
"form" to "from", standardize hyphenation (e.g., "level-two" and "level-three"
where used as compound adjectives), and clean punctuation/spacing in the
sentence about moving the 'x' to the top corner so the paragraph reads clearly
and consistently with other headings.
♻️ Duplicate comments (3)
apps/pro-web/lib/hooks/use-mb-chat.tsx (2)

426-455: Gate tool-call debug logs behind dev mode.

console.log in normalizeToolCall and onToolCall will run on every tool call in production. Please gate or remove.

🔧 Suggested fix
 function normalizeToolCall(toolCall: ToolCall<string, unknown>): AiToolCall {
-	console.log('normalizeToolCall object —> ', { toolCall })
+	if (appConfig.features.devMode) {
+		console.log('normalizeToolCall object —> ', { toolCall })
+	}
@@
 	onToolCall({ toolCall }: { toolCall: ToolCall<string, unknown> }) {
-		console.log('Tool call:', toolCall)
+		if (appConfig.features.devMode) {
+			console.log('Tool call:', toolCall)
+		}

Also applies to: 869-879


1129-1182: Derived scopeTitleSnapshot isn’t propagated.

You compute scopeTitleSnapshot but still send scopeTitleSnapshotForMeta, so the derived fallback is dropped.

🔧 Suggested fix
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
@@
-								scope_title_snapshot: scopeTitleSnapshotForMeta,
+								scope_title_snapshot: scopeTitleSnapshot,
@@
-			scopeTitleSnapshot: scopeTitleSnapshotForMeta,
+			scopeTitleSnapshot: scopeTitleSnapshot,

Also applies to: 1252-1267

apps/pro-web/lib/hooks/use-workspace-chat.tsx (1)

944-953: Include docNodeId in scoped history + pass fallback docNodeId to MBChat.

The scoped history fetch still omits docNodeId, so new-scheme rows (doc_node_id + bare scope_id) won’t match. Also, the scope payload passes docRootId ?? null, which drops the documentKey fallback.

🔧 Suggested fix
-				if (threadId && jwt && scopeType === 'heading_thread' && scopeId) {
-					const window = await selectScopeConversationWindow({
-						threadId,
-						scopeType,
-						scopeId,
-						jwt,
-					})
+				if (threadId && jwt && scopeType === 'heading_thread' && scopeId) {
+					const normalizedScopeId =
+						docNodeId && scopeId.startsWith(`${docNodeId}::`)
+							? scopeId.slice(`${docNodeId}::`.length)
+							: scopeId
+					const window = await selectScopeConversationWindow({
+						threadId,
+						scopeType,
+						scopeId: normalizedScopeId,
+						docNodeId,
+						jwt,
+					})
@@
-					docNodeId: docRootId ?? null,
+					docNodeId: docNodeId ?? null,

Also applies to: 1011-1016

🧹 Nitpick comments (1)
apps/pro-web/lib/hooks/use-mb-chat.tsx (1)

329-329: Avoid shadowing DOC_CONTEXT_POLICY.

There’s already a top-level constant; re-declaring inside the provider is redundant and makes it easier to diverge. Consider reusing the outer constant.

Comment on lines 668 to 686
const opMetaForDb: Record<
string,
string | boolean | number | undefined
> = {
uiMode: opMetaNow.uiMode,
scopeType: opMetaNow.scopeType,
scopeId: opMetaNow.scopeId || '',
docNodeId: opMetaNow.docNodeId || '',
actionType: opMetaNow.actionType,
excludeFromQChain: opMetaNow.excludeFromQChain,
ragEligible: opMetaNow.ragEligible,
mutationAllowed: opMetaNow.mutationAllowed,
mutationApplied: opMetaNow.mutationApplied,
intentSource: opMetaNow.intentSource,
docContextIncluded: opMetaNow.docContextIncluded,
headingContentVersion: opMetaNow.headingContentVersion || '',
docContextPolicy: opMetaNow.docContextPolicy || undefined,
// scopeTitleSnapshot: opMetaNow.scopeTitleSnapshot || '',
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Store nulls for scope/doc fields to preserve query semantics.

Empty strings for scopeId, docNodeId, and headingContentVersion break _isNull filters, so doc_root history/RAG lookups can miss new rows. Prefer null (or omit the keys).

✅ Suggested fix
-					const opMetaForDb: Record<
-						string,
-						string | boolean | number | undefined
-					> = {
+					const opMetaForDb: Record<
+						string,
+						string | boolean | number | null | undefined
+					> = {
 						uiMode: opMetaNow.uiMode,
 						scopeType: opMetaNow.scopeType,
-						scopeId: opMetaNow.scopeId || '',
-						docNodeId: opMetaNow.docNodeId || '',
+						scopeId: opMetaNow.scopeId ?? null,
+						docNodeId: opMetaNow.docNodeId ?? null,
@@
-						headingContentVersion: opMetaNow.headingContentVersion || '',
-						docContextPolicy: opMetaNow.docContextPolicy || undefined,
+						headingContentVersion: opMetaNow.headingContentVersion ?? null,
+						docContextPolicy: opMetaNow.docContextPolicy ?? null,
 					}
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-mb-chat.tsx` around lines 668 - 686, opMetaForDb
is storing empty strings for scopeId, docNodeId, and headingContentVersion which
breaks database _isNull queries; update the assignment in use-mb-chat where
opMetaForDb is built (references: opMetaForDb and opMetaNow) to set those fields
to null instead of '' (or omit the keys when missing), and adjust the value type
to allow null so the stored rows preserve proper null semantics for _isNull
filters.

Comment on lines +248 to +254
await act(async () => {
// 2. Act: Trigger "Rewrite"
await latestCtx.handleWorkspaceEdit(
'Rewrite this',
'rewrite',
'section-1',
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pass a valid selectionRange to handleWorkspaceEdit.

A string is passed where { start, end } | null is expected, which can leak an invalid type into state.

✅ Suggested fix
-			await latestCtx.handleWorkspaceEdit(
-				'Rewrite this',
-				'rewrite',
-				'section-1',
-			)
+			await latestCtx.handleWorkspaceEdit(
+				'Rewrite this',
+				'rewrite',
+				null,
+			)
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-chat.rag.test.tsx` around lines 248 -
254, The test is passing a string ('section-1') as the third argument to
latestCtx.handleWorkspaceEdit where the function expects a selectionRange of
shape { start, end } | null; change the call in the test to pass a valid
selectionRange object (or null) instead of the string—e.g., provide { start: 0,
end: 0 } or a range that corresponds to 'section-1'—so handleWorkspaceEdit
receives the correct type.

Comment on lines +21 to +25
that will show the headings and conversations around those sections. We can also slightly indent this third level subsection. The conversations around the level two subsections will also be below the level 3 subsection accordions so that people can follow the level 2 subsection conversations more easily.

### 5) In our current chat threads the first question and answer is not collapsible form the very top

title section, but instead has a 'x' mark to close the entire thread. We should just add the collapse/expand arrow at the top header section and move the 'x' to the very top corner in a blank row section at the very top of the thread component. Please draft a build plan with references to specific files we need to update to implement these changes in our current codebase.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix minor typos in the requirement list.

Spelling and hyphenation tweaks will keep the doc clean.

✏️ Suggested edits
- that will show the headings and conversations around those sections. We can also slightly indent this third level subsection.
+ that will show the headings and conversations around those sections. We can also slightly indent this third-level subsection.
@@
- title section, but instead has a 'x' mark to close the entire thread.
+ title section, but instead has a 'x' mark to close the entire thread.
@@
- first question and answer is not collapsible form the very top
+ first question and answer is not collapsible from the very top
🧰 Tools
🪛 LanguageTool

[grammar] ~21-~21: Use a hyphen to join words.
Context: .... We can also slightly indent this third level subsection. The conversations arou...

(QB_NEW_EN_HYPHEN)


[grammar] ~23-~23: Ensure spelling is correct
Context: ... question and answer is not collapsible form the very top title section, but instea...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
In `@CHAT_REFACTOR.md` around lines 21 - 25, Fix the minor typos and hyphenation
in the requirements text around the heading "### 5) In our current chat threads
the first question and answer is not collapsible form the very top" and the
surrounding paragraphs that mention "third level subsection" and "level two
subsections": correct "form" to "from", standardize hyphenation (e.g.,
"level-two" and "level-three" where used as compound adjectives), and clean
punctuation/spacing in the sentence about moving the 'x' to the top corner so
the paragraph reads clearly and consistently with other headings.

Comment on lines +287 to +334
### 7) Suggested implementation order (fastest path)

1. Locate current thread UI entrypoint (where X + first Q/A are rendered).
2. Add docNodeId support (types first; DB/ORM if needed).
3. Implement outline builder (or adapt existing).
4. Update thread fetch to return outline + messages grouped.
5. Implement recursive accordion renderer with required message ordering.
6. Add L2 horizontal row + See more.
7. Add top chrome row + collapsible title.
8. Update offline cache + add tests.
“Files to update” starter checklist (copy/paste into your ticket) Use this as your working list—replace paths once rg reveals the exact names.

- Schema/ORM
- prisma/schema.prisma (Message: add docNodeId; optional DocumentNode
model)

- prisma/migrations/\* (backfill)

- Server
- src/server/api/routers/thread.ts (thread fetch includes outline + grouping)
- src/server/services/threadService.ts (joins + grouping)
- src/server/services/documentOutline.ts (outline build/load)
- Shared types
- packages/shared/src/types/chat.ts
- Client state
- src/stores/threadStore.ts (expandedNodeIds, seeMoreByNodeId)
- src/lib/indexeddb/threadCache.ts (if present)
- UI
- src/components/chat/ThreadView.tsx (top chrome row + title accordion)
- src/components/chat/ThreadNodeAccordion.tsx (new) (recursive renderer)
- src/components/chat/MessageList.tsx (render by docNodeId groups)

<!-- Page 8 -->

- src/components/ui/accordion.tsx (if tweaks needed)
- src/styles/chatThread.css or Tailwind classes in components
If you paste the output of these two commands (just the filenames) I can turn the “likely” file list above into an exact, repo-accurate set of file paths and a step-by-step PR breakdown:

```bash
rg -n --files | rg -i "thread|chat|message|accordion|document"
rg -n "ThreadView|ChatThread|DocumentThread|XMarkIcon|Accordion" src packages
```

According to a document from the project knowledge base (these PDF exports don’t include a publish date), here’s a fastest-path build plan with specific file touchpoints to implement the nested markdown-header accordions + updated thread header behavior.

### 7) Suggested implementation order (fastest path)

### 1) Locate the current thread UI entrypoint (where the X + first Q/A are rendered)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Resolve duplicate headings + heading level jump to satisfy markdownlint.

The document repeats several headings (e.g., multiple “Suggested implementation order” / “User Request”) and also skips heading levels around the “Handoff summary.” Rename or demote duplicates and keep level increments by one to avoid MD024/MD001 violations.

Also applies to: 684-688, 848-855, 938-944, 1104-1123

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

332-332: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
In `@CHAT_REFACTOR.md` around lines 287 - 334, Duplicate and out-of-order Markdown
headings in CHAT_REFACTOR.md (e.g., "Suggested implementation order", "User
Request", "Handoff summary") are triggering markdownlint MD024/MD001; locate
each repeated heading block (noted around lines 287, 684-688, 848-855, 938-944,
1104-1123) and either rename or demote duplicates and adjust heading levels so
they increment by only one step (e.g., H2 -> H3, H3 -> H4) to restore a single
unique heading instance and correct hierarchical order across the document;
ensure any references or TOC entries match the new names/levels.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/pro-web/services/hasura/hasura.service.ts`:
- Around line 726-741: Remove the redundant explicit doc_node_id in the
saveNewMessage signature and eliminate both uses of "as any": change the
parameter to params: Partial<SaveNewMessageParams> (so doc_node_id still comes
from the type), destructure with const { jwt, docNodeId, doc_node_id, ...object
} = params (no as any), and replace the canonicalDocNodeId declaration with an
explicit type string | null, e.g. const canonicalDocNodeId: string | null =
(docNodeId ?? doc_node_id ?? null) as string | null (or without the final cast
if inference suffices).
♻️ Duplicate comments (1)
apps/pro-web/services/hasura/hasura.service.ts (1)

831-837: Retry failure still swallowed after logging.
Line 831 logs retryErr but falls through to the generic error, masking the retry cause.

🐛 Suggested fix
 			} catch (retryErr) {
 				console.error('Error saving new message (retry failed):', retryErr)
+				throw new Error('Failed to save new message (retry without optional fields also failed).')
 			}

Comment on lines +726 to +741
export async function saveNewMessage(
params: Partial<SaveNewMessageParams> & { doc_node_id?: string | null },
) {
const { jwt, docNodeId, doc_node_id, ...object } = params as any
const client = getHasuraClient({ jwt })

// During migration, callers may pass either camelCase or snake_case.
// Only send the camelCase field to Hasura (GraphQL naming is camelCase here).
const canonicalDocNodeId: string | null = (docNodeId ??
doc_node_id ??
null) as any
const insertObject =
canonicalDocNodeId == null
? object
: { ...object, docNodeId: canonicalDocNodeId }

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect SaveNewMessageParams definition
rg -n -C5 "SaveNewMessageParams" apps/pro-web/services/hasura/hasura.service.type.ts

Repository: bitcashorg/masterbots

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Also check if SaveNewMessageParams is defined elsewhere
fd -e ts -e tsx . apps/pro-web | xargs rg "type SaveNewMessageParams|interface SaveNewMessageParams" 2>/dev/null

Repository: bitcashorg/masterbots

Length of output: 154


🏁 Script executed:

#!/bin/bash
# Check how saveNewMessage is called to understand the flexibility needs
rg -n "saveNewMessage\(" apps/pro-web --type ts --type tsx -B2 -A2 | head -80

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Read the hasura.service.type.ts file to find SaveNewMessageParams
cat apps/pro-web/services/hasura/hasura.service.type.ts | grep -n "SaveNewMessageParams" -A 20

Repository: bitcashorg/masterbots

Length of output: 730


🏁 Script executed:

#!/bin/bash
# Search for saveNewMessage function calls across the codebase
rg "saveNewMessage" apps/pro-web --type ts -B 2 -A 2

Repository: bitcashorg/masterbots

Length of output: 1149


🏁 Script executed:

#!/bin/bash
# Also check the full saveNewMessage implementation to understand current type handling
sed -n '726,750p' apps/pro-web/services/hasura/hasura.service.ts

Repository: bitcashorg/masterbots

Length of output: 826


🏁 Script executed:

#!/bin/bash
# Find HasuraServiceParams definition
rg -n "interface HasuraServiceParams|type HasuraServiceParams" apps/pro-web --type ts -A 10

Repository: bitcashorg/masterbots

Length of output: 914


🏁 Script executed:

#!/bin/bash
# Check what properties exist in HasuraServiceParams
grep -n "HasuraServiceParams" apps/pro-web/services/hasura/hasura.service.type.ts -A 10

Repository: bitcashorg/masterbots

Length of output: 1538


Remove as any and redundant type union in saveNewMessage.

Line 734–736 uses unnecessary as any which should be replaced with the explicit type as string | null. Additionally, the function parameter includes a redundant doc_node_id?: string | null that already exists in SaveNewMessageParams. Line 729's as any on the destructure can be removed by eliminating the redundant parameter.

♻️ Suggested refactor
 export async function saveNewMessage(
-  params: Partial<SaveNewMessageParams> & { doc_node_id?: string | null },
+  params: Partial<SaveNewMessageParams>,
 ) {
-  const { jwt, docNodeId, doc_node_id, ...object } = params as any
+  const { jwt, docNodeId, doc_node_id, ...object } = params as Partial<SaveNewMessageParams>
   const client = getHasuraClient({ jwt })

   // During migration, callers may pass either camelCase or snake_case.
   // Only send the camelCase field to Hasura (GraphQL naming is camelCase here).
-  const canonicalDocNodeId: string | null = (docNodeId ??
-    doc_node_id ??
-    null) as any
+  const canonicalDocNodeId = (docNodeId ?? doc_node_id ?? null) as string | null
   const insertObject =
     canonicalDocNodeId == null
       ? object
       : { ...object, docNodeId: canonicalDocNodeId }

Per coding guidelines, avoid any and prefer specific types.

🤖 Prompt for AI Agents
In `@apps/pro-web/services/hasura/hasura.service.ts` around lines 726 - 741,
Remove the redundant explicit doc_node_id in the saveNewMessage signature and
eliminate both uses of "as any": change the parameter to params:
Partial<SaveNewMessageParams> (so doc_node_id still comes from the type),
destructure with const { jwt, docNodeId, doc_node_id, ...object } = params (no
as any), and replace the canonicalDocNodeId declaration with an explicit type
string | null, e.g. const canonicalDocNodeId: string | null = (docNodeId ??
doc_node_id ?? null) as string | null (or without the final cast if inference
suffices).

Copy link
Member Author

@AndlerRL AndlerRL left a comment

Choose a reason for hiding this comment

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

Woah, a lots of changes to do in the observations! Definitely there is a consistent issue between logics and steps for the RAG system. There are parts that aren't connected, others connected but with wrong structure and other with the structure and connected but not well referenced (not grabbing the actual variables to make the system work).

We need to improve this code since it has a lot of code repetitions, overkills strategies, cumbersome validations and not so clear flows and flows that violates the Modular Development that we are following.

I left my comments to take them into account, along with the coderabbitai feedbvacks that we should take has the " 👀 " reaction from me on it.

Conceptually, the logic makes sense, I could see how the RAG is connecting to the thread documents and how these document sections are related with the root doc, parent sections to be later embedded and pushed into the simple rag system, creating relationships and context modular and optimize the AI responses. While this concept is correct, the application of it went a bit away from the expected structure and logic. Is making his own logic while "connecting" with the actual, tagging it as "legacy" where it shouldn't because this RAG system need to be built according to the current architecture, not creating a new one and threat everything as "old".

I will be creating a development plan to gather ALL the feedbacks and do a structured fix of them.

@coderabbitai after reviewing and giving this review, proceed to gather also all the feedbacks and create a development plan, to recap all the observations in order.

$$;

-- 3) Hasura-facing wrapper (text -> vector cast)
CREATE OR REPLACE FUNCTION public.search_assistant_answers_gql(
Copy link
Member Author

Choose a reason for hiding this comment

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

We can remove this.

@coderabbitai elaborate an issue to track the deletion of this table. The GraphQL table is overkill since from the main table we get the graphql.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@AndlerRL, I'll create an issue to track the removal of the redundant GraphQL wrapper function.


🏁 Script executed:

#!/bin/bash
# Create GitHub issue to track deletion of search_assistant_answers_gql wrapper

gh issue create \
  --repo bitcashorg/masterbots \
  --title "Remove redundant search_assistant_answers_gql wrapper function" \
  --body "## Context

The \`search_assistant_answers_gql\` function and \`assistant_answer_hits_view\` were introduced in migration \`1766629067789_assistant_answers_fn\` as a Hasura-facing wrapper to convert text embeddings to vector format for similarity search.

## Issue

This GraphQL wrapper is overkill since Hasura can expose the main \`assistant_answers\` table directly via GraphQL, eliminating the need for an intermediate view and wrapper function.

## Proposed Changes

Remove the following from the database:
- \`public.assistant_answer_hits_view\` (view)
- \`public.search_assistant_answers_gql\` (function)

Consider whether \`public.search_assistant_answers\` core function should be retained for server-side SQL calls or if it can also be removed in favor of direct table queries.

## References

- PR: `#600`
- Discussion: https://github.com/bitcashorg/masterbots/pull/600#discussion_r2699695376
- Migration file: \`apps/hasura/migrations/masterbots/1766629067789_assistant_answers_fn/up.sql\`
- Requested by: `@AndlerRL`" \
  --assignee AndlerRL

Length of output: 51


@AndlerRL, I've created issue #602 to track the removal of the redundant GraphQL wrapper function: #602

The issue documents:

  • The redundancy of search_assistant_answers_gql and assistant_answer_hits_view
  • The rationale that direct Hasura GraphQL access to the main table is sufficient
  • References to this PR and comment discussion
  • Assigned to you for follow-up

🧠 Learnings used
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 542
File: apps/web/components/layout/profile/layout.tsx:67-68
Timestamp: 2025-08-29T20:34:24.433Z
Learning: AndlerRL prefers skeleton loading states over conditional rendering for better UX when components are waiting for data to load. Components should show loading indicators rather than blank spaces.

Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-11-21T19:47:50.195Z
Learning: For repo bitcashorg/masterbots (apps/pro-web), AndlerRL prefers integrating TanStack Query into React Context Hooks wherever they fit to manage shared server state and side effects, with a longer-term plan to align broader state management with TanStack Query and potentially replace React Context hooks. Captured from PR `#584` discussion on Nov 21, 2025.

ON public.assistant_answers (thread_id, scope_type, scope_id, turn_index DESC);

-- 3) New scoped retrieval function (keep existing search_assistant_answers_gql unchanged)
CREATE OR REPLACE FUNCTION public.search_assistant_answers_scoped_gql(
Copy link
Member Author

Choose a reason for hiding this comment

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

We have to remove this too. _gql tables and functions are redundant. Graphql use by default the SQL tables and functions and converts it for Graphql system if necessary.

@coderabbitai proceed to include this into the new issue that tracks the removal of these tables and functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for existing issues related to _gql removal or cleanup
gh issue list --repo bitcashorg/masterbots --search "_gql redundant OR remove _gql OR cleanup _gql" --limit 20 --json number,title,state,body

Length of output: 2


🏁 Script executed:

#!/bin/bash
# Also search for issues that might mention "graphql" and "functions" or "tables"
gh issue list --repo bitcashorg/masterbots --search "graphql functions tables remove OR graphql redundant" --limit 20 --json number,title,state,body

Length of output: 2


🏁 Script executed:

#!/bin/bash
# Search all open issues to find any that might be tracking this cleanup
gh issue list --repo bitcashorg/masterbots --state open --limit 50 --json number,title,body | jq '.[] | select(.title | test("gql|GraphQL|function|cleanup"; "i")) | {number, title, body}'

Length of output: 16162


🏁 Script executed:

#!/bin/bash
# Update issue `#602` to include the scoped function
gh issue edit 602 --repo bitcashorg/masterbots --body "## Context

The \`search_assistant_answers_gql\` function and \`assistant_answer_hits_view\` were introduced in migration \`1766629067789_assistant_answers_fn\` as a Hasura-facing wrapper to convert text embeddings to vector format for similarity search.

Additionally, the \`search_assistant_answers_scoped_gql\` function was introduced in migration \`1766957388317__op_meta_and_scoped_rag\` as another GraphQL wrapper for scoped RAG retrieval.

## Issue

These GraphQL wrappers are overkill since Hasura can expose the main \`assistant_answers\` table directly via GraphQL, eliminating the need for intermediate views and wrapper functions.

## Proposed Changes

Remove the following from the database:
- \`public.assistant_answer_hits_view\` (view)
- \`public.search_assistant_answers_gql\` (function)
- \`public.search_assistant_answers_scoped_gql\` (function)

Consider whether \`public.search_assistant_answers\` core function should be retained for server-side SQL calls or if it can also be removed in favor of direct table queries.

## References

- PR: `#600`
- Discussion (original): https://github.com/bitcashorg/masterbots/pull/600#discussion_r2699695376
- Discussion (scoped function): https://github.com/bitcashorg/masterbots/pull/600#discussion_r2699742431
- Migration files:
  - \`apps/hasura/migrations/masterbots/1766629067789_assistant_answers_fn/up.sql\`
  - \`apps/hasura/migrations/masterbots/1766957388317__op_meta_and_scoped_rag/up.sql\`
- Requested by: `@AndlerRL`"

Length of output: 51


@AndlerRL, I've updated issue #602 to include search_assistant_answers_scoped_gql in the tracking list. The issue now covers all three redundant GraphQL entities:

  • public.assistant_answer_hits_view (view)
  • public.search_assistant_answers_gql (function)
  • public.search_assistant_answers_scoped_gql (function)

I've also added references to this migration file (1766957388317__op_meta_and_scoped_rag/up.sql) and this discussion thread to the issue description.

You can view the updated issue here: #602


🧠 Learnings used
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 542
File: apps/web/components/layout/profile/layout.tsx:67-68
Timestamp: 2025-08-29T20:34:24.433Z
Learning: AndlerRL prefers skeleton loading states over conditional rendering for better UX when components are waiting for data to load. Components should show loading indicators rather than blank spaces.

Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-11-21T19:47:50.195Z
Learning: For repo bitcashorg/masterbots (apps/pro-web), AndlerRL prefers integrating TanStack Query into React Context Hooks wherever they fit to manage shared server state and side effects, with a longer-term plan to align broader state management with TanStack Query and potentially replace React Context hooks. Captured from PR `#584` discussion on Nov 21, 2025.

const embeddingText = vectorToPgText(embedding)

// 4) compute next turn index per thread (keep existing behavior; do NOT scope-partition here)
const queryMaxTurnIndex = /* GraphQL */ `
Copy link
Member Author

Choose a reason for hiding this comment

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

For performance and system optimization, it is better to use DrizzleORM to connect directly to the DB, so these side-effects for RAG capabilities doesn't affect UX

}
}

async function hasuraGql<TData>(
Copy link
Member Author

Choose a reason for hiding this comment

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

We can remove this. We have to use mb-drizzle instead for RAG actions.

args.push('doc_node_id: $docNodeId')
}

return /* GraphQL */ `
Copy link
Member Author

Choose a reason for hiding this comment

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

We need to replace here too to use DrizzleORM instead.

? (error as any).message
: String(error)

if (
Copy link
Member Author

Choose a reason for hiding this comment

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

Improve this guard and the overall catch block... Why it has a variable alphabet here... ?

* key mismatches (naming conventions can differ between tables/filters/codegen).
* This preserves runtime behavior and prevents TS excess-property failures.
*/
function buildScopeWhere(args: {
Copy link
Member Author

Choose a reason for hiding this comment

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

If this is used to query documents to get the relations (no immediate chat relation), we are good however, if we are not doing it, and we are building this while we are interacting with the user, then it is better to use Drizzle ORM. Revise the usage of this.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK this is a duplicated. The select-scope-conversation-window.ts is doing the same... We definitely we want Drizzle and remove this one.

}
}

export async function selectScopeConversationWindow(args: {
Copy link
Member Author

Choose a reason for hiding this comment

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

use-mb-chat.tsx and use-workspace-chat.tsx has a conflict with this logic. We rather want to simplify this and remove this one and use Drizzle instead.


export interface ChatMessageProps extends React.ComponentProps<'div'> {
message: Message & Partial<MBMessage>
message: any
Copy link
Member Author

Choose a reason for hiding this comment

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

Revert this change.

const fb = asThreadLike(fallbackThread)

return (
ft?.slug ?? ft?.thread?.slug ?? fb?.slug ?? asString(paramThreadSlug) // (useParams can be string | string[])
Copy link
Member Author

Choose a reason for hiding this comment

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

Simplify this. we may not need this due we already have a strong active Thread validation. If the thread doesn't have a slug, the issue might be somewhere else.

AndlerRL

This comment was marked as duplicate.

@AndlerRL
Copy link
Member Author

Woah, a lots of changes to do in the observations! Definitely there is a consistent issue between logics and steps for the RAG system. There are parts that aren't connected, others connected but with wrong structure and other with the structure and connected but not well referenced (not grabbing the actual variables to make the system work).

We need to improve this code since it has a lot of code repetitions, overkills strategies, cumbersome validations and not so clear flows and flows that violates the Modular Development that we are following.

I left my comments to take them into account, along with the coderabbitai feedbvacks that we should take has the " 👀 " reaction from me on it.

Conceptually, the logic makes sense, I could see how the RAG is connecting to the thread documents and how these document sections are related with the root doc, parent sections to be later embedded and pushed into the simple rag system, creating relationships and context modular and optimize the AI responses. While this concept is correct, the application of it went a bit away from the expected structure and logic. Is making his own logic while "connecting" with the actual, tagging it as "legacy" where it shouldn't because this RAG system need to be built according to the current architecture, not creating a new one and threat everything as "old".

I will be creating a development plan to gather ALL the feedbacks and do a structured fix of them.

@coderabbitai after reviewing and giving this review, proceed to gather also all the feedbacks and create a development plan, to recap all the observations in order.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@AndlerRL, I'll gather all the feedback from this PR and create a comprehensive development plan. Let me fetch all the review comments first.


🏁 Script executed:

#!/bin/bash

# Fetch all review comments on PR `#600`
echo "=== Fetching PR `#600` Review Comments ==="
gh pr view 600 --repo bitcashorg/masterbots --json reviews,comments | jq '.'

echo ""
echo "=== Fetching Review Comments with Reactions ==="
# Get detailed review comments with reactions
gh api graphql -f query='
query {
  repository(owner: "bitcashorg", name: "masterbots") {
    pullRequest(number: 600) {
      reviews(first: 100) {
        nodes {
          author {
            login
          }
          body
          createdAt
          comments(first: 100) {
            nodes {
              body
              path
              position
              reactions(first: 10) {
                nodes {
                  content
                  user {
                    login
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}' | jq '.'

echo ""
echo "=== Fetching Issue Comments ==="
gh pr view 600 --repo bitcashorg/masterbots --json comments --jq '.comments[] | {author: .author.login, body: .body, createdAt: .createdAt}'

Length of output: 266036


@AndlerRL thanks — I’ve consolidated all feedback (yours plus the CodeRabbit comments across Jan 13–15) and organized a development plan that aligns the RAG work with the current architecture, removes duplication, and simplifies flows.

Executive summary of issues to address

  • Architecture alignment: stop treating current codepaths as “legacy”; build RAG on the existing architecture and data model.
  • Modularity and duplication: multiple duplications (types, PromptEnhancer, hasuraGql, scope types) and “double-cast” patterns; move to shared utilities and strict types.
  • Data shape and indices: Drizzle/Postgres operator classes on indices are off; some function where clauses need NULL-safe comparisons; a few migrations/comments are misleading.
  • Error handling and observability: several silent failure paths and swallowed errors; add best-effort logs and avoid hidden returns.
  • UI/UX consistency: SSR/CSR hazards, inline handlers, non-functional buttons, duplicated components; normalize message shapes and centralize components.
  • Tests and quality: add edge-case tests (token budgets, retrieval filtering), docstring coverage, and consolidate URL parsing utilities.

Proposed multi-PR development plan (prioritized, with acceptance criteria)
Phase 0 — Stabilize schema/indexes and Hasura surface (P0)

  • Migrations and SQL
    • Fix NULL-safe predicates: use IS NOT DISTINCT FROM for aa.scope_id in public.search_assistant_answers_scoped_gql. Acceptance: migration applies cleanly; results for doc_root (NULL scope_id) are returned.
    • Clean up misleading comments and artifacts in down/up migrations; guard conditional indexes that depend on columns existing. Acceptance: re-run up/down locally without errors.
    • Consider partial indexes for nullable columns (document_operations) after we review query patterns. Acceptance: decision documented; if added, EXPLAIN shows usage.
  • Drizzle schema operator classes
    • Correct .op(...) per column type (uuid_ops, text_ops, int4_ops, enum_ops, timestamptz_ops) across assistant_answers, message, document_operations indices. Acceptance: drizzle sync ok; resulting SQL uses correct op classes; pg_stat_user_indexes confirms expected usage.
  • Hasura metadata
    • Verify function signatures and permissions; adjust user access for document_operations with optional thread_id strategy (either keep restricted or _or thread_id IS NULL path after product decision). Acceptance: GraphQL queries return expected rows under role user; no permission regressions.

Phase 1 — Types and shared utilities consolidation (P0)

  • Centralize types
    • Export ScopeType, ActionType, UiMode, IntentSource, DocContextPolicy, OperationMeta from apps/pro-web/types/operation-meta.ts; replace duplicates in hasura.service.ts, select-scope-conversation-window.ts, chat-panel-pro.tsx, etc. Acceptance: no local type aliases remain; imports unified.
  • Shared Hasura GraphQL helper
    • Extract server-side hasuraGql into a shared action module or reuse getHasuraClient pattern; ensure admin secrets are server-only (no NEXT_PUBLIC fallback). Acceptance: one canonical client/helper; no server secrets referenced in client.
  • Message normalization
    • Introduce normalizeMessages utility with runtime guards; replace unknown[] + double casts in chat-list and related components. Acceptance: no “as unknown as …” patterns in message processing code.

Phase 2 — Server actions/services and RAG pipeline (P1)

  • Embedding
    • Consolidate embedText1536: add trim/empty guard, 30s AbortController timeout, optional dimension warning (!= 1536). Make one canonical implementation; re-export if needed. Acceptance: all embedding calls use the same helper; empty text returns [] without API call; timeout exercised in tests.
  • Ingestion (apps/pro-web/app/actions/chat-memory/ingest-assistant-answer.ts)
    • Preserve non-schema errors with warnings; only retry on schema-lag; if retry fails, surface a combined error. Acceptance: logs visible in dev; non-schema errors not silently swallowed.
    • Deduplicate AssistantAnswerScopeType by importing from shared types; move hasuraGql duplication to shared module. Acceptance: no duplicated type/helper.
  • Retrieval (retrieve-assistant-answer-memories.ts)
    • Replace unsafe casts with strongly typed mapping; clamp estimateTokens >= 1 (or import shared estimator); escape regex in tryExtractExpectedType; add gentle backoff for transient errors distinct from schema retries. Acceptance: retries behave deterministically; types compile without any-casts.
  • Service layer (hasura.service.ts)
    • SaveNewMessage retry: preserve canonicalDocNodeId in retry object; if retry fails, throw a clear retry-failed error; import ScopeType from types. Acceptance: improved error message and correct field propagation confirmed in unit test.
  • Select-scope conversation window
    • Add warning logs on fail-open branch. Acceptance: captured in console under dev flag.

Phase 3 — UI components and hooks (P1)

  • PromptEnhancer (duplicate in pro-web and web)
    • Extract to a shared component; remove unused imports; add disabled/tooltip for Templates until wired; add stable handlers; memoize computed fields; ensure Raw/Structured modes remain in sync. Acceptance: single source of truth; e2e behavior identical across apps; lint passes.
  • Chat list and message pairs
    • Replace double-casts with normalizeMessages; extract isDocLinkedThread util; ensure SSR-safe IDs; remove dead branches gated by shouldShowUserMessage or comment clearly if intentional. Acceptance: no unsafe casts; unit tests green (segment index, grouping).
  • Outline renderer and workspace components
    • Move hooks to top (no early returns before hooks); export GroupedPairsResult if consumed; stable handlers in SectionSubaccordionRow; ensure timers cleared on unmount in text editor; switch props to interface per guidelines; consider moving clamp to shared utils. Acceptance: hooks order validated; eslint rules pass.
  • Shared accordion / URL utils
    • Optional-chaining array index (categories?.[0]); consolidate duplicate slug/URL parsing; in threadQuestionUrl fallback, avoid malformed paths (or route through profilesThreadUrl); in url.ts escape regex when matching domain slugs. Acceptance: added unit tests for malformed URL prevention.

Phase 4 — Hooks and observability (P1)

  • use-onboarding: remove default export, unused session; replace magic number with const; remove commented code. Acceptance: no default export; small refactor OK.
  • use-intersection-observer: ref pattern for onIntersect to avoid re-subscribe; dev-only warn in catch; tightened deps. Acceptance: observer stability improved.
  • use-mb-chat / use-workspace-chat
    • Do not call hooks inside async handlers; centralize OperationMeta/Scope types; reduce any-casts; map scopedHistory to expected shape instead of double-cast; extract duplicated mb_op_meta construction; add finalizeDocOpLogging helper to remove repetition; maintain fail-open logs. Acceptance: types align; fewer casts; tests passing.

Phase 5 — Tests, docstrings, and CI polish (P2)

  • Add edge-case tests
    • simple-rag prompt: empty inputs; below-min-cosine; whitespace content. Retrieval: retry backoff branching and regex escaping. URL utils: fallback path generation.
  • Docstring coverage
    • Raise coverage (target ≥80%) focusing on new shared utilities, embedding, retrieval, and url helpers. Acceptance: pre-merge docstring check passes.
  • Vitest config and paths
    • Evaluate vite-tsconfig-paths vs manual alias; keep consistent root tsconfig alias if adopted. Acceptance: test runner stable.

Phase 6 — Documentation and flags (P2)

  • Workspace debug getters
    • Convert DEBUG_FLAGS to getters so devtools toggles work without refresh (or export getDebugFlags function). Acceptance: toggling window flags affects logs without reload.
  • Update AGENTS/CLAUDE/README as needed
    • Note shared PromptEnhancer and canonical Hasura helper; document feature flags (simpleRagEnabled, documentThreadOutline).

Sequencing and rollout

  • Deliver as 3–4 PRs to minimize risk:
    1. Schema & metadata fixes (Phase 0) + Drizzle operator classes + Hasura metadata adjustments. Run migrations, verify Hasura console and tests.
    2. Types + shared utilities + service layer refactors (Phases 1–2). Ensure all code compiles with stricter types.
    3. UI/hooks + PromptEnhancer extraction + URL/util fixes (Phases 3–4).
    4. Tests, docstrings, docs (Phases 5–6).
  • Feature gating: keep Simple RAG and document-thread outline behind existing flags. No behavioral changes to users unless flags enabled.

Key acceptance checks (spotlight)

  • Vector search functions return correct rows for both NULL and non-NULL scope_id.
  • All Drizzle index op classes match column types; EXPLAINs use the intended indexes.
  • No “as unknown as …” double-casts remain in message pipelines.
  • PromptEnhancer single source with synced raw/structured state; Templates button disabled or wired.
  • SaveNewMessage retry includes canonicalDocNodeId; errors surface clearly.
  • DEBUG flags read dynamically at runtime.
  • Docstring coverage ≥80%.

Open questions / decisions

  • document_operations user visibility for rows with NULL thread_id: should end-users see their “orphan” ops? I proposed an _or thread_id IS NULL filter; confirm desired policy.
  • Keep the ivfflat lists default now, then revisit with production row counts? I’ve parked as a follow-up optimization.
  • Do you want an umbrella tracking issue to link all tasks and PRs?

If you approve, I’ll open a single tracking issue and start with Phase 0 (schema/index + metadata) and Phase 1 (types/shared) branches.

Development plan for PR #600 follow-up

  • Phase 0: Schema/metadata/index correctness (P0)
  • Phase 1: Types + shared utilities consolidation (P0)
  • Phase 2: Server actions/services and RAG pipeline hardening (P1)
  • Phase 3: UI/hooks cleanup and PromptEnhancer extraction (P1)
  • Phase 4: Tests, docstrings, and docs (P2)

Would you like me to open a tracking issue and kick off Phase 0 and Phase 1?


✏️ Learnings added
Learnt from: AndlerRL
URL: https://github.com/bitcashorg/masterbots/pull/600

Timestamp: 2026-01-22T23:10:37.037Z
Learning: For bitcashorg/masterbots, RAG changes must align with the current architecture (no “legacy” labeling of existing code paths). New RAG components should integrate with existing modules and data shapes rather than introducing parallel logic.

Learnt from: AndlerRL
URL: https://github.com/bitcashorg/masterbots/pull/600

Timestamp: 2026-01-22T23:10:37.037Z
Learning: AndlerRL emphasizes Modular Development: avoid duplication by centralizing shared utilities/types, prefer small composable units, and keep flows simple and explicit (no cumbersome validations or over-engineering).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-11-07T18:41:56.562Z
Learning: Successfully created comprehensive GitHub issue `#581` consolidating 16 unresolved observations from PR `#579` review, including critical TDZ crash, migration bugs, transaction safety issues, and validation improvements, with specific file paths, line numbers, and action items organized by priority (4 critical, 8 major, 2 minor, 2 acknowledged for future PRs).

Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 542
File: apps/web/components/layout/profile/layout.tsx:67-68
Timestamp: 2025-08-29T20:34:24.433Z
Learning: AndlerRL prefers skeleton loading states over conditional rendering for better UX when components are waiting for data to load. Components should show loading indicators rather than blank spaces.

Learnt from: AndlerRL
Repo: bitcashorg/masterbots PR: 0
File: :0-0
Timestamp: 2025-11-21T19:47:50.195Z
Learning: For repo bitcashorg/masterbots (apps/pro-web), AndlerRL prefers integrating TanStack Query into React Context Hooks wherever they fit to manage shared server state and side effects, with a longer-term plan to align broader state management with TanStack Query and potentially replace React Context hooks. Captured from PR `#584` discussion on Nov 21, 2025.

💡 Use @coderabbitai plan to create a plan for code changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants